winamp/Src/external_dependencies/openmpt-trunk/mptrack/ModConvert.cpp

676 lines
20 KiB
C++
Raw Normal View History

2024-09-24 14:54:57 +02:00
/*
* 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<wNumWarnings> &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<wNumWarnings> 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<std::array<ModCommand::PARAM, MAX_EFFECTS>> effMemory(GetNumChannels());
std::vector<ModCommand::VOL> volMemory(GetNumChannels(), 0);
std::vector<ModCommand::INSTR> 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<ModCommand::NOTE>(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