/* * 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(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 order(fileHeader.numOrders); file.ReadVector(order, fileHeader.numOrders); std::vector 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::max() + 255) / rowsPerPat); ORDERINDEX curOrd = static_cast(ord.x / rowsPerPat); ROWINDEX curRow = static_cast(ord.x % rowsPerPat); const CHANNELINDEX numChannels = std::min(patHeader.numChannels.get(), static_cast(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(); 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(target / rowsPerPat); ModCommand *mBreak = Patterns[pat].GetpModCommand(curRow, m_nChannels - 1); mBreak->command = CMD_PATTERNBREAK; mBreak->param = static_cast(target % rowsPerPat); } break; case 0x0C: // Jump to end of order { m->param = static_cast(patternEnd / rowsPerPat); ModCommand *mBreak = Patterns[pat].GetpModCommand(curRow, m_nChannels - 1); mBreak->command = CMD_PATTERNBREAK; mBreak->param = static_cast(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(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