From 20d28e80a5c861a9d5f449ea911ab75b4f37ad0d Mon Sep 17 00:00:00 2001 From: Jef Date: Tue, 24 Sep 2024 14:54:57 +0200 Subject: Initial community commit --- .../openmpt-trunk/mptrack/MPTHacks.cpp | 482 +++++++++++++++++++++ 1 file changed, 482 insertions(+) create mode 100644 Src/external_dependencies/openmpt-trunk/mptrack/MPTHacks.cpp (limited to 'Src/external_dependencies/openmpt-trunk/mptrack/MPTHacks.cpp') diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/MPTHacks.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/MPTHacks.cpp new file mode 100644 index 00000000..27446f92 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/MPTHacks.cpp @@ -0,0 +1,482 @@ +/* + * 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 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 ¯o : 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 -- cgit