/* * 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