cog/Frameworks/OpenMPT/OpenMPT/soundlib/Load_mtm.cpp

325 lines
9.0 KiB
C++

/*
* 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