1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318
|
/*
* 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::pos_type>(GetHeaderMinimumAdditionalSize(fileHeader))))
{
return false;
}
if(loadFlags == onlyVerifyHeader)
{
return true;
}
InitializeGlobals(MOD_TYPE_MTM, fileHeader.numChannels);
m_songName = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, fileHeader.songName);
m_nSamples = fileHeader.numSamples;
m_modFormat.formatName = UL_("MultiTracker");
m_modFormat.type = UL_("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].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)
{
ConvertModCommand(*m, cmd, param);
#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++)
{
bool hasSpeedOnRow = false, hasTempoOnRow = false;
for(const ModCommand &m : pattern.GetRow(row))
{
if(m.command == CMD_SPEED)
hasSpeedOnRow = true;
else if(m.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++)
{
for(const ModCommand &m : pattern.GetRow(row))
{
if(m.command == CMD_SPEED || m.command == CMD_TEMPO)
{
const bool writeTempo = m.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
|