/* * modcommand.cpp * -------------- * Purpose: Various functions for writing effects to patterns, converting ModCommands, etc. * Notes : (currently none) * Authors: OpenMPT Devs * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. */ #include "stdafx.h" #include "Sndfile.h" #include "mod_specifications.h" #include "Tables.h" OPENMPT_NAMESPACE_BEGIN const EffectType effectTypes[] = { EFFECT_TYPE_NORMAL, EFFECT_TYPE_NORMAL, EFFECT_TYPE_PITCH, EFFECT_TYPE_PITCH, EFFECT_TYPE_PITCH, EFFECT_TYPE_PITCH, EFFECT_TYPE_VOLUME, EFFECT_TYPE_VOLUME, EFFECT_TYPE_VOLUME, EFFECT_TYPE_PANNING, EFFECT_TYPE_NORMAL, EFFECT_TYPE_VOLUME, EFFECT_TYPE_GLOBAL, EFFECT_TYPE_VOLUME, EFFECT_TYPE_GLOBAL, EFFECT_TYPE_NORMAL, EFFECT_TYPE_GLOBAL, EFFECT_TYPE_GLOBAL, EFFECT_TYPE_NORMAL, EFFECT_TYPE_NORMAL, EFFECT_TYPE_NORMAL, EFFECT_TYPE_VOLUME, EFFECT_TYPE_VOLUME, EFFECT_TYPE_GLOBAL, EFFECT_TYPE_GLOBAL, EFFECT_TYPE_NORMAL, EFFECT_TYPE_PITCH, EFFECT_TYPE_PANNING, EFFECT_TYPE_PITCH, EFFECT_TYPE_PANNING, EFFECT_TYPE_NORMAL, EFFECT_TYPE_NORMAL, EFFECT_TYPE_NORMAL, EFFECT_TYPE_NORMAL, EFFECT_TYPE_NORMAL, EFFECT_TYPE_PITCH, EFFECT_TYPE_PITCH, EFFECT_TYPE_NORMAL, EFFECT_TYPE_PITCH, EFFECT_TYPE_PITCH, EFFECT_TYPE_PITCH, EFFECT_TYPE_PITCH, EFFECT_TYPE_NORMAL, EFFECT_TYPE_NORMAL, EFFECT_TYPE_NORMAL, EFFECT_TYPE_NORMAL, }; static_assert(std::size(effectTypes) == MAX_EFFECTS); const EffectType volumeEffectTypes[] = { EFFECT_TYPE_NORMAL, EFFECT_TYPE_VOLUME, EFFECT_TYPE_PANNING, EFFECT_TYPE_VOLUME, EFFECT_TYPE_VOLUME, EFFECT_TYPE_VOLUME, EFFECT_TYPE_VOLUME, EFFECT_TYPE_PITCH, EFFECT_TYPE_PITCH, EFFECT_TYPE_PANNING, EFFECT_TYPE_PANNING, EFFECT_TYPE_PITCH, EFFECT_TYPE_PITCH, EFFECT_TYPE_PITCH, EFFECT_TYPE_NORMAL, EFFECT_TYPE_NORMAL, }; static_assert(std::size(volumeEffectTypes) == MAX_VOLCMDS); EffectType ModCommand::GetEffectType(COMMAND cmd) { if(cmd < std::size(effectTypes)) return effectTypes[cmd]; else return EFFECT_TYPE_NORMAL; } EffectType ModCommand::GetVolumeEffectType(VOLCMD volcmd) { if(volcmd < std::size(volumeEffectTypes)) return volumeEffectTypes[volcmd]; else return EFFECT_TYPE_NORMAL; } // Convert an Exx command (MOD) to Sxx command (S3M) void ModCommand::ExtendedMODtoS3MEffect() { if(command != CMD_MODCMDEX) return; command = CMD_S3MCMDEX; switch(param & 0xF0) { case 0x00: command = CMD_NONE; break; // No filter control case 0x10: command = CMD_PORTAMENTOUP; param |= 0xF0; break; case 0x20: command = CMD_PORTAMENTODOWN; param |= 0xF0; break; case 0x30: param = (param & 0x0F) | 0x10; break; case 0x40: param = (param & 0x03) | 0x30; break; case 0x50: param = (param & 0x0F) | 0x20; break; case 0x60: param = (param & 0x0F) | 0xB0; break; case 0x70: param = (param & 0x03) | 0x40; break; case 0x90: command = CMD_RETRIG; param = (param & 0x0F); break; case 0xA0: if(param & 0x0F) { command = CMD_VOLUMESLIDE; param = (param << 4) | 0x0F; } else command = CMD_NONE; break; case 0xB0: if(param & 0x0F) { command = CMD_VOLUMESLIDE; param = 0xF0 | static_cast<PARAM>(std::min(param & 0x0F, 0x0E)); } else command = CMD_NONE; break; case 0xC0: if(param == 0xC0) { command = CMD_NONE; note = NOTE_NOTECUT; } break; // this does different things in IT and ST3 case 0xD0: if(param == 0xD0) { command = CMD_NONE; } break; // ditto // rest are the same or handled elsewhere } } // Convert an Sxx command (S3M) to Exx command (MOD) void ModCommand::ExtendedS3MtoMODEffect() { if(command != CMD_S3MCMDEX) return; command = CMD_MODCMDEX; switch(param & 0xF0) { case 0x10: param = (param & 0x0F) | 0x30; break; case 0x20: param = (param & 0x0F) | 0x50; break; case 0x30: param = (param & 0x0F) | 0x40; break; case 0x40: param = (param & 0x0F) | 0x70; break; case 0x50: command = CMD_XFINEPORTAUPDOWN; break; // map to unused X5x case 0x60: command = CMD_XFINEPORTAUPDOWN; break; // map to unused X6x case 0x80: command = CMD_PANNING8; param = (param & 0x0F) * 0x11; break; // FT2 does actually not support E8x case 0x90: command = CMD_XFINEPORTAUPDOWN; break; // map to unused X9x case 0xA0: command = CMD_XFINEPORTAUPDOWN; break; // map to unused XAx case 0xB0: param = (param & 0x0F) | 0x60; break; case 0x70: command = CMD_NONE; break; // No NNA / envelope control in MOD/XM format // rest are the same or handled elsewhere } } // Convert a mod command from one format to another. void ModCommand::Convert(MODTYPE fromType, MODTYPE toType, const CSoundFile &sndFile) { if(fromType == toType) { return; } if(fromType == MOD_TYPE_MTM) { // Special MTM fixups. // Retrigger with param 0 if(command == CMD_MODCMDEX && param == 0x90) { command = CMD_NONE; } else if(command == CMD_VIBRATO) { // Vibrato is approximately half as deep compared to MOD/S3M. uint8 speed = (param & 0xF0); uint8 depth = (param & 0x0F) >> 1; param = speed | depth; } // Apart from these special fixups, do a regular conversion from MOD. fromType = MOD_TYPE_MOD; } if(command == CMD_DIGIREVERSESAMPLE && toType != MOD_TYPE_DIGI) { command = CMD_S3MCMDEX; param = 0x9F; } // helper variables const bool oldTypeIsMOD = (fromType == MOD_TYPE_MOD), oldTypeIsXM = (fromType == MOD_TYPE_XM), oldTypeIsS3M = (fromType == MOD_TYPE_S3M), oldTypeIsIT = (fromType == MOD_TYPE_IT), oldTypeIsMPT = (fromType == MOD_TYPE_MPT), oldTypeIsMOD_XM = (oldTypeIsMOD || oldTypeIsXM), oldTypeIsS3M_IT_MPT = (oldTypeIsS3M || oldTypeIsIT || oldTypeIsMPT), oldTypeIsIT_MPT = (oldTypeIsIT || oldTypeIsMPT); const bool newTypeIsMOD = (toType == MOD_TYPE_MOD), newTypeIsXM = (toType == MOD_TYPE_XM), newTypeIsS3M = (toType == MOD_TYPE_S3M), newTypeIsIT = (toType == MOD_TYPE_IT), newTypeIsMPT = (toType == MOD_TYPE_MPT), newTypeIsMOD_XM = (newTypeIsMOD || newTypeIsXM), newTypeIsS3M_IT_MPT = (newTypeIsS3M || newTypeIsIT || newTypeIsMPT), newTypeIsIT_MPT = (newTypeIsIT || newTypeIsMPT); const CModSpecifications &newSpecs = CSoundFile::GetModSpecifications(toType); ////////////////////////// // Convert 8-bit Panning if(command == CMD_PANNING8) { if(newTypeIsS3M) { param = (param + 1) >> 1; } else if(oldTypeIsS3M) { if(param == 0xA4) { // surround remap command = static_cast<COMMAND>((toType & (MOD_TYPE_IT | MOD_TYPE_MPT)) ? CMD_S3MCMDEX : CMD_XFINEPORTAUPDOWN); param = 0x91; } else { param = mpt::saturate_cast<PARAM>(param * 2u); } } } // End if(command == CMD_PANNING8) // Re-map \xx to Zxx if the new format only knows the latter command. if(command == CMD_SMOOTHMIDI && !newSpecs.HasCommand(CMD_SMOOTHMIDI) && newSpecs.HasCommand(CMD_MIDI)) { command = CMD_MIDI; } /////////////////////////////////////////////////////////////////////////////////////// // MPTM to anything: Convert param control, extended envelope control, note delay+cut if(oldTypeIsMPT) { if(IsPcNote()) { COMMAND newCmd = static_cast<COMMAND>(note == NOTE_PC ? CMD_MIDI : CMD_SMOOTHMIDI); if(!newSpecs.HasCommand(newCmd)) { newCmd = CMD_MIDI; // assuming that this was CMD_SMOOTHMIDI if(!newSpecs.HasCommand(newCmd)) { newCmd = CMD_NONE; } } param = static_cast<PARAM>(std::min(static_cast<uint16>(maxColumnValue), GetValueEffectCol()) * 0x7F / maxColumnValue); command = newCmd; // might be removed later volcmd = VOLCMD_NONE; note = NOTE_NONE; instr = 0; } if((command == CMD_S3MCMDEX) && ((param & 0xF0) == 0x70) && ((param & 0x0F) > 0x0C)) { // Extended pitch envelope control commands param = 0x7C; } else if(command == CMD_DELAYCUT) { command = CMD_S3MCMDEX; // When converting to MOD/XM, this will be converted to CMD_MODCMDEX later param = 0xD0 | (param >> 4); // Preserve delay nibble } else if(command == CMD_FINETUNE || command == CMD_FINETUNE_SMOOTH) { // Convert finetune from +/-128th of a semitone to (extra-)fine portamento (assumes linear slides, plus we're missing the actual pitch wheel depth of the instrument) if(param < 0x80) { command = CMD_PORTAMENTODOWN; param = 0x80 - param; } else if(param > 0x80) { command = CMD_PORTAMENTOUP; param -= 0x80; } if(param <= 30) param = 0xE0 | ((param + 1u) / 2u); else param = 0xF0 | std::min(static_cast<PARAM>((param + 7u) / 8u), PARAM(15)); } } // End if(oldTypeIsMPT) ///////////////////////////////////////// // Convert MOD / XM to S3M / IT / MPTM if(oldTypeIsMOD_XM && newTypeIsS3M_IT_MPT) { switch(command) { case CMD_ARPEGGIO: if(!param) command = CMD_NONE; // 000 does nothing in MOD/XM break; case CMD_MODCMDEX: ExtendedMODtoS3MEffect(); break; case CMD_VOLUME: // Effect column volume command overrides the volume column in XM. if(volcmd == VOLCMD_NONE || volcmd == VOLCMD_VOLUME) { volcmd = VOLCMD_VOLUME; vol = param; if(vol > 64) vol = 64; command = CMD_NONE; param = 0; } else if(volcmd == VOLCMD_PANNING) { std::swap(vol, param); volcmd = VOLCMD_VOLUME; if(vol > 64) vol = 64; command = CMD_S3MCMDEX; param = 0x80 | (param / 4); // XM volcol panning is actually 4-Bit, so we can use 4-Bit panning here. } break; case CMD_PORTAMENTOUP: if(param > 0xDF) param = 0xDF; break; case CMD_PORTAMENTODOWN: if(param > 0xDF) param = 0xDF; break; case CMD_XFINEPORTAUPDOWN: switch(param & 0xF0) { case 0x10: command = CMD_PORTAMENTOUP; param = (param & 0x0F) | 0xE0; break; case 0x20: command = CMD_PORTAMENTODOWN; param = (param & 0x0F) | 0xE0; break; case 0x50: case 0x60: case 0x70: case 0x90: case 0xA0: command = CMD_S3MCMDEX; // Surround remap (this is the "official" command) if(toType & MOD_TYPE_S3M && param == 0x91) { command = CMD_PANNING8; param = 0xA4; } break; } break; case CMD_KEYOFF: if(note == NOTE_NONE) { note = newTypeIsS3M ? NOTE_NOTECUT : NOTE_KEYOFF; command = CMD_S3MCMDEX; if(param == 0) instr = 0; param = 0xD0 | (param & 0x0F); } break; case CMD_PANNINGSLIDE: // swap L/R, convert to fine slide if(param & 0xF0) { param = 0xF0 | std::min(PARAM(0x0E), static_cast<PARAM>(param >> 4)); } else { param = 0x0F | (std::min(PARAM(0x0E), static_cast<PARAM>(param & 0x0F)) << 4); } break; default: break; } } // End if(oldTypeIsMOD_XM && newTypeIsS3M_IT_MPT) ///////////////////////////////////////// // Convert S3M / IT / MPTM to MOD / XM else if(oldTypeIsS3M_IT_MPT && newTypeIsMOD_XM) { if(note == NOTE_NOTECUT) { // convert note cut to C00 if possible or volume command otherwise (MOD/XM has no real way of cutting notes that cannot be "undone" by volume commands) note = NOTE_NONE; if(command == CMD_NONE || !newTypeIsXM) { command = CMD_VOLUME; param = 0; } else { volcmd = VOLCMD_VOLUME; vol = 0; } } else if(note == NOTE_FADE) { // convert note fade to note off note = NOTE_KEYOFF; } switch(command) { case CMD_S3MCMDEX: ExtendedS3MtoMODEffect(); break; case CMD_TONEPORTAVOL: // Can't do fine slides and portamento/vibrato at the same time :( case CMD_VIBRATOVOL: // ditto if(volcmd == VOLCMD_NONE && (((param & 0xF0) && ((param & 0x0F) == 0x0F)) || ((param & 0x0F) && ((param & 0xF0) == 0xF0)))) { // Try to salvage portamento/vibrato if(command == CMD_TONEPORTAVOL) volcmd = VOLCMD_TONEPORTAMENTO; else if(command == CMD_VIBRATOVOL) volcmd = VOLCMD_VIBRATODEPTH; vol = 0; } [[fallthrough]]; case CMD_VOLUMESLIDE: if((param & 0xF0) && ((param & 0x0F) == 0x0F)) { command = CMD_MODCMDEX; param = (param >> 4) | 0xA0; } else if((param & 0x0F) && ((param & 0xF0) == 0xF0)) { command = CMD_MODCMDEX; param = (param & 0x0F) | 0xB0; } break; case CMD_PORTAMENTOUP: if(param >= 0xF0) { command = CMD_MODCMDEX; param = (param & 0x0F) | 0x10; } else if(param >= 0xE0) { if(newTypeIsXM) { command = CMD_XFINEPORTAUPDOWN; param = 0x10 | (param & 0x0F); } else { command = CMD_MODCMDEX; param = (((param & 0x0F) + 3) >> 2) | 0x10; } } else { command = CMD_PORTAMENTOUP; } break; case CMD_PORTAMENTODOWN: if(param >= 0xF0) { command = CMD_MODCMDEX; param = (param & 0x0F) | 0x20; } else if(param >= 0xE0) { if(newTypeIsXM) { command = CMD_XFINEPORTAUPDOWN; param = 0x20 | (param & 0x0F); } else { command = CMD_MODCMDEX; param = (((param & 0x0F) + 3) >> 2) | 0x20; } } else { command = CMD_PORTAMENTODOWN; } break; case CMD_TEMPO: if(param < 0x20) command = CMD_NONE; // no tempo slides break; case CMD_PANNINGSLIDE: // swap L/R, convert fine slides to normal slides if((param & 0x0F) == 0x0F && (param & 0xF0)) { param = (param >> 4); } else if((param & 0xF0) == 0xF0 && (param & 0x0F)) { param = (param & 0x0F) << 4; } else if(param & 0x0F) { param = 0xF0; } else if(param & 0xF0) { param = 0x0F; } else { param = 0; } break; case CMD_RETRIG: // Retrig: Q0y doesn't change volume in IT/S3M, but R0y in XM takes the last x parameter if(param != 0 && (param & 0xF0) == 0) { param |= 0x80; } break; default: break; } } // End if(oldTypeIsS3M_IT_MPT && newTypeIsMOD_XM) /////////////////////// // Convert IT to S3M else if(oldTypeIsIT_MPT && newTypeIsS3M) { if(note == NOTE_KEYOFF || note == NOTE_FADE) note = NOTE_NOTECUT; switch(command) { case CMD_S3MCMDEX: switch(param & 0xF0) { case 0x70: command = CMD_NONE; break; // No NNA / envelope control in S3M format case 0x90: if(param == 0x91) { // surround remap (this is the "official" command) command = CMD_PANNING8; param = 0xA4; } else if(param == 0x90) { command = CMD_PANNING8; param = 0x40; } break; } break; case CMD_GLOBALVOLUME: param = (std::min(PARAM(0x80), param) + 1) / 2u; break; default: break; } } // End if(oldTypeIsIT_MPT && newTypeIsS3M) ////////////////////// // Convert IT to XM if(oldTypeIsIT_MPT && newTypeIsXM) { switch(command) { case CMD_VIBRATO: // With linear slides, strength is roughly doubled. param = (param & 0xF0) | (((param & 0x0F) + 1) / 2u); break; case CMD_GLOBALVOLUME: param = (std::min(PARAM(0x80), param) + 1) / 2u; break; } } // End if(oldTypeIsIT_MPT && newTypeIsXM) ////////////////////// // Convert XM to IT if(oldTypeIsXM && newTypeIsIT_MPT) { switch(command) { case CMD_VIBRATO: // With linear slides, strength is roughly halved. param = (param & 0xF0) | std::min(static_cast<PARAM>((param & 0x0F) * 2u), PARAM(15)); break; case CMD_GLOBALVOLUME: param = std::min(PARAM(0x40), param) * 2u; break; } } // End if(oldTypeIsIT_MPT && newTypeIsXM) /////////////////////////////////// // MOD / XM Speed/Tempo limits if(newTypeIsMOD_XM) { switch(command) { case CMD_SPEED: param = std::min(param, PARAM(0x1F)); break; break; case CMD_TEMPO: param = std::max(param, PARAM(0x20)); break; } } /////////////////////////////////////////////////////////////////////// // Convert MOD to anything - adjust effect memory, remove Invert Loop if(oldTypeIsMOD) { switch(command) { case CMD_TONEPORTAVOL: // lacks memory -> 500 is the same as 300 if(param == 0x00) command = CMD_TONEPORTAMENTO; break; case CMD_VIBRATOVOL: // lacks memory -> 600 is the same as 400 if(param == 0x00) command = CMD_VIBRATO; break; case CMD_PORTAMENTOUP: // lacks memory -> remove case CMD_PORTAMENTODOWN: case CMD_VOLUMESLIDE: if(param == 0x00) command = CMD_NONE; break; case CMD_MODCMDEX: // This would turn into "Set Active Macro", so let's better remove it case CMD_S3MCMDEX: if((param & 0xF0) == 0xF0) command = CMD_NONE; break; } } // End if(oldTypeIsMOD && newTypeIsXM) ///////////////////////////////////////////////////////////////////// // Convert anything to MOD - remove volume column, remove Set Macro if(newTypeIsMOD) { // convert note off events if(IsSpecialNote()) { note = NOTE_NONE; // no effect present, so just convert note off to volume 0 if(command == CMD_NONE) { command = CMD_VOLUME; param = 0; // EDx effect present, so convert it to ECx } else if((command == CMD_MODCMDEX) && ((param & 0xF0) == 0xD0)) { param = 0xC0 | (param & 0x0F); } } if(command != CMD_NONE) switch(command) { case CMD_RETRIG: // MOD only has E9x command = CMD_MODCMDEX; param = 0x90 | (param & 0x0F); break; case CMD_MODCMDEX: // This would turn into "Invert Loop", so let's better remove it if((param & 0xF0) == 0xF0) command = CMD_NONE; break; } if(command == CMD_NONE) switch(volcmd) { case VOLCMD_VOLUME: command = CMD_VOLUME; param = vol; break; case VOLCMD_PANNING: command = CMD_PANNING8; param = vol < 64 ? vol << 2 : 255; break; case VOLCMD_VOLSLIDEDOWN: command = CMD_VOLUMESLIDE; param = vol; break; case VOLCMD_VOLSLIDEUP: command = CMD_VOLUMESLIDE; param = vol << 4; break; case VOLCMD_FINEVOLDOWN: command = CMD_MODCMDEX; param = 0xB0 | vol; break; case VOLCMD_FINEVOLUP: command = CMD_MODCMDEX; param = 0xA0 | vol; break; case VOLCMD_PORTADOWN: command = CMD_PORTAMENTODOWN; param = vol << 2; break; case VOLCMD_PORTAUP: command = CMD_PORTAMENTOUP; param = vol << 2; break; case VOLCMD_TONEPORTAMENTO: command = CMD_TONEPORTAMENTO; param = vol << 2; break; case VOLCMD_VIBRATODEPTH: command = CMD_VIBRATO; param = vol; break; case VOLCMD_VIBRATOSPEED: command = CMD_VIBRATO; param = vol << 4; break; } volcmd = VOLCMD_NONE; } // End if(newTypeIsMOD) /////////////////////////////////////////////////// // Convert anything to S3M - adjust volume column if(newTypeIsS3M) { if(command == CMD_NONE) switch(volcmd) { case VOLCMD_VOLSLIDEDOWN: command = CMD_VOLUMESLIDE; param = vol; volcmd = VOLCMD_NONE; break; case VOLCMD_VOLSLIDEUP: command = CMD_VOLUMESLIDE; param = vol << 4; volcmd = VOLCMD_NONE; break; case VOLCMD_FINEVOLDOWN: command = CMD_VOLUMESLIDE; param = 0xF0 | vol; volcmd = VOLCMD_NONE; break; case VOLCMD_FINEVOLUP: command = CMD_VOLUMESLIDE; param = (vol << 4) | 0x0F; volcmd = VOLCMD_NONE; break; case VOLCMD_PORTADOWN: command = CMD_PORTAMENTODOWN; param = vol << 2; volcmd = VOLCMD_NONE; break; case VOLCMD_PORTAUP: command = CMD_PORTAMENTOUP; param = vol << 2; volcmd = VOLCMD_NONE; break; case VOLCMD_TONEPORTAMENTO: command = CMD_TONEPORTAMENTO; param = vol << 2; volcmd = VOLCMD_NONE; break; case VOLCMD_VIBRATODEPTH: command = CMD_VIBRATO; param = vol; volcmd = VOLCMD_NONE; break; case VOLCMD_VIBRATOSPEED: command = CMD_VIBRATO; param = vol << 4; volcmd = VOLCMD_NONE; break; case VOLCMD_PANSLIDELEFT: command = CMD_PANNINGSLIDE; param = vol << 4; volcmd = VOLCMD_NONE; break; case VOLCMD_PANSLIDERIGHT: command = CMD_PANNINGSLIDE; param = vol; volcmd = VOLCMD_NONE; break; } } // End if(newTypeIsS3M) //////////////////////////////////////////////////////////////////////// // Convert anything to XM - adjust volume column, breaking EDx command if(newTypeIsXM) { // remove EDx if no note is next to it, or it will retrigger the note in FT2 mode if(command == CMD_MODCMDEX && (param & 0xF0) == 0xD0 && note == NOTE_NONE) { command = CMD_NONE; param = 0; } if(IsSpecialNote()) { // Instrument numbers next to Note Off reset instrument settings instr = 0; if(command == CMD_MODCMDEX && (param & 0xF0) == 0xD0) { // Note Off + Note Delay does nothing when using envelopes. note = NOTE_NONE; command = CMD_KEYOFF; param &= 0x0F; } } // Convert some commands which behave differently or don't exist if(command == CMD_NONE) switch(volcmd) { case VOLCMD_PORTADOWN: command = CMD_PORTAMENTODOWN; param = vol << 2; volcmd = VOLCMD_NONE; break; case VOLCMD_PORTAUP: command = CMD_PORTAMENTOUP; param = vol << 2; volcmd = VOLCMD_NONE; break; case VOLCMD_TONEPORTAMENTO: command = CMD_TONEPORTAMENTO; param = ImpulseTrackerPortaVolCmd[vol & 0x0F]; volcmd = VOLCMD_NONE; break; } } // End if(newTypeIsXM) /////////////////////////////////////////////////// // Convert anything to IT - adjust volume column if(newTypeIsIT_MPT) { // Convert some commands which behave differently or don't exist if(!oldTypeIsIT_MPT && command == CMD_NONE) switch(volcmd) { case VOLCMD_PANSLIDELEFT: command = CMD_PANNINGSLIDE; param = vol << 4; volcmd = VOLCMD_NONE; break; case VOLCMD_PANSLIDERIGHT: command = CMD_PANNINGSLIDE; param = vol; volcmd = VOLCMD_NONE; break; case VOLCMD_VIBRATOSPEED: command = CMD_VIBRATO; param = vol << 4; volcmd = VOLCMD_NONE; break; case VOLCMD_TONEPORTAMENTO: command = CMD_TONEPORTAMENTO; param = vol << 4; volcmd = VOLCMD_NONE; break; } switch(volcmd) { case VOLCMD_VOLSLIDEDOWN: case VOLCMD_VOLSLIDEUP: case VOLCMD_FINEVOLDOWN: case VOLCMD_FINEVOLUP: case VOLCMD_PORTADOWN: case VOLCMD_PORTAUP: case VOLCMD_TONEPORTAMENTO: case VOLCMD_VIBRATODEPTH: // OpenMPT-specific commands case VOLCMD_OFFSET: vol = std::min(vol, VOL(9)); break; } } // End if(newTypeIsIT_MPT) // Fix volume column offset for formats that don't have it. if(volcmd == VOLCMD_OFFSET && !newSpecs.HasVolCommand(VOLCMD_OFFSET) && (command == CMD_NONE || command == CMD_OFFSET || !newSpecs.HasCommand(command))) { const ModCommand::PARAM oldOffset = (command == CMD_OFFSET) ? param : 0; command = CMD_OFFSET; volcmd = VOLCMD_NONE; SAMPLEINDEX smp = instr; if(smp > 0 && smp <= sndFile.GetNumInstruments() && IsNote() && sndFile.Instruments[smp] != nullptr) smp = sndFile.Instruments[smp]->Keyboard[note - NOTE_MIN]; if(smp > 0 && smp <= sndFile.GetNumSamples() && vol <= std::size(ModSample().cues)) { const ModSample &sample = sndFile.GetSample(smp); if(vol == 0) param = mpt::saturate_cast<ModCommand::PARAM>(Util::muldivr_unsigned(sample.nLength, oldOffset, 65536u)); else param = mpt::saturate_cast<ModCommand::PARAM>((sample.cues[vol - 1] + (oldOffset * 256u) + 128u) / 256u); } else { param = vol << 3; } } if((command == CMD_REVERSEOFFSET || command == CMD_OFFSETPERCENTAGE) && !newSpecs.HasCommand(command)) { command = CMD_OFFSET; } if(!newSpecs.HasNote(note)) note = NOTE_NONE; // ensure the commands really exist in this format if(!newSpecs.HasCommand(command)) command = CMD_NONE; if(!newSpecs.HasVolCommand(volcmd)) volcmd = VOLCMD_NONE; } bool ModCommand::IsContinousCommand(const CSoundFile &sndFile) const { switch(command) { case CMD_ARPEGGIO: case CMD_TONEPORTAMENTO: case CMD_VIBRATO: case CMD_TREMOLO: case CMD_RETRIG: case CMD_TREMOR: case CMD_FINEVIBRATO: case CMD_PANBRELLO: case CMD_SMOOTHMIDI: case CMD_NOTESLIDEUP: case CMD_NOTESLIDEDOWN: case CMD_NOTESLIDEUPRETRIG: case CMD_NOTESLIDEDOWNRETRIG: return true; case CMD_PORTAMENTOUP: case CMD_PORTAMENTODOWN: if(!param && sndFile.GetType() == MOD_TYPE_MOD) return false; if(sndFile.GetType() & (MOD_TYPE_MOD | MOD_TYPE_XM | MOD_TYPE_MT2 | MOD_TYPE_MED | MOD_TYPE_AMF0 | MOD_TYPE_DIGI | MOD_TYPE_STP | MOD_TYPE_DTM)) return true; if(param >= 0xF0) return false; if(param >= 0xE0 && sndFile.GetType() != MOD_TYPE_DBM) return false; return true; case CMD_VOLUMESLIDE: case CMD_TONEPORTAVOL: case CMD_VIBRATOVOL: case CMD_GLOBALVOLSLIDE: case CMD_CHANNELVOLSLIDE: case CMD_PANNINGSLIDE: if(!param && sndFile.GetType() == MOD_TYPE_MOD) return false; if(sndFile.GetType() & (MOD_TYPE_MOD | MOD_TYPE_XM | MOD_TYPE_AMF0 | MOD_TYPE_MED | MOD_TYPE_DIGI)) return true; if((param & 0xF0) == 0xF0 && (param & 0x0F)) return false; if((param & 0x0F) == 0x0F && (param & 0xF0)) return false; return true; case CMD_TEMPO: return (param < 0x20); default: return false; } } bool ModCommand::IsContinousVolColCommand() const { switch(volcmd) { case VOLCMD_VOLSLIDEUP: case VOLCMD_VOLSLIDEDOWN: case VOLCMD_VIBRATOSPEED: case VOLCMD_VIBRATODEPTH: case VOLCMD_PANSLIDELEFT: case VOLCMD_PANSLIDERIGHT: case VOLCMD_TONEPORTAMENTO: case VOLCMD_PORTAUP: case VOLCMD_PORTADOWN: return true; default: return false; } } bool ModCommand::IsSlideUpDownCommand() const { switch(command) { case CMD_VOLUMESLIDE: case CMD_TONEPORTAVOL: case CMD_VIBRATOVOL: case CMD_GLOBALVOLSLIDE: case CMD_CHANNELVOLSLIDE: case CMD_PANNINGSLIDE: return true; default: return false; } } bool ModCommand::IsGlobalCommand(COMMAND command, PARAM param) { switch(command) { case CMD_POSITIONJUMP: case CMD_PATTERNBREAK: case CMD_SPEED: case CMD_TEMPO: case CMD_GLOBALVOLUME: case CMD_GLOBALVOLSLIDE: case CMD_MIDI: case CMD_SMOOTHMIDI: case CMD_DBMECHO: return true; case CMD_MODCMDEX: switch(param & 0xF0) { case 0x00: // LED Filter case 0x60: // Pattern Loop case 0xE0: // Row Delay return true; default: return false; } case CMD_XFINEPORTAUPDOWN: case CMD_S3MCMDEX: switch(param & 0xF0) { case 0x60: // Tick Delay case 0x90: // Sound Control case 0xB0: // Pattern Loop case 0xE0: // Row Delay return true; default: return false; } default: return false; } } // "Importance" of every FX command. Table is used for importing from formats with multiple effect colums // and is approximately the same as in SchismTracker. size_t ModCommand::GetEffectWeight(COMMAND cmd) { // Effect weights, sorted from lowest to highest weight. static constexpr COMMAND weights[] = { CMD_NONE, CMD_DUMMY, CMD_XPARAM, CMD_SETENVPOSITION, CMD_KEYOFF, CMD_TREMOLO, CMD_FINEVIBRATO, CMD_VIBRATO, CMD_XFINEPORTAUPDOWN, CMD_FINETUNE, CMD_FINETUNE_SMOOTH, CMD_PANBRELLO, CMD_S3MCMDEX, CMD_MODCMDEX, CMD_DELAYCUT, CMD_MIDI, CMD_SMOOTHMIDI, CMD_PANNINGSLIDE, CMD_PANNING8, CMD_NOTESLIDEUPRETRIG, CMD_NOTESLIDEUP, CMD_NOTESLIDEDOWNRETRIG, CMD_NOTESLIDEDOWN, CMD_PORTAMENTOUP, CMD_PORTAMENTODOWN, CMD_VOLUMESLIDE, CMD_VIBRATOVOL, CMD_VOLUME, CMD_DIGIREVERSESAMPLE, CMD_REVERSEOFFSET, CMD_OFFSETPERCENTAGE, CMD_OFFSET, CMD_TREMOR, CMD_RETRIG, CMD_ARPEGGIO, CMD_TONEPORTAMENTO, CMD_TONEPORTAVOL, CMD_DBMECHO, CMD_GLOBALVOLSLIDE, CMD_CHANNELVOLUME, CMD_GLOBALVOLSLIDE, CMD_GLOBALVOLUME, CMD_TEMPO, CMD_SPEED, CMD_POSITIONJUMP, CMD_PATTERNBREAK, }; static_assert(std::size(weights) == MAX_EFFECTS); for(size_t i = 0; i < std::size(weights); i++) { if(weights[i] == cmd) { return i; } } // Invalid / unknown command. return 0; } // Try to convert a fx column command (&effect) into a volume column command. // Returns true if successful. // Some commands can only be converted by losing some precision. // If moving the command into the volume column is more important than accuracy, use force = true. // (Code translated from SchismTracker and mainly supposed to be used with loaders ported from this tracker) bool ModCommand::ConvertVolEffect(uint8 &effect, uint8 ¶m, bool force) { switch(effect) { case CMD_NONE: effect = VOLCMD_NONE; return true; case CMD_VOLUME: effect = VOLCMD_VOLUME; param = std::min(param, PARAM(64)); break; case CMD_PORTAMENTOUP: // if not force, reject when dividing causes loss of data in LSB, or if the final value is too // large to fit. (volume column Ex/Fx are four times stronger than effect column) if(!force && ((param & 3) || param >= 0xE0)) return false; param /= 4; effect = VOLCMD_PORTAUP; break; case CMD_PORTAMENTODOWN: if(!force && ((param & 3) || param >= 0xE0)) return false; param /= 4; effect = VOLCMD_PORTADOWN; break; case CMD_TONEPORTAMENTO: if(param >= 0xF0) { // hack for people who can't type F twice :) effect = VOLCMD_TONEPORTAMENTO; param = 9; return true; } for(uint8 n = 0; n < 10; n++) { if(force ? (param <= ImpulseTrackerPortaVolCmd[n]) : (param == ImpulseTrackerPortaVolCmd[n])) { effect = VOLCMD_TONEPORTAMENTO; param = n; return true; } } return false; case CMD_VIBRATO: if(force) param = std::min(static_cast<PARAM>(param & 0x0F), PARAM(9)); else if((param & 0x0F) > 9 || (param & 0xF0) != 0) return false; param &= 0x0F; effect = VOLCMD_VIBRATODEPTH; break; case CMD_FINEVIBRATO: if(force) param = 0; else if(param) return false; effect = VOLCMD_VIBRATODEPTH; break; case CMD_PANNING8: if(param == 255) param = 64; else param /= 4; effect = VOLCMD_PANNING; break; case CMD_VOLUMESLIDE: if(param == 0) return false; if((param & 0xF) == 0) // Dx0 / Cx { param >>= 4; effect = VOLCMD_VOLSLIDEUP; } else if((param & 0xF0) == 0) // D0x / Dx { effect = VOLCMD_VOLSLIDEDOWN; } else if((param & 0xF) == 0xF) // DxF / Ax { param >>= 4; effect = VOLCMD_FINEVOLUP; } else if((param & 0xF0) == 0xF0) // DFx / Bx { param &= 0xF; effect = VOLCMD_FINEVOLDOWN; } else // ??? { return false; } break; case CMD_S3MCMDEX: switch (param >> 4) { case 8: effect = VOLCMD_PANNING; param = ((param & 0xF) << 2) + 2; return true; case 0: case 1: case 2: case 0xF: if(force) { effect = param = 0; return true; } break; default: break; } return false; default: return false; } return true; } // Try to combine two commands into one. Returns true on success and the combined command is placed in eff1 / param1. bool ModCommand::CombineEffects(uint8 &eff1, uint8 ¶m1, uint8 &eff2, uint8 ¶m2) { if(eff1 == CMD_VOLUMESLIDE && (eff2 == CMD_VIBRATO || eff2 == CMD_TONEPORTAVOL) && param2 == 0) { // Merge commands if(eff2 == CMD_VIBRATO) { eff1 = CMD_VIBRATOVOL; } else { eff1 = CMD_TONEPORTAVOL; } eff2 = CMD_NONE; return true; } else if(eff2 == CMD_VOLUMESLIDE && (eff1 == CMD_VIBRATO || eff1 == CMD_TONEPORTAVOL) && param1 == 0) { // Merge commands if(eff1 == CMD_VIBRATO) { eff1 = CMD_VIBRATOVOL; } else { eff1 = CMD_TONEPORTAVOL; } param1 = param2; eff2 = CMD_NONE; return true; } else if(eff1 == CMD_OFFSET && eff2 == CMD_S3MCMDEX && param2 == 0x9F) { // Reverse offset eff1 = CMD_REVERSEOFFSET; eff2 = CMD_NONE; return true; } else if(eff1 == CMD_S3MCMDEX && param1 == 0x9F && eff2 == CMD_OFFSET) { // Reverse offset eff1 = CMD_REVERSEOFFSET; param1 = param2; eff2 = CMD_NONE; return true; } else { return false; } } std::pair<EffectCommand, ModCommand::PARAM> ModCommand::TwoRegularCommandsToMPT(uint8 &effect1, uint8 ¶m1, uint8 &effect2, uint8 ¶m2) { for(uint8 n = 0; n < 4; n++) { if(ModCommand::ConvertVolEffect(effect1, param1, (n > 1))) { return {CMD_NONE, ModCommand::PARAM(0)}; } std::swap(effect1, effect2); std::swap(param1, param2); } // Can only keep one command :( if(GetEffectWeight(static_cast<COMMAND>(effect1)) > GetEffectWeight(static_cast<COMMAND>(effect2))) { std::swap(effect1, effect2); std::swap(param1, param2); } std::pair<EffectCommand, PARAM> lostCommand = {static_cast<EffectCommand>(effect1), param1}; effect1 = VOLCMD_NONE; param1 = 0; return lostCommand; } OPENMPT_NAMESPACE_END