Cog/Frameworks/OpenMPT/OpenMPT/soundlib/Load_mod.cpp
2019-06-30 15:45:32 -07:00

2395 lines
71 KiB
C++

/*
* Load_mod.cpp
* ------------
* Purpose: MOD / NST (ProTracker / NoiseTracker), M15 / STK (Ultimate Soundtracker / Soundtracker) and ST26 (SoundTracker 2.6 / Ice Tracker) module loader / saver
* Notes : "2000 LOC for processing MOD files?!" you say? Well, this file also contains loaders for some formats that are almost identical to MOD, and extensive
* heuristics for more or less broken MOD files and files saved with tons of different trackers, to allow for the most optimal playback.
* 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"
#include "Tables.h"
#ifndef MODPLUG_NO_FILESAVE
#include "../common/mptFileIO.h"
#endif
#ifdef MPT_EXTERNAL_SAMPLES
// For loading external data in Startrekker files
#include "../common/mptPathString.h"
#endif // MPT_EXTERNAL_SAMPLES
OPENMPT_NAMESPACE_BEGIN
void CSoundFile::ConvertModCommand(ModCommand &m)
{
switch(m.command)
{
case 0x00: if(m.param) m.command = CMD_ARPEGGIO; break;
case 0x01: m.command = CMD_PORTAMENTOUP; break;
case 0x02: m.command = CMD_PORTAMENTODOWN; break;
case 0x03: m.command = CMD_TONEPORTAMENTO; break;
case 0x04: m.command = CMD_VIBRATO; break;
case 0x05: m.command = CMD_TONEPORTAVOL; break;
case 0x06: m.command = CMD_VIBRATOVOL; break;
case 0x07: m.command = CMD_TREMOLO; break;
case 0x08: m.command = CMD_PANNING8; break;
case 0x09: m.command = CMD_OFFSET; break;
case 0x0A: m.command = CMD_VOLUMESLIDE; break;
case 0x0B: m.command = CMD_POSITIONJUMP; break;
case 0x0C: m.command = CMD_VOLUME; break;
case 0x0D: m.command = CMD_PATTERNBREAK; m.param = ((m.param >> 4) * 10) + (m.param & 0x0F); break;
case 0x0E: m.command = CMD_MODCMDEX; break;
case 0x0F:
// For a very long time, this code imported 0x20 as CMD_SPEED for MOD files, but this seems to contradict
// pretty much the majority of other MOD player out there.
// 0x20 is Speed: Impulse Tracker, Scream Tracker, old ModPlug
// 0x20 is Tempo: ProTracker, XMPlay, Imago Orpheus, Cubic Player, ChibiTracker, BeRoTracker, DigiTrakker, DigiTrekker, Disorder Tracker 2, DMP, Extreme's Tracker, ...
if(m.param < 0x20)
m.command = CMD_SPEED;
else
m.command = CMD_TEMPO;
break;
// Extension for XM extended effects
case 'G' - 55: m.command = CMD_GLOBALVOLUME; break; //16
case 'H' - 55: m.command = CMD_GLOBALVOLSLIDE; break;
case 'K' - 55: m.command = CMD_KEYOFF; break;
case 'L' - 55: m.command = CMD_SETENVPOSITION; break;
case 'P' - 55: m.command = CMD_PANNINGSLIDE; break;
case 'R' - 55: m.command = CMD_RETRIG; break;
case 'T' - 55: m.command = CMD_TREMOR; break;
case 'X' - 55: m.command = CMD_XFINEPORTAUPDOWN; break;
case 'Y' - 55: m.command = CMD_PANBRELLO; break; //34
case 'Z' - 55: m.command = CMD_MIDI; break; //35
case '\\' - 56: m.command = CMD_SMOOTHMIDI; break; //rewbs.smoothVST: 36 - note: this is actually displayed as "-" in FT2, but seems to be doing nothing.
//case ':' - 21: m.command = CMD_DELAYCUT; break; //37
case '#' + 3: m.command = CMD_XPARAM; break; //rewbs.XMfixes - Xm.param is 38
default: m.command = CMD_NONE;
}
}
#ifndef MODPLUG_NO_FILESAVE
void CSoundFile::ModSaveCommand(uint8 &command, uint8 &param, bool toXM, bool compatibilityExport) const
{
switch(command)
{
case CMD_NONE: command = param = 0; break;
case CMD_ARPEGGIO: command = 0; break;
case CMD_PORTAMENTOUP:
if (GetType() & (MOD_TYPE_S3M|MOD_TYPE_IT|MOD_TYPE_STM|MOD_TYPE_MPT))
{
if ((param & 0xF0) == 0xE0) { command = 0x0E; param = ((param & 0x0F) >> 2) | 0x10; break; }
else if ((param & 0xF0) == 0xF0) { command = 0x0E; param &= 0x0F; param |= 0x10; break; }
}
command = 0x01;
break;
case CMD_PORTAMENTODOWN:
if(GetType() & (MOD_TYPE_S3M|MOD_TYPE_IT|MOD_TYPE_STM|MOD_TYPE_MPT))
{
if ((param & 0xF0) == 0xE0) { command = 0x0E; param= ((param & 0x0F) >> 2) | 0x20; break; }
else if ((param & 0xF0) == 0xF0) { command = 0x0E; param &= 0x0F; param |= 0x20; break; }
}
command = 0x02;
break;
case CMD_TONEPORTAMENTO: command = 0x03; break;
case CMD_VIBRATO: command = 0x04; break;
case CMD_TONEPORTAVOL: command = 0x05; break;
case CMD_VIBRATOVOL: command = 0x06; break;
case CMD_TREMOLO: command = 0x07; break;
case CMD_PANNING8:
command = 0x08;
if(GetType() & MOD_TYPE_S3M)
{
if(param <= 0x80)
{
param = MIN(param << 1, 0xFF);
}
else if(param == 0xA4) // surround
{
if(compatibilityExport || !toXM)
{
command = param = 0;
}
else
{
command = 'X' - 55;
param = 91;
}
}
}
break;
case CMD_OFFSET: command = 0x09; break;
case CMD_VOLUMESLIDE: command = 0x0A; break;
case CMD_POSITIONJUMP: command = 0x0B; break;
case CMD_VOLUME: command = 0x0C; break;
case CMD_PATTERNBREAK: command = 0x0D; param = ((param / 10) << 4) | (param % 10); break;
case CMD_MODCMDEX: command = 0x0E; break;
case CMD_SPEED: command = 0x0F; param = std::min<uint8>(param, 0x1F); break;
case CMD_TEMPO: command = 0x0F; param = std::max<uint8>(param, 0x20); break;
case CMD_GLOBALVOLUME: command = 'G' - 55; break;
case CMD_GLOBALVOLSLIDE: command = 'H' - 55; break;
case CMD_KEYOFF: command = 'K' - 55; break;
case CMD_SETENVPOSITION: command = 'L' - 55; break;
case CMD_PANNINGSLIDE: command = 'P' - 55; break;
case CMD_RETRIG: command = 'R' - 55; break;
case CMD_TREMOR: command = 'T' - 55; break;
case CMD_XFINEPORTAUPDOWN: command = 'X' - 55;
if(compatibilityExport && param >= 0x30) // X1x and X2x are legit, everything above are MPT extensions, which don't belong here.
param = 0; // Don't set command to 0 to indicate that there *was* some X command here...
break;
case CMD_PANBRELLO:
if(compatibilityExport)
command = param = 0;
else
command = 'Y' - 55;
break;
case CMD_MIDI:
if(compatibilityExport)
command = param = 0;
else
command = 'Z' - 55;
break;
case CMD_SMOOTHMIDI: //rewbs.smoothVST: 36
if(compatibilityExport)
command = param = 0;
else
command = '\\' - 56;
break;
case CMD_XPARAM: //rewbs.XMfixes - XParam is 38
if(compatibilityExport)
command = param = 0;
else
command = '#' + 3;
break;
case CMD_S3MCMDEX:
switch(param & 0xF0)
{
case 0x10: command = 0x0E; param = (param & 0x0F) | 0x30; break;
case 0x20: command = 0x0E; param = (param & 0x0F) | 0x50; break;
case 0x30: command = 0x0E; param = (param & 0x0F) | 0x40; break;
case 0x40: command = 0x0E; param = (param & 0x0F) | 0x70; break;
case 0x90:
if(compatibilityExport)
command = param = 0;
else
command = 'X' - 55;
break;
case 0xB0: command = 0x0E; param = (param & 0x0F) | 0x60; break;
case 0xA0:
case 0x50:
case 0x70:
case 0x60: command = param = 0; break;
default: command = 0x0E; break;
}
break;
default:
command = param = 0;
}
// Don't even think about saving XM effects in MODs...
if(command > 0x0F && !toXM)
{
command = param = 0;
}
}
#endif // MODPLUG_NO_FILESAVE
// File Header
struct MODFileHeader
{
uint8be numOrders;
uint8be restartPos;
uint8be orderList[128];
};
MPT_BINARY_STRUCT(MODFileHeader, 130)
// Sample Header
struct MODSampleHeader
{
char name[22];
uint16be length;
uint8be finetune;
uint8be volume;
uint16be loopStart;
uint16be loopLength;
// Convert an MOD sample header to OpenMPT's internal sample header.
void ConvertToMPT(ModSample &mptSmp, bool is4Chn) const
{
mptSmp.Initialize(MOD_TYPE_MOD);
mptSmp.nLength = length * 2;
mptSmp.nFineTune = MOD2XMFineTune(finetune & 0x0F);
mptSmp.nVolume = 4u * std::min<uint8>(volume, 64);
SmpLength lStart = loopStart * 2;
SmpLength lLength = loopLength * 2;
// See if loop start is incorrect as words, but correct as bytes (like in Soundtracker modules)
if(lLength > 2 && (lStart + lLength > mptSmp.nLength)
&& (lStart / 2 + lLength <= mptSmp.nLength))
{
lStart /= 2;
}
if(mptSmp.nLength == 2)
{
mptSmp.nLength = 0;
}
if(mptSmp.nLength)
{
mptSmp.nLoopStart = lStart;
mptSmp.nLoopEnd = lStart + lLength;
if(mptSmp.nLoopStart >= mptSmp.nLength)
{
mptSmp.nLoopStart = mptSmp.nLength - 1;
}
if(mptSmp.nLoopStart > mptSmp.nLoopEnd || mptSmp.nLoopEnd < 4 || mptSmp.nLoopEnd - mptSmp.nLoopStart < 4)
{
mptSmp.nLoopStart = 0;
mptSmp.nLoopEnd = 0;
}
// Fix for most likely broken sample loops. This fixes super_sufm_-_new_life.mod (M.K.) which has a long sample which is looped from 0 to 4.
// This module also has notes outside of the Amiga frequency range, so we cannot say that it should be played using ProTracker one-shot loops.
// On the other hand, "Crew Generation" by Necros (6CHN) has a sample with a similar loop, which is supposed to be played.
// To be able to correctly play both modules, we will draw a somewhat arbitrary line here and trust the loop points in MODs with more than
// 4 channels, even if they are tiny and at the very beginning of the sample.
if(mptSmp.nLoopEnd <= 8 && mptSmp.nLoopStart == 0 && mptSmp.nLength > mptSmp.nLoopEnd && is4Chn)
{
mptSmp.nLoopEnd = 0;
}
if(mptSmp.nLoopEnd > mptSmp.nLoopStart)
{
mptSmp.uFlags.set(CHN_LOOP);
}
}
}
// Convert OpenMPT's internal sample header to a MOD sample header.
SmpLength ConvertToMOD(const ModSample &mptSmp)
{
SmpLength writeLength = mptSmp.HasSampleData() ? mptSmp.nLength : 0;
// If the sample size is odd, we have to add a padding byte, as all sample sizes in MODs are even.
if((writeLength % 2u) != 0)
{
writeLength++;
}
LimitMax(writeLength, SmpLength(0x1FFFE));
length = static_cast<uint16>(writeLength / 2u);
if(mptSmp.RelativeTone < 0)
{
finetune = 0x08;
} else if(mptSmp.RelativeTone > 0)
{
finetune = 0x07;
} else
{
finetune = XM2MODFineTune(mptSmp.nFineTune);
}
volume = static_cast<uint8>(mptSmp.nVolume / 4u);
loopStart = 0;
loopLength = 1;
if(mptSmp.uFlags[CHN_LOOP] && (mptSmp.nLoopStart + 2u) < writeLength)
{
const SmpLength loopEnd = Clamp(mptSmp.nLoopEnd, (mptSmp.nLoopStart & ~1) + 2u, writeLength) & ~1;
loopStart = static_cast<uint16>(mptSmp.nLoopStart / 2u);
loopLength = static_cast<uint16>((loopEnd - (mptSmp.nLoopStart & ~1)) / 2u);
}
return writeLength;
}
// Compute a "rating" of this sample header by counting invalid header data to ultimately reject garbage files.
uint32 GetInvalidByteScore() const
{
return ((volume > 64) ? 1 : 0)
+ ((finetune > 15) ? 1 : 0)
+ ((loopStart > length * 2) ? 1 : 0);
}
// Suggested threshold for rejecting invalid files based on cumulated score returned by GetInvalidByteScore
enum : uint32 { INVALID_BYTE_THRESHOLD = 40 };
// This threshold is used for files where the file magic only gives a
// fragile result which alone would lead to too many false positives.
// In particular, the files from Inconexia demo by Iguana
// (https://www.pouet.net/prod.php?which=830) which have 3 \0 bytes in
// the file magic tend to cause misdetection of random files.
enum : uint32 { INVALID_BYTE_FRAGILE_THRESHOLD = 1 };
// Retrieve the internal sample format flags for this sample.
static SampleIO GetSampleFormat()
{
return SampleIO(
SampleIO::_8bit,
SampleIO::mono,
SampleIO::bigEndian,
SampleIO::signedPCM);
}
};
MPT_BINARY_STRUCT(MODSampleHeader, 30)
// Synthesized StarTrekker instruments
struct AMInstrument
{
char am[2]; // "AM"
char zero[4];
uint16be startLevel; // Start level
uint16be attack1Level; // Attack 1 level
uint16be attack1Speed; // Attack 1 speed
uint16be attack2Level; // Attack 2 level
uint16be attack2Speed; // Attack 2 speed
uint16be sustainLevel; // Sustain level
uint16be decaySpeed; // Decay speed
uint16be sustainTime; // Sustain time
uint16be nt; // ?
uint16be releaseSpeed; // Release speed
uint16be waveform; // Waveform
int16be pitchFall; // Pitch fall
uint16be vibAmp; // Vibrato amplitude
uint16be vibSpeed; // Vibrato speed
uint16be octave; // Base frequency
void ConvertToMPT(ModSample &sample, ModInstrument &ins, mpt::fast_prng &rng) const
{
sample.nLength = waveform == 3 ? 1024 : 32;
sample.nLoopStart = 0;
sample.nLoopEnd = sample.nLength;
sample.uFlags.set(CHN_LOOP);
sample.nVolume = 256; // prelude.mod has volume 0 in sample header
sample.nVibDepth = mpt::saturate_cast<uint8>(vibAmp * 2);
sample.nVibRate = static_cast<uint8>(vibSpeed);
sample.nVibType = VIB_SINE;
sample.RelativeTone = static_cast<int8>(-12 * octave);
if(sample.AllocateSample())
{
int8 *p = sample.sample8();
for(SmpLength i = 0; i < sample.nLength; i++)
{
switch(waveform)
{
default:
case 0: p[i] = ModSinusTable[i * 2]; break; // Sine
case 1: p[i] = static_cast<int8>(-128 + i * 8); break; // Saw
case 2: p[i] = i < 16 ? -128 : 127; break; // Square
case 3: p[i] = mpt::random<int8>(rng); break; // Noise
}
}
}
InstrumentEnvelope &volEnv = ins.VolEnv;
volEnv.dwFlags.set(ENV_ENABLED);
volEnv.reserve(6);
volEnv.push_back(0, static_cast<EnvelopeNode::value_t>(startLevel / 4));
const struct
{
uint16 level, speed;
} points[] = { { startLevel, 0 }, { attack1Level, attack1Speed }, { attack2Level, attack2Speed }, { sustainLevel, decaySpeed }, { sustainLevel, sustainTime }, { 0, releaseSpeed } };
for(uint8 i = 1; i < CountOf(points); i++)
{
int duration = std::min(points[i].speed, uint16(256));
// Sustain time is already in ticks, no need to compute the segment duration.
if(i != 4)
{
if(duration == 0)
{
volEnv.dwFlags.set(ENV_LOOP);
volEnv.nLoopStart = volEnv.nLoopEnd = static_cast<uint8>(volEnv.size() - 1);
break;
}
// Startrekker increments / decrements the envelope level by the stage speed
// until it reaches the next stage level.
int a, b;
if(points[i].level > points[i - 1].level)
{
a = points[i].level - points[i - 1].level;
b = 256 - points[i - 1].level;
} else
{
a = points[i - 1].level - points[i].level;
b = points[i - 1].level;
}
// Release time is again special.
if(i == 5)
b = 256;
else if(b == 0)
b = 1;
duration = std::max((256 * a) / (duration * b), 1);
}
if(duration > 0)
{
volEnv.push_back(volEnv.back().tick + static_cast<EnvelopeNode::tick_t>(duration), static_cast<EnvelopeNode::value_t>(points[i].level / 4));
}
}
if(pitchFall)
{
InstrumentEnvelope &pitchEnv = ins.PitchEnv;
pitchEnv.dwFlags.set(ENV_ENABLED);
pitchEnv.reserve(2);
pitchEnv.push_back(0, ENVELOPE_MID);
pitchEnv.push_back(static_cast<EnvelopeNode::tick_t>(1024 / abs(pitchFall)), pitchFall > 0 ? ENVELOPE_MIN : ENVELOPE_MAX);
}
}
};
MPT_BINARY_STRUCT(AMInstrument, 36)
struct PT36IffChunk
{
// IFF chunk names
enum ChunkIdentifiers
{
idVERS = MagicBE("VERS"),
idINFO = MagicBE("INFO"),
idCMNT = MagicBE("CMNT"),
idPTDT = MagicBE("PTDT"),
};
uint32be signature; // IFF chunk name
uint32be chunksize; // chunk size without header
};
MPT_BINARY_STRUCT(PT36IffChunk, 8)
struct PT36InfoChunk
{
char name[32];
uint16be numSamples;
uint16be numOrders;
uint16be numPatterns;
uint16be volume;
uint16be tempo;
uint16be flags;
uint16be dateDay;
uint16be dateMonth;
uint16be dateYear;
uint16be dateHour;
uint16be dateMinute;
uint16be dateSecond;
uint16be playtimeHour;
uint16be playtimeMinute;
uint16be playtimeSecond;
uint16be playtimeMsecond;
};
MPT_BINARY_STRUCT(PT36InfoChunk, 64)
// Check if header magic equals a given string.
static bool IsMagic(const char *magic1, const char (&magic2)[5])
{
return std::memcmp(magic1, magic2, 4) == 0;
}
static uint32 ReadSample(FileReader &file, MODSampleHeader &sampleHeader, ModSample &sample, char (&sampleName)[MAX_SAMPLENAME], bool is4Chn)
{
file.ReadStruct(sampleHeader);
sampleHeader.ConvertToMPT(sample, is4Chn);
mpt::String::Read<mpt::String::spacePadded>(sampleName, sampleHeader.name);
// Get rid of weird characters in sample names.
for(auto &c : sampleName)
{
if(c > 0 && c < ' ')
{
c = ' ';
}
}
// Check for invalid values
return sampleHeader.GetInvalidByteScore();
}
// Parse the order list to determine how many patterns are used in the file.
static PATTERNINDEX GetNumPatterns(FileReader &file, ModSequence &Order, ORDERINDEX numOrders, SmpLength totalSampleLen, CHANNELINDEX &numChannels, bool checkForWOW)
{
PATTERNINDEX numPatterns = 0; // Total number of patterns in file (determined by going through the whole order list) with pattern number < 128
PATTERNINDEX officialPatterns = 0; // Number of patterns only found in the "official" part of the order list (i.e. order positions < claimed order length)
PATTERNINDEX numPatternsIllegal = 0; // Total number of patterns in file, also counting in "invalid" pattern indexes >= 128
for(ORDERINDEX ord = 0; ord < 128; ord++)
{
PATTERNINDEX pat = Order[ord];
if(pat < 128 && numPatterns <= pat)
{
numPatterns = pat + 1;
if(ord < numOrders)
{
officialPatterns = numPatterns;
}
}
if(pat >= numPatternsIllegal)
{
numPatternsIllegal = pat + 1;
}
}
// Remove the garbage patterns past the official order end now that we don't need them anymore.
Order.resize(numOrders);
const size_t patternStartOffset = file.GetPosition();
const size_t sizeWithoutPatterns = totalSampleLen + patternStartOffset;
if(checkForWOW && sizeWithoutPatterns + numPatterns * 8 * 256 == file.GetLength())
{
// Check if this is a Mod's Grave WOW file... Never seen one of those, but apparently they *do* exist.
// WOW files should use the M.K. magic but are actually 8CHN files.
numChannels = 8;
} else if(numPatterns != officialPatterns && numChannels == 4 && !checkForWOW)
{
// Fix SoundTracker modules where "hidden" patterns should be ignored.
// razor-1911.mod (MD5 b75f0f471b0ae400185585ca05bf7fe8, SHA1 4de31af234229faec00f1e85e1e8f78f405d454b)
// and captain_fizz.mod (MD5 55bd89fe5a8e345df65438dbfc2df94e, SHA1 9e0e8b7dc67939885435ea8d3ff4be7704207a43)
// seem to have the "correct" file size when only taking the "official" patterns into account,
// but they only play correctly when also loading the inofficial patterns.
// On the other hand, the SoundTracker module
// wolf1.mod (MD5 a4983d7a432d324ce8261b019257f4ed, SHA1 aa6b399d02546bcb6baf9ec56a8081730dea3f44),
// wolf3.mod (MD5 af60840815aa9eef43820a7a04417fa6, SHA1 24d6c2e38894f78f6c5c6a4b693a016af8fa037b)
// and jean_baudlot_-_bad_dudes_vs_dragonninja-dragonf.mod (MD5 fa48e0f805b36bdc1833f6b82d22d936, SHA1 39f2f8319f4847fe928b9d88eee19d79310b9f91)
// only play correctly if we ignore the hidden patterns.
// Hence, we have a peek at the first hidden pattern and check if it contains a lot of illegal data.
// If that is the case, we assume it's part of the sample data and only consider the "official" patterns.
file.Seek(patternStartOffset + officialPatterns * 1024);
int illegalBytes = 0;
for(int i = 0; i < 256; i++)
{
uint8 data[4];
file.ReadArray(data);
if(data[0] & 0xE0)
{
illegalBytes++;
if(illegalBytes > 64)
{
numPatterns = officialPatterns;
break;
}
}
}
file.Seek(patternStartOffset);
}
#ifdef MPT_BUILD_DEBUG
// Check if the "hidden" patterns in the order list are actually real, i.e. if they are saved in the file.
// OpenMPT did this check in the past, but no other tracker appears to do this.
// Interestingly, (broken) variants of the ProTracker modules
// "killing butterfly" (MD5 bd676358b1dbb40d40f25435e845cf6b, SHA1 9df4ae21214ff753802756b616a0cafaeced8021),
// "quartex" by Reflex (MD5 35526bef0fb21cb96394838d94c14bab, SHA1 116756c68c7b6598dcfbad75a043477fcc54c96c),
// seem to have the "correct" file size when only taking the "official" patterns into account, but they only play
// correctly when also loading the inofficial patterns.
// See also the above check for ambiguities with SoundTracker modules.
// Keep this assertion in the code to find potential other broken MODs.
if(numPatterns != officialPatterns && sizeWithoutPatterns + officialPatterns * numChannels * 256 == file.GetLength())
{
MPT_ASSERT(false);
//numPatterns = officialPatterns;
} else
#endif
if(numPatternsIllegal > numPatterns && sizeWithoutPatterns + numPatternsIllegal * numChannels * 256 == file.GetLength())
{
// Even those illegal pattern indexes (> 128) appear to be valid... What a weird file!
// e.g. NIETNU.MOD, where the end of the order list is filled with FF rather than 00, and the file actually contains 256 patterns.
numPatterns = numPatternsIllegal;
} else if(numPatternsIllegal >= 0xFF)
{
// Patterns FE and FF are used with S3M semantics (e.g. some MODs written with old OpenMPT versions)
Order.Replace(0xFE, Order.GetIgnoreIndex());
Order.Replace(0xFF, Order.GetInvalidPatIndex());
}
return numPatterns;
}
void CSoundFile::ReadMODPatternEntry(FileReader &file, ModCommand &m)
{
uint8 data[4];
file.ReadArray(data);
ReadMODPatternEntry(data, m);
}
void CSoundFile::ReadMODPatternEntry(const uint8 (&data)[4], ModCommand &m)
{
// Read Period
uint16 period = (((static_cast<uint16>(data[0]) & 0x0F) << 8) | data[1]);
size_t note = NOTE_NONE;
if(period > 0 && period != 0xFFF)
{
note = mpt::size(ProTrackerPeriodTable) + 23 + NOTE_MIN;
for(size_t i = 0; i < mpt::size(ProTrackerPeriodTable); i++)
{
if(period >= ProTrackerPeriodTable[i])
{
if(period != ProTrackerPeriodTable[i] && i != 0)
{
uint16 p1 = ProTrackerPeriodTable[i - 1];
uint16 p2 = ProTrackerPeriodTable[i];
if(p1 - period < (period - p2))
{
note = i + 23 + NOTE_MIN;
break;
}
}
note = i + 24 + NOTE_MIN;
break;
}
}
}
m.note = static_cast<ModCommand::NOTE>(note);
// Read Instrument
m.instr = (data[2] >> 4) | (data[0] & 0x10);
// Read Effect
m.command = data[2] & 0x0F;
m.param = data[3];
}
struct MODMagicResult
{
const MPT_UCHAR_TYPE *madeWithTracker = nullptr;
uint32 invalidByteThreshold = MODSampleHeader::INVALID_BYTE_THRESHOLD;
CHANNELINDEX numChannels = 0;
bool isNoiseTracker = false;
bool isStartrekker = false;
bool isGenericMultiChannel = false;
bool setMODVBlankTiming = false;
};
static bool CheckMODMagic(const char magic[4], MODMagicResult &result)
{
if(IsMagic(magic, "M.K.") // ProTracker and compatible
|| IsMagic(magic, "M!K!") // ProTracker (>64 patterns)
|| IsMagic(magic, "PATT") // ProTracker 3.6
|| IsMagic(magic, "NSMS") // kingdomofpleasure.mod by bee hunter
|| IsMagic(magic, "LARD")) // judgement_day_gvine.mod by 4-mat
{
result.madeWithTracker = UL_("Generic ProTracker or compatible");
result.numChannels = 4;
} else if(IsMagic(magic, "M&K!") // "His Master's Noise" musicdisk
|| IsMagic(magic, "FEST") // "His Master's Noise" musicdisk
|| IsMagic(magic, "N.T."))
{
result.madeWithTracker = UL_("NoiseTracker");
result.isNoiseTracker = true;
result.numChannels = 4;
} else if(IsMagic(magic, "OKTA")
|| IsMagic(magic, "OCTA"))
{
// Oktalyzer
result.madeWithTracker = UL_("Oktalyzer");
result.numChannels = 8;
} else if(IsMagic(magic, "CD81")
|| IsMagic(magic, "CD61"))
{
// Octalyser on Atari STe/Falcon
result.madeWithTracker = UL_("Octalyser (Atari)");
result.numChannels = magic[2] - '0';
} else if(IsMagic(magic, "M\0\0\0") || IsMagic(magic, "8\0\0\0"))
{
// Inconexia demo by Iguana, delta samples (https://www.pouet.net/prod.php?which=830)
result.madeWithTracker = UL_("Inconexia demo (delta samples)");
result.invalidByteThreshold = MODSampleHeader::INVALID_BYTE_FRAGILE_THRESHOLD;
result.numChannels = (magic[0] == '8') ? 8 : 4;
} else if(!memcmp(magic, "FA0", 3) && magic[3] >= '4' && magic[3] <= '8')
{
// Digital Tracker on Atari Falcon
result.madeWithTracker = UL_("Digital Tracker");
result.numChannels = magic[3] - '0';
} else if((!memcmp(magic, "FLT", 3) || !memcmp(magic, "EXO", 3)) && magic[3] >= '4' && magic[3] <= '9')
{
// FLTx / EXOx - Startrekker by Exolon / Fairlight
result.madeWithTracker = UL_("Startrekker");
result.isStartrekker = true;
result.setMODVBlankTiming = true;
result.numChannels = magic[3] - '0';
} else if(magic[0] >= '1' && magic[0] <= '9' && !memcmp(magic + 1, "CHN", 3))
{
// xCHN - Many trackers
result.madeWithTracker = UL_("Generic MOD-compatible Tracker");
result.isGenericMultiChannel = true;
result.numChannels = magic[0] - '0';
} else if(magic[0] >= '1' && magic[0] <= '9' && magic[1]>='0' && magic[1] <= '9'
&& (!memcmp(magic + 2, "CH", 2) || !memcmp(magic + 2, "CN", 2)))
{
// xxCN / xxCH - Many trackers
result.madeWithTracker = UL_("Generic MOD-compatible Tracker");
result.isGenericMultiChannel = true;
result.numChannels = (magic[0] - '0') * 10 + magic[1] - '0';
} else if(!memcmp(magic, "TDZ", 3) && magic[3] >= '4' && magic[3] <= '9')
{
// TDZx - TakeTracker
result.madeWithTracker = UL_("TakeTracker");
result.numChannels = magic[3] - '0';
} else
{
return false;
}
return true;
}
CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderMOD(MemoryFileReader file, const uint64 *pfilesize)
{
if(!file.CanRead(1080 + 4))
{
return ProbeWantMoreData;
}
file.Seek(1080);
char magic[4];
file.ReadArray(magic);
MODMagicResult modMagicResult;
if(!CheckMODMagic(magic, modMagicResult))
{
return ProbeFailure;
}
file.Seek(20);
uint32 invalidBytes = 0;
for(SAMPLEINDEX smp = 1; smp <= 31; smp++)
{
MODSampleHeader sampleHeader;
file.ReadStruct(sampleHeader);
invalidBytes += sampleHeader.GetInvalidByteScore();
}
if(invalidBytes > modMagicResult.invalidByteThreshold)
{
return ProbeFailure;
}
MPT_UNREFERENCED_PARAMETER(pfilesize);
return ProbeSuccess;
}
bool CSoundFile::ReadMOD(FileReader &file, ModLoadingFlags loadFlags)
{
char magic[4];
if(!file.Seek(1080) || !file.ReadArray(magic))
{
return false;
}
InitializeGlobals(MOD_TYPE_MOD);
MODMagicResult modMagicResult;
if(!CheckMODMagic(magic, modMagicResult)
|| modMagicResult.numChannels < 1
|| modMagicResult.numChannels > MAX_BASECHANNELS)
{
return false;
}
if(loadFlags == onlyVerifyHeader)
{
return true;
}
m_nChannels = modMagicResult.numChannels;
bool isNoiseTracker = modMagicResult.isNoiseTracker;
bool isStartrekker = modMagicResult.isStartrekker;
bool isGenericMultiChannel = modMagicResult.isGenericMultiChannel;
bool isInconexia = IsMagic(magic, "M\0\0\0") || IsMagic(magic, "8\0\0\0");
// A loop length of zero will freeze ProTracker, so assume that modules having such a value were not meant to be played on Amiga. Fixes LHS_MI.MOD
bool hasRepLen0 = false;
if(modMagicResult.setMODVBlankTiming)
{
m_playBehaviour.set(kMODVBlankTiming);
}
// Startrekker 8 channel mod (needs special treatment, see below)
const bool isFLT8 = isStartrekker && m_nChannels == 8;
// Only apply VBlank tests to M.K. (ProTracker) modules.
const bool isMdKd = IsMagic(magic, "M.K.");
// Adjust finetune values for modules saved with "His Master's Noisetracker"
const bool isHMNT = IsMagic(magic, "M&K!") || IsMagic(magic, "FEST");
// Reading song title
file.Seek(0);
file.ReadString<mpt::String::spacePadded>(m_songName, 20);
// Load Sample Headers
SmpLength totalSampleLen = 0;
m_nSamples = 31;
uint32 invalidBytes = 0;
for(SAMPLEINDEX smp = 1; smp <= 31; smp++)
{
MODSampleHeader sampleHeader;
invalidBytes += ReadSample(file, sampleHeader, Samples[smp], m_szNames[smp], m_nChannels == 4);
totalSampleLen += Samples[smp].nLength;
if(isHMNT)
{
Samples[smp].nFineTune = -static_cast<int8>(sampleHeader.finetune << 3);
} else if(Samples[smp].nLength > 65535)
{
isNoiseTracker = false;
}
if(sampleHeader.length && !sampleHeader.loopLength)
{
hasRepLen0 = true;
}
}
// If there is too much binary garbage in the sample headers, reject the file.
if(invalidBytes > modMagicResult.invalidByteThreshold)
{
return false;
}
// Read order information
MODFileHeader fileHeader;
file.ReadStruct(fileHeader);
file.Skip(4); // Magic bytes (we already parsed these)
ReadOrderFromArray(Order(), fileHeader.orderList);
ORDERINDEX realOrders = fileHeader.numOrders;
if(realOrders > 128)
{
// beatwave.mod by Sidewinder claims to have 129 orders. (MD5: 8a029ac498d453beb929db9a73c3c6b4, SHA1: f7b76fb9f477b07a2e78eb10d8624f0df262cde7 - the version from ModArchive, not ModLand)
realOrders = 128;
} else if(realOrders == 0)
{
// Is this necessary?
realOrders = 128;
while(realOrders > 1 && Order()[realOrders - 1] == 0)
{
realOrders--;
}
}
// Get number of patterns (including some order list sanity checks)
PATTERNINDEX numPatterns = GetNumPatterns(file, Order(), realOrders, totalSampleLen, m_nChannels, isMdKd);
if(isMdKd && GetNumChannels() == 8)
{
// M.K. with 8 channels = Grave Composer
modMagicResult.madeWithTracker = UL_("Mod's Grave");
}
if(isFLT8)
{
// FLT8 has only even order items, so divide by two.
for(auto &pat : Order())
{
pat /= 2u;
}
}
// Restart position sanity checks
realOrders--;
Order().SetRestartPos(fileHeader.restartPos);
// (Ultimate) Soundtracker didn't have a restart position, but instead stored a default tempo in this value.
// The default value for this is 0x78 (120 BPM). This is probably the reason why some M.K. modules
// have this weird restart position. I think I've read somewhere that NoiseTracker actually writes 0x78 there.
// M.K. files that have restart pos == 0x78: action's batman by DJ Uno, VALLEY.MOD, WormsTDC.MOD, ZWARTZ.MOD
// Files that have an order list longer than 0x78 with restart pos = 0x78: my_shoe_is_barking.mod, papermix.mod
// - in both cases it does not appear like the restart position should be used.
MPT_ASSERT(fileHeader.restartPos != 0x78 || fileHeader.restartPos + 1u >= realOrders);
if(fileHeader.restartPos > realOrders || (fileHeader.restartPos == 0x78 && m_nChannels == 4))
{
Order().SetRestartPos(0);
}
m_nDefaultSpeed = 6;
m_nDefaultTempo.Set(125);
m_nMinPeriod = 14 * 4;
m_nMaxPeriod = 3424 * 4;
// Prevent clipping based on number of channels... If all channels are playing at full volume, "256 / #channels"
// is the maximum possible sample pre-amp without getting distortion (Compatible mix levels given).
// The more channels we have, the less likely it is that all of them are used at the same time, though, so cap at 32...
m_nSamplePreAmp = Clamp(256 / m_nChannels, 32, 128);
m_SongFlags.reset(); // SONG_ISAMIGA will be set conditionally
// Setup channel pan positions and volume
SetupMODPanning();
// Before loading patterns, apply some heuristics:
// - Scan patterns to check if file could be a NoiseTracker file in disguise.
// In this case, the parameter of Dxx commands needs to be ignored.
// - Use the same code to find notes that would be out-of-range on Amiga.
// - Detect 7-bit panning.
bool onlyAmigaNotes = true;
bool fix7BitPanning = false;
uint8 maxPanning = 0; // For detecting 8xx-as-sync
if(!isNoiseTracker)
{
bool leftPanning = false, extendedPanning = false; // For detecting 800-880 panning
isNoiseTracker = isMdKd;
for(PATTERNINDEX pat = 0; pat < numPatterns; pat++)
{
uint16 patternBreaks = 0;
for(uint32 i = 0; i < 256; i++)
{
ModCommand m;
ReadMODPatternEntry(file, m);
if(!m.IsAmigaNote())
{
isNoiseTracker = onlyAmigaNotes = false;
}
if((m.command > 0x06 && m.command < 0x0A)
|| (m.command == 0x0E && m.param > 0x01)
|| (m.command == 0x0F && m.param > 0x1F)
|| (m.command == 0x0D && ++patternBreaks > 1))
{
isNoiseTracker = false;
}
if(m.command == 0x08)
{
maxPanning = std::max(maxPanning, m.param);
if(m.param < 0x80)
leftPanning = true;
else if(m.param > 0x8F && m.param != 0xA4)
extendedPanning = true;
} else if(m.command == 0x0E && (m.param & 0xF0) == 0x80)
{
maxPanning = std::max<uint8>(maxPanning, m.param << 4);
}
}
}
fix7BitPanning = leftPanning && !extendedPanning;
}
file.Seek(1084);
const CHANNELINDEX readChannels = (isFLT8 ? 4 : m_nChannels); // 4 channels per pattern in FLT8 format.
if(isFLT8) numPatterns++; // as one logical pattern consists of two real patterns in FLT8 format, the highest pattern number has to be increased by one.
bool hasTempoCommands = false, definitelyCIA = false; // for detecting VBlank MODs
// Heuristic for rejecting E0x commands that are most likely not intended to actually toggle the Amiga LED filter, like in naen_leijasi_ptk.mod by ilmarque
bool filterState = false;
int filterTransitions = 0;
// Reading patterns
Patterns.ResizeArray(numPatterns);
for(PATTERNINDEX pat = 0; pat < numPatterns; pat++)
{
ModCommand *rowBase = nullptr;
if(isFLT8)
{
// FLT8: Only create "even" patterns and either write to channel 1 to 4 (even patterns) or 5 to 8 (odd patterns).
PATTERNINDEX actualPattern = pat / 2u;
if((pat % 2u) == 0 && !Patterns.Insert(actualPattern, 64))
{
break;
}
rowBase = Patterns[actualPattern].GetpModCommand(0, (pat % 2u) == 0 ? 0 : 4);
} else
{
if(!Patterns.Insert(pat, 64))
{
break;
}
rowBase = Patterns[pat].GetpModCommand(0, 0);
}
if(rowBase == nullptr || !(loadFlags & loadPatternData))
{
break;
}
// For detecting PT1x mode
std::vector<ModCommand::INSTR> lastInstrument(GetNumChannels(), 0);
std::vector<uint8> instrWithoutNoteCount(GetNumChannels(), 0);
for(ROWINDEX row = 0; row < 64; row++, rowBase += m_nChannels)
{
// If we have more than one Fxx command on this row and one can be interpreted as speed
// and the other as tempo, we can be rather sure that it is not a VBlank mod.
bool hasSpeedOnRow = false, hasTempoOnRow = false;
for(CHANNELINDEX chn = 0; chn < readChannels; chn++)
{
ModCommand &m = rowBase[chn];
ReadMODPatternEntry(file, m);
if(m.command || m.param)
{
if(isStartrekker && m.command == 0x0E)
{
// No support for Startrekker assembly macros
m.command = CMD_NONE;
m.param = 0;
} else if(isStartrekker && m.command == 0x0F && m.param > 0x1F)
{
// Startrekker caps speed at 31 ticks per row
m.param = 0x1F;
}
ConvertModCommand(m);
}
// Perform some checks for our heuristics...
if(m.command == CMD_TEMPO)
{
hasTempoOnRow = true;
if(m.param < 100)
hasTempoCommands = true;
} else if(m.command == CMD_SPEED)
{
hasSpeedOnRow = true;
} else if(m.command == CMD_PATTERNBREAK && isNoiseTracker)
{
m.param = 0;
} else if(m.command == CMD_PANNING8 && fix7BitPanning)
{
// Fix MODs with 7-bit + surround panning
if(m.param == 0xA4)
{
m.command = CMD_S3MCMDEX;
m.param = 0x91;
} else
{
m.param = mpt::saturate_cast<ModCommand::PARAM>(m.param * 2);
}
} else if(m.command == CMD_MODCMDEX && m.param < 0x10)
{
// Count LED filter transitions
bool newState = !(m.param & 0x01);
if(newState != filterState)
{
filterState = newState;
filterTransitions++;
}
}
if(m.note == NOTE_NONE && m.instr > 0 && !isFLT8)
{
if(lastInstrument[chn] > 0 && lastInstrument[chn] != m.instr)
{
// Arbitrary threshold for enabling sample swapping: 4 consecutive "sample swaps" in one pattern.
if(++instrWithoutNoteCount[chn] >= 4)
{
m_playBehaviour.set(kMODSampleSwap);
}
}
} else if(m.note != NOTE_NONE)
{
instrWithoutNoteCount[chn] = 0;
}
if(m.instr != 0)
{
lastInstrument[chn] = m.instr;
}
}
if(hasSpeedOnRow && hasTempoOnRow) definitelyCIA = true;
}
}
if(onlyAmigaNotes && !hasRepLen0 && (IsMagic(magic, "M.K.") || IsMagic(magic, "M!K!") || IsMagic(magic, "PATT")))
{
// M.K. files that don't exceed the Amiga note limit (fixes mod.mothergoose)
m_SongFlags.set(SONG_AMIGALIMITS);
// Need this for professionaltracker.mod by h0ffman (SHA1: 9a7c52cbad73ed2a198ee3fa18d3704ea9f546ff)
m_SongFlags.set(SONG_PT_MODE);
m_playBehaviour.set(kMODSampleSwap);
m_playBehaviour.set(kMODOutOfRangeNoteDelay);
m_playBehaviour.set(kMODTempoOnSecondTick);
// Arbitrary threshold for deciding that 8xx effects are only used as sync markers
if(maxPanning < 0x20)
{
m_playBehaviour.set(kMODIgnorePanning);
if(fileHeader.restartPos != 0x7F)
{
// Don't enable these hacks for ScreamTracker modules (restart position = 0x7F), to fix e.g. sample 10 in BASIC001.MOD (SHA1: 11298a5620e677beaa50bd4ed00c3710b75c81af)
// Note: restart position = 0x7F can also be found in ProTracker modules, e.g. professionaltracker.mod by h0ffman
m_playBehaviour.set(kMODOneShotLoops);
}
}
} else if(!onlyAmigaNotes && fileHeader.restartPos == 0x7F && isMdKd && fileHeader.restartPos + 1u >= realOrders)
{
modMagicResult.madeWithTracker = UL_("ScreamTracker");
}
if(onlyAmigaNotes && !isGenericMultiChannel && filterTransitions < 7)
{
m_SongFlags.set(SONG_ISAMIGA);
}
if(isInconexia)
{
m_playBehaviour.set(kMODIgnorePanning);
}
// Reading samples
if(loadFlags & loadSampleData)
{
file.Seek(1084 + (readChannels * 64 * 4) * numPatterns);
for(SAMPLEINDEX smp = 1; smp <= 31; smp++)
{
ModSample &sample = Samples[smp];
if(sample.nLength)
{
SampleIO::Encoding encoding = SampleIO::signedPCM;
if(isInconexia)
encoding = SampleIO::deltaPCM;
else if(file.ReadMagic("ADPCM"))
encoding = SampleIO::ADPCM;
SampleIO sampleIO(
SampleIO::_8bit,
SampleIO::mono,
SampleIO::littleEndian,
encoding);
// Fix sample 6 in MOD.shorttune2, which has a replen longer than the sample itself.
// ProTracker reads beyond the end of the sample when playing. Normally samples are
// adjacent in PT's memory, so we simply read into the next sample in the file.
FileReader::off_t nextSample = file.GetPosition() + sampleIO.CalculateEncodedSize(sample.nLength);
if(isMdKd && onlyAmigaNotes)
sample.nLength = std::max(sample.nLength, sample.nLoopEnd);
sampleIO.ReadSample(sample, file);
file.Seek(nextSample);
}
}
}
#if defined(MPT_EXTERNAL_SAMPLES) || defined(MPT_BUILD_FUZZER)
// Detect Startrekker files with external synth instruments.
// Note: Synthesized AM samples may overwrite existing samples (e.g. sample 1 in fa.worse face.mod),
// hence they are loaded here after all regular samples have been loaded.
if((loadFlags & loadSampleData) && isStartrekker)
{
#ifdef MPT_EXTERNAL_SAMPLES
InputFile amFile;
FileReader amData;
mpt::PathString filename = file.GetFileName();
if(!filename.empty())
{
// Find instrument definition file
const mpt::PathString exts[] = { P_(".nt"), P_(".NT"), P_(".as"), P_(".AS") };
for(const auto &ext : exts)
{
mpt::PathString infoName = filename + ext;
char stMagic[16];
if(infoName.IsFile() && amFile.Open(infoName) && (amData = GetFileReader(amFile)).IsValid() && amData.ReadArray(stMagic))
{
if(!memcmp(stMagic, "ST1.2 ModuleINFO", 16))
modMagicResult.madeWithTracker = UL_("Startrekker 1.2");
else if(!memcmp(stMagic, "ST1.3 ModuleINFO", 16))
modMagicResult.madeWithTracker = UL_("Startrekker 1.3");
else if(!memcmp(stMagic, "AudioSculpture10", 16))
modMagicResult.madeWithTracker = UL_("AudioSculpture 1.0");
else
continue;
if(amData.Seek(144))
{
// Looks like a valid instrument definition file!
m_nInstruments = 31;
break;
}
}
}
}
#elif defined(MPT_BUILD_FUZZER)
// For fuzzing this part of the code, just take random data from patterns
FileReader amData = file.GetChunkAt(1084, 31 * 120);
m_nInstruments = 31;
#endif
for(SAMPLEINDEX smp = 1; smp <= m_nInstruments; smp++)
{
// For Startrekker AM synthesis, we need instrument envelopes.
ModInstrument *ins = AllocateInstrument(smp, smp);
if(ins == nullptr)
{
break;
}
mpt::String::Copy(ins->name, m_szNames[smp]);
AMInstrument am;
// Allow partial reads for fa.worse face.mod
if(amData.ReadStructPartial(am) && !memcmp(am.am, "AM", 2) && am.waveform < 4)
{
am.ConvertToMPT(Samples[smp], *ins, AccessPRNG());
}
// This extra padding is probably present to have identical block sizes for AM and FM instruments.
amData.Skip(120 - sizeof(AMInstrument));
}
}
#endif // MPT_EXTERNAL_SAMPLES || MPT_BUILD_FUZZER
// Fix VBlank MODs. Arbitrary threshold: 10 minutes.
// Basically, this just converts all tempo commands into speed commands
// for MODs which are supposed to have VBlank timing (instead of CIA timing).
// There is no perfect way to do this, since both MOD types look the same,
// but the most reliable way is to simply check for extremely long songs
// (as this would indicate that e.g. a F30 command was really meant to set
// the ticks per row to 48, and not the tempo to 48 BPM).
// In the pattern loader above, a second condition is used: Only tempo commands
// below 100 BPM are taken into account. Furthermore, only M.K. (ProTracker)
// modules are checked.
if(isMdKd && hasTempoCommands && !definitelyCIA)
{
const double songTime = GetLength(eNoAdjust).front().duration;
if(songTime >= 600.0)
{
m_playBehaviour.set(kMODVBlankTiming);
if(GetLength(eNoAdjust, GetLengthTarget(songTime)).front().targetReached)
{
// This just makes things worse, song is at least as long as in CIA mode (e.g. in "Stary Hallway" by Neurodancer)
// Obviously we should keep using CIA timing then...
m_playBehaviour.reset(kMODVBlankTiming);
} else
{
modMagicResult.madeWithTracker = UL_("ProTracker (VBlank)");
}
}
}
std::transform(std::begin(magic), std::end(magic), std::begin(magic), [](unsigned char c) -> unsigned char { return (c < ' ') ? ' ' : c; });
m_modFormat.formatName = mpt::format(U_("ProTracker MOD (%1)"))(mpt::ToUnicode(mpt::CharsetASCII, std::string(std::begin(magic), std::end(magic))));
m_modFormat.type = U_("mod");
if(modMagicResult.madeWithTracker) m_modFormat.madeWithTracker = modMagicResult.madeWithTracker;
m_modFormat.charset = mpt::CharsetISO8859_1;
return true;
}
// Check if a name string is valid (i.e. doesn't contain binary garbage data)
template<size_t N>
static uint32 CountInvalidChars(const char (&name)[N])
{
uint32 invalidChars = 0;
for(int8 c : name) // char can be signed or unsigned
{
// Check for any Extended ASCII and control characters
if(c != 0 && c < ' ')
invalidChars++;
}
return invalidChars;
}
// We'll have to do some heuristic checks to find out whether this is an old Ultimate Soundtracker module
// or if it was made with the newer Soundtracker versions.
// Thanks for Fraggie for this information! (https://www.un4seen.com/forum/?topic=14471.msg100829#msg100829)
enum STVersions
{
UST1_00, // Ultimate Soundtracker 1.0-1.21 (K. Obarski)
UST1_80, // Ultimate Soundtracker 1.8-2.0 (K. Obarski)
ST2_00_Exterminator, // SoundTracker 2.0 (The Exterminator), D.O.C. Sountracker II (Unknown/D.O.C.)
ST_III, // Defjam Soundtracker III (Il Scuro/Defjam), Alpha Flight SoundTracker IV (Alpha Flight), D.O.C. SoundTracker IV (Unknown/D.O.C.), D.O.C. SoundTracker VI (Unknown/D.O.C.)
ST_IX, // D.O.C. SoundTracker IX (Unknown/D.O.C.)
MST1_00, // Master Soundtracker 1.0 (Tip/The New Masters)
ST2_00, // SoundTracker 2.0, 2.1, 2.2 (Unknown/D.O.C.)
};
struct M15FileHeaders
{
char songname[20];
MODSampleHeader sampleHeaders[15];
MODFileHeader fileHeader;
};
MPT_BINARY_STRUCT(M15FileHeaders, 20 + 15 * 30 + 130)
typedef uint8 M15PatternData[64][4][4];
static bool ValidateHeader(const M15FileHeaders &fileHeaders)
{
// In theory, sample and song names should only ever contain printable ASCII chars and null.
// However, there are quite a few SoundTracker modules in the wild with random
// characters. To still be able to distguish them from other formats, we just reject
// files with *too* many bogus characters. Arbitrary threshold: 48 bogus characters in total
// or more than 5 invalid characters just in the title alone.
uint32 invalidChars = CountInvalidChars(fileHeaders.songname);
if(invalidChars > 5)
{
return false;
}
SmpLength totalSampleLen = 0;
uint8 allVolumes = 0;
for(SAMPLEINDEX smp = 0; smp < 15; smp++)
{
const MODSampleHeader &sampleHeader = fileHeaders.sampleHeaders[smp];
invalidChars += CountInvalidChars(sampleHeader.name);
// Sanity checks - invalid character count adjusted for ata.mod (MD5 937b79b54026fa73a1a4d3597c26eace, SHA1 3322ca62258adb9e0ae8e9afe6e0c29d39add874)
if(invalidChars > 48
|| sampleHeader.volume > 64
|| sampleHeader.finetune != 0
|| sampleHeader.length > 32768)
{
return false;
}
totalSampleLen += sampleHeader.length;
allVolumes |= sampleHeader.volume;
}
// Reject any files with no (or only silent) samples at all, as this might just be a random binary file (e.g. ID3 tags with tons of padding)
if(totalSampleLen == 0 || allVolumes == 0)
{
return false;
}
// Sanity check: No more than 128 positions. ST's GUI limits tempo to [1, 220].
// There are some mods with a tempo of 0 (explora3-death.mod) though, so ignore the lower limit.
if(fileHeaders.fileHeader.numOrders > 128 || fileHeaders.fileHeader.restartPos > 220)
{
return false;
}
uint8 maxPattern = *std::max_element(std::begin(fileHeaders.fileHeader.orderList), std::end(fileHeaders.fileHeader.orderList));
// Sanity check: 64 patterns max.
if(maxPattern > 63)
{
return false;
}
// No playable song, and lots of null values => most likely a sparse binary file but not a module
if(fileHeaders.fileHeader.restartPos == 0 && fileHeaders.fileHeader.numOrders == 0 && maxPattern == 0)
{
return false;
}
return true;
}
static uint32 CountIllegalM15PatternBytes(const M15PatternData &patternData)
{
uint32 illegalBytes = 0;
for(uint8 row = 0; row < 64; ++row)
{
for(uint8 channel = 0; channel < 4; ++channel)
{
if(patternData[row][channel][0] & 0xF0u)
{
illegalBytes++;
}
}
}
return illegalBytes;
}
template <typename TFileReader>
static bool ValidateFirstM15Pattern(TFileReader &file)
{
M15PatternData patternData;
if(!file.ReadArray(patternData))
{
return false;
}
file.SkipBack(sizeof(patternData));
uint32 invalidBytes = CountIllegalM15PatternBytes(patternData);
// [threshold for all patterns combined] / [max patterns] * [margin, do not reject too much]
if(invalidBytes > 512 / 64 * 2)
{
return false;
}
return true;
}
CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderM15(MemoryFileReader file, const uint64 *pfilesize)
{
M15FileHeaders fileHeaders;
if(!file.ReadStruct(fileHeaders))
{
return ProbeWantMoreData;
}
if(!ValidateHeader(fileHeaders))
{
return ProbeFailure;
}
if(!file.CanRead(sizeof(M15PatternData)))
{
return ProbeWantMoreData;
}
if(!ValidateFirstM15Pattern(file))
{
return ProbeFailure;
}
MPT_UNREFERENCED_PARAMETER(pfilesize);
return ProbeSuccess;
}
bool CSoundFile::ReadM15(FileReader &file, ModLoadingFlags loadFlags)
{
file.Rewind();
M15FileHeaders fileHeaders;
if(!file.ReadStruct(fileHeaders))
{
return false;
}
if(!ValidateHeader(fileHeaders))
{
return false;
}
if(!ValidateFirstM15Pattern(file))
{
return false;
}
char songname[20];
std::memcpy(songname, fileHeaders.songname, 20);
InitializeGlobals(MOD_TYPE_MOD);
m_playBehaviour.reset(kMODOneShotLoops);
m_playBehaviour.set(kMODIgnorePanning);
m_playBehaviour.set(kMODSampleSwap); // untested
m_nChannels = 4;
STVersions minVersion = UST1_00;
bool hasDiskNames = true;
SmpLength totalSampleLen = 0;
m_nSamples = 15;
file.Seek(20);
for(SAMPLEINDEX smp = 1; smp <= 15; smp++)
{
MODSampleHeader sampleHeader;
ReadSample(file, sampleHeader, Samples[smp], m_szNames[smp], true);
totalSampleLen += Samples[smp].nLength;
if(m_szNames[smp][0] && ((memcmp(m_szNames[smp], "st-", 3) && memcmp(m_szNames[smp], "ST-", 3)) || m_szNames[smp][5] != ':'))
{
// Ultimate Soundtracker 1.8 and D.O.C. SoundTracker IX always have sample names containing disk names.
hasDiskNames = false;
}
// Loop start is always in bytes, not words, so don't trust the auto-fix magic in the sample header conversion (fixes loop of "st-01:asia" in mod.drag 10)
if(sampleHeader.loopLength > 1)
{
Samples[smp].nLoopStart = sampleHeader.loopStart;
Samples[smp].nLoopEnd = sampleHeader.loopStart + sampleHeader.loopLength * 2;
Samples[smp].SanitizeLoops();
}
// UST only handles samples up to 9999 bytes. Master Soundtracker 1.0 and SoundTracker 2.0 introduce 32KB samples.
if(sampleHeader.length > 4999 || sampleHeader.loopStart > 9999)
minVersion = std::max(minVersion, MST1_00);
}
MODFileHeader fileHeader;
file.ReadStruct(fileHeader);
ReadOrderFromArray(Order(), fileHeader.orderList);
PATTERNINDEX numPatterns = GetNumPatterns(file, Order(), fileHeader.numOrders, totalSampleLen, m_nChannels, false);
// Most likely just a file with lots of NULs at the start
if(fileHeader.restartPos == 0 && fileHeader.numOrders == 0 && numPatterns <= 1)
{
return false;
}
// Let's see if the file is too small (including some overhead for broken files like sll7.mod or ghostbus.mod)
if(file.BytesLeft() + 65536 < numPatterns * 64u * 4u * 4u + totalSampleLen)
return false;
if(loadFlags == onlyVerifyHeader)
return true;
// Now we can be pretty sure that this is a valid Soundtracker file. Set up default song settings.
// explora3-death.mod has a tempo of 0
if(!fileHeader.restartPos)
fileHeader.restartPos = 0x78;
// jjk55 by Jesper Kyd has a weird tempo set, but it needs to be ignored.
if(!memcmp(songname, "jjk55", 6))
fileHeader.restartPos = 0x78;
// Sample 7 in echoing.mod won't "loop" correctly if we don't convert the VBlank tempo.
m_nDefaultTempo.Set(fileHeader.restartPos * 25 / 24);
if(fileHeader.restartPos != 0x78)
{
// Convert to CIA timing
m_nDefaultTempo = TEMPO((709379.0 * 125.0 / 50.0) / ((240 - fileHeader.restartPos) * 122.0));
if(minVersion > UST1_80)
{
// D.O.C. SoundTracker IX re-introduced the variable tempo after some other versions dropped it.
minVersion = std::max(minVersion, hasDiskNames ? ST_IX : MST1_00);
} else
{
// Ultimate Soundtracker 1.8 adds variable tempo
minVersion = std::max(minVersion, hasDiskNames ? UST1_80 : ST2_00_Exterminator);
}
}
m_nMinPeriod = 113 * 4;
m_nMaxPeriod = 856 * 4;
m_nSamplePreAmp = 64;
m_SongFlags.set(SONG_PT_MODE);
mpt::String::Read<mpt::String::spacePadded>(m_songName, songname);
// Setup channel pan positions and volume
SetupMODPanning();
FileReader::off_t patOffset = file.GetPosition();
// Scan patterns to identify Ultimate Soundtracker modules.
uint32 illegalBytes = 0;
for(PATTERNINDEX pat = 0; pat < numPatterns; pat++)
{
bool patternInUse = std::find(Order().cbegin(), Order().cend(), pat) != Order().cend();
uint8 numDxx = 0;
uint8 emptyCmds = 0;
M15PatternData patternData;
file.ReadArray(patternData);
if(patternInUse)
{
illegalBytes += CountIllegalM15PatternBytes(patternData);
// Reject files that contain a lot of illegal pattern data.
// STK.the final remix (MD5 5ff13cdbd77211d1103be7051a7d89c9, SHA1 e94dba82a5da00a4758ba0c207eb17e3a89c3aa3)
// has one illegal byte, so we only reject after an arbitrary threshold has been passed.
// This also allows to play some rather damaged files like
// crockets.mod (MD5 995ed9f44cab995a0eeb19deb52e2a8b, SHA1 6c79983c3b7d55c9bc110b625eaa07ce9d75f369)
// but naturally we cannot recover the broken data.
// We only check patterns that are actually being used in the order list, because some bad rips of the
// "operation wolf" soundtrack have 15 patterns for several songs, but the last few patterns are just garbage.
// Apart from those hidden patterns, the files play fine.
// Example: operation wolf - wolf1.mod (MD5 739acdbdacd247fbefcac7bc2d8abe6b, SHA1 e6b4813daacbf95f41ce9ec3b22520a2ae07eed8)
if(illegalBytes > 512)
return false;
}
for(ROWINDEX row = 0; row < 64; row++)
{
for(CHANNELINDEX chn = 0; chn < 4; chn++)
{
const uint8 (&data)[4] = patternData[row][chn];
const uint8 eff = data[2] & 0x0F, param = data[3];
// Check for empty space between the last Dxx command and the beginning of another pattern
if(emptyCmds != 0 && !memcmp(data, "\0\0\0\0", 4))
{
emptyCmds++;
if(emptyCmds > 32)
{
// Since there is a lot of empty space after the last Dxx command,
// we assume it's supposed to be a pattern break effect.
minVersion = ST2_00;
}
} else
{
emptyCmds = 0;
}
switch(eff)
{
case 1:
case 2:
if(param > 0x1F && minVersion == UST1_80)
{
// If a 1xx / 2xx effect has a parameter greater than 0x20, it is assumed to be UST.
minVersion = hasDiskNames ? UST1_80 : UST1_00;
} else if(eff == 1 && param > 0 && param < 0x03)
{
// This doesn't look like an arpeggio.
minVersion = std::max(minVersion, ST2_00_Exterminator);
} else if(eff == 1 && (param == 0x37 || param == 0x47) && minVersion <= ST2_00_Exterminator)
{
// This suspiciously looks like an arpeggio.
// Catch sleepwalk.mod by Karsten Obarski, which has a default tempo of 125 rather than 120 in the header, so gets mis-identified as a later tracker version.
minVersion = hasDiskNames ? UST1_80 : UST1_00;
}
break;
case 0x0B:
minVersion = ST2_00;
break;
case 0x0C:
case 0x0D:
case 0x0E:
minVersion = std::max(minVersion, ST2_00_Exterminator);
if(eff == 0x0D)
{
emptyCmds = 1;
if(param == 0 && row == 0)
{
// Fix a possible tracking mistake in Blood Money title - who wants to do a pattern break on the first row anyway?
break;
}
numDxx++;
}
break;
case 0x0F:
minVersion = std::max(minVersion, ST_III);
break;
}
}
}
if(numDxx > 0 && numDxx < 3)
{
// Not many Dxx commands in one pattern means they were probably pattern breaks
minVersion = ST2_00;
}
}
file.Seek(patOffset);
// Reading patterns
if(loadFlags & loadPatternData)
Patterns.ResizeArray(numPatterns);
for(PATTERNINDEX pat = 0; pat < numPatterns; pat++)
{
M15PatternData patternData;
file.ReadArray(patternData);
if(!(loadFlags & loadPatternData) || !Patterns.Insert(pat, 64))
{
continue;
}
uint8 autoSlide[4] = { 0, 0, 0, 0 };
for(ROWINDEX row = 0; row < 64; row++)
{
PatternRow rowBase = Patterns[pat].GetpModCommand(row, 0);
for(CHANNELINDEX chn = 0; chn < 4; chn++)
{
ModCommand &m = rowBase[chn];
ReadMODPatternEntry(patternData[row][chn], m);
if(!m.param || m.command == 0x0E)
{
autoSlide[chn] = 0;
}
if(m.command || m.param)
{
if(autoSlide[chn] != 0)
{
if(autoSlide[chn] & 0xF0)
{
m.volcmd = VOLCMD_VOLSLIDEUP;
m.vol = autoSlide[chn] >> 4;
} else
{
m.volcmd = VOLCMD_VOLSLIDEDOWN;
m.vol = autoSlide[chn] & 0x0F;
}
}
if(m.command == 0x0D)
{
if(minVersion != ST2_00)
{
// Dxy is volume slide in some Soundtracker versions, D00 is a pattern break in the latest versions.
m.command = 0x0A;
} else
{
m.param = 0;
}
} else if(m.command == 0x0C)
{
// Volume is sent as-is to the chip, which ignores the highest bit.
m.param &= 0x7F;
} else if(m.command == 0x0E && (m.param > 0x01 || minVersion < ST_IX))
{
// Import auto-slides as normal slides and fake them using volume column slides.
m.command = 0x0A;
autoSlide[chn] = m.param;
} else if(m.command == 0x0F)
{
// Only the low nibble is evaluated in Soundtracker.
m.param &= 0x0F;
}
if(minVersion <= UST1_80)
{
// UST effects
switch(m.command)
{
case 0:
// jackdance.mod by Karsten Obarski has 0xy arpeggios...
if(m.param < 0x03)
{
m.command = CMD_NONE;
} else
{
m.command = CMD_ARPEGGIO;
}
break;
case 1:
m.command = CMD_ARPEGGIO;
break;
case 2:
if(m.param & 0x0F)
{
m.command = CMD_PORTAMENTOUP;
m.param &= 0x0F;
} else if(m.param >> 4)
{
m.command = CMD_PORTAMENTODOWN;
m.param >>= 4;
}
break;
default:
m.command = CMD_NONE;
break;
}
} else
{
ConvertModCommand(m);
}
} else
{
autoSlide[chn] = 0;
}
}
}
}
const MPT_UCHAR_TYPE *madeWithTracker = UL_("");
switch(minVersion)
{
case UST1_00:
madeWithTracker = UL_("Ultimate Soundtracker 1.0-1.21");
break;
case UST1_80:
madeWithTracker = UL_("Ultimate Soundtracker 1.8-2.0");
break;
case ST2_00_Exterminator:
madeWithTracker = UL_("SoundTracker 2.0 / D.O.C. SoundTracker II");
break;
case ST_III:
madeWithTracker = UL_("Defjam Soundtracker III / Alpha Flight SoundTracker IV / D.O.C. SoundTracker IV / VI");
break;
case ST_IX:
madeWithTracker = UL_("D.O.C. SoundTracker IX");
break;
case MST1_00:
madeWithTracker = UL_("Master Soundtracker 1.0");
break;
case ST2_00:
madeWithTracker = UL_("SoundTracker 2.0 / 2.1 / 2.2");
break;
}
m_modFormat.formatName = U_("Soundtracker");
m_modFormat.type = U_("stk");
m_modFormat.madeWithTracker = madeWithTracker;
m_modFormat.charset = mpt::CharsetISO8859_1;
// Reading samples
if(loadFlags & loadSampleData)
{
for(SAMPLEINDEX smp = 1; smp <= 15; smp++)
{
// Looped samples in (Ultimate) Soundtracker seem to ignore all sample data before the actual loop start.
// This avoids the clicks in the first sample of pretend.mod by Karsten Obarski.
file.Skip(Samples[smp].nLoopStart);
Samples[smp].nLength -= Samples[smp].nLoopStart;
Samples[smp].nLoopEnd -= Samples[smp].nLoopStart;
Samples[smp].nLoopStart = 0;
MODSampleHeader::GetSampleFormat().ReadSample(Samples[smp], file);
}
}
return true;
}
CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderICE(MemoryFileReader file, const uint64 *pfilesize)
{
if(!file.CanRead(1464 + 4))
{
return ProbeWantMoreData;
}
file.Seek(1464);
char magic[4];
file.ReadArray(magic);
if(!IsMagic(magic, "MTN\0") && !IsMagic(magic, "IT10"))
{
return ProbeFailure;
}
file.Seek(20);
uint32 invalidBytes = 0;
for(SAMPLEINDEX smp = 1; smp <= 31; smp++)
{
MODSampleHeader sampleHeader;
if(!file.ReadStruct(sampleHeader))
{
return ProbeWantMoreData;
}
invalidBytes += sampleHeader.GetInvalidByteScore();
}
if(invalidBytes > MODSampleHeader::INVALID_BYTE_THRESHOLD)
{
return ProbeFailure;
}
const uint8 numOrders = file.ReadUint8();
const uint8 numTracks = file.ReadUint8();
if(numOrders > 128)
{
return ProbeFailure;
}
uint8 tracks[128 * 4];
file.ReadArray(tracks);
for(auto track : tracks)
{
if(track > numTracks)
{
return ProbeFailure;
}
}
MPT_UNREFERENCED_PARAMETER(pfilesize);
return ProbeSuccess;
}
// SoundTracker 2.6 / Ice Tracker variation of the MOD format
// The only real difference to other SoundTracker formats is the way patterns are stored:
// Every pattern consists of four independent, re-usable tracks.
bool CSoundFile::ReadICE(FileReader &file, ModLoadingFlags loadFlags)
{
char magic[4];
if(!file.Seek(1464) || !file.ReadArray(magic))
{
return false;
}
InitializeGlobals(MOD_TYPE_MOD);
m_playBehaviour.reset(kMODOneShotLoops);
m_playBehaviour.set(kMODIgnorePanning);
m_playBehaviour.set(kMODSampleSwap); // untested
if(IsMagic(magic, "MTN\0"))
{
m_modFormat.formatName = U_("MnemoTroN SoundTracker");
m_modFormat.type = U_("st26");
m_modFormat.madeWithTracker = U_("SoundTracker 2.6");
m_modFormat.charset = mpt::CharsetISO8859_1;
} else if(IsMagic(magic, "IT10"))
{
m_modFormat.formatName = U_("Ice Tracker");
m_modFormat.type = U_("ice");
m_modFormat.madeWithTracker = U_("Ice Tracker 1.0 / 1.1");
m_modFormat.charset = mpt::CharsetISO8859_1;
} else
{
return false;
}
// Reading song title
file.Seek(0);
file.ReadString<mpt::String::spacePadded>(m_songName, 20);
// Load Samples
m_nSamples = 31;
uint32 invalidBytes = 0;
for(SAMPLEINDEX smp = 1; smp <= 31; smp++)
{
MODSampleHeader sampleHeader;
invalidBytes += ReadSample(file, sampleHeader, Samples[smp], m_szNames[smp], true);
}
if(invalidBytes > MODSampleHeader::INVALID_BYTE_THRESHOLD)
{
return false;
}
const uint8 numOrders = file.ReadUint8();
const uint8 numTracks = file.ReadUint8();
if(numOrders > 128)
{
return false;
}
uint8 tracks[128 * 4];
file.ReadArray(tracks);
for(auto track : tracks)
{
if(track > numTracks)
{
return false;
}
}
if(loadFlags == onlyVerifyHeader)
{
return true;
}
// Now we can be pretty sure that this is a valid MOD file. Set up default song settings.
m_nChannels = 4;
m_nInstruments = 0;
m_nDefaultSpeed = 6;
m_nDefaultTempo.Set(125);
m_nMinPeriod = 14 * 4;
m_nMaxPeriod = 3424 * 4;
m_nSamplePreAmp = 64;
m_SongFlags.set(SONG_PT_MODE);
// Setup channel pan positions and volume
SetupMODPanning();
// Reading patterns
Order().resize(numOrders);
uint8 speed[2] = { 0, 0 }, speedPos = 0;
Patterns.ResizeArray(numOrders);
for(PATTERNINDEX pat = 0; pat < numOrders; pat++)
{
Order()[pat] = pat;
if(!Patterns.Insert(pat, 64))
continue;
for(CHANNELINDEX chn = 0; chn < 4; chn++)
{
file.Seek(1468 + tracks[pat * 4 + chn] * 64u * 4u);
ModCommand *m = Patterns[pat].GetpModCommand(0, chn);
for(ROWINDEX row = 0; row < 64; row++, m += 4)
{
ReadMODPatternEntry(file, *m);
if((m->command || m->param)
&& !(m->command == 0x0E && m->param >= 0x10) // Exx only sets filter
&& !(m->command >= 0x05 && m->command <= 0x09)) // These don't exist in ST2.6
{
ConvertModCommand(*m);
} else
{
m->command = CMD_NONE;
}
}
}
// Handle speed command with both nibbles set - this enables auto-swing (alternates between the two nibbles)
auto m = Patterns[pat].begin();
for(ROWINDEX row = 0; row < 64; row++)
{
for(CHANNELINDEX chn = 0; chn < 4; chn++, m++)
{
if(m->command == CMD_SPEED || m->command == CMD_TEMPO)
{
m->command = CMD_SPEED;
speedPos = 0;
if(m->param & 0xF0)
{
if((m->param >> 4) != (m->param & 0x0F) && (m->param & 0x0F) != 0)
{
// Both nibbles set
speed[0] = m->param >> 4;
speed[1] = m->param & 0x0F;
speedPos = 1;
}
m->param >>= 4;
}
}
}
if(speedPos)
{
Patterns[pat].WriteEffect(EffectWriter(CMD_SPEED, speed[speedPos - 1]).Row(row));
speedPos++;
if(speedPos == 3) speedPos = 1;
}
}
}
// Reading samples
if(loadFlags & loadSampleData)
{
file.Seek(1468 + numTracks * 64u * 4u);
for(SAMPLEINDEX smp = 1; smp <= 31; smp++) if(Samples[smp].nLength)
{
SampleIO(
SampleIO::_8bit,
SampleIO::mono,
SampleIO::littleEndian,
SampleIO::signedPCM)
.ReadSample(Samples[smp], file);
}
}
return true;
}
struct PT36Header
{
char magicFORM[4]; // "FORM"
uint32be size;
char magicMODL[4]; // "MODL"
};
MPT_BINARY_STRUCT(PT36Header, 12)
static bool ValidateHeader(const PT36Header &fileHeader)
{
if(std::memcmp(fileHeader.magicFORM, "FORM", 4))
{
return false;
}
if(std::memcmp(fileHeader.magicMODL, "MODL", 4))
{
return false;
}
return true;
}
CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderPT36(MemoryFileReader file, const uint64 *pfilesize)
{
PT36Header fileHeader;
if(!file.ReadStruct(fileHeader))
{
return ProbeWantMoreData;
}
if(!ValidateHeader(fileHeader))
{
return ProbeFailure;
}
MPT_UNREFERENCED_PARAMETER(pfilesize);
return ProbeSuccess;
}
// ProTracker 3.6 version of the MOD format
// Basically just a normal ProTracker mod with different magic, wrapped in an IFF file.
// The "PTDT" chunk is passed to the normal MOD loader.
bool CSoundFile::ReadPT36(FileReader &file, ModLoadingFlags loadFlags)
{
file.Rewind();
PT36Header fileHeader;
if(!file.ReadStruct(fileHeader))
{
return false;
}
if(!ValidateHeader(fileHeader))
{
return false;
}
bool ok = false, infoOk = false;
FileReader commentChunk;
mpt::ustring version;
PT36InfoChunk info;
MemsetZero(info);
// Go through IFF chunks...
PT36IffChunk iffHead;
if(!file.ReadStruct(iffHead))
{
return false;
}
// First chunk includes "MODL" magic in size
iffHead.chunksize -= 4;
do
{
// All chunk sizes include chunk header
iffHead.chunksize -= 8;
if(loadFlags == onlyVerifyHeader && iffHead.signature == PT36IffChunk::idPTDT)
{
return true;
}
FileReader chunk = file.ReadChunk(iffHead.chunksize);
if(!chunk.IsValid())
{
break;
}
switch(iffHead.signature)
{
case PT36IffChunk::idVERS:
chunk.Skip(4);
if(chunk.ReadMagic("PT") && iffHead.chunksize > 6)
{
chunk.ReadString<mpt::String::maybeNullTerminated>(version, mpt::CharsetISO8859_1, iffHead.chunksize - 6);
}
break;
case PT36IffChunk::idINFO:
infoOk = chunk.ReadStruct(info);
break;
case PT36IffChunk::idCMNT:
commentChunk = chunk;
break;
case PT36IffChunk::idPTDT:
ok = ReadMOD(chunk, loadFlags);
break;
}
} while(file.ReadStruct(iffHead));
if(version.empty())
{
version = U_("3.6");
}
// both an info chunk and a module are required
if(ok && infoOk)
{
bool vblank = (info.flags & 0x100) == 0;
m_playBehaviour.set(kMODVBlankTiming, vblank);
if(info.volume != 0)
m_nSamplePreAmp = std::min<uint16>(64, info.volume);
if(info.tempo != 0 && !vblank)
m_nDefaultTempo.Set(info.tempo);
if(info.name[0])
mpt::String::Read<mpt::String::maybeNullTerminated>(m_songName, info.name);
if(IsInRange(info.dateMonth, 1, 12) && IsInRange(info.dateDay, 1, 31) && IsInRange(info.dateHour, 0, 23)
&& IsInRange(info.dateMinute, 0, 59) && IsInRange(info.dateSecond, 0, 59))
{
FileHistory mptHistory;
mptHistory.loadDate.tm_year = info.dateYear;
mptHistory.loadDate.tm_mon = info.dateMonth - 1;
mptHistory.loadDate.tm_mday = info.dateDay;
mptHistory.loadDate.tm_hour = info.dateHour;
mptHistory.loadDate.tm_min = info.dateMinute;
mptHistory.loadDate.tm_sec = info.dateSecond;
m_FileHistory.push_back(mptHistory);
}
}
if(ok)
{
if(commentChunk.IsValid())
{
std::string author;
commentChunk.ReadString<mpt::String::maybeNullTerminated>(author, 32);
if(author != "UNNAMED AUTHOR")
m_songArtist = mpt::ToUnicode(mpt::CharsetISO8859_1, author);
if(!commentChunk.NoBytesLeft())
{
m_songMessage.ReadFixedLineLength(commentChunk, commentChunk.BytesLeft(), 40, 0);
}
}
m_modFormat.madeWithTracker = U_("ProTracker ") + version;
}
m_SongFlags.set(SONG_PT_MODE);
m_playBehaviour.set(kMODIgnorePanning);
m_playBehaviour.set(kMODOneShotLoops);
m_playBehaviour.set(kMODSampleSwap);
return ok;
}
#ifndef MODPLUG_NO_FILESAVE
bool CSoundFile::SaveMod(std::ostream &f) const
{
if(m_nChannels == 0)
{
return false;
}
// Write song title
{
char name[20];
mpt::String::Write<mpt::String::maybeNullTerminated>(name, m_songName);
mpt::IO::Write(f, name);
}
std::vector<SmpLength> sampleLength(32, 0);
std::vector<SAMPLEINDEX> sampleSource(32, 0);
if(GetNumInstruments())
{
INSTRUMENTINDEX lastIns = std::min(INSTRUMENTINDEX(31), GetNumInstruments());
for(INSTRUMENTINDEX ins = 1; ins <= lastIns; ins++) if (Instruments[ins])
{
// Find some valid sample associated with this instrument.
for(size_t i = 0; i < CountOf(Instruments[ins]->Keyboard); i++)
{
if(Instruments[ins]->Keyboard[i] > 0 && Instruments[ins]->Keyboard[i] <= GetNumSamples())
{
sampleSource[ins] = Instruments[ins]->Keyboard[i];
break;
}
}
}
} else
{
for(SAMPLEINDEX i = 1; i <= 31; i++)
{
sampleSource[i] = i;
}
}
// Write sample headers
for(SAMPLEINDEX smp = 1; smp <= 31; smp++)
{
MODSampleHeader sampleHeader;
mpt::String::Write<mpt::String::maybeNullTerminated>(sampleHeader.name, m_szNames[sampleSource[smp]]);
sampleLength[smp] = sampleHeader.ConvertToMOD(sampleSource[smp] <= GetNumSamples() ? GetSample(sampleSource[smp]) : ModSample(MOD_TYPE_MOD));
mpt::IO::Write(f, sampleHeader);
}
// Write order list
MODFileHeader fileHeader;
MemsetZero(fileHeader);
PATTERNINDEX writePatterns = 0;
uint8 writtenOrders = 0;
for(ORDERINDEX ord = 0; ord < Order().GetLength() && writtenOrders < 128; ord++)
{
// Ignore +++ and --- patterns in order list, as well as high patterns (MOD officially only supports up to 128 patterns)
if(ord == Order().GetRestartPos())
{
fileHeader.restartPos = writtenOrders;
}
if(Order()[ord] < 128)
{
fileHeader.orderList[writtenOrders++] = static_cast<uint8>(Order()[ord]);
if(writePatterns <= Order()[ord])
{
writePatterns = Order()[ord] + 1;
}
}
}
fileHeader.numOrders = writtenOrders;
mpt::IO::Write(f, fileHeader);
// Write magic bytes
char modMagic[4];
CHANNELINDEX writeChannels = std::min(CHANNELINDEX(99), GetNumChannels());
if(writeChannels == 4)
{
// ProTracker may not load files with more than 64 patterns correctly if we do not specify the M!K! magic.
if(writePatterns <= 64)
memcpy(modMagic, "M.K.", 4);
else
memcpy(modMagic, "M!K!", 4);
} else if(writeChannels < 10)
{
memcpy(modMagic, "0CHN", 4);
modMagic[0] += static_cast<char>(writeChannels);
} else
{
memcpy(modMagic, "00CH", 4);
modMagic[0] += static_cast<char>(writeChannels / 10u);
modMagic[1] += static_cast<char>(writeChannels % 10u);
}
mpt::IO::Write(f, modMagic);
// Write patterns
std::vector<uint8> events;
for(PATTERNINDEX pat = 0; pat < writePatterns; pat++)
{
if(!Patterns.IsValidPat(pat))
{
// Invent empty pattern
events.assign(writeChannels * 64 * 4, 0);
mpt::IO::Write(f, events);
continue;
}
for(ROWINDEX row = 0; row < 64; row++)
{
if(row >= Patterns[pat].GetNumRows())
{
// Invent empty row
events.assign(writeChannels * 4, 0);
mpt::IO::Write(f, events);
continue;
}
PatternRow rowBase = Patterns[pat].GetRow(row);
events.resize(writeChannels * 4);
size_t eventByte = 0;
for(CHANNELINDEX chn = 0; chn < writeChannels; chn++)
{
ModCommand &m = rowBase[chn];
uint8 command = m.command, param = m.param;
ModSaveCommand(command, param, false, true);
if(m.volcmd == VOLCMD_VOLUME && !command && !param)
{
// Maybe we can save some volume commands...
command = 0x0C;
param = MIN(m.vol, 64);
}
uint16 period = 0;
// Convert note to period
if(m.note >= 24 + NOTE_MIN && m.note < mpt::size(ProTrackerPeriodTable) + 24 + NOTE_MIN)
{
period = ProTrackerPeriodTable[m.note - 24 - NOTE_MIN];
}
uint8 instr = (m.instr <= 31) ? m.instr : 0;
events[eventByte++] = ((period >> 8) & 0x0F) | (instr & 0x10);
events[eventByte++] = period & 0xFF;
events[eventByte++] = ((instr & 0x0F) << 4) | (command & 0x0F);
events[eventByte++] = param;
}
mpt::IO::WriteRaw(f, mpt::as_span(events.data(), eventByte));
}
}
//Check for unsaved patterns
for(PATTERNINDEX pat = writePatterns; pat < Patterns.Size(); pat++)
{
if(Patterns.IsValidPat(pat))
{
AddToLog("Warning: This track contains at least one pattern after the highest pattern number referred to in the sequence. Such patterns are not saved in the MOD format.");
break;
}
}
// Writing samples
for(SAMPLEINDEX smp = 1; smp <= 31; smp++)
{
if(sampleLength[smp] == 0)
{
continue;
}
const ModSample &sample = Samples[sampleSource[smp]];
const mpt::IO::Offset sampleStart = mpt::IO::TellWrite(f);
const size_t writtenBytes = MODSampleHeader::GetSampleFormat().WriteSample(f, sample, sampleLength[smp]);
const int8 silence = 0;
// Write padding byte if the sample size is odd.
if((writtenBytes % 2u) != 0)
{
mpt::IO::Write(f, silence);
}
if(!sample.uFlags[CHN_LOOP] && writtenBytes >= 2)
{
// First two bytes of oneshot samples have to be 0 due to PT's one-shot loop
const mpt::IO::Offset sampleEnd = mpt::IO::TellWrite(f);
mpt::IO::SeekAbsolute(f, sampleStart);
mpt::IO::Write(f, silence);
mpt::IO::Write(f, silence);
mpt::IO::SeekAbsolute(f, sampleEnd);
}
}
return true;
}
#endif // MODPLUG_NO_FILESAVE
OPENMPT_NAMESPACE_END