/* * Load_ptm.cpp * ------------ * Purpose: PTM (PolyTracker) module loader * Notes : (currently none) * Authors: Olivier Lapicque * 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 PTMFileHeader { char songname[28]; // Name of song, asciiz string uint8le dosEOF; // 26 uint8le versionLo; // 03 version of file, currently 0203h uint8le versionHi; // 02 uint8le reserved1; // Reserved, set to 0 uint16le numOrders; // Number of orders (0..256) uint16le numSamples; // Number of instruments (1..255) uint16le numPatterns; // Number of patterns (1..128) uint16le numChannels; // Number of channels (voices) used (1..32) uint16le flags; // Set to 0 uint8le reserved2[2]; // Reserved, set to 0 char magic[4]; // Song identification, 'PTMF' uint8le reserved3[16]; // Reserved, set to 0 uint8le chnPan[32]; // Channel panning settings, 0..15, 0 = left, 7 = middle, 15 = right uint8le orders[256]; // Order list, valid entries 0..nOrders-1 uint16le patOffsets[128]; // Pattern offsets (*16) }; MPT_BINARY_STRUCT(PTMFileHeader, 608) struct PTMSampleHeader { enum SampleFlags { smpTypeMask = 0x03, smpPCM = 0x01, smpLoop = 0x04, smpPingPong = 0x08, smp16Bit = 0x10, }; uint8le flags; // Sample type (see SampleFlags) char filename[12]; // Name of external sample file uint8le volume; // Default volume uint16le c4speed; // C-4 speed (yep, not C-5) uint8le smpSegment[2]; // Sample segment (used internally) uint32le dataOffset; // Offset of sample data uint32le length; // Sample size (in bytes) uint32le loopStart; // Start of loop uint32le loopEnd; // End of loop uint8le gusdata[14]; char samplename[28]; // Name of sample, ASCIIZ char magic[4]; // Sample identification, 'PTMS' // Convert an PTM sample header to OpenMPT's internal sample header. SampleIO ConvertToMPT(ModSample &mptSmp) const { mptSmp.Initialize(MOD_TYPE_S3M); mptSmp.nVolume = std::min(volume.get(), uint8(64)) * 4; mptSmp.nC5Speed = c4speed * 2; mptSmp.filename = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, filename); SampleIO sampleIO( SampleIO::_8bit, SampleIO::mono, SampleIO::littleEndian, SampleIO::deltaPCM); if((flags & smpTypeMask) == smpPCM) { mptSmp.nLength = length; mptSmp.nLoopStart = loopStart; mptSmp.nLoopEnd = loopEnd; if(mptSmp.nLoopEnd > mptSmp.nLoopStart) mptSmp.nLoopEnd--; if(flags & smpLoop) mptSmp.uFlags.set(CHN_LOOP); if(flags & smpPingPong) mptSmp.uFlags.set(CHN_PINGPONGLOOP); if(flags & smp16Bit) { sampleIO |= SampleIO::_16bit; sampleIO |= SampleIO::PTM8Dto16; mptSmp.nLength /= 2; mptSmp.nLoopStart /= 2; mptSmp.nLoopEnd /= 2; } } return sampleIO; } }; MPT_BINARY_STRUCT(PTMSampleHeader, 80) static bool ValidateHeader(const PTMFileHeader &fileHeader) { if(std::memcmp(fileHeader.magic, "PTMF", 4) || fileHeader.dosEOF != 26 || fileHeader.versionHi > 2 || fileHeader.flags != 0 || !fileHeader.numChannels || fileHeader.numChannels > 32 || !fileHeader.numOrders || fileHeader.numOrders > 256 || !fileHeader.numSamples || fileHeader.numSamples > 255 || !fileHeader.numPatterns || fileHeader.numPatterns > 128 ) { return false; } return true; } static uint64 GetHeaderMinimumAdditionalSize(const PTMFileHeader &fileHeader) { return fileHeader.numSamples * sizeof(PTMSampleHeader); } CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderPTM(MemoryFileReader file, const uint64 *pfilesize) { PTMFileHeader fileHeader; if(!file.ReadStruct(fileHeader)) { return ProbeWantMoreData; } if(!ValidateHeader(fileHeader)) { return ProbeFailure; } return ProbeAdditionalSize(file, pfilesize, GetHeaderMinimumAdditionalSize(fileHeader)); } bool CSoundFile::ReadPTM(FileReader &file, ModLoadingFlags loadFlags) { file.Rewind(); PTMFileHeader 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; } InitializeGlobals(MOD_TYPE_PTM); m_songName = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, fileHeader.songname); m_modFormat.formatName = U_("PolyTracker"); m_modFormat.type = U_("ptm"); m_modFormat.madeWithTracker = mpt::format(U_("PolyTracker %1.%2"))(fileHeader.versionHi.get(), mpt::ufmt::hex0<2>(fileHeader.versionLo.get())); m_modFormat.charset = mpt::Charset::CP437; m_SongFlags = SONG_ITCOMPATGXX | SONG_ITOLDEFFECTS; m_nChannels = fileHeader.numChannels; m_nSamples = std::min(static_cast(fileHeader.numSamples), static_cast(MAX_SAMPLES - 1)); ReadOrderFromArray(Order(), fileHeader.orders, fileHeader.numOrders, 0xFF, 0xFE); // Reading channel panning for(CHANNELINDEX chn = 0; chn < m_nChannels; chn++) { ChnSettings[chn].Reset(); ChnSettings[chn].nPan = ((fileHeader.chnPan[chn] & 0x0F) << 4) + 4; } // Reading samples FileReader sampleHeaderChunk = file.ReadChunk(fileHeader.numSamples * sizeof(PTMSampleHeader)); for(SAMPLEINDEX smp = 0; smp < m_nSamples; smp++) { PTMSampleHeader sampleHeader; sampleHeaderChunk.ReadStruct(sampleHeader); ModSample &sample = Samples[smp + 1]; m_szNames[smp + 1] = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, sampleHeader.samplename); SampleIO sampleIO = sampleHeader.ConvertToMPT(sample); if((loadFlags & loadSampleData) && sample.nLength && file.Seek(sampleHeader.dataOffset)) { sampleIO.ReadSample(sample, file); } } // Reading Patterns if(!(loadFlags & loadPatternData)) { return true; } Patterns.ResizeArray(fileHeader.numPatterns); for(PATTERNINDEX pat = 0; pat < fileHeader.numPatterns; pat++) { if(!Patterns.Insert(pat, 64) || fileHeader.patOffsets[pat] == 0 || !file.Seek(fileHeader.patOffsets[pat] << 4)) { continue; } ModCommand *rowBase = Patterns[pat].GetpModCommand(0, 0); ROWINDEX row = 0; while(row < 64 && file.CanRead(1)) { uint8 b = file.ReadUint8(); if(b == 0) { row++; rowBase += m_nChannels; continue; } CHANNELINDEX chn = (b & 0x1F); ModCommand dummy = ModCommand(); ModCommand &m = chn < GetNumChannels() ? rowBase[chn] : dummy; if(b & 0x20) { const auto [note, instr] = file.ReadArray(); m.note = note; m.instr = instr; if(m.note == 254) m.note = NOTE_NOTECUT; else if(!m.note || m.note > 120) m.note = NOTE_NONE; } if(b & 0x40) { const auto [command, param] = file.ReadArray(); m.command = command; m.param = param; static constexpr EffectCommand effTrans[] = { CMD_GLOBALVOLUME, CMD_RETRIG, CMD_FINEVIBRATO, CMD_NOTESLIDEUP, CMD_NOTESLIDEDOWN, CMD_NOTESLIDEUPRETRIG, CMD_NOTESLIDEDOWNRETRIG, CMD_REVERSEOFFSET }; if(m.command < 0x10) { // Beware: Effect letters are as in MOD, but portamento and volume slides behave like in S3M (i.e. fine slides share the same effect letters) ConvertModCommand(m); } else if(m.command < 0x10 + CountOf(effTrans)) { m.command = effTrans[m.command - 0x10]; } else { m.command = CMD_NONE; } switch(m.command) { case CMD_PANNING8: // Don't be surprised about the strange formula, this is directly translated from original disassembly... m.command = CMD_S3MCMDEX; m.param = 0x80 | ((std::max(m.param >> 3, 1u) - 1u) & 0x0F); break; case CMD_GLOBALVOLUME: m.param = std::min(m.param, uint8(0x40)) * 2u; break; } } if(b & 0x80) { m.volcmd = VOLCMD_VOLUME; m.vol = file.ReadUint8(); } } } return true; } OPENMPT_NAMESPACE_END