Load_dtm.cpp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607
  1. /*
  2. * Load_dtm.cpp
  3. * ------------
  4. * Purpose: Digital Tracker / Digital Home Studio module Loader (DTM)
  5. * Notes : (currently none)
  6. * Authors: OpenMPT Devs
  7. * The OpenMPT source code is released under the BSD license. Read LICENSE for more details.
  8. */
  9. #include "stdafx.h"
  10. #include "Loaders.h"
  11. OPENMPT_NAMESPACE_BEGIN
  12. enum PatternFormats : uint32
  13. {
  14. DTM_PT_PATTERN_FORMAT = 0,
  15. DTM_204_PATTERN_FORMAT = MagicBE("2.04"),
  16. DTM_206_PATTERN_FORMAT = MagicBE("2.06"),
  17. };
  18. struct DTMFileHeader
  19. {
  20. char magic[4];
  21. uint32be headerSize;
  22. uint16be type; // 0 = module
  23. uint8be stereoMode; // FF = panoramic stereo, 00 = old stereo
  24. uint8be bitDepth; // Typically 8, sometimes 16, but is not actually used anywhere?
  25. uint16be reserved; // Usually 0, but not in unknown title 1.dtm and unknown title 2.dtm
  26. uint16be speed;
  27. uint16be tempo;
  28. uint32be forcedSampleRate; // Seems to be ignored in newer files
  29. };
  30. MPT_BINARY_STRUCT(DTMFileHeader, 22)
  31. // IFF-style Chunk
  32. struct DTMChunk
  33. {
  34. // 32-Bit chunk identifiers
  35. enum ChunkIdentifiers
  36. {
  37. idS_Q_ = MagicBE("S.Q."),
  38. idPATT = MagicBE("PATT"),
  39. idINST = MagicBE("INST"),
  40. idIENV = MagicBE("IENV"),
  41. idDAPT = MagicBE("DAPT"),
  42. idDAIT = MagicBE("DAIT"),
  43. idTEXT = MagicBE("TEXT"),
  44. idPATN = MagicBE("PATN"),
  45. idTRKN = MagicBE("TRKN"),
  46. idVERS = MagicBE("VERS"),
  47. idSV19 = MagicBE("SV19"),
  48. };
  49. uint32be id;
  50. uint32be length;
  51. size_t GetLength() const
  52. {
  53. return length;
  54. }
  55. ChunkIdentifiers GetID() const
  56. {
  57. return static_cast<ChunkIdentifiers>(id.get());
  58. }
  59. };
  60. MPT_BINARY_STRUCT(DTMChunk, 8)
  61. struct DTMSample
  62. {
  63. uint32be reserved; // 0x204 for first sample, 0x208 for second, etc...
  64. uint32be length; // in bytes
  65. uint8be finetune; // -8....7
  66. uint8be volume; // 0...64
  67. uint32be loopStart; // in bytes
  68. uint32be loopLength; // ditto
  69. char name[22];
  70. uint8be stereo;
  71. uint8be bitDepth;
  72. uint16be transpose;
  73. uint16be unknown;
  74. uint32be sampleRate;
  75. void ConvertToMPT(ModSample &mptSmp, uint32 forcedSampleRate, uint32 formatVersion) const
  76. {
  77. mptSmp.Initialize(MOD_TYPE_IT);
  78. mptSmp.nLength = length;
  79. mptSmp.nLoopStart = loopStart;
  80. mptSmp.nLoopEnd = mptSmp.nLoopStart + loopLength;
  81. // In revolution to come.dtm, the file header says samples rate is 24512 Hz, but samples say it's 50000 Hz
  82. // Digital Home Studio ignores the header setting in 2.04-/2.06-style modules
  83. mptSmp.nC5Speed = (formatVersion == DTM_PT_PATTERN_FORMAT && forcedSampleRate > 0) ? forcedSampleRate : sampleRate;
  84. int32 transposeAmount = MOD2XMFineTune(finetune);
  85. if(formatVersion == DTM_206_PATTERN_FORMAT && transpose > 0 && transpose != 48)
  86. {
  87. // Digital Home Studio applies this unconditionally, but some old songs sound wrong then (delirium.dtm).
  88. // Digital Tracker 2.03 ignores the setting.
  89. // Maybe this should not be applied for "real" Digital Tracker modules?
  90. transposeAmount += (48 - transpose) * 128;
  91. }
  92. mptSmp.Transpose(transposeAmount * (1.0 / (12.0 * 128.0)));
  93. mptSmp.nVolume = std::min(volume.get(), uint8(64)) * 4u;
  94. if(stereo & 1)
  95. {
  96. mptSmp.uFlags.set(CHN_STEREO);
  97. mptSmp.nLength /= 2u;
  98. mptSmp.nLoopStart /= 2u;
  99. mptSmp.nLoopEnd /= 2u;
  100. }
  101. if(bitDepth > 8)
  102. {
  103. mptSmp.uFlags.set(CHN_16BIT);
  104. mptSmp.nLength /= 2u;
  105. mptSmp.nLoopStart /= 2u;
  106. mptSmp.nLoopEnd /= 2u;
  107. }
  108. if(mptSmp.nLoopEnd > mptSmp.nLoopStart + 1)
  109. {
  110. mptSmp.uFlags.set(CHN_LOOP);
  111. } else
  112. {
  113. mptSmp.nLoopStart = mptSmp.nLoopEnd = 0;
  114. }
  115. }
  116. };
  117. MPT_BINARY_STRUCT(DTMSample, 50)
  118. struct DTMInstrument
  119. {
  120. uint16be insNum;
  121. uint8be unknown1;
  122. uint8be envelope; // 0xFF = none
  123. uint8be sustain; // 0xFF = no sustain point
  124. uint16be fadeout;
  125. uint8be vibRate;
  126. uint8be vibDepth;
  127. uint8be modulationRate;
  128. uint8be modulationDepth;
  129. uint8be breathRate;
  130. uint8be breathDepth;
  131. uint8be volumeRate;
  132. uint8be volumeDepth;
  133. };
  134. MPT_BINARY_STRUCT(DTMInstrument, 15)
  135. struct DTMEnvelope
  136. {
  137. struct DTMEnvPoint
  138. {
  139. uint8be value;
  140. uint8be tick;
  141. };
  142. uint16be numPoints;
  143. DTMEnvPoint points[16];
  144. };
  145. MPT_BINARY_STRUCT(DTMEnvelope::DTMEnvPoint, 2)
  146. MPT_BINARY_STRUCT(DTMEnvelope, 34)
  147. struct DTMText
  148. {
  149. uint16be textType; // 0 = pattern, 1 = free, 2 = song
  150. uint32be textLength;
  151. uint16be tabWidth;
  152. uint16be reserved;
  153. uint16be oddLength;
  154. };
  155. MPT_BINARY_STRUCT(DTMText, 12)
  156. static bool ValidateHeader(const DTMFileHeader &fileHeader)
  157. {
  158. if(std::memcmp(fileHeader.magic, "D.T.", 4)
  159. || fileHeader.headerSize < sizeof(fileHeader) - 8u
  160. || fileHeader.headerSize > 256 // Excessively long song title?
  161. || fileHeader.type != 0)
  162. {
  163. return false;
  164. }
  165. return true;
  166. }
  167. CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderDTM(MemoryFileReader file, const uint64 *pfilesize)
  168. {
  169. DTMFileHeader fileHeader;
  170. if(!file.ReadStruct(fileHeader))
  171. {
  172. return ProbeWantMoreData;
  173. }
  174. if(!ValidateHeader(fileHeader))
  175. {
  176. return ProbeFailure;
  177. }
  178. MPT_UNREFERENCED_PARAMETER(pfilesize);
  179. return ProbeSuccess;
  180. }
  181. bool CSoundFile::ReadDTM(FileReader &file, ModLoadingFlags loadFlags)
  182. {
  183. file.Rewind();
  184. DTMFileHeader fileHeader;
  185. if(!file.ReadStruct(fileHeader))
  186. {
  187. return false;
  188. }
  189. if(!ValidateHeader(fileHeader))
  190. {
  191. return false;
  192. }
  193. if(loadFlags == onlyVerifyHeader)
  194. {
  195. return true;
  196. }
  197. InitializeGlobals(MOD_TYPE_DTM);
  198. InitializeChannels();
  199. m_SongFlags.set(SONG_ITCOMPATGXX | SONG_ITOLDEFFECTS);
  200. m_playBehaviour.reset(kITVibratoTremoloPanbrello);
  201. // Various files have a default speed or tempo of 0
  202. if(fileHeader.tempo)
  203. m_nDefaultTempo.Set(fileHeader.tempo);
  204. if(fileHeader.speed)
  205. m_nDefaultSpeed = fileHeader.speed;
  206. if(fileHeader.stereoMode == 0)
  207. SetupMODPanning(true);
  208. file.ReadString<mpt::String::maybeNullTerminated>(m_songName, fileHeader.headerSize - (sizeof(fileHeader) - 8u));
  209. auto chunks = ChunkReader(file).ReadChunks<DTMChunk>(1);
  210. // Read order list
  211. if(FileReader chunk = chunks.GetChunk(DTMChunk::idS_Q_))
  212. {
  213. uint16 ordLen = chunk.ReadUint16BE();
  214. uint16 restartPos = chunk.ReadUint16BE();
  215. chunk.Skip(4); // Reserved
  216. ReadOrderFromFile<uint8>(Order(), chunk, ordLen);
  217. Order().SetRestartPos(restartPos);
  218. } else
  219. {
  220. return false;
  221. }
  222. // Read pattern properties
  223. uint32 patternFormat;
  224. if(FileReader chunk = chunks.GetChunk(DTMChunk::idPATT))
  225. {
  226. m_nChannels = chunk.ReadUint16BE();
  227. if(m_nChannels < 1 || m_nChannels > 32)
  228. {
  229. return false;
  230. }
  231. Patterns.ResizeArray(chunk.ReadUint16BE()); // Number of stored patterns, may be lower than highest pattern number
  232. patternFormat = chunk.ReadUint32BE();
  233. if(patternFormat != DTM_PT_PATTERN_FORMAT && patternFormat != DTM_204_PATTERN_FORMAT && patternFormat != DTM_206_PATTERN_FORMAT)
  234. {
  235. return false;
  236. }
  237. } else
  238. {
  239. return false;
  240. }
  241. // Read global info
  242. if(FileReader chunk = chunks.GetChunk(DTMChunk::idSV19))
  243. {
  244. chunk.Skip(2); // Ticks per quarter note, typically 24
  245. uint32 fractionalTempo = chunk.ReadUint32BE();
  246. m_nDefaultTempo = TEMPO(m_nDefaultTempo.GetInt() + fractionalTempo / 4294967296.0);
  247. uint16be panning[32];
  248. chunk.ReadArray(panning);
  249. for(CHANNELINDEX chn = 0; chn < 32 && chn < GetNumChannels(); chn++)
  250. {
  251. // Panning is in range 0...180, 90 = center
  252. ChnSettings[chn].nPan = static_cast<uint16>(128 + Util::muldivr(std::min(static_cast<int>(panning[chn]), int(180)) - 90, 128, 90));
  253. }
  254. chunk.Skip(16);
  255. // Chunk ends here for old DTM modules
  256. if(chunk.CanRead(2))
  257. {
  258. m_nDefaultGlobalVolume = std::min(chunk.ReadUint16BE(), static_cast<uint16>(MAX_GLOBAL_VOLUME));
  259. }
  260. chunk.Skip(128);
  261. uint16be volume[32];
  262. if(chunk.ReadArray(volume))
  263. {
  264. for(CHANNELINDEX chn = 0; chn < 32 && chn < GetNumChannels(); chn++)
  265. {
  266. // Volume is in range 0...128, 64 = normal
  267. ChnSettings[chn].nVolume = static_cast<uint8>(std::min(static_cast<int>(volume[chn]), int(128)) / 2);
  268. }
  269. m_nSamplePreAmp *= 2; // Compensate for channel volume range
  270. }
  271. }
  272. // Read song message
  273. if(FileReader chunk = chunks.GetChunk(DTMChunk::idTEXT))
  274. {
  275. DTMText text;
  276. chunk.ReadStruct(text);
  277. if(text.oddLength == 0xFFFF)
  278. {
  279. chunk.Skip(1);
  280. }
  281. m_songMessage.Read(chunk, chunk.BytesLeft(), SongMessage::leCRLF);
  282. }
  283. // Read sample headers
  284. if(FileReader chunk = chunks.GetChunk(DTMChunk::idINST))
  285. {
  286. uint16 numSamples = chunk.ReadUint16BE();
  287. bool newSamples = (numSamples >= 0x8000);
  288. numSamples &= 0x7FFF;
  289. if(numSamples >= MAX_SAMPLES || !chunk.CanRead(numSamples * (sizeof(DTMSample) + (newSamples ? 2u : 0u))))
  290. {
  291. return false;
  292. }
  293. m_nSamples = numSamples;
  294. for(SAMPLEINDEX smp = 1; smp <= numSamples; smp++)
  295. {
  296. SAMPLEINDEX realSample = newSamples ? (chunk.ReadUint16BE() + 1u) : smp;
  297. DTMSample dtmSample;
  298. chunk.ReadStruct(dtmSample);
  299. if(realSample < 1 || realSample >= MAX_SAMPLES)
  300. {
  301. continue;
  302. }
  303. m_nSamples = std::max(m_nSamples, realSample);
  304. ModSample &mptSmp = Samples[realSample];
  305. dtmSample.ConvertToMPT(mptSmp, fileHeader.forcedSampleRate, patternFormat);
  306. m_szNames[realSample] = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, dtmSample.name);
  307. }
  308. if(chunk.ReadUint16BE() == 0x0004)
  309. {
  310. // Digital Home Studio instruments
  311. m_nInstruments = std::min(static_cast<INSTRUMENTINDEX>(m_nSamples), static_cast<INSTRUMENTINDEX>(MAX_INSTRUMENTS - 1));
  312. FileReader envChunk = chunks.GetChunk(DTMChunk::idIENV);
  313. while(chunk.CanRead(sizeof(DTMInstrument)))
  314. {
  315. DTMInstrument instr;
  316. chunk.ReadStruct(instr);
  317. if(instr.insNum < GetNumInstruments())
  318. {
  319. ModSample &sample = Samples[instr.insNum + 1];
  320. sample.nVibDepth = instr.vibDepth;
  321. sample.nVibRate = instr.vibRate;
  322. sample.nVibSweep = 255;
  323. ModInstrument *mptIns = AllocateInstrument(instr.insNum + 1, instr.insNum + 1);
  324. if(mptIns != nullptr)
  325. {
  326. InstrumentEnvelope &mptEnv = mptIns->VolEnv;
  327. mptIns->nFadeOut = std::min(static_cast<uint16>(instr.fadeout), uint16(0xFFF));
  328. if(instr.envelope != 0xFF && envChunk.Seek(2 + sizeof(DTMEnvelope) * instr.envelope))
  329. {
  330. DTMEnvelope env;
  331. envChunk.ReadStruct(env);
  332. mptEnv.dwFlags.set(ENV_ENABLED);
  333. mptEnv.resize(std::min({ static_cast<std::size_t>(env.numPoints), std::size(env.points), static_cast<std::size_t>(MAX_ENVPOINTS) }));
  334. for(size_t i = 0; i < mptEnv.size(); i++)
  335. {
  336. mptEnv[i].value = std::min(uint8(64), static_cast<uint8>(env.points[i].value));
  337. mptEnv[i].tick = env.points[i].tick;
  338. }
  339. if(instr.sustain != 0xFF)
  340. {
  341. mptEnv.dwFlags.set(ENV_SUSTAIN);
  342. mptEnv.nSustainStart = mptEnv.nSustainEnd = instr.sustain;
  343. }
  344. if(!mptEnv.empty())
  345. {
  346. mptEnv.dwFlags.set(ENV_LOOP);
  347. mptEnv.nLoopStart = mptEnv.nLoopEnd = static_cast<uint8>(mptEnv.size() - 1);
  348. }
  349. }
  350. }
  351. }
  352. }
  353. }
  354. }
  355. // Read pattern data
  356. for(auto &chunk : chunks.GetAllChunks(DTMChunk::idDAPT))
  357. {
  358. chunk.Skip(4); // FF FF FF FF
  359. PATTERNINDEX patNum = chunk.ReadUint16BE();
  360. ROWINDEX numRows = chunk.ReadUint16BE();
  361. if(patternFormat == DTM_206_PATTERN_FORMAT)
  362. {
  363. // The stored data is actually not row-based, but tick-based.
  364. numRows /= m_nDefaultSpeed;
  365. }
  366. if(!(loadFlags & loadPatternData) || patNum > 255 || !Patterns.Insert(patNum, numRows))
  367. {
  368. continue;
  369. }
  370. if(patternFormat == DTM_206_PATTERN_FORMAT)
  371. {
  372. chunk.Skip(4);
  373. for(CHANNELINDEX chn = 0; chn < GetNumChannels(); chn++)
  374. {
  375. uint16 length = chunk.ReadUint16BE();
  376. if(length % 2u) length++;
  377. FileReader rowChunk = chunk.ReadChunk(length);
  378. int tick = 0;
  379. std::div_t position = { 0, 0 };
  380. while(rowChunk.CanRead(6) && static_cast<ROWINDEX>(position.quot) < numRows)
  381. {
  382. ModCommand *m = Patterns[patNum].GetpModCommand(position.quot, chn);
  383. const auto [note, volume, instr, command, param, delay] = rowChunk.ReadArray<uint8, 6>();
  384. if(note > 0 && note <= 96)
  385. {
  386. m->note = note + NOTE_MIN + 12;
  387. if(position.rem)
  388. {
  389. m->command = CMD_MODCMDEX;
  390. m->param = 0xD0 | static_cast<ModCommand::PARAM>(std::min(position.rem, 15));
  391. }
  392. } else if(note & 0x80)
  393. {
  394. // Lower 7 bits contain note, probably intended for MIDI-like note-on/note-off events
  395. if(position.rem)
  396. {
  397. m->command = CMD_MODCMDEX;
  398. m->param = 0xC0 | static_cast<ModCommand::PARAM>(std::min(position.rem, 15));
  399. } else
  400. {
  401. m->note = NOTE_NOTECUT;
  402. }
  403. }
  404. if(volume)
  405. {
  406. m->volcmd = VOLCMD_VOLUME;
  407. m->vol = std::min(volume, uint8(64)); // Volume can go up to 255, but we do not support over-amplification at the moment.
  408. }
  409. if(instr)
  410. {
  411. m->instr = instr;
  412. }
  413. if(command || param)
  414. {
  415. m->command = command;
  416. m->param = param;
  417. ConvertModCommand(*m);
  418. #ifdef MODPLUG_TRACKER
  419. m->Convert(MOD_TYPE_MOD, MOD_TYPE_IT, *this);
  420. #endif
  421. // G is 8-bit volume
  422. // P is tremor (need to disable oldfx)
  423. }
  424. if(delay & 0x80)
  425. tick += (delay & 0x7F) * 0x100 + rowChunk.ReadUint8();
  426. else
  427. tick += delay;
  428. position = std::div(tick, m_nDefaultSpeed);
  429. }
  430. }
  431. } else
  432. {
  433. ModCommand *m = Patterns[patNum].GetpModCommand(0, 0);
  434. for(ROWINDEX row = 0; row < numRows; row++)
  435. {
  436. for(CHANNELINDEX chn = 0; chn < GetNumChannels(); chn++, m++)
  437. {
  438. const auto data = chunk.ReadArray<uint8, 4>();
  439. if(patternFormat == DTM_204_PATTERN_FORMAT)
  440. {
  441. const auto [note, instrVol, instrCmd, param] = data;
  442. if(note > 0 && note < 0x80)
  443. {
  444. m->note = (note >> 4) * 12 + (note & 0x0F) + NOTE_MIN + 11;
  445. }
  446. uint8 vol = instrVol >> 2;
  447. if(vol)
  448. {
  449. m->volcmd = VOLCMD_VOLUME;
  450. m->vol = vol - 1u;
  451. }
  452. m->instr = ((instrVol & 0x03) << 4) | (instrCmd >> 4);
  453. m->command = instrCmd & 0x0F;
  454. m->param = param;
  455. } else
  456. {
  457. ReadMODPatternEntry(data, *m);
  458. m->instr |= data[0] & 0x30; // Allow more than 31 instruments
  459. }
  460. ConvertModCommand(*m);
  461. // Fix commands without memory and slide nibble precedence
  462. switch(m->command)
  463. {
  464. case CMD_PORTAMENTOUP:
  465. case CMD_PORTAMENTODOWN:
  466. if(!m->param)
  467. {
  468. m->command = CMD_NONE;
  469. }
  470. break;
  471. case CMD_VOLUMESLIDE:
  472. case CMD_TONEPORTAVOL:
  473. case CMD_VIBRATOVOL:
  474. if(m->param & 0xF0)
  475. {
  476. m->param &= 0xF0;
  477. } else if(!m->param)
  478. {
  479. m->command = CMD_NONE;
  480. }
  481. break;
  482. default:
  483. break;
  484. }
  485. #ifdef MODPLUG_TRACKER
  486. m->Convert(MOD_TYPE_MOD, MOD_TYPE_IT, *this);
  487. #endif
  488. }
  489. }
  490. }
  491. }
  492. // Read pattern names
  493. if(FileReader chunk = chunks.GetChunk(DTMChunk::idPATN))
  494. {
  495. PATTERNINDEX pat = 0;
  496. std::string name;
  497. while(chunk.CanRead(1) && pat < Patterns.Size())
  498. {
  499. chunk.ReadNullString(name, 32);
  500. Patterns[pat].SetName(name);
  501. pat++;
  502. }
  503. }
  504. // Read channel names
  505. if(FileReader chunk = chunks.GetChunk(DTMChunk::idTRKN))
  506. {
  507. CHANNELINDEX chn = 0;
  508. std::string name;
  509. while(chunk.CanRead(1) && chn < GetNumChannels())
  510. {
  511. chunk.ReadNullString(name, 32);
  512. ChnSettings[chn].szName = name;
  513. chn++;
  514. }
  515. }
  516. // Read sample data
  517. for(auto &chunk : chunks.GetAllChunks(DTMChunk::idDAIT))
  518. {
  519. SAMPLEINDEX smp = chunk.ReadUint16BE();
  520. if(smp >= GetNumSamples() || !(loadFlags & loadSampleData))
  521. {
  522. continue;
  523. }
  524. ModSample &mptSmp = Samples[smp + 1];
  525. SampleIO(
  526. mptSmp.uFlags[CHN_16BIT] ? SampleIO::_16bit : SampleIO::_8bit,
  527. mptSmp.uFlags[CHN_STEREO] ? SampleIO::stereoInterleaved: SampleIO::mono,
  528. SampleIO::bigEndian,
  529. SampleIO::signedPCM).ReadSample(mptSmp, chunk);
  530. }
  531. // Is this accurate?
  532. mpt::ustring tracker;
  533. if(patternFormat == DTM_206_PATTERN_FORMAT)
  534. {
  535. tracker = U_("Digital Home Studio");
  536. } else if(FileReader chunk = chunks.GetChunk(DTMChunk::idVERS))
  537. {
  538. uint32 version = chunk.ReadUint32BE();
  539. tracker = MPT_UFORMAT("Digital Tracker {}.{}")(version >> 4, version & 0x0F);
  540. } else
  541. {
  542. tracker = U_("Digital Tracker");
  543. }
  544. m_modFormat.formatName = U_("Digital Tracker");
  545. m_modFormat.type = U_("dtm");
  546. m_modFormat.madeWithTracker = std::move(tracker);
  547. m_modFormat.charset = mpt::Charset::Amiga_no_C1;
  548. return true;
  549. }
  550. OPENMPT_NAMESPACE_END