123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324 |
- /*
- * Load_mtm.cpp
- * ------------
- * Purpose: MTM (MultiTracker) 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
- // File Header
- struct MTMFileHeader
- {
- char id[3]; // MTM file marker
- uint8le version; // Tracker version
- char songName[20]; // ASCIIZ songname
- uint16le numTracks; // Number of tracks saved
- uint8le lastPattern; // Last pattern number saved
- uint8le lastOrder; // Last order number to play (songlength-1)
- uint16le commentSize; // Length of comment field
- uint8le numSamples; // Number of samples saved
- uint8le attribute; // Attribute byte (unused)
- uint8le beatsPerTrack; // Numbers of rows in every pattern (MultiTracker itself does not seem to support values != 64)
- uint8le numChannels; // Number of channels used
- uint8le panPos[32]; // Channel pan positions
- };
- MPT_BINARY_STRUCT(MTMFileHeader, 66)
- // Sample Header
- struct MTMSampleHeader
- {
- char samplename[22];
- uint32le length;
- uint32le loopStart;
- uint32le loopEnd;
- int8le finetune;
- uint8le volume;
- uint8le attribute;
- // Convert an MTM sample header to OpenMPT's internal sample header.
- void ConvertToMPT(ModSample &mptSmp) const
- {
- mptSmp.Initialize();
- mptSmp.nVolume = std::min(uint16(volume * 4), uint16(256));
- if(length > 2)
- {
- mptSmp.nLength = length;
- mptSmp.nLoopStart = loopStart;
- mptSmp.nLoopEnd = std::max(loopEnd.get(), uint32(1)) - 1;
- LimitMax(mptSmp.nLoopEnd, mptSmp.nLength);
- if(mptSmp.nLoopStart + 4 >= mptSmp.nLoopEnd)
- mptSmp.nLoopStart = mptSmp.nLoopEnd = 0;
- if(mptSmp.nLoopEnd > 2)
- mptSmp.uFlags.set(CHN_LOOP);
- 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
- mptSmp.nC5Speed = ModSample::TransposeToFrequency(0, finetune * 16);
- if(attribute & 0x01)
- {
- mptSmp.uFlags.set(CHN_16BIT);
- mptSmp.nLength /= 2;
- mptSmp.nLoopStart /= 2;
- mptSmp.nLoopEnd /= 2;
- }
- }
- }
- };
- MPT_BINARY_STRUCT(MTMSampleHeader, 37)
- static bool ValidateHeader(const MTMFileHeader &fileHeader)
- {
- if(std::memcmp(fileHeader.id, "MTM", 3)
- || fileHeader.version >= 0x20
- || fileHeader.lastOrder > 127
- || fileHeader.beatsPerTrack > 64
- || fileHeader.numChannels > 32
- || fileHeader.numChannels == 0
- )
- {
- return false;
- }
- return true;
- }
- static uint64 GetHeaderMinimumAdditionalSize(const MTMFileHeader &fileHeader)
- {
- return sizeof(MTMSampleHeader) * fileHeader.numSamples + 128 + 192 * fileHeader.numTracks + 64 * (fileHeader.lastPattern + 1) + fileHeader.commentSize;
- }
- CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderMTM(MemoryFileReader file, const uint64 *pfilesize)
- {
- MTMFileHeader fileHeader;
- if(!file.ReadStruct(fileHeader))
- {
- return ProbeWantMoreData;
- }
- if(!ValidateHeader(fileHeader))
- {
- return ProbeFailure;
- }
- return ProbeAdditionalSize(file, pfilesize, GetHeaderMinimumAdditionalSize(fileHeader));
- }
- bool CSoundFile::ReadMTM(FileReader &file, ModLoadingFlags loadFlags)
- {
- file.Rewind();
- MTMFileHeader fileHeader;
- if(!file.ReadStruct(fileHeader))
- {
- return false;
- }
- if(!ValidateHeader(fileHeader))
- {
- return false;
- }
- if(!file.CanRead(mpt::saturate_cast<FileReader::off_t>(GetHeaderMinimumAdditionalSize(fileHeader))))
- {
- return false;
- }
- if(loadFlags == onlyVerifyHeader)
- {
- return true;
- }
- InitializeGlobals(MOD_TYPE_MTM);
- m_songName = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, fileHeader.songName);
- m_nSamples = fileHeader.numSamples;
- m_nChannels = fileHeader.numChannels;
-
- m_modFormat.formatName = U_("MultiTracker");
- m_modFormat.type = U_("mtm");
- m_modFormat.madeWithTracker = MPT_UFORMAT("MultiTracker {}.{}")(fileHeader.version >> 4, fileHeader.version & 0x0F);
- m_modFormat.charset = mpt::Charset::CP437;
- // Reading instruments
- for(SAMPLEINDEX smp = 1; smp <= GetNumSamples(); smp++)
- {
- MTMSampleHeader sampleHeader;
- file.ReadStruct(sampleHeader);
- sampleHeader.ConvertToMPT(Samples[smp]);
- m_szNames[smp] = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, sampleHeader.samplename);
- }
- // Setting Channel Pan Position
- for(CHANNELINDEX chn = 0; chn < GetNumChannels(); chn++)
- {
- ChnSettings[chn].Reset();
- ChnSettings[chn].nPan = ((fileHeader.panPos[chn] & 0x0F) << 4) + 8;
- }
- // Reading pattern order
- uint8 orders[128];
- file.ReadArray(orders);
- ReadOrderFromArray(Order(), orders, fileHeader.lastOrder + 1, 0xFF, 0xFE);
- // Reading Patterns
- const ROWINDEX rowsPerPat = fileHeader.beatsPerTrack ? fileHeader.beatsPerTrack : 64;
- FileReader tracks = file.ReadChunk(192 * fileHeader.numTracks);
- if(loadFlags & loadPatternData)
- Patterns.ResizeArray(fileHeader.lastPattern + 1);
- bool hasSpeed = false, hasTempo = false;
- for(PATTERNINDEX pat = 0; pat <= fileHeader.lastPattern; pat++)
- {
- if(!(loadFlags & loadPatternData) || !Patterns.Insert(pat, rowsPerPat))
- {
- file.Skip(64);
- continue;
- }
- for(CHANNELINDEX chn = 0; chn < 32; chn++)
- {
- uint16 track = file.ReadUint16LE();
- if(track == 0 || track > fileHeader.numTracks || chn >= GetNumChannels())
- {
- continue;
- }
- tracks.Seek(192 * (track - 1));
- ModCommand *m = Patterns[pat].GetpModCommand(0, chn);
- for(ROWINDEX row = 0; row < rowsPerPat; row++, m += GetNumChannels())
- {
- const auto [noteInstr, instrCmd, par] = tracks.ReadArray<uint8, 3>();
- if(noteInstr & 0xFC)
- m->note = (noteInstr >> 2) + 36 + NOTE_MIN;
- m->instr = ((noteInstr & 0x03) << 4) | (instrCmd >> 4);
- uint8 cmd = instrCmd & 0x0F;
- uint8 param = par;
- if(cmd == 0x0A)
- {
- if(param & 0xF0) param &= 0xF0; else param &= 0x0F;
- } else if(cmd == 0x08)
- {
- // No 8xx panning in MultiTracker, only E8x
- cmd = param = 0;
- } else if(cmd == 0x0E)
- {
- // MultiTracker does not support these commands
- switch(param & 0xF0)
- {
- case 0x00:
- case 0x30:
- case 0x40:
- case 0x60:
- case 0x70:
- case 0xF0:
- cmd = param = 0;
- break;
- }
- }
- if(cmd != 0 || param != 0)
- {
- m->command = cmd;
- m->param = param;
- ConvertModCommand(*m);
- #ifdef MODPLUG_TRACKER
- m->Convert(MOD_TYPE_MTM, MOD_TYPE_S3M, *this);
- #endif
- if(m->command == CMD_SPEED)
- hasSpeed = true;
- else if(m->command == CMD_TEMPO)
- hasTempo = true;
- }
- }
- }
- }
- // Curiously, speed commands reset the tempo to 125 in MultiTracker, and tempo commands reset the speed to 6.
- // External players of the time (e.g. DMP) did not implement this quirk and assumed a more ProTracker-like interpretation of speed and tempo.
- // Quite a few musicians created MTMs that make use DMP's speed and tempo interpretation, which in return means that they will play too
- // 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.
- // 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
- // commands are found on the same line, and a MultiTracker-like interpretation when they are never found on the same line.
- if(hasSpeed && hasTempo)
- {
- bool hasSpeedAndTempoOnSameRow = false;
- for(const auto &pattern : Patterns)
- {
- for(ROWINDEX row = 0; row < pattern.GetNumRows(); row++)
- {
- const auto rowBase = pattern.GetRow(row);
- bool hasSpeedOnRow = false, hasTempoOnRow = false;
- for(CHANNELINDEX chn = 0; chn < GetNumChannels(); chn++)
- {
- if(rowBase[chn].command == CMD_SPEED)
- hasSpeedOnRow = true;
- else if(rowBase[chn].command == CMD_TEMPO)
- hasTempoOnRow = true;
- }
- if(hasSpeedOnRow && hasTempoOnRow)
- {
- hasSpeedAndTempoOnSameRow = true;
- break;
- }
- }
- if(hasSpeedAndTempoOnSameRow)
- break;
- }
- if(!hasSpeedAndTempoOnSameRow)
- {
- for(auto &pattern : Patterns)
- {
- for(ROWINDEX row = 0; row < pattern.GetNumRows(); row++)
- {
- const auto rowBase = pattern.GetRow(row);
- for(CHANNELINDEX chn = 0; chn < GetNumChannels(); chn++)
- {
- if(rowBase[chn].command == CMD_SPEED || rowBase[chn].command == CMD_TEMPO)
- {
- const bool writeTempo = rowBase[chn].command == CMD_SPEED;
- pattern.WriteEffect(EffectWriter(writeTempo ? CMD_TEMPO : CMD_SPEED, writeTempo ? 125 : 6).Row(row));
- break;
- }
- }
- }
- }
- }
- }
- if(fileHeader.commentSize != 0)
- {
- // Read message with a fixed line length of 40 characters
- // (actually the last character is always null, so make that 39 + 1 padding byte)
- m_songMessage.ReadFixedLineLength(file, fileHeader.commentSize, 39, 1);
- }
- // Reading Samples
- if(loadFlags & loadSampleData)
- {
- for(SAMPLEINDEX smp = 1; smp <= GetNumSamples(); smp++)
- {
- SampleIO(
- Samples[smp].uFlags[CHN_16BIT] ? SampleIO::_16bit : SampleIO::_8bit,
- SampleIO::mono,
- SampleIO::littleEndian,
- SampleIO::unsignedPCM)
- .ReadSample(Samples[smp], file);
- }
- }
- m_nMinPeriod = 64;
- m_nMaxPeriod = 32767;
- return true;
- }
- OPENMPT_NAMESPACE_END
|