/* * Load_dtm.cpp * ------------ * Purpose: Digital Tracker / Digital Home Studio module Loader (DTM) * 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" #include "ChunkReader.h" OPENMPT_NAMESPACE_BEGIN enum PatternFormats : uint32 { DTM_PT_PATTERN_FORMAT = 0, DTM_204_PATTERN_FORMAT = MagicBE("2.04"), DTM_206_PATTERN_FORMAT = MagicBE("2.06"), }; struct DTMFileHeader { char magic[4]; uint32be headerSize; uint16be type; // 0 = module uint8be stereoMode; // FF = panoramic stereo, 00 = old stereo uint8be bitDepth; // Typically 8, sometimes 16, but is not actually used anywhere? uint16be reserved; // Usually 0, but not in unknown title 1.dtm and unknown title 2.dtm uint16be speed; uint16be tempo; uint32be forcedSampleRate; // Seems to be ignored in newer files }; MPT_BINARY_STRUCT(DTMFileHeader, 22) // IFF-style Chunk struct DTMChunk { // 32-Bit chunk identifiers enum ChunkIdentifiers { idS_Q_ = MagicBE("S.Q."), idPATT = MagicBE("PATT"), idINST = MagicBE("INST"), idIENV = MagicBE("IENV"), idDAPT = MagicBE("DAPT"), idDAIT = MagicBE("DAIT"), idTEXT = MagicBE("TEXT"), idPATN = MagicBE("PATN"), idTRKN = MagicBE("TRKN"), idVERS = MagicBE("VERS"), idSV19 = MagicBE("SV19"), }; uint32be id; uint32be length; size_t GetLength() const { return length; } ChunkIdentifiers GetID() const { return static_cast(id.get()); } }; MPT_BINARY_STRUCT(DTMChunk, 8) struct DTMSample { uint32be reserved; // 0x204 for first sample, 0x208 for second, etc... uint32be length; // in bytes uint8be finetune; // -8....7 uint8be volume; // 0...64 uint32be loopStart; // in bytes uint32be loopLength; // ditto char name[22]; uint8be stereo; uint8be bitDepth; uint16be transpose; uint16be unknown; uint32be sampleRate; void ConvertToMPT(ModSample &mptSmp, uint32 forcedSampleRate, uint32 formatVersion) const { mptSmp.Initialize(MOD_TYPE_IT); mptSmp.nLength = length; mptSmp.nLoopStart = loopStart; mptSmp.nLoopEnd = mptSmp.nLoopStart + loopLength; // In revolution to come.dtm, the file header says samples rate is 24512 Hz, but samples say it's 50000 Hz // Digital Home Studio ignores the header setting in 2.04-/2.06-style modules mptSmp.nC5Speed = (formatVersion == DTM_PT_PATTERN_FORMAT && forcedSampleRate > 0) ? forcedSampleRate : sampleRate; int32 transposeAmount = MOD2XMFineTune(finetune); if(formatVersion == DTM_206_PATTERN_FORMAT && transpose > 0 && transpose != 48) { // Digital Home Studio applies this unconditionally, but some old songs sound wrong then (delirium.dtm). // Digital Tracker 2.03 ignores the setting. // Maybe this should not be applied for "real" Digital Tracker modules? transposeAmount += (48 - transpose) * 128; } mptSmp.Transpose(transposeAmount * (1.0 / (12.0 * 128.0))); mptSmp.nVolume = std::min(volume.get(), uint8(64)) * 4u; if(stereo & 1) { mptSmp.uFlags.set(CHN_STEREO); mptSmp.nLength /= 2u; mptSmp.nLoopStart /= 2u; mptSmp.nLoopEnd /= 2u; } if(bitDepth > 8) { mptSmp.uFlags.set(CHN_16BIT); mptSmp.nLength /= 2u; mptSmp.nLoopStart /= 2u; mptSmp.nLoopEnd /= 2u; } if(mptSmp.nLoopEnd > mptSmp.nLoopStart + 1) { mptSmp.uFlags.set(CHN_LOOP); } else { mptSmp.nLoopStart = mptSmp.nLoopEnd = 0; } } }; MPT_BINARY_STRUCT(DTMSample, 50) struct DTMInstrument { uint16be insNum; uint8be unknown1; uint8be envelope; // 0xFF = none uint8be sustain; // 0xFF = no sustain point uint16be fadeout; uint8be vibRate; uint8be vibDepth; uint8be modulationRate; uint8be modulationDepth; uint8be breathRate; uint8be breathDepth; uint8be volumeRate; uint8be volumeDepth; }; MPT_BINARY_STRUCT(DTMInstrument, 15) struct DTMEnvelope { struct DTMEnvPoint { uint8be value; uint8be tick; }; uint16be numPoints; DTMEnvPoint points[16]; }; MPT_BINARY_STRUCT(DTMEnvelope::DTMEnvPoint, 2) MPT_BINARY_STRUCT(DTMEnvelope, 34) struct DTMText { uint16be textType; // 0 = pattern, 1 = free, 2 = song uint32be textLength; uint16be tabWidth; uint16be reserved; uint16be oddLength; }; MPT_BINARY_STRUCT(DTMText, 12) static bool ValidateHeader(const DTMFileHeader &fileHeader) { if(std::memcmp(fileHeader.magic, "D.T.", 4) || fileHeader.headerSize < sizeof(fileHeader) - 8u || fileHeader.headerSize > 256 // Excessively long song title? || fileHeader.type != 0) { return false; } return true; } CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderDTM(MemoryFileReader file, const uint64 *pfilesize) { DTMFileHeader fileHeader; if(!file.ReadStruct(fileHeader)) { return ProbeWantMoreData; } if(!ValidateHeader(fileHeader)) { return ProbeFailure; } MPT_UNREFERENCED_PARAMETER(pfilesize); return ProbeSuccess; } bool CSoundFile::ReadDTM(FileReader &file, ModLoadingFlags loadFlags) { file.Rewind(); DTMFileHeader fileHeader; if(!file.ReadStruct(fileHeader)) { return false; } if(!ValidateHeader(fileHeader)) { return false; } if(loadFlags == onlyVerifyHeader) { return true; } InitializeGlobals(MOD_TYPE_DTM); InitializeChannels(); m_SongFlags.set(SONG_ITCOMPATGXX | SONG_ITOLDEFFECTS); m_playBehaviour.reset(kITVibratoTremoloPanbrello); // Various files have a default speed or tempo of 0 if(fileHeader.tempo) m_nDefaultTempo.Set(fileHeader.tempo); if(fileHeader.speed) m_nDefaultSpeed = fileHeader.speed; if(fileHeader.stereoMode == 0) SetupMODPanning(true); file.ReadString(m_songName, fileHeader.headerSize - (sizeof(fileHeader) - 8u)); auto chunks = ChunkReader(file).ReadChunks(1); // Read order list if(FileReader chunk = chunks.GetChunk(DTMChunk::idS_Q_)) { uint16 ordLen = chunk.ReadUint16BE(); uint16 restartPos = chunk.ReadUint16BE(); chunk.Skip(4); // Reserved ReadOrderFromFile(Order(), chunk, ordLen); Order().SetRestartPos(restartPos); } else { return false; } // Read pattern properties uint32 patternFormat; if(FileReader chunk = chunks.GetChunk(DTMChunk::idPATT)) { m_nChannels = chunk.ReadUint16BE(); if(m_nChannels < 1 || m_nChannels > 32) { return false; } Patterns.ResizeArray(chunk.ReadUint16BE()); // Number of stored patterns, may be lower than highest pattern number patternFormat = chunk.ReadUint32BE(); if(patternFormat != DTM_PT_PATTERN_FORMAT && patternFormat != DTM_204_PATTERN_FORMAT && patternFormat != DTM_206_PATTERN_FORMAT) { return false; } } else { return false; } // Read global info if(FileReader chunk = chunks.GetChunk(DTMChunk::idSV19)) { chunk.Skip(2); // Ticks per quarter note, typically 24 uint32 fractionalTempo = chunk.ReadUint32BE(); m_nDefaultTempo = TEMPO(m_nDefaultTempo.GetInt() + fractionalTempo / 4294967296.0); uint16be panning[32]; chunk.ReadArray(panning); for(CHANNELINDEX chn = 0; chn < 32 && chn < GetNumChannels(); chn++) { // Panning is in range 0...180, 90 = center ChnSettings[chn].nPan = static_cast(128 + Util::muldivr(std::min(static_cast(panning[chn]), int(180)) - 90, 128, 90)); } chunk.Skip(16); // Chunk ends here for old DTM modules if(chunk.CanRead(2)) { m_nDefaultGlobalVolume = std::min(chunk.ReadUint16BE(), static_cast(MAX_GLOBAL_VOLUME)); } chunk.Skip(128); uint16be volume[32]; if(chunk.ReadArray(volume)) { for(CHANNELINDEX chn = 0; chn < 32 && chn < GetNumChannels(); chn++) { // Volume is in range 0...128, 64 = normal ChnSettings[chn].nVolume = static_cast(std::min(static_cast(volume[chn]), int(128)) / 2); } m_nSamplePreAmp *= 2; // Compensate for channel volume range } } // Read song message if(FileReader chunk = chunks.GetChunk(DTMChunk::idTEXT)) { DTMText text; chunk.ReadStruct(text); if(text.oddLength == 0xFFFF) { chunk.Skip(1); } m_songMessage.Read(chunk, chunk.BytesLeft(), SongMessage::leCRLF); } // Read sample headers if(FileReader chunk = chunks.GetChunk(DTMChunk::idINST)) { uint16 numSamples = chunk.ReadUint16BE(); bool newSamples = (numSamples >= 0x8000); numSamples &= 0x7FFF; if(numSamples >= MAX_SAMPLES || !chunk.CanRead(numSamples * (sizeof(DTMSample) + (newSamples ? 2u : 0u)))) { return false; } m_nSamples = numSamples; for(SAMPLEINDEX smp = 1; smp <= numSamples; smp++) { SAMPLEINDEX realSample = newSamples ? (chunk.ReadUint16BE() + 1u) : smp; DTMSample dtmSample; chunk.ReadStruct(dtmSample); if(realSample < 1 || realSample >= MAX_SAMPLES) { continue; } m_nSamples = std::max(m_nSamples, realSample); ModSample &mptSmp = Samples[realSample]; dtmSample.ConvertToMPT(mptSmp, fileHeader.forcedSampleRate, patternFormat); m_szNames[realSample] = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, dtmSample.name); } if(chunk.ReadUint16BE() == 0x0004) { // Digital Home Studio instruments m_nInstruments = std::min(static_cast(m_nSamples), static_cast(MAX_INSTRUMENTS - 1)); FileReader envChunk = chunks.GetChunk(DTMChunk::idIENV); while(chunk.CanRead(sizeof(DTMInstrument))) { DTMInstrument instr; chunk.ReadStruct(instr); if(instr.insNum < GetNumInstruments()) { ModSample &sample = Samples[instr.insNum + 1]; sample.nVibDepth = instr.vibDepth; sample.nVibRate = instr.vibRate; sample.nVibSweep = 255; ModInstrument *mptIns = AllocateInstrument(instr.insNum + 1, instr.insNum + 1); if(mptIns != nullptr) { InstrumentEnvelope &mptEnv = mptIns->VolEnv; mptIns->nFadeOut = std::min(static_cast(instr.fadeout), uint16(0xFFF)); if(instr.envelope != 0xFF && envChunk.Seek(2 + sizeof(DTMEnvelope) * instr.envelope)) { DTMEnvelope env; envChunk.ReadStruct(env); mptEnv.dwFlags.set(ENV_ENABLED); mptEnv.resize(std::min({ static_cast(env.numPoints), std::size(env.points), static_cast(MAX_ENVPOINTS) })); for(size_t i = 0; i < mptEnv.size(); i++) { mptEnv[i].value = std::min(uint8(64), static_cast(env.points[i].value)); mptEnv[i].tick = env.points[i].tick; } if(instr.sustain != 0xFF) { mptEnv.dwFlags.set(ENV_SUSTAIN); mptEnv.nSustainStart = mptEnv.nSustainEnd = instr.sustain; } if(!mptEnv.empty()) { mptEnv.dwFlags.set(ENV_LOOP); mptEnv.nLoopStart = mptEnv.nLoopEnd = static_cast(mptEnv.size() - 1); } } } } } } } // Read pattern data for(auto &chunk : chunks.GetAllChunks(DTMChunk::idDAPT)) { chunk.Skip(4); // FF FF FF FF PATTERNINDEX patNum = chunk.ReadUint16BE(); ROWINDEX numRows = chunk.ReadUint16BE(); if(patternFormat == DTM_206_PATTERN_FORMAT) { // The stored data is actually not row-based, but tick-based. numRows /= m_nDefaultSpeed; } if(!(loadFlags & loadPatternData) || patNum > 255 || !Patterns.Insert(patNum, numRows)) { continue; } if(patternFormat == DTM_206_PATTERN_FORMAT) { chunk.Skip(4); for(CHANNELINDEX chn = 0; chn < GetNumChannels(); chn++) { uint16 length = chunk.ReadUint16BE(); if(length % 2u) length++; FileReader rowChunk = chunk.ReadChunk(length); int tick = 0; std::div_t position = { 0, 0 }; while(rowChunk.CanRead(6) && static_cast(position.quot) < numRows) { ModCommand *m = Patterns[patNum].GetpModCommand(position.quot, chn); const auto [note, volume, instr, command, param, delay] = rowChunk.ReadArray(); if(note > 0 && note <= 96) { m->note = note + NOTE_MIN + 12; if(position.rem) { m->command = CMD_MODCMDEX; m->param = 0xD0 | static_cast(std::min(position.rem, 15)); } } else if(note & 0x80) { // Lower 7 bits contain note, probably intended for MIDI-like note-on/note-off events if(position.rem) { m->command = CMD_MODCMDEX; m->param = 0xC0 | static_cast(std::min(position.rem, 15)); } else { m->note = NOTE_NOTECUT; } } if(volume) { m->volcmd = VOLCMD_VOLUME; m->vol = std::min(volume, uint8(64)); // Volume can go up to 255, but we do not support over-amplification at the moment. } if(instr) { m->instr = instr; } if(command || param) { m->command = command; m->param = param; ConvertModCommand(*m); #ifdef MODPLUG_TRACKER m->Convert(MOD_TYPE_MOD, MOD_TYPE_IT, *this); #endif // G is 8-bit volume // P is tremor (need to disable oldfx) } if(delay & 0x80) tick += (delay & 0x7F) * 0x100 + rowChunk.ReadUint8(); else tick += delay; position = std::div(tick, m_nDefaultSpeed); } } } else { ModCommand *m = Patterns[patNum].GetpModCommand(0, 0); for(ROWINDEX row = 0; row < numRows; row++) { for(CHANNELINDEX chn = 0; chn < GetNumChannels(); chn++, m++) { const auto data = chunk.ReadArray(); if(patternFormat == DTM_204_PATTERN_FORMAT) { const auto [note, instrVol, instrCmd, param] = data; if(note > 0 && note < 0x80) { m->note = (note >> 4) * 12 + (note & 0x0F) + NOTE_MIN + 11; } uint8 vol = instrVol >> 2; if(vol) { m->volcmd = VOLCMD_VOLUME; m->vol = vol - 1u; } m->instr = ((instrVol & 0x03) << 4) | (instrCmd >> 4); m->command = instrCmd & 0x0F; m->param = param; } else { ReadMODPatternEntry(data, *m); m->instr |= data[0] & 0x30; // Allow more than 31 instruments } ConvertModCommand(*m); // Fix commands without memory and slide nibble precedence switch(m->command) { case CMD_PORTAMENTOUP: case CMD_PORTAMENTODOWN: if(!m->param) { m->command = CMD_NONE; } break; case CMD_VOLUMESLIDE: case CMD_TONEPORTAVOL: case CMD_VIBRATOVOL: if(m->param & 0xF0) { m->param &= 0xF0; } else if(!m->param) { m->command = CMD_NONE; } break; default: break; } #ifdef MODPLUG_TRACKER m->Convert(MOD_TYPE_MOD, MOD_TYPE_IT, *this); #endif } } } } // Read pattern names if(FileReader chunk = chunks.GetChunk(DTMChunk::idPATN)) { PATTERNINDEX pat = 0; std::string name; while(chunk.CanRead(1) && pat < Patterns.Size()) { chunk.ReadNullString(name, 32); Patterns[pat].SetName(name); pat++; } } // Read channel names if(FileReader chunk = chunks.GetChunk(DTMChunk::idTRKN)) { CHANNELINDEX chn = 0; std::string name; while(chunk.CanRead(1) && chn < GetNumChannels()) { chunk.ReadNullString(name, 32); ChnSettings[chn].szName = name; chn++; } } // Read sample data for(auto &chunk : chunks.GetAllChunks(DTMChunk::idDAIT)) { SAMPLEINDEX smp = chunk.ReadUint16BE(); if(smp >= GetNumSamples() || !(loadFlags & loadSampleData)) { continue; } ModSample &mptSmp = Samples[smp + 1]; SampleIO( mptSmp.uFlags[CHN_16BIT] ? SampleIO::_16bit : SampleIO::_8bit, mptSmp.uFlags[CHN_STEREO] ? SampleIO::stereoInterleaved: SampleIO::mono, SampleIO::bigEndian, SampleIO::signedPCM).ReadSample(mptSmp, chunk); } // Is this accurate? mpt::ustring tracker; if(patternFormat == DTM_206_PATTERN_FORMAT) { tracker = U_("Digital Home Studio"); } else if(FileReader chunk = chunks.GetChunk(DTMChunk::idVERS)) { uint32 version = chunk.ReadUint32BE(); tracker = mpt::format(U_("Digital Tracker %1.%2"))(version >> 4, version & 0x0F); } else { tracker = U_("Digital Tracker"); } m_modFormat.formatName = U_("Digital Tracker"); m_modFormat.type = U_("dtm"); m_modFormat.madeWithTracker = std::move(tracker); m_modFormat.charset = mpt::Charset::ISO8859_1; return true; } OPENMPT_NAMESPACE_END