123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607 |
- /*
- * 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"
- 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<ChunkIdentifiers>(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<mpt::String::maybeNullTerminated>(m_songName, fileHeader.headerSize - (sizeof(fileHeader) - 8u));
- auto chunks = ChunkReader(file).ReadChunks<DTMChunk>(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<uint8>(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<uint16>(128 + Util::muldivr(std::min(static_cast<int>(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<uint16>(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<uint8>(std::min(static_cast<int>(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<INSTRUMENTINDEX>(m_nSamples), static_cast<INSTRUMENTINDEX>(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<uint16>(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<std::size_t>(env.numPoints), std::size(env.points), static_cast<std::size_t>(MAX_ENVPOINTS) }));
- for(size_t i = 0; i < mptEnv.size(); i++)
- {
- mptEnv[i].value = std::min(uint8(64), static_cast<uint8>(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<uint8>(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<ROWINDEX>(position.quot) < numRows)
- {
- ModCommand *m = Patterns[patNum].GetpModCommand(position.quot, chn);
- const auto [note, volume, instr, command, param, delay] = rowChunk.ReadArray<uint8, 6>();
- if(note > 0 && note <= 96)
- {
- m->note = note + NOTE_MIN + 12;
- if(position.rem)
- {
- m->command = CMD_MODCMDEX;
- m->param = 0xD0 | static_cast<ModCommand::PARAM>(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<ModCommand::PARAM>(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<uint8, 4>();
- 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_UFORMAT("Digital Tracker {}.{}")(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::Amiga_no_C1;
- return true;
- }
- OPENMPT_NAMESPACE_END
|