cog/Frameworks/OpenMPT/OpenMPT/soundlib/Load_plm.cpp

403 lines
11 KiB
C++

/*
* Load_plm.cpp
* ------------
* Purpose: PLM (Disorder Tracker 2) module loader
* Notes : (currently none)
* Authors: OpenMPT Devs
* The OpenMPT source code is released under the BSD license. Read LICENSE for more details.
*/
#include "stdafx.h"
#include "Loaders.h"
OPENMPT_NAMESPACE_BEGIN
struct PLMFileHeader
{
char magic[4]; // "PLM\x1A"
uint8le headerSize; // Number of bytes in header, including magic bytes
uint8le version; // version code of file format (0x10)
char songName[48];
uint8le numChannels;
uint8le flags; // unused?
uint8le maxVol; // Maximum volume for vol slides, normally 0x40
uint8le amplify; // SoundBlaster amplify, 0x40 = no amplify
uint8le tempo;
uint8le speed;
uint8le panPos[32]; // 0...15
uint8le numSamples;
uint8le numPatterns;
uint16le numOrders;
};
MPT_BINARY_STRUCT(PLMFileHeader, 96)
struct PLMSampleHeader
{
enum SampleFlags
{
smp16Bit = 1,
smpPingPong = 2,
};
char magic[4]; // "PLS\x1A"
uint8le headerSize; // Number of bytes in header, including magic bytes
uint8le version;
char name[32];
char filename[12];
uint8le panning; // 0...15, 255 = no pan
uint8le volume; // 0...64
uint8le flags; // See SampleFlags
uint16le sampleRate;
char unused[4];
uint32le loopStart;
uint32le loopEnd;
uint32le length;
};
MPT_BINARY_STRUCT(PLMSampleHeader, 71)
struct PLMPatternHeader
{
uint32le size;
uint8le numRows;
uint8le numChannels;
uint8le color;
char name[25];
};
MPT_BINARY_STRUCT(PLMPatternHeader, 32)
struct PLMOrderItem
{
uint16le x; // Starting position of pattern
uint8le y; // Number of first channel
uint8le pattern;
};
MPT_BINARY_STRUCT(PLMOrderItem, 4)
static bool ValidateHeader(const PLMFileHeader &fileHeader)
{
if(std::memcmp(fileHeader.magic, "PLM\x1A", 4)
|| fileHeader.version != 0x10
|| fileHeader.numChannels == 0 || fileHeader.numChannels > 32
|| fileHeader.headerSize < sizeof(PLMFileHeader)
)
{
return false;
}
return true;
}
static uint64 GetHeaderMinimumAdditionalSize(const PLMFileHeader &fileHeader)
{
return fileHeader.headerSize - sizeof(PLMFileHeader) + 4 * (fileHeader.numOrders + fileHeader.numPatterns + fileHeader.numSamples);
}
CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderPLM(MemoryFileReader file, const uint64 *pfilesize)
{
PLMFileHeader fileHeader;
if(!file.ReadStruct(fileHeader))
{
return ProbeWantMoreData;
}
if(!ValidateHeader(fileHeader))
{
return ProbeFailure;
}
return ProbeAdditionalSize(file, pfilesize, GetHeaderMinimumAdditionalSize(fileHeader));
}
bool CSoundFile::ReadPLM(FileReader &file, ModLoadingFlags loadFlags)
{
file.Rewind();
PLMFileHeader fileHeader;
if(!file.ReadStruct(fileHeader))
{
return false;
}
if(!ValidateHeader(fileHeader))
{
return false;
}
if(!file.CanRead(mpt::saturate_cast<FileReader::off_t>(GetHeaderMinimumAdditionalSize(fileHeader))))
{
return false;
}
if(loadFlags == onlyVerifyHeader)
{
return true;
}
if(!file.Seek(fileHeader.headerSize))
{
return false;
}
InitializeGlobals(MOD_TYPE_PLM);
InitializeChannels();
m_SongFlags = SONG_ITOLDEFFECTS;
m_playBehaviour.set(kApplyOffsetWithoutNote);
m_modFormat.formatName = U_("Disorder Tracker 2");
m_modFormat.type = U_("plm");
m_modFormat.charset = mpt::Charset::CP437;
// Some PLMs use ASCIIZ, some space-padding strings...weird. Oh, and the file browser stops at 0 bytes in the name, the main GUI doesn't.
m_songName = mpt::String::ReadBuf(mpt::String::spacePadded, fileHeader.songName);
m_nChannels = fileHeader.numChannels + 1; // Additional channel for writing pattern breaks
m_nSamplePreAmp = fileHeader.amplify;
m_nDefaultTempo.Set(fileHeader.tempo);
m_nDefaultSpeed = fileHeader.speed;
for(CHANNELINDEX chn = 0; chn < fileHeader.numChannels; chn++)
{
ChnSettings[chn].nPan = fileHeader.panPos[chn] * 0x11;
}
m_nSamples = fileHeader.numSamples;
std::vector<PLMOrderItem> order(fileHeader.numOrders);
file.ReadVector(order, fileHeader.numOrders);
std::vector<uint32le> patternPos, samplePos;
file.ReadVector(patternPos, fileHeader.numPatterns);
file.ReadVector(samplePos, fileHeader.numSamples);
for(SAMPLEINDEX smp = 0; smp < fileHeader.numSamples; smp++)
{
ModSample &sample = Samples[smp + 1];
sample.Initialize();
PLMSampleHeader sampleHeader;
if(samplePos[smp] == 0
|| !file.Seek(samplePos[smp])
|| !file.ReadStruct(sampleHeader))
continue;
m_szNames[smp + 1] = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, sampleHeader.name);
sample.filename = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, sampleHeader.filename);
if(sampleHeader.panning <= 15)
{
sample.uFlags.set(CHN_PANNING);
sample.nPan = sampleHeader.panning * 0x11;
}
sample.nGlobalVol = std::min(sampleHeader.volume.get(), uint8(64));
sample.nC5Speed = sampleHeader.sampleRate;
sample.nLoopStart = sampleHeader.loopStart;
sample.nLoopEnd = sampleHeader.loopEnd;
sample.nLength = sampleHeader.length;
if(sampleHeader.flags & PLMSampleHeader::smp16Bit)
{
sample.nLoopStart /= 2;
sample.nLoopEnd /= 2;
sample.nLength /= 2;
}
if(sample.nLoopEnd > sample.nLoopStart)
{
sample.uFlags.set(CHN_LOOP);
if(sampleHeader.flags & PLMSampleHeader::smpPingPong) sample.uFlags.set(CHN_PINGPONGLOOP);
}
sample.SanitizeLoops();
if(loadFlags & loadSampleData)
{
file.Seek(samplePos[smp] + sampleHeader.headerSize);
SampleIO(
(sampleHeader.flags & PLMSampleHeader::smp16Bit) ? SampleIO::_16bit : SampleIO::_8bit,
SampleIO::mono,
SampleIO::littleEndian,
SampleIO::unsignedPCM)
.ReadSample(sample, file);
}
}
if(!(loadFlags & loadPatternData))
{
return true;
}
// PLM is basically one huge continuous pattern, so we split it up into smaller patterns.
const ROWINDEX rowsPerPat = 64;
uint32 maxPos = 0;
static constexpr ModCommand::COMMAND effTrans[] =
{
CMD_NONE,
CMD_PORTAMENTOUP,
CMD_PORTAMENTODOWN,
CMD_TONEPORTAMENTO,
CMD_VOLUMESLIDE,
CMD_TREMOLO,
CMD_VIBRATO,
CMD_S3MCMDEX, // Tremolo Waveform
CMD_S3MCMDEX, // Vibrato Waveform
CMD_TEMPO,
CMD_SPEED,
CMD_POSITIONJUMP, // Jump to order
CMD_POSITIONJUMP, // Break to end of this order
CMD_OFFSET,
CMD_S3MCMDEX, // GUS Panning
CMD_RETRIG,
CMD_S3MCMDEX, // Note Delay
CMD_S3MCMDEX, // Note Cut
CMD_S3MCMDEX, // Pattern Delay
CMD_FINEVIBRATO,
CMD_VIBRATOVOL,
CMD_TONEPORTAVOL,
CMD_OFFSETPERCENTAGE,
};
Order().clear();
for(const auto &ord : order)
{
if(ord.pattern >= fileHeader.numPatterns
|| ord.y > fileHeader.numChannels
|| !file.Seek(patternPos[ord.pattern])) continue;
PLMPatternHeader patHeader;
file.ReadStruct(patHeader);
if(!patHeader.numRows) continue;
static_assert(ORDERINDEX_MAX >= (std::numeric_limits<decltype(ord.x)>::max() + 255) / rowsPerPat);
ORDERINDEX curOrd = static_cast<ORDERINDEX>(ord.x / rowsPerPat);
ROWINDEX curRow = static_cast<ROWINDEX>(ord.x % rowsPerPat);
const CHANNELINDEX numChannels = std::min(patHeader.numChannels.get(), static_cast<uint8>(fileHeader.numChannels - ord.y));
const uint32 patternEnd = ord.x + patHeader.numRows;
maxPos = std::max(maxPos, patternEnd);
ModCommand::NOTE lastNote[32] = { 0 };
for(ROWINDEX r = 0; r < patHeader.numRows; r++, curRow++)
{
if(curRow >= rowsPerPat)
{
curRow = 0;
curOrd++;
}
if(curOrd >= Order().size())
{
Order().resize(curOrd + 1);
Order()[curOrd] = Patterns.InsertAny(rowsPerPat);
}
PATTERNINDEX pat = Order()[curOrd];
if(!Patterns.IsValidPat(pat)) break;
ModCommand *m = Patterns[pat].GetpModCommand(curRow, ord.y);
for(CHANNELINDEX c = 0; c < numChannels; c++, m++)
{
const auto [note, instr, volume, command, param] = file.ReadArray<uint8, 5>();
if(note > 0 && note < 0x90)
lastNote[c] = m->note = (note >> 4) * 12 + (note & 0x0F) + 12 + NOTE_MIN;
else
m->note = NOTE_NONE;
m->instr = instr;
m->volcmd = VOLCMD_VOLUME;
if(volume != 0xFF)
m->vol = volume;
else
m->volcmd = VOLCMD_NONE;
if(command < std::size(effTrans))
{
m->command = effTrans[command];
m->param = param;
// Fix some commands
switch(command)
{
case 0x07: // Tremolo waveform
m->param = 0x40 | (m->param & 0x03);
break;
case 0x08: // Vibrato waveform
m->param = 0x30 | (m->param & 0x03);
break;
case 0x0B: // Jump to order
if(m->param < order.size())
{
uint16 target = order[m->param].x;
m->param = static_cast<ModCommand::PARAM>(target / rowsPerPat);
ModCommand *mBreak = Patterns[pat].GetpModCommand(curRow, m_nChannels - 1);
mBreak->command = CMD_PATTERNBREAK;
mBreak->param = static_cast<ModCommand::PARAM>(target % rowsPerPat);
}
break;
case 0x0C: // Jump to end of order
{
m->param = static_cast<ModCommand::PARAM>(patternEnd / rowsPerPat);
ModCommand *mBreak = Patterns[pat].GetpModCommand(curRow, m_nChannels - 1);
mBreak->command = CMD_PATTERNBREAK;
mBreak->param = static_cast<ModCommand::PARAM>(patternEnd % rowsPerPat);
}
break;
case 0x0E: // GUS Panning
m->param = 0x80 | (m->param & 0x0F);
break;
case 0x10: // Delay Note
m->param = 0xD0 | std::min(m->param, ModCommand::PARAM(0x0F));
break;
case 0x11: // Cut Note
m->param = 0xC0 | std::min(m->param, ModCommand::PARAM(0x0F));
break;
case 0x12: // Pattern Delay
m->param = 0xE0 | std::min(m->param, ModCommand::PARAM(0x0F));
break;
case 0x04: // Volume Slide
case 0x14: // Vibrato + Volume Slide
case 0x15: // Tone Portamento + Volume Slide
// If both nibbles of a volume slide are set, act as fine volume slide up
if((m->param & 0xF0) && (m->param & 0x0F) && (m->param & 0xF0) != 0xF0)
{
m->param |= 0x0F;
}
break;
case 0x0D:
case 0x16:
// Offset without note
if(m->note == NOTE_NONE)
{
m->note = lastNote[c];
}
break;
}
}
}
if(patHeader.numChannels > numChannels)
{
file.Skip(5 * (patHeader.numChannels - numChannels));
}
}
}
// Module ends with the last row of the last order item
ROWINDEX endPatSize = maxPos % rowsPerPat;
ORDERINDEX endOrder = static_cast<ORDERINDEX>(maxPos / rowsPerPat);
if(endPatSize > 0 && Order().IsValidPat(endOrder))
{
Patterns[Order()[endOrder]].Resize(endPatSize, false);
}
// If there are still any non-existent patterns in our order list, insert some blank patterns.
PATTERNINDEX blankPat = PATTERNINDEX_INVALID;
for(auto &pat : Order())
{
if(pat == Order.GetInvalidPatIndex())
{
if(blankPat == PATTERNINDEX_INVALID)
{
blankPat = Patterns.InsertAny(rowsPerPat);
}
pat = blankPat;
}
}
return true;
}
OPENMPT_NAMESPACE_END