/* * ModConvert.cpp * -------------- * Purpose: Converting between various module formats. * Notes : Incomplete list of MPTm-only features and extensions in the old formats: * Features only available for MPTm: * - User definable tunings. * - Extended pattern range * - Extended sequence * - Multiple sequences ("songs") * - Pattern-specific time signatures * - Pattern effects :xy, S7D, S7E * - Long instrument envelopes * - Envelope release node (this was previously also usable in the IT format, but is now deprecated in that format) * - Fractional tempo * - Song-specific resampling * - Alternative tempo modes (only usable in legacy XM / IT files) * * Extended features in IT/XM/S3M (not all listed below are available in all of those formats): * - Plugins * - Extended ranges for * - Sample count * - Instrument count * - Pattern count * - Sequence size * - Row count * - Channel count * - Tempo limits * - Extended sample/instrument properties. * - MIDI mapping directives * - Version info * - Channel names * - Pattern names * - For more info, see e.g. SaveExtendedSongProperties(), SaveExtendedInstrumentProperties() * 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 "Mainfrm.h" #include "InputHandler.h" #include "../tracklib/SampleEdit.h" #include "../soundlib/modsmp_ctrl.h" #include "../soundlib/mod_specifications.h" #include "ModConvert.h" OPENMPT_NAMESPACE_BEGIN // Trim envelopes and remove release nodes. static void UpdateEnvelopes(InstrumentEnvelope &mptEnv, const CModSpecifications &specs, std::bitset &warnings) { // shorten instrument envelope if necessary (for mod conversion) const uint8 envMax = specs.envelopePointsMax; #define TRIMENV(envLen) if(envLen >= envMax) { envLen = envMax - 1; warnings.set(wTrimmedEnvelopes); } if(mptEnv.size() > envMax) { mptEnv.resize(envMax); warnings.set(wTrimmedEnvelopes); } TRIMENV(mptEnv.nLoopStart); TRIMENV(mptEnv.nLoopEnd); TRIMENV(mptEnv.nSustainStart); TRIMENV(mptEnv.nSustainEnd); if(mptEnv.nReleaseNode != ENV_RELEASE_NODE_UNSET) { if(specs.hasReleaseNode) { TRIMENV(mptEnv.nReleaseNode); } else { mptEnv.nReleaseNode = ENV_RELEASE_NODE_UNSET; warnings.set(wReleaseNode); } } #undef TRIMENV } bool CModDoc::ChangeModType(MODTYPE nNewType) { std::bitset warnings; warnings.reset(); PATTERNINDEX nResizedPatterns = 0; const MODTYPE nOldType = m_SndFile.GetType(); if(nNewType == nOldType) return true; const bool oldTypeIsXM = (nOldType == MOD_TYPE_XM), oldTypeIsS3M = (nOldType == MOD_TYPE_S3M), oldTypeIsIT = (nOldType == MOD_TYPE_IT), oldTypeIsMPT = (nOldType == MOD_TYPE_MPT), oldTypeIsS3M_IT_MPT = (oldTypeIsS3M || oldTypeIsIT || oldTypeIsMPT), oldTypeIsIT_MPT = (oldTypeIsIT || oldTypeIsMPT); const bool newTypeIsMOD = (nNewType == MOD_TYPE_MOD), newTypeIsXM = (nNewType == MOD_TYPE_XM), newTypeIsS3M = (nNewType == MOD_TYPE_S3M), newTypeIsIT = (nNewType == MOD_TYPE_IT), newTypeIsMPT = (nNewType == MOD_TYPE_MPT), newTypeIsMOD_XM = (newTypeIsMOD || newTypeIsXM), newTypeIsIT_MPT = (newTypeIsIT || newTypeIsMPT); const CModSpecifications &specs = m_SndFile.GetModSpecifications(nNewType); // Check if conversion to 64 rows is necessary for(const auto &pat : m_SndFile.Patterns) { if(pat.IsValid() && pat.GetNumRows() != 64) nResizedPatterns++; } if((m_SndFile.GetNumInstruments() || nResizedPatterns) && (nNewType & (MOD_TYPE_MOD|MOD_TYPE_S3M))) { if(Reporting::Confirm( "This operation will convert all instruments to samples,\n" "and resize all patterns to 64 rows.\n" "Do you want to continue?", "Warning") != cnfYes) return false; BeginWaitCursor(); CriticalSection cs; // Converting instruments to samples if(m_SndFile.GetNumInstruments()) { ConvertInstrumentsToSamples(); warnings.set(wInstrumentsToSamples); } // Resizing all patterns to 64 rows for(auto &pat : m_SndFile.Patterns) if(pat.IsValid() && pat.GetNumRows() != 64) { ROWINDEX origRows = pat.GetNumRows(); pat.Resize(64); if(origRows < 64) { // Try to save short patterns by inserting a pattern break. pat.WriteEffect(EffectWriter(CMD_PATTERNBREAK, 0).Row(origRows - 1).RetryNextRow()); } warnings.set(wResizedPatterns); } // Removing all instrument headers from channels for(auto &chn : m_SndFile.m_PlayState.Chn) { chn.pModInstrument = nullptr; } for(INSTRUMENTINDEX nIns = 0; nIns <= m_SndFile.GetNumInstruments(); nIns++) { delete m_SndFile.Instruments[nIns]; m_SndFile.Instruments[nIns] = nullptr; } m_SndFile.m_nInstruments = 0; EndWaitCursor(); } //End if (((m_SndFile.m_nInstruments) || (b64)) && (nNewType & (MOD_TYPE_MOD|MOD_TYPE_S3M))) BeginWaitCursor(); ///////////////////////////// // Converting pattern data // When converting to MOD, get the new sample transpose setting right here so that we can compensate notes in the pattern. if(newTypeIsMOD && !oldTypeIsXM) { for(SAMPLEINDEX smp = 1; smp <= m_SndFile.GetNumSamples(); smp++) { m_SndFile.GetSample(smp).FrequencyToTranspose(); } } bool onlyAmigaNotes = true; for(auto &pat : m_SndFile.Patterns) if(pat.IsValid()) { // This is used for -> MOD/XM conversion std::vector> effMemory(GetNumChannels()); std::vector volMemory(GetNumChannels(), 0); std::vector instrMemory(GetNumChannels(), 0); bool addBreak = false; // When converting to XM, avoid the E60 bug. CHANNELINDEX chn = 0; ROWINDEX row = 0; for(auto m = pat.begin(); m != pat.end(); m++, chn++) { if(chn >= GetNumChannels()) { chn = 0; row++; } ModCommand::INSTR instr = m->instr; if(m->instr) instrMemory[chn] = instr; else instr = instrMemory[chn]; // Deal with volume column slide memory (it's not shared with the effect column) if(oldTypeIsIT_MPT && (newTypeIsMOD_XM || newTypeIsS3M)) { switch(m->volcmd) { case VOLCMD_VOLSLIDEUP: case VOLCMD_VOLSLIDEDOWN: case VOLCMD_FINEVOLUP: case VOLCMD_FINEVOLDOWN: if(m->vol == 0) m->vol = volMemory[chn]; else volMemory[chn] = m->vol; break; } } // Deal with MOD/XM commands without effect memory if(oldTypeIsS3M_IT_MPT && newTypeIsMOD_XM) { switch(m->command) { // No effect memory in XM / MOD case CMD_ARPEGGIO: case CMD_S3MCMDEX: case CMD_MODCMDEX: // These have effect memory in XM, but it is spread over several commands (for fine and extra-fine slides), so the easiest way to fix this is to just always use the previous value. case CMD_PORTAMENTOUP: case CMD_PORTAMENTODOWN: case CMD_VOLUMESLIDE: if(m->param == 0) m->param = effMemory[chn][m->command]; else effMemory[chn][m->command] = m->param; break; } } // Adjust effect memory for MOD files if(newTypeIsMOD) { switch(m->command) { case CMD_PORTAMENTOUP: case CMD_PORTAMENTODOWN: case CMD_TONEPORTAVOL: case CMD_VIBRATOVOL: case CMD_VOLUMESLIDE: // ProTracker doesn't have effect memory for these commands, so let's try to fix them if(m->param == 0) m->param = effMemory[chn][m->command]; else effMemory[chn][m->command] = m->param; break; } // Compensate for loss of transpose information if(m->IsNote() && instr && instr <= GetNumSamples()) { const int newNote = m->note + m_SndFile.GetSample(instr).RelativeTone; m->note = static_cast(Clamp(newNote, specs.noteMin, specs.noteMax)); } if(!m->IsAmigaNote()) { onlyAmigaNotes = false; } } m->Convert(nOldType, nNewType, m_SndFile); // When converting to XM, avoid the E60 bug. if(newTypeIsXM) { switch(m->command) { case CMD_MODCMDEX: if(m->param == 0x60 && row > 0) { addBreak = true; } break; case CMD_POSITIONJUMP: case CMD_PATTERNBREAK: addBreak = false; break; } } // Fix Row Delay commands when converting between MOD/XM and S3M/IT. // FT2 only considers the rightmost command, ST3/IT only the leftmost... if((nOldType & (MOD_TYPE_S3M | MOD_TYPE_IT | MOD_TYPE_MPT)) && (nNewType & (MOD_TYPE_MOD | MOD_TYPE_XM)) && m->command == CMD_MODCMDEX && (m->param & 0xF0) == 0xE0) { if(oldTypeIsIT_MPT || m->param != 0xE0) { // If the leftmost row delay command is SE0, ST3 ignores it, IT doesn't. // Delete all commands right of the first command auto p = m + 1; for(CHANNELINDEX c = chn + 1; c < m_SndFile.GetNumChannels(); c++, p++) { if(p->command == CMD_S3MCMDEX && (p->param & 0xF0) == 0xE0) { p->command = CMD_NONE; } } } } else if((nOldType & (MOD_TYPE_MOD | MOD_TYPE_XM)) && (nNewType & (MOD_TYPE_S3M | MOD_TYPE_IT | MOD_TYPE_MPT)) && m->command == CMD_S3MCMDEX && (m->param & 0xF0) == 0xE0) { // Delete all commands left of the last command auto p = m - 1; for(CHANNELINDEX c = 0; c < chn; c++, p--) { if(p->command == CMD_S3MCMDEX && (p->param & 0xF0) == 0xE0) { p->command = CMD_NONE; } } } } if(addBreak) { pat.WriteEffect(EffectWriter(CMD_PATTERNBREAK, 0).Row(pat.GetNumRows() - 1)); } } //////////////////////////////////////////////// // Converting instrument / sample / etc. data // Do some sample conversion const bool newTypeHasPingPongLoops = !(newTypeIsMOD || newTypeIsS3M); for(SAMPLEINDEX smp = 1; smp <= m_SndFile.GetNumSamples(); smp++) { ModSample &sample = m_SndFile.GetSample(smp); GetSampleUndo().PrepareUndo(smp, sundo_none, "Song Conversion"); // Too many samples? Only 31 samples allowed in MOD format... if(newTypeIsMOD && smp > 31 && sample.nLength > 0) { warnings.set(wMOD31Samples); } // No auto-vibrato in MOD/S3M if((newTypeIsMOD || newTypeIsS3M) && (sample.nVibDepth | sample.nVibRate | sample.nVibSweep) != 0) { warnings.set(wSampleAutoVibrato); } // No sustain loops for MOD/S3M/XM bool ignoreLoopConversion = false; if(newTypeIsMOD_XM || newTypeIsS3M) { // Sustain loops - convert to normal loops if(sample.uFlags[CHN_SUSTAINLOOP]) { warnings.set(wSampleSustainLoops); // Prepare conversion to regular loop if(!newTypeHasPingPongLoops) { ignoreLoopConversion = true; if(!SampleEdit::ConvertPingPongLoop(sample, m_SndFile, true)) warnings.set(wSampleBidiLoops); } } } // No ping-pong loops in MOD/S3M if(!ignoreLoopConversion && !newTypeHasPingPongLoops && sample.HasPingPongLoop()) { if(!SampleEdit::ConvertPingPongLoop(sample, m_SndFile, false)) warnings.set(wSampleBidiLoops); } if(newTypeIsMOD && sample.RelativeTone != 0) { warnings.set(wMODSampleFrequency); } if(!CSoundFile::SupportsOPL(nNewType) && sample.uFlags[CHN_ADLIB]) { warnings.set(wAdlibInstruments); } sample.Convert(nOldType, nNewType); } for(INSTRUMENTINDEX ins = 1; ins <= m_SndFile.GetNumInstruments(); ins++) { ModInstrument *pIns = m_SndFile.Instruments[ins]; if(pIns == nullptr) { continue; } // Convert IT/MPT to XM (fix instruments) if(newTypeIsXM) { for(size_t i = 0; i < std::size(pIns->NoteMap); i++) { if (pIns->NoteMap[i] && pIns->NoteMap[i] != (i + 1)) { warnings.set(wBrokenNoteMap); break; } } // Convert sustain loops to sustain "points" if(pIns->VolEnv.nSustainStart != pIns->VolEnv.nSustainEnd) { warnings.set(wInstrumentSustainLoops); } if(pIns->PanEnv.nSustainStart != pIns->PanEnv.nSustainEnd) { warnings.set(wInstrumentSustainLoops); } } // Convert MPT to anything - remove instrument tunings, Pitch/Tempo Lock, filter variation if(oldTypeIsMPT) { if(pIns->pTuning != nullptr) { warnings.set(wInstrumentTuning); } if(pIns->pitchToTempoLock.GetRaw() != 0) { warnings.set(wPitchToTempoLock); } if((pIns->nCutSwing | pIns->nResSwing) != 0) { warnings.set(wFilterVariation); } } pIns->Convert(nOldType, nNewType); } if(newTypeIsMOD) { // Not supported in MOD format auto firstPat = std::find_if(m_SndFile.Order().cbegin(), m_SndFile.Order().cend(), [this](PATTERNINDEX pat) { return m_SndFile.Patterns.IsValidPat(pat); }); bool firstPatValid = firstPat != m_SndFile.Order().cend(); bool lossy = false; if(m_SndFile.m_nDefaultSpeed != 6) { if(firstPatValid) { m_SndFile.Patterns[*firstPat].WriteEffect(EffectWriter(CMD_SPEED, ModCommand::PARAM(m_SndFile.m_nDefaultSpeed)).RetryNextRow()); } m_SndFile.m_nDefaultSpeed = 6; lossy = true; } if(m_SndFile.m_nDefaultTempo != TEMPO(125, 0)) { if(firstPatValid) { m_SndFile.Patterns[*firstPat].WriteEffect(EffectWriter(CMD_TEMPO, ModCommand::PARAM(m_SndFile.m_nDefaultTempo.GetInt())).RetryNextRow()); } m_SndFile.m_nDefaultTempo.Set(125); lossy = true; } if(m_SndFile.m_nDefaultGlobalVolume != MAX_GLOBAL_VOLUME || m_SndFile.m_nSamplePreAmp != 48 || m_SndFile.m_nVSTiVolume != 48) { m_SndFile.m_nDefaultGlobalVolume = MAX_GLOBAL_VOLUME; m_SndFile.m_nSamplePreAmp = 48; m_SndFile.m_nVSTiVolume = 48; lossy = true; } if(lossy) { warnings.set(wMODGlobalVars); } } // Is the "restart position" value allowed in this format? for(SEQUENCEINDEX seq = 0; seq < m_SndFile.Order.GetNumSequences(); seq++) { if(m_SndFile.Order(seq).GetRestartPos() > 0 && !specs.hasRestartPos) { // Try to fix it by placing a pattern jump command in the pattern. if(!m_SndFile.Order.RestartPosToPattern(seq)) { // Couldn't fix it! :( warnings.set(wRestartPos); } } } // Fix channel settings (pan/vol) for(CHANNELINDEX nChn = 0; nChn < GetNumChannels(); nChn++) { if(newTypeIsMOD_XM || newTypeIsS3M) { if(m_SndFile.ChnSettings[nChn].nVolume != 64 || m_SndFile.ChnSettings[nChn].dwFlags[CHN_SURROUND]) { m_SndFile.ChnSettings[nChn].nVolume = 64; m_SndFile.ChnSettings[nChn].dwFlags.reset(CHN_SURROUND); warnings.set(wChannelVolSurround); } } if(newTypeIsXM) { if(m_SndFile.ChnSettings[nChn].nPan != 128) { m_SndFile.ChnSettings[nChn].nPan = 128; warnings.set(wChannelPanning); } } } // Check for patterns with custom time signatures (fixing will be applied in the pattern container) if(!specs.hasPatternSignatures) { for(const auto &pat: m_SndFile.Patterns) { if(pat.GetOverrideSignature()) { warnings.set(wPatternSignatures); break; } } } // Check whether the new format supports embedding the edit history in the file. if(oldTypeIsIT_MPT && !newTypeIsIT_MPT && GetSoundFile().GetFileHistory().size() > 0) { warnings.set(wEditHistory); } if((nOldType & MOD_TYPE_XM) && m_SndFile.m_playBehaviour[kFT2VolumeRamping]) { warnings.set(wVolRamp); } CriticalSection cs; m_SndFile.ChangeModTypeTo(nNewType); // In case we need to update IT bidi loop handling pre-computation or loops got changed... m_SndFile.PrecomputeSampleLoops(false); // Song flags if(!(specs.songFlags & SONG_LINEARSLIDES) && m_SndFile.m_SongFlags[SONG_LINEARSLIDES]) { warnings.set(wLinearSlides); } if(oldTypeIsXM && newTypeIsIT_MPT) { m_SndFile.m_SongFlags.set(SONG_ITCOMPATGXX); } else if(newTypeIsMOD && GetNumChannels() == 4 && onlyAmigaNotes) { m_SndFile.m_SongFlags.set(SONG_ISAMIGA); m_SndFile.InitAmigaResampler(); } m_SndFile.m_SongFlags &= (specs.songFlags | SONG_PLAY_FLAGS); // Adjust mix levels if(newTypeIsMOD || newTypeIsS3M) { m_SndFile.SetMixLevels(MixLevels::Compatible); } if(oldTypeIsMPT && m_SndFile.GetMixLevels() != MixLevels::Compatible && m_SndFile.GetMixLevels() != MixLevels::CompatibleFT2) { warnings.set(wMixmode); } if(!specs.hasFractionalTempo && m_SndFile.m_nDefaultTempo.GetFract() != 0) { m_SndFile.m_nDefaultTempo.Set(m_SndFile.m_nDefaultTempo.GetInt(), 0); warnings.set(wFractionalTempo); } ChangeFileExtension(nNewType); // Check mod specifications Limit(m_SndFile.m_nDefaultTempo, specs.GetTempoMin(), specs.GetTempoMax()); Limit(m_SndFile.m_nDefaultSpeed, specs.speedMin, specs.speedMax); for(INSTRUMENTINDEX i = 1; i <= m_SndFile.GetNumInstruments(); i++) if(m_SndFile.Instruments[i] != nullptr) { UpdateEnvelopes(m_SndFile.Instruments[i]->VolEnv, specs, warnings); UpdateEnvelopes(m_SndFile.Instruments[i]->PanEnv, specs, warnings); UpdateEnvelopes(m_SndFile.Instruments[i]->PitchEnv, specs, warnings); } // XM requires instruments, so we create them right away. if(newTypeIsXM && GetNumInstruments() == 0) { ConvertSamplesToInstruments(); } // XM has no global volume if(newTypeIsXM && m_SndFile.m_nDefaultGlobalVolume != MAX_GLOBAL_VOLUME) { if(!GlobalVolumeToPattern()) { warnings.set(wGlobalVolumeNotSupported); } } // Resampling is only saved in MPTM if(!newTypeIsMPT && m_SndFile.m_nResampling != SRCMODE_DEFAULT) { warnings.set(wResamplingMode); m_SndFile.m_nResampling = SRCMODE_DEFAULT; } cs.Leave(); if(warnings[wResizedPatterns]) { AddToLog(LogInformation, MPT_UFORMAT("{} patterns have been resized to 64 rows")(nResizedPatterns)); } static constexpr struct { ConversionWarning warning; const char *mesage; } messages[] = { // Pattern warnings { wRestartPos, "Restart position is not supported by the new format." }, { wPatternSignatures, "Pattern-specific time signatures are not supported by the new format." }, { wChannelVolSurround, "Channel volume and surround are not supported by the new format." }, { wChannelPanning, "Channel panning is not supported by the new format." }, // Sample warnings { wSampleBidiLoops, "Sample bidi loops are not supported by the new format." }, { wSampleSustainLoops, "New format doesn't support sample sustain loops." }, { wSampleAutoVibrato, "New format doesn't support sample autovibrato." }, { wMODSampleFrequency, "Sample C-5 frequencies will be lost." }, { wMOD31Samples, "Samples above 31 will be lost when saving as MOD. Consider rearranging samples if there are unused slots available." }, { wAdlibInstruments, "OPL instruments are not supported by this format." }, // Instrument warnings { wInstrumentsToSamples, "All instruments have been converted to samples." }, { wTrimmedEnvelopes, "Instrument envelopes have been shortened." }, { wInstrumentSustainLoops, "Sustain loops were converted to sustain points." }, { wInstrumentTuning, "Instrument tunings will be lost." }, { wPitchToTempoLock, "Pitch / Tempo Lock instrument property is not supported by the new format." }, { wBrokenNoteMap, "Instrument Note Mapping is not supported by the new format." }, { wReleaseNode, "Instrument envelope release nodes are not supported by the new format." }, { wFilterVariation, "Random filter variation is not supported by the new format." }, // General warnings { wMODGlobalVars, "Default speed, tempo and global volume will be lost." }, { wLinearSlides, "Linear Frequency Slides not supported by the new format." }, { wEditHistory, "Edit history will not be saved in the new format." }, { wMixmode, "Consider setting the mix levels to \"Compatible\" in the song properties when working with legacy formats." }, { wVolRamp, "Fasttracker 2 compatible super soft volume ramping gets lost when converting XM files to another type." }, { wGlobalVolumeNotSupported, "Default global volume is not supported by the new format." }, { wResamplingMode, "Song-specific resampling mode is not supported by the new format." }, { wFractionalTempo, "Fractional tempo is not supported by the new format." }, }; for(const auto &msg : messages) { if(warnings[msg.warning]) AddToLog(LogInformation, mpt::ToUnicode(mpt::Charset::UTF8, msg.mesage)); } SetModified(); GetPatternUndo().ClearUndo(); UpdateAllViews(nullptr, GeneralHint().General().ModType()); EndWaitCursor(); // Update effect key commands CMainFrame::GetInputHandler()->SetEffectLetters(m_SndFile.GetModSpecifications()); return true; } OPENMPT_NAMESPACE_END