winamp/Src/external_dependencies/openmpt-trunk/mptrack/MPTHacks.cpp
2024-09-24 14:54:57 +02:00

483 lines
13 KiB
C++

/*
* MPTHacks.cpp
* ------------
* Purpose: Find out if MOD/XM/S3M/IT modules have MPT-specific hacks and fix them.
* Notes : This is not finished yet. Still need to handle:
* - Out-of-range sample pre-amp settings
* - Comments in XM files
* - Many auto-fix actions (so that the auto-fix mode can actually be used at some point!)
* Maybe there should be two options if hacks are found: Convert the song to MPTM or remove hacks.
* Authors: OpenMPT Devs
* The OpenMPT source code is released under the BSD license. Read LICENSE for more details.
*/
#include "stdafx.h"
#include "Moddoc.h"
#include "../soundlib/modsmp_ctrl.h"
#include "../soundlib/mod_specifications.h"
OPENMPT_NAMESPACE_BEGIN
// Find and fix envelopes where two nodes are on the same tick.
bool FindIncompatibleEnvelopes(InstrumentEnvelope &env, bool autofix)
{
bool found = false;
for(uint32 i = 1; i < env.size(); i++)
{
if(env[i].tick <= env[i - 1].tick) // "<=" so we can fix envelopes "on the fly"
{
found = true;
if(autofix)
{
env[i].tick = env[i - 1].tick + 1;
}
}
}
return found;
}
// Go through the module to find out if it contains any hacks introduced by (Open)MPT
bool CModDoc::HasMPTHacks(const bool autofix)
{
const CModSpecifications *originalSpecs = &m_SndFile.GetModSpecifications();
// retrieve original (not hacked) specs.
MODTYPE modType = m_SndFile.GetBestSaveFormat();
switch(modType)
{
case MOD_TYPE_MOD:
originalSpecs = &ModSpecs::mod;
break;
case MOD_TYPE_XM:
originalSpecs = &ModSpecs::xm;
break;
case MOD_TYPE_S3M:
originalSpecs = &ModSpecs::s3m;
break;
case MOD_TYPE_IT:
originalSpecs = &ModSpecs::it;
break;
}
bool foundHacks = false, foundHere = false;
ClearLog();
// Check for plugins
#ifndef NO_PLUGINS
foundHere = false;
for(const auto &plug : m_SndFile.m_MixPlugins)
{
if(plug.IsValidPlugin())
{
foundHere = foundHacks = true;
break;
}
// REQUIRES AUTOFIX
}
if(foundHere)
AddToLog("Found plugins");
#endif // NO_PLUGINS
// Check for invalid order items
if(!originalSpecs->hasIgnoreIndex && mpt::contains(m_SndFile.Order(), m_SndFile.Order.GetIgnoreIndex()))
{
foundHacks = true;
AddToLog("This format does not support separator (+++) patterns");
if(autofix)
{
m_SndFile.Order().RemovePattern(m_SndFile.Order.GetIgnoreIndex());
}
}
if(!originalSpecs->hasStopIndex && m_SndFile.Order().GetLengthFirstEmpty() != m_SndFile.Order().GetLengthTailTrimmed())
{
foundHacks = true;
AddToLog("The pattern sequence should end after the first stop (---) index in this format.");
if(autofix)
{
m_SndFile.Order().RemovePattern(m_SndFile.Order.GetInvalidPatIndex());
}
}
// Global volume
if(modType == MOD_TYPE_XM && m_SndFile.m_nDefaultGlobalVolume != MAX_GLOBAL_VOLUME)
{
foundHacks = true;
AddToLog("XM format does not support default global volume");
if(autofix)
{
GlobalVolumeToPattern();
}
}
// Pattern count
if(m_SndFile.Patterns.GetNumPatterns() > originalSpecs->patternsMax)
{
AddToLog(MPT_AFORMAT("Found too many patterns ({} allowed)")(originalSpecs->patternsMax));
foundHacks = true;
// REQUIRES (INTELLIGENT) AUTOFIX
}
// Check for too big/small patterns
foundHere = false;
for(auto &pat : m_SndFile.Patterns)
{
if(pat.IsValid())
{
const ROWINDEX patSize = pat.GetNumRows();
if(patSize > originalSpecs->patternRowsMax)
{
foundHacks = foundHere = true;
if(autofix)
{
// REQUIRES (INTELLIGENT) AUTOFIX
} else
{
break;
}
} else if(patSize < originalSpecs->patternRowsMin)
{
foundHacks = foundHere = true;
if(autofix)
{
pat.Resize(originalSpecs->patternRowsMin);
pat.WriteEffect(EffectWriter(CMD_PATTERNBREAK, 0).Row(patSize - 1).RetryNextRow());
} else
{
break;
}
}
}
}
if(foundHere)
{
AddToLog(MPT_AFORMAT("Found incompatible pattern lengths (must be between {} and {} rows)")(originalSpecs->patternRowsMin, originalSpecs->patternRowsMax));
}
// Check for invalid pattern commands
foundHere = false;
m_SndFile.Patterns.ForEachModCommand([originalSpecs, &foundHere, autofix, modType] (ModCommand &m)
{
// definitely not perfect yet. :)
// Probably missing: Some extended effect parameters
if(!originalSpecs->HasNote(m.note))
{
foundHere = true;
if(autofix)
m.note = NOTE_NONE;
}
if(!originalSpecs->HasCommand(m.command))
{
foundHere = true;
if(autofix)
m.command = CMD_NONE;
}
if(!originalSpecs->HasVolCommand(m.volcmd))
{
foundHere = true;
if(autofix)
m.volcmd = VOLCMD_NONE;
}
if(modType == MOD_TYPE_XM) // ModPlug XM extensions
{
if(m.command == CMD_XFINEPORTAUPDOWN && m.param >= 0x30)
{
foundHere = true;
if(autofix)
m.command = CMD_NONE;
}
} else if(modType == MOD_TYPE_IT) // ModPlug IT extensions
{
if((m.command == CMD_S3MCMDEX) && ((m.param & 0xF0) == 0x90) && (m.param != 0x91))
{
foundHere = true;
if(autofix)
m.command = CMD_NONE;
}
}
});
if(foundHere)
{
AddToLog("Found invalid pattern commands");
foundHacks = true;
}
// Check for pattern names
const PATTERNINDEX numNamedPatterns = m_SndFile.Patterns.GetNumNamedPatterns();
if(numNamedPatterns > 0 && !originalSpecs->hasPatternNames)
{
AddToLog("Found pattern names");
foundHacks = true;
if(autofix)
{
for(PATTERNINDEX i = 0; i < numNamedPatterns; i++)
{
m_SndFile.Patterns[i].SetName("");
}
}
}
// Check for too many channels
if(m_SndFile.GetNumChannels() > originalSpecs->channelsMax || m_SndFile.GetNumChannels() < originalSpecs->channelsMin)
{
AddToLog(MPT_AFORMAT("Found incompatible channel count (must be between {} and {} channels)")(originalSpecs->channelsMin, originalSpecs->channelsMax));
foundHacks = true;
if(autofix)
{
std::vector<bool> usedChannels;
CheckUsedChannels(usedChannels);
RemoveChannels(usedChannels);
// REQUIRES (INTELLIGENT) AUTOFIX
}
}
// Check for channel names
foundHere = false;
for(CHANNELINDEX i = 0; i < m_SndFile.GetNumChannels(); i++)
{
if(!m_SndFile.ChnSettings[i].szName.empty())
{
foundHere = foundHacks = true;
if(autofix)
m_SndFile.ChnSettings[i].szName = "";
else
break;
}
}
if(foundHere)
AddToLog("Found channel names");
// Check for too many samples
if(m_SndFile.GetNumSamples() > originalSpecs->samplesMax)
{
AddToLog(MPT_AFORMAT("Found too many samples ({} allowed)")(originalSpecs->samplesMax));
foundHacks = true;
// REQUIRES (INTELLIGENT) AUTOFIX
}
// Check for sample extensions
foundHere = false;
for(SAMPLEINDEX i = 1; i <= m_SndFile.GetNumSamples(); i++)
{
ModSample &smp = m_SndFile.GetSample(i);
if(modType == MOD_TYPE_XM && smp.GetNumChannels() > 1)
{
foundHere = foundHacks = true;
if(autofix)
{
ctrlSmp::ConvertToMono(smp, m_SndFile, ctrlSmp::mixChannels);
} else
{
break;
}
}
}
if(foundHere)
AddToLog("Stereo samples are not supported in the original XM format");
// Check for too many instruments
if(m_SndFile.GetNumInstruments() > originalSpecs->instrumentsMax)
{
AddToLog(MPT_AFORMAT("Found too many instruments ({} allowed)")(originalSpecs->instrumentsMax));
foundHacks = true;
// REQUIRES (INTELLIGENT) AUTOFIX
}
// Check for instrument extensions
foundHere = false;
bool foundEnvelopes = false;
for(INSTRUMENTINDEX i = 1; i <= m_SndFile.GetNumInstruments(); i++)
{
ModInstrument *instr = m_SndFile.Instruments[i];
if(instr == nullptr) continue;
// Extended instrument attributes
if(instr->filterMode != FilterMode::Unchanged || instr->nVolRampUp != 0 || instr->resampling != SRCMODE_DEFAULT ||
instr->nCutSwing != 0 || instr->nResSwing != 0 || instr->nMixPlug != 0 || instr->pitchToTempoLock.GetRaw() != 0 ||
instr->nDCT == DuplicateCheckType::Plugin ||
instr->VolEnv.nReleaseNode != ENV_RELEASE_NODE_UNSET ||
instr->PanEnv.nReleaseNode != ENV_RELEASE_NODE_UNSET ||
instr->PitchEnv.nReleaseNode != ENV_RELEASE_NODE_UNSET
)
{
foundHere = foundHacks = true;
if(autofix)
{
instr->filterMode = FilterMode::Unchanged;
instr->nVolRampUp = 0;
instr->resampling = SRCMODE_DEFAULT;
instr->nCutSwing = 0;
instr->nResSwing = 0;
instr->nMixPlug = 0;
instr->pitchToTempoLock.Set(0);
if(instr->nDCT == DuplicateCheckType::Plugin) instr->nDCT = DuplicateCheckType::None;
instr->VolEnv.nReleaseNode = instr->PanEnv.nReleaseNode = instr->PitchEnv.nReleaseNode = ENV_RELEASE_NODE_UNSET;
}
}
// Incompatible envelope shape
foundEnvelopes |= FindIncompatibleEnvelopes(instr->VolEnv, autofix);
foundEnvelopes |= FindIncompatibleEnvelopes(instr->PanEnv, autofix);
foundEnvelopes |= FindIncompatibleEnvelopes(instr->PitchEnv, autofix);
foundHacks |= foundEnvelopes;
}
if(foundHere)
AddToLog("Found MPT instrument extensions");
if(foundEnvelopes)
AddToLog("Two envelope points may not share the same tick.");
// Check for too many orders
if(m_SndFile.Order().GetLengthTailTrimmed() > originalSpecs->ordersMax)
{
AddToLog(MPT_AFORMAT("Found too many orders ({} allowed)")(originalSpecs->ordersMax));
foundHacks = true;
if(autofix)
{
// Can we be more intelligent here and maybe remove stop patterns and such?
m_SndFile.Order().resize(originalSpecs->ordersMax);
}
}
// Check for invalid default tempo
if(m_SndFile.m_nDefaultTempo > originalSpecs->GetTempoMax() || m_SndFile.m_nDefaultTempo < originalSpecs->GetTempoMin())
{
AddToLog(MPT_AFORMAT("Found incompatible default tempo (must be between {} and {})")(originalSpecs->GetTempoMin().GetInt(), originalSpecs->GetTempoMax().GetInt()));
foundHacks = true;
if(autofix)
m_SndFile.m_nDefaultTempo = Clamp(m_SndFile.m_nDefaultTempo, originalSpecs->GetTempoMin(), originalSpecs->GetTempoMax());
}
// Check for invalid default speed
if(m_SndFile.m_nDefaultSpeed > originalSpecs->speedMax || m_SndFile.m_nDefaultSpeed < originalSpecs->speedMin)
{
AddToLog(MPT_AFORMAT("Found incompatible default speed (must be between {} and {})")(originalSpecs->speedMin, originalSpecs->speedMax));
foundHacks = true;
if(autofix)
m_SndFile.m_nDefaultSpeed = Clamp(m_SndFile.m_nDefaultSpeed, originalSpecs->speedMin, originalSpecs->speedMax);
}
// Check for invalid rows per beat / measure values
if(m_SndFile.m_nDefaultRowsPerBeat >= originalSpecs->patternRowsMax || m_SndFile.m_nDefaultRowsPerMeasure >= originalSpecs->patternRowsMax)
{
AddToLog("Found incompatible rows per beat / measure");
foundHacks = true;
if(autofix)
{
m_SndFile.m_nDefaultRowsPerBeat = Clamp(m_SndFile.m_nDefaultRowsPerBeat, 1u, (originalSpecs->patternRowsMax - 1));
m_SndFile.m_nDefaultRowsPerMeasure = Clamp(m_SndFile.m_nDefaultRowsPerMeasure, m_SndFile.m_nDefaultRowsPerBeat, (originalSpecs->patternRowsMax - 1));
}
}
// Find pattern-specific time signatures
if(!originalSpecs->hasPatternSignatures)
{
foundHere = false;
for(auto &pat : m_SndFile.Patterns)
{
if(pat.GetOverrideSignature())
{
if(!foundHere)
AddToLog("Found pattern-specific time signatures");
if(autofix)
pat.RemoveSignature();
foundHacks = foundHere = true;
if(!autofix)
break;
}
}
}
// Check for new tempo modes
if(m_SndFile.m_nTempoMode != TempoMode::Classic)
{
AddToLog("Found incompatible tempo mode (only classic tempo mode allowed)");
foundHacks = true;
if(autofix)
m_SndFile.m_nTempoMode = TempoMode::Classic;
}
// Check for extended filter range flag
if(m_SndFile.m_SongFlags[SONG_EXFILTERRANGE])
{
AddToLog("Found extended filter range");
foundHacks = true;
if(autofix)
m_SndFile.m_SongFlags.reset(SONG_EXFILTERRANGE);
}
// Player flags
if((modType & (MOD_TYPE_XM|MOD_TYPE_IT)) && !m_SndFile.m_playBehaviour[MSF_COMPATIBLE_PLAY])
{
AddToLog("Compatible play is deactivated");
foundHacks = true;
if(autofix)
m_SndFile.SetDefaultPlaybackBehaviour(modType);
}
// Check for restart position where it should not be
for(SEQUENCEINDEX seq = 0; seq < m_SndFile.Order.GetNumSequences(); seq++)
{
if(m_SndFile.Order(seq).GetRestartPos() > 0 && !originalSpecs->hasRestartPos)
{
AddToLog("Found restart position");
foundHacks = true;
if(autofix)
{
m_SndFile.Order.RestartPosToPattern(seq);
}
}
}
if(!originalSpecs->hasArtistName && !m_SndFile.m_songArtist.empty() && !(modType & (MOD_TYPE_MOD | MOD_TYPE_S3M)))
{
AddToLog("Found artist name");
foundHacks = true;
if(autofix)
{
m_SndFile.m_songArtist.clear();
}
}
if(m_SndFile.GetMixLevels() != MixLevels::Compatible && m_SndFile.GetMixLevels() != MixLevels::CompatibleFT2)
{
AddToLog("Found incorrect mix levels (only compatible mix levels allowed)");
foundHacks = true;
if(autofix)
m_SndFile.SetMixLevels(modType == MOD_TYPE_XM ? MixLevels::CompatibleFT2 : MixLevels::Compatible);
}
// Check for extended MIDI macros
if(modType == MOD_TYPE_IT)
{
for(const auto &macro : m_SndFile.m_MidiCfg)
{
for(const auto c : std::string_view{macro})
{
if(c == 's')
{
foundHacks = true;
AddToLog("Found SysEx checksum variable in MIDI macro");
break;
}
}
}
}
if(autofix && foundHacks)
SetModified();
return foundHacks;
}
OPENMPT_NAMESPACE_END