1
0

Load_mtm.cpp 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. /*
  2. * Load_mtm.cpp
  3. * ------------
  4. * Purpose: MTM (MultiTracker) module loader
  5. * Notes : (currently none)
  6. * Authors: Olivier Lapicque
  7. * OpenMPT Devs
  8. * The OpenMPT source code is released under the BSD license. Read LICENSE for more details.
  9. */
  10. #include "stdafx.h"
  11. #include "Loaders.h"
  12. OPENMPT_NAMESPACE_BEGIN
  13. // File Header
  14. struct MTMFileHeader
  15. {
  16. char id[3]; // MTM file marker
  17. uint8le version; // Tracker version
  18. char songName[20]; // ASCIIZ songname
  19. uint16le numTracks; // Number of tracks saved
  20. uint8le lastPattern; // Last pattern number saved
  21. uint8le lastOrder; // Last order number to play (songlength-1)
  22. uint16le commentSize; // Length of comment field
  23. uint8le numSamples; // Number of samples saved
  24. uint8le attribute; // Attribute byte (unused)
  25. uint8le beatsPerTrack; // Numbers of rows in every pattern (MultiTracker itself does not seem to support values != 64)
  26. uint8le numChannels; // Number of channels used
  27. uint8le panPos[32]; // Channel pan positions
  28. };
  29. MPT_BINARY_STRUCT(MTMFileHeader, 66)
  30. // Sample Header
  31. struct MTMSampleHeader
  32. {
  33. char samplename[22];
  34. uint32le length;
  35. uint32le loopStart;
  36. uint32le loopEnd;
  37. int8le finetune;
  38. uint8le volume;
  39. uint8le attribute;
  40. // Convert an MTM sample header to OpenMPT's internal sample header.
  41. void ConvertToMPT(ModSample &mptSmp) const
  42. {
  43. mptSmp.Initialize();
  44. mptSmp.nVolume = std::min(uint16(volume * 4), uint16(256));
  45. if(length > 2)
  46. {
  47. mptSmp.nLength = length;
  48. mptSmp.nLoopStart = loopStart;
  49. mptSmp.nLoopEnd = std::max(loopEnd.get(), uint32(1)) - 1;
  50. LimitMax(mptSmp.nLoopEnd, mptSmp.nLength);
  51. if(mptSmp.nLoopStart + 4 >= mptSmp.nLoopEnd)
  52. mptSmp.nLoopStart = mptSmp.nLoopEnd = 0;
  53. if(mptSmp.nLoopEnd > 2)
  54. mptSmp.uFlags.set(CHN_LOOP);
  55. mptSmp.nFineTune = finetune; // Uses MOD units but allows the full int8 range rather than just -8...+7 so we keep the value as-is and convert it during playback
  56. mptSmp.nC5Speed = ModSample::TransposeToFrequency(0, finetune * 16);
  57. if(attribute & 0x01)
  58. {
  59. mptSmp.uFlags.set(CHN_16BIT);
  60. mptSmp.nLength /= 2;
  61. mptSmp.nLoopStart /= 2;
  62. mptSmp.nLoopEnd /= 2;
  63. }
  64. }
  65. }
  66. };
  67. MPT_BINARY_STRUCT(MTMSampleHeader, 37)
  68. static bool ValidateHeader(const MTMFileHeader &fileHeader)
  69. {
  70. if(std::memcmp(fileHeader.id, "MTM", 3)
  71. || fileHeader.version >= 0x20
  72. || fileHeader.lastOrder > 127
  73. || fileHeader.beatsPerTrack > 64
  74. || fileHeader.numChannels > 32
  75. || fileHeader.numChannels == 0
  76. )
  77. {
  78. return false;
  79. }
  80. return true;
  81. }
  82. static uint64 GetHeaderMinimumAdditionalSize(const MTMFileHeader &fileHeader)
  83. {
  84. return sizeof(MTMSampleHeader) * fileHeader.numSamples + 128 + 192 * fileHeader.numTracks + 64 * (fileHeader.lastPattern + 1) + fileHeader.commentSize;
  85. }
  86. CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderMTM(MemoryFileReader file, const uint64 *pfilesize)
  87. {
  88. MTMFileHeader fileHeader;
  89. if(!file.ReadStruct(fileHeader))
  90. {
  91. return ProbeWantMoreData;
  92. }
  93. if(!ValidateHeader(fileHeader))
  94. {
  95. return ProbeFailure;
  96. }
  97. return ProbeAdditionalSize(file, pfilesize, GetHeaderMinimumAdditionalSize(fileHeader));
  98. }
  99. bool CSoundFile::ReadMTM(FileReader &file, ModLoadingFlags loadFlags)
  100. {
  101. file.Rewind();
  102. MTMFileHeader fileHeader;
  103. if(!file.ReadStruct(fileHeader))
  104. {
  105. return false;
  106. }
  107. if(!ValidateHeader(fileHeader))
  108. {
  109. return false;
  110. }
  111. if(!file.CanRead(mpt::saturate_cast<FileReader::off_t>(GetHeaderMinimumAdditionalSize(fileHeader))))
  112. {
  113. return false;
  114. }
  115. if(loadFlags == onlyVerifyHeader)
  116. {
  117. return true;
  118. }
  119. InitializeGlobals(MOD_TYPE_MTM);
  120. m_songName = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, fileHeader.songName);
  121. m_nSamples = fileHeader.numSamples;
  122. m_nChannels = fileHeader.numChannels;
  123. m_modFormat.formatName = U_("MultiTracker");
  124. m_modFormat.type = U_("mtm");
  125. m_modFormat.madeWithTracker = MPT_UFORMAT("MultiTracker {}.{}")(fileHeader.version >> 4, fileHeader.version & 0x0F);
  126. m_modFormat.charset = mpt::Charset::CP437;
  127. // Reading instruments
  128. for(SAMPLEINDEX smp = 1; smp <= GetNumSamples(); smp++)
  129. {
  130. MTMSampleHeader sampleHeader;
  131. file.ReadStruct(sampleHeader);
  132. sampleHeader.ConvertToMPT(Samples[smp]);
  133. m_szNames[smp] = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, sampleHeader.samplename);
  134. }
  135. // Setting Channel Pan Position
  136. for(CHANNELINDEX chn = 0; chn < GetNumChannels(); chn++)
  137. {
  138. ChnSettings[chn].Reset();
  139. ChnSettings[chn].nPan = ((fileHeader.panPos[chn] & 0x0F) << 4) + 8;
  140. }
  141. // Reading pattern order
  142. uint8 orders[128];
  143. file.ReadArray(orders);
  144. ReadOrderFromArray(Order(), orders, fileHeader.lastOrder + 1, 0xFF, 0xFE);
  145. // Reading Patterns
  146. const ROWINDEX rowsPerPat = fileHeader.beatsPerTrack ? fileHeader.beatsPerTrack : 64;
  147. FileReader tracks = file.ReadChunk(192 * fileHeader.numTracks);
  148. if(loadFlags & loadPatternData)
  149. Patterns.ResizeArray(fileHeader.lastPattern + 1);
  150. bool hasSpeed = false, hasTempo = false;
  151. for(PATTERNINDEX pat = 0; pat <= fileHeader.lastPattern; pat++)
  152. {
  153. if(!(loadFlags & loadPatternData) || !Patterns.Insert(pat, rowsPerPat))
  154. {
  155. file.Skip(64);
  156. continue;
  157. }
  158. for(CHANNELINDEX chn = 0; chn < 32; chn++)
  159. {
  160. uint16 track = file.ReadUint16LE();
  161. if(track == 0 || track > fileHeader.numTracks || chn >= GetNumChannels())
  162. {
  163. continue;
  164. }
  165. tracks.Seek(192 * (track - 1));
  166. ModCommand *m = Patterns[pat].GetpModCommand(0, chn);
  167. for(ROWINDEX row = 0; row < rowsPerPat; row++, m += GetNumChannels())
  168. {
  169. const auto [noteInstr, instrCmd, par] = tracks.ReadArray<uint8, 3>();
  170. if(noteInstr & 0xFC)
  171. m->note = (noteInstr >> 2) + 36 + NOTE_MIN;
  172. m->instr = ((noteInstr & 0x03) << 4) | (instrCmd >> 4);
  173. uint8 cmd = instrCmd & 0x0F;
  174. uint8 param = par;
  175. if(cmd == 0x0A)
  176. {
  177. if(param & 0xF0) param &= 0xF0; else param &= 0x0F;
  178. } else if(cmd == 0x08)
  179. {
  180. // No 8xx panning in MultiTracker, only E8x
  181. cmd = param = 0;
  182. } else if(cmd == 0x0E)
  183. {
  184. // MultiTracker does not support these commands
  185. switch(param & 0xF0)
  186. {
  187. case 0x00:
  188. case 0x30:
  189. case 0x40:
  190. case 0x60:
  191. case 0x70:
  192. case 0xF0:
  193. cmd = param = 0;
  194. break;
  195. }
  196. }
  197. if(cmd != 0 || param != 0)
  198. {
  199. m->command = cmd;
  200. m->param = param;
  201. ConvertModCommand(*m);
  202. #ifdef MODPLUG_TRACKER
  203. m->Convert(MOD_TYPE_MTM, MOD_TYPE_S3M, *this);
  204. #endif
  205. if(m->command == CMD_SPEED)
  206. hasSpeed = true;
  207. else if(m->command == CMD_TEMPO)
  208. hasTempo = true;
  209. }
  210. }
  211. }
  212. }
  213. // Curiously, speed commands reset the tempo to 125 in MultiTracker, and tempo commands reset the speed to 6.
  214. // External players of the time (e.g. DMP) did not implement this quirk and assumed a more ProTracker-like interpretation of speed and tempo.
  215. // Quite a few musicians created MTMs that make use DMP's speed and tempo interpretation, which in return means that they will play too
  216. // fast or too slow in MultiTracker. On the other hand there are also a few MTMs that break when using ProTracker-like speed and tempo.
  217. // As a way to support as many modules of both types as possible, we will assume a ProTracker-like interpretation if both speed and tempo
  218. // commands are found on the same line, and a MultiTracker-like interpretation when they are never found on the same line.
  219. if(hasSpeed && hasTempo)
  220. {
  221. bool hasSpeedAndTempoOnSameRow = false;
  222. for(const auto &pattern : Patterns)
  223. {
  224. for(ROWINDEX row = 0; row < pattern.GetNumRows(); row++)
  225. {
  226. const auto rowBase = pattern.GetRow(row);
  227. bool hasSpeedOnRow = false, hasTempoOnRow = false;
  228. for(CHANNELINDEX chn = 0; chn < GetNumChannels(); chn++)
  229. {
  230. if(rowBase[chn].command == CMD_SPEED)
  231. hasSpeedOnRow = true;
  232. else if(rowBase[chn].command == CMD_TEMPO)
  233. hasTempoOnRow = true;
  234. }
  235. if(hasSpeedOnRow && hasTempoOnRow)
  236. {
  237. hasSpeedAndTempoOnSameRow = true;
  238. break;
  239. }
  240. }
  241. if(hasSpeedAndTempoOnSameRow)
  242. break;
  243. }
  244. if(!hasSpeedAndTempoOnSameRow)
  245. {
  246. for(auto &pattern : Patterns)
  247. {
  248. for(ROWINDEX row = 0; row < pattern.GetNumRows(); row++)
  249. {
  250. const auto rowBase = pattern.GetRow(row);
  251. for(CHANNELINDEX chn = 0; chn < GetNumChannels(); chn++)
  252. {
  253. if(rowBase[chn].command == CMD_SPEED || rowBase[chn].command == CMD_TEMPO)
  254. {
  255. const bool writeTempo = rowBase[chn].command == CMD_SPEED;
  256. pattern.WriteEffect(EffectWriter(writeTempo ? CMD_TEMPO : CMD_SPEED, writeTempo ? 125 : 6).Row(row));
  257. break;
  258. }
  259. }
  260. }
  261. }
  262. }
  263. }
  264. if(fileHeader.commentSize != 0)
  265. {
  266. // Read message with a fixed line length of 40 characters
  267. // (actually the last character is always null, so make that 39 + 1 padding byte)
  268. m_songMessage.ReadFixedLineLength(file, fileHeader.commentSize, 39, 1);
  269. }
  270. // Reading Samples
  271. if(loadFlags & loadSampleData)
  272. {
  273. for(SAMPLEINDEX smp = 1; smp <= GetNumSamples(); smp++)
  274. {
  275. SampleIO(
  276. Samples[smp].uFlags[CHN_16BIT] ? SampleIO::_16bit : SampleIO::_8bit,
  277. SampleIO::mono,
  278. SampleIO::littleEndian,
  279. SampleIO::unsignedPCM)
  280. .ReadSample(Samples[smp], file);
  281. }
  282. }
  283. m_nMinPeriod = 64;
  284. m_nMaxPeriod = 32767;
  285. return true;
  286. }
  287. OPENMPT_NAMESPACE_END