Skip to content

Commit

Permalink
Merged revision(s) 20744 from trunk/OpenMPT:
Browse files Browse the repository at this point in the history
[Mod] XM: All MPT versions up to v1.11 set both normal and pingpong loop flags for pingpong-looped samples, not just MPT 1.09. Apart from this, a file made with MPT 1.06 or newer will be bit-identical when re-saved in MPT 1.16. Make the version reporting more precise.
........
Merged revision(s) 20747-20748 from trunk/OpenMPT:
[Imp] XM: Improve detection of MPT-made files, and tell them apart from files created with PlayerPRO.
........
[Var] XM: Document more cases of unusal instrument header sizes.
........



git-svn-id: https://source.openmpt.org/svn/openmpt/branches/OpenMPT-1.31@20759 56274372-70c3-4bfc-bfc3-4c3a0b034d27
  • Loading branch information
sagamusix committed May 11, 2024
1 parent 9bef879 commit c1787b1
Showing 1 changed file with 114 additions and 40 deletions.
154 changes: 114 additions & 40 deletions soundlib/Load_xm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -359,19 +359,20 @@ static void ReadXMPatterns(FileReader &file, const XMFileHeader &fileHeader, CSo

enum TrackerVersions
{
verUnknown = 0x00, // Probably not made with MPT
verOldModPlug = 0x01, // Made with MPT Alpha / Beta
verNewModPlug = 0x02, // Made with MPT (not Alpha / Beta)
verModPlug1_09 = 0x04, // Made with MPT 1.09 or possibly other version
verOpenMPT = 0x08, // Made with OpenMPT
verConfirmed = 0x10, // We are very sure that we found the correct tracker version.

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)
verUnknown = 0x00, // Probably not made with MPT
verOldModPlug = 0x01, // Made with MPT Alpha / Beta
verNewModPlug = 0x02, // Made with MPT (not Alpha / Beta)
verModPlugBidiFlag = 0x04, // MPT up to v1.11 sets both normal loop and pingpong loop flags
verOpenMPT = 0x08, // Made with OpenMPT
verConfirmed = 0x10, // We are very sure that we found the correct tracker version.

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
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 @@ -606,13 +607,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 @@ -673,10 +691,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 @@ -696,9 +712,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 @@ -744,11 +762,25 @@ 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.
// 4-mat's eternity.xm has an empty instruments with a header size of 29.
// Another module using that size is funky_dumbass.xm. Mysterious!
// Note: This may happen when the XM Commenter by Aka (XMC.EXE) adds empty instruments at the end of the list,
// which would explain the latter case, but in eternity.xm the empty slots are not at the end of the list.
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.
if(instrumentWithSamplesEncountered || (lastSampleHeaderSize != -1 && instrHeader.sampleHeaderSize != lastSampleHeaderSize))
madeWith = verPlayerPRO | verConfirmed;
lastSampleHeaderSize = instrHeader.sampleHeaderSize;
}
}

if(AllocateInstrument(instr) == nullptr)
Expand All @@ -758,24 +790,38 @@ 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.
// Note: This may happen when running an FT2-made XM through PutInst and adding new instrument slots.
madeWith.reset(verFT2Generic);
madeWith.set(verFT2Clone);
}

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 @@ -810,6 +856,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 @@ -818,12 +876,16 @@ bool CSoundFile::ReadXM(FileReader &file, ModLoadingFlags loadFlags)
instrHeader.instrument.ApplyAutoVibratoToMPT(Samples[mptSample]);

m_szNames[mptSample] = mpt::String::ReadBuf(mpt::String::spacePadded, sampleHeader.name);

if((sampleHeader.flags & 3) == 3 && madeWith[verNewModPlug])
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)))
{
// MPT 1.09 and maybe newer / older versions set both loop flags for bidi loops.
madeWith.set(verModPlug1_09);
// 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);
}
if(sampleFlags.back().GetEncoding() == SampleIO::ADPCM)
anyADPCM = true;
Expand Down Expand Up @@ -882,6 +944,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 @@ -892,6 +955,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 @@ -906,6 +970,7 @@ bool CSoundFile::ReadXM(FileReader &file, ModLoadingFlags loadFlags)
Patterns[pat].SetName(patName);
}
madeWith.set(verConfirmed);
madeWith.reset(verPlayerPRO);
}

// Read channel names: "CNAM"
Expand All @@ -917,6 +982,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 @@ -927,19 +993,27 @@ bool CSoundFile::ReadXM(FileReader &file, ModLoadingFlags loadFlags)
if(file.GetPosition() != oldPos)
{
madeWith.set(verConfirmed);
madeWith.reset(verPlayerPRO);
}
}

if(madeWith[verConfirmed])
{
if(madeWith[verModPlug1_09])
if(madeWith[verModPlugBidiFlag])
{
m_dwLastSavedWithVersion = MPT_V("1.09.00.00");
madeWithTracker = U_("ModPlug Tracker 1.09");
} else if(madeWith[verNewModPlug])
m_dwLastSavedWithVersion = MPT_V("1.11");
madeWithTracker = U_("ModPlug Tracker 1.0 - 1.11");
} else if(madeWith[verNewModPlug] && !madeWith[verPlayerPRO])
{
m_dwLastSavedWithVersion = MPT_V("1.16.00.00");
madeWithTracker = U_("ModPlug Tracker 1.10 - 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 @@ -986,7 +1060,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 c1787b1

Please sign in to comment.