Skip to content

Commit

Permalink
[Imp] XM: Improve detection of MPT-made files, and tell them apart fr…
Browse files Browse the repository at this point in the history
…om files created with PlayerPRO.

git-svn-id: https://source.openmpt.org/svn/openmpt/trunk/OpenMPT@20747 56274372-70c3-4bfc-bfc3-4c3a0b034d27
  • Loading branch information
sagamusix committed May 9, 2024
1 parent 1d04a55 commit 5f34f22
Showing 1 changed file with 96 additions and 22 deletions.
118 changes: 96 additions & 22 deletions soundlib/Load_xm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -368,9 +368,10 @@ enum TrackerVersions
verFT2Generic = 0x20, // "FastTracker v2.00", but FastTracker has NOT been ruled out
verOther = 0x40, // Something we don't know, testing for DigiTrakker.
verFT2Clone = 0x80, // NOT FT2: itype changed between instruments, or \0 found in song title
verDigiTrakker = 0x100, // Probably DigiTrakker
verUNMO3 = 0x200, // TODO: UNMO3-ed XMs are detected as MPT 1.16
verEmptyOrders = 0x400, // Allow empty order list like in OpenMPT (FT2 just plays pattern 0 if the order list is empty according to the header)
verPlayerPRO = 0x100, // Could be PlayerPRO
verDigiTrakker = 0x200, // Probably DigiTrakker
verUNMO3 = 0x400, // TODO: UNMO3-ed XMs are detected as MPT 1.16
verEmptyOrders = 0x800, // Allow empty order list like in OpenMPT (FT2 just plays pattern 0 if the order list is empty according to the header)
};
DECLARE_FLAGSET(TrackerVersions)

Expand Down Expand Up @@ -605,13 +606,30 @@ bool CSoundFile::ReadXM(FileReader &file, ModLoadingFlags loadFlags)

if(!memcmp(fileHeader.trackerName, "FastTracker v2.00 ", 20) && fileHeader.size == 276)
{
const std::string_view songName{fileHeader.songName, sizeof(fileHeader.songName)};
if(fileHeader.version < 0x0104)
{
madeWith = verFT2Generic | verConfirmed;
else if(memchr(fileHeader.songName, '\0', 20) != nullptr)
} else if(const auto firstNull = songName.find('\0'); firstNull != std::string_view::npos)
{
// FT2 pads the song title with spaces, some other trackers use null chars
madeWith = verFT2Clone | verNewModPlug | verEmptyOrders;
else
madeWith = verFT2Generic | verNewModPlug;
// PlayerPRO filles the remaining buffer after the null terminator with space characters.
// PlayerPRO does not support song restart position.
if(fileHeader.restartPos)
madeWith = verFT2Clone | verNewModPlug | verEmptyOrders;
else if(firstNull == songName.size() - 1)
madeWith = verFT2Clone | verNewModPlug | verPlayerPRO | verEmptyOrders;
else if(songName.find_first_not_of(' ', firstNull + 1) == std::string_view::npos)
madeWith = verPlayerPRO | verConfirmed;
else
madeWith = verFT2Clone | verNewModPlug | verEmptyOrders;
} else
{
if(fileHeader.restartPos)
madeWith = verFT2Generic | verNewModPlug;
else
madeWith = verFT2Generic | verNewModPlug | verPlayerPRO;
}
} else if(!memcmp(fileHeader.trackerName, "FastTracker v 2.00 ", 20))
{
// MPT 1.0 (exact version to be determined later)
Expand Down Expand Up @@ -672,10 +690,8 @@ bool CSoundFile::ReadXM(FileReader &file, ModLoadingFlags loadFlags)
m_SongFlags.reset();
m_SongFlags.set(SONG_LINEARSLIDES, (fileHeader.flags & XMFileHeader::linearSlides) != 0);
m_SongFlags.set(SONG_EXFILTERRANGE, (fileHeader.flags & XMFileHeader::extendedFilterRange) != 0);
if(m_SongFlags[SONG_EXFILTERRANGE] && madeWith == (verFT2Generic | verNewModPlug))
{
madeWith = verFT2Clone | verNewModPlug | verConfirmed;
}
if(m_SongFlags[SONG_EXFILTERRANGE] && madeWith[verNewModPlug])
madeWith = verFT2Clone | verNewModPlug | verConfirmed | verEmptyOrders;

ReadOrderFromFile<uint8>(Order(), file, fileHeader.orders);
if(fileHeader.orders == 0 && !madeWith[verEmptyOrders])
Expand All @@ -695,9 +711,11 @@ bool CSoundFile::ReadXM(FileReader &file, ModLoadingFlags loadFlags)
// In case of XM versions < 1.04, we need to memorize the sample flags for all samples, as they are not stored immediately after the sample headers.
std::vector<SampleIO> sampleFlags;
uint8 sampleReserved = 0;
int instrType = -1;
int16 lastInstrType = -1, lastSampleReserved = -1;
int64 lastSampleHeaderSize = -1;
bool unsupportedSamples = false;
bool anyADPCM = false;
bool instrumentWithSamplesEncountered = false;

// Reading instruments
for(INSTRUMENTINDEX instr = 1; instr <= m_nInstruments; instr++)
Expand Down Expand Up @@ -743,11 +761,23 @@ bool CSoundFile::ReadXM(FileReader &file, ModLoadingFlags loadFlags)
else if(madeWith[verFT2Clone | verFT2Generic] && instrHeader.size != 33)
{
// Sure isn't FT2.
// Note: FT2 NORMALLY writes shdr=40 for all samples, but sometimes it
// just happens to write random garbage there instead. Surprise!
// Note: 4-mat's eternity.xm has an instrument header size of 29.
madeWith = verUnknown;
}
if(instrHeader.size != 33)
{
madeWith.reset(verPlayerPRO);
} else if(instrHeader.sampleHeaderSize > sizeof(XMSample) && madeWith[verPlayerPRO])
{
// Older PlayerPRO versions appear to write garbage in the sampleHeaderSize field, and it's different for each sample.
// Note: FT2 NORMALLY writes sampleHeaderSize=40 for all samples, but for any instruments before the first
// instrument that has numSamples != 0, sampleHeaderSize will be uninitialized. It will always be the same
// value, though.
// Note: 4-mat's eternity.xm has an instrument header size of 29 (without the previously described condition).
// Another module with that size is funky_dumbass.xm. Mysterious!
if(instrumentWithSamplesEncountered || (lastSampleHeaderSize != -1 && instrHeader.sampleHeaderSize != lastSampleHeaderSize))
madeWith = verPlayerPRO | verConfirmed;
lastSampleHeaderSize = instrHeader.sampleHeaderSize;
}
}

if(AllocateInstrument(instr) == nullptr)
Expand All @@ -757,10 +787,10 @@ bool CSoundFile::ReadXM(FileReader &file, ModLoadingFlags loadFlags)

instrHeader.ConvertToMPT(*Instruments[instr]);

if(instrType == -1)
if(lastInstrType == -1)
{
instrType = instrHeader.type;
} else if(instrType != instrHeader.type && madeWith[verFT2Generic])
lastInstrType = instrHeader.type;
} else if(lastInstrType != instrHeader.type && madeWith[verFT2Generic])
{
// FT2 writes some random junk for the instrument type field,
// but it's always the SAME junk for every instrument saved.
Expand All @@ -770,11 +800,24 @@ bool CSoundFile::ReadXM(FileReader &file, ModLoadingFlags loadFlags)

if(instrHeader.numSamples > 0)
{
instrumentWithSamplesEncountered = true;

// Yep, there are some samples associated with this instrument.

// If MIDI settings are present, this is definitely not an old MPT or PlayerPRO.
if((instrHeader.instrument.midiEnabled | instrHeader.instrument.midiChannel | instrHeader.instrument.midiProgram | instrHeader.instrument.muteComputer) != 0)
madeWith.reset(verOldModPlug | verNewModPlug | verPlayerPRO);
if(instrHeader.size != 263 || instrHeader.type != 0)
madeWith.reset(verPlayerPRO);
if(!madeWith[verConfirmed] && madeWith[verPlayerPRO])
{
// Definitely not an old MPT.
madeWith.reset(verOldModPlug | verNewModPlug);
// Note: Earlier (?) PlayerPRO versions do not seem to set the loop points to 0xFF (george_megas_-_q.xm)
if((!(instrHeader.instrument.volFlags & XMInstrument::envLoop) && instrHeader.instrument.volLoopStart == 0xFF && instrHeader.instrument.volLoopEnd == 0xFF)
|| (!(instrHeader.instrument.panFlags & XMInstrument::envLoop) && instrHeader.instrument.panLoopStart == 0xFF && instrHeader.instrument.panLoopEnd == 0xFF))
{
madeWith.set(verConfirmed);
madeWith.reset(verNewModPlug);
}
}

// Read sample headers
Expand Down Expand Up @@ -809,6 +852,18 @@ bool CSoundFile::ReadXM(FileReader &file, ModLoadingFlags loadFlags)
sampleSize[sample] = sampleHeader.length;
sampleReserved |= sampleHeader.reserved;

if(sampleHeader.reserved != 0 && sampleHeader.reserved != 0xAD)
madeWith.reset(verOldModPlug | verNewModPlug | verOpenMPT);

if(lastSampleReserved == -1)
lastSampleReserved = sampleHeader.reserved;
else if(lastSampleReserved != sampleHeader.reserved)
madeWith.reset(verPlayerPRO);
if(sampleHeader.pan != 128)
madeWith.reset(verPlayerPRO);
if((sampleHeader.finetune & 0x0F) && sampleHeader.finetune != 127)
madeWith.reset(verPlayerPRO);

if(sample < sampleSlots.size())
{
SAMPLEINDEX mptSample = sampleSlots[sample];
Expand All @@ -817,6 +872,13 @@ bool CSoundFile::ReadXM(FileReader &file, ModLoadingFlags loadFlags)
instrHeader.instrument.ApplyAutoVibratoToMPT(Samples[mptSample]);

m_szNames[mptSample] = mpt::String::ReadBuf(mpt::String::spacePadded, sampleHeader.name);
if(madeWith[verFT2Generic | verFT2Clone] && madeWith[verNewModPlug | verPlayerPRO] && !madeWith[verConfirmed]
&& (sampleHeader.reserved > 22 || std::find_if(std::begin(sampleHeader.name) + sampleHeader.reserved, std::end(sampleHeader.name), [](char c) { return c != ' '; }) != std::end(sampleHeader.name)))
{
// FT2 stores the sample name length here (it just copies the entire Pascal string, but that string might have ended with spaces even before space-padding it in the file, so we cannot do an exact length comparison)
madeWith.reset(verFT2Generic);
madeWith.set(verFT2Clone | verConfirmed);
}

if((sampleHeader.flags & 3) == 3 && madeWith[verNewModPlug])
madeWith.set(verModPlugBidiFlag);
Expand Down Expand Up @@ -878,6 +940,7 @@ bool CSoundFile::ReadXM(FileReader &file, ModLoadingFlags loadFlags)
{
m_songMessage.Read(file, file.ReadUint32LE(), SongMessage::leCR);
madeWith.set(verConfirmed);
madeWith.reset(verPlayerPRO);
}

// Read midi config: "MIDI"
Expand All @@ -888,6 +951,7 @@ bool CSoundFile::ReadXM(FileReader &file, ModLoadingFlags loadFlags)
m_MidiCfg.Sanitize();
hasMidiConfig = true;
madeWith.set(verConfirmed);
madeWith.reset(verPlayerPRO);
}

// Read pattern names: "PNAM"
Expand All @@ -902,6 +966,7 @@ bool CSoundFile::ReadXM(FileReader &file, ModLoadingFlags loadFlags)
Patterns[pat].SetName(patName);
}
madeWith.set(verConfirmed);
madeWith.reset(verPlayerPRO);
}

// Read channel names: "CNAM"
Expand All @@ -913,6 +978,7 @@ bool CSoundFile::ReadXM(FileReader &file, ModLoadingFlags loadFlags)
file.ReadString<mpt::String::maybeNullTerminated>(ChnSettings[chn].szName, MAX_CHANNELNAME);
}
madeWith.set(verConfirmed);
madeWith.reset(verPlayerPRO);
}

// Read mix plugins information
Expand All @@ -923,6 +989,7 @@ bool CSoundFile::ReadXM(FileReader &file, ModLoadingFlags loadFlags)
if(file.GetPosition() != oldPos)
{
madeWith.set(verConfirmed);
madeWith.reset(verPlayerPRO);
}
}

Expand All @@ -932,10 +999,17 @@ bool CSoundFile::ReadXM(FileReader &file, ModLoadingFlags loadFlags)
{
m_dwLastSavedWithVersion = MPT_V("1.11");
madeWithTracker = U_("ModPlug Tracker 1.0 - 1.11");
} else if(madeWith[verNewModPlug])
} else if(madeWith[verNewModPlug] && !madeWith[verPlayerPRO])
{
m_dwLastSavedWithVersion = MPT_V("1.16");
madeWithTracker = U_("ModPlug Tracker 1.0 - 1.16");
} else if(madeWith[verNewModPlug] && madeWith[verPlayerPRO])
{
m_dwLastSavedWithVersion = MPT_V("1.16");
madeWithTracker = U_("ModPlug Tracker 1.0 - 1.16 / PlayerPRO");
} else if(!madeWith[verNewModPlug] && madeWith[verPlayerPRO])
{
madeWithTracker = U_("PlayerPRO");
}
}

Expand Down Expand Up @@ -982,7 +1056,7 @@ bool CSoundFile::ReadXM(FileReader &file, ModLoadingFlags loadFlags)

if(madeWithTracker.empty())
{
if(madeWith[verDigiTrakker] && sampleReserved == 0 && (instrType ? instrType : -1) == -1)
if(madeWith[verDigiTrakker] && sampleReserved == 0 && (lastInstrType ? lastInstrType : -1) == -1)
{
madeWithTracker = U_("DigiTrakker");
} else if(madeWith[verFT2Generic])
Expand Down

0 comments on commit 5f34f22

Please sign in to comment.