/* * Snd_fx.cpp * ----------- * Purpose: Processing of pattern commands, song length calculation... * Notes : This needs some heavy refactoring. * I thought of actually adding an effect interface class. Every pattern effect * could then be moved into its own class that inherits from the effect interface. * If effect handling differs severly between module formats, every format would have * its own class for that effect. Then, a call chain of effect classes could be set up * for each format, since effects cannot be processed in the same order in all formats. * Authors: Olivier Lapicque * 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" #ifdef MODPLUG_TRACKER #include "../mptrack/Moddoc.h" #endif // MODPLUG_TRACKER #include "tuning.h" #include "Tables.h" #include "modsmp_ctrl.h" // For updating the loop wraparound data with the invert loop effect #include "plugins/PlugInterface.h" #include "OPL.h" #include "MIDIEvents.h" OPENMPT_NAMESPACE_BEGIN // Formats which have 7-bit (0...128) instead of 6-bit (0...64) global volume commands, or which are imported to this range (mostly formats which are converted to IT internally) #ifdef MODPLUG_TRACKER static constexpr auto GLOBALVOL_7BIT_FORMATS_EXT = MOD_TYPE_MT2; #else static constexpr auto GLOBALVOL_7BIT_FORMATS_EXT = MOD_TYPE_NONE; #endif // MODPLUG_TRACKER static constexpr auto GLOBALVOL_7BIT_FORMATS = MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_IMF | MOD_TYPE_J2B | MOD_TYPE_MID | MOD_TYPE_AMS | MOD_TYPE_DBM | MOD_TYPE_PTM | MOD_TYPE_MDL | MOD_TYPE_DTM | GLOBALVOL_7BIT_FORMATS_EXT; // Compensate frequency slide LUTs depending on whether we are handling periods or frequency - "up" and "down" in function name are seen from frequency perspective. static uint32 GetLinearSlideDownTable (const CSoundFile *sndFile, uint32 i) { MPT_ASSERT(i < std::size(LinearSlideDownTable)); return sndFile->m_playBehaviour[kPeriodsAreHertz] ? LinearSlideDownTable[i] : LinearSlideUpTable[i]; } static uint32 GetLinearSlideUpTable (const CSoundFile *sndFile, uint32 i) { MPT_ASSERT(i < std::size(LinearSlideDownTable)); return sndFile->m_playBehaviour[kPeriodsAreHertz] ? LinearSlideUpTable[i] : LinearSlideDownTable[i]; } static uint32 GetFineLinearSlideDownTable(const CSoundFile *sndFile, uint32 i) { MPT_ASSERT(i < std::size(FineLinearSlideDownTable)); return sndFile->m_playBehaviour[kPeriodsAreHertz] ? FineLinearSlideDownTable[i] : FineLinearSlideUpTable[i]; } static uint32 GetFineLinearSlideUpTable (const CSoundFile *sndFile, uint32 i) { MPT_ASSERT(i < std::size(FineLinearSlideDownTable)); return sndFile->m_playBehaviour[kPeriodsAreHertz] ? FineLinearSlideUpTable[i] : FineLinearSlideDownTable[i]; } //////////////////////////////////////////////////////////// // Length // Memory class for GetLength() code class GetLengthMemory { protected: const CSoundFile &sndFile; public: std::unique_ptr state; struct ChnSettings { uint32 ticksToRender = 0; // When using sample sync, we still need to render this many ticks bool incChanged = false; // When using sample sync, note frequency has changed uint8 vol = 0xFF; }; std::vector chnSettings; double elapsedTime; static constexpr uint32 IGNORE_CHANNEL = uint32_max; GetLengthMemory(const CSoundFile &sf) : sndFile(sf) , state(std::make_unique(sf.m_PlayState)) { Reset(); } void Reset() { if(state->m_midiMacroEvaluationResults) state->m_midiMacroEvaluationResults.emplace(); elapsedTime = 0.0; state->m_lTotalSampleCount = 0; state->m_nMusicSpeed = sndFile.m_nDefaultSpeed; state->m_nMusicTempo = sndFile.m_nDefaultTempo; state->m_nGlobalVolume = sndFile.m_nDefaultGlobalVolume; chnSettings.assign(sndFile.GetNumChannels(), ChnSettings()); const auto muteFlag = CSoundFile::GetChannelMuteFlag(); for(CHANNELINDEX chn = 0; chn < sndFile.GetNumChannels(); chn++) { state->Chn[chn].Reset(ModChannel::resetTotal, sndFile, chn, muteFlag); state->Chn[chn].nOldGlobalVolSlide = 0; state->Chn[chn].nOldChnVolSlide = 0; state->Chn[chn].nNote = state->Chn[chn].nNewNote = state->Chn[chn].nLastNote = NOTE_NONE; } } // Increment playback position of sample and envelopes on a channel void RenderChannel(CHANNELINDEX channel, uint32 tickDuration, uint32 portaStart = uint32_max) { ModChannel &chn = state->Chn[channel]; uint32 numTicks = chnSettings[channel].ticksToRender; if(numTicks == IGNORE_CHANNEL || numTicks == 0 || (!chn.IsSamplePlaying() && !chnSettings[channel].incChanged) || chn.pModSample == nullptr) { return; } const SamplePosition loopStart(chn.dwFlags[CHN_LOOP] ? chn.nLoopStart : 0u, 0); const SamplePosition sampleEnd(chn.dwFlags[CHN_LOOP] ? chn.nLoopEnd : chn.nLength, 0); const SmpLength loopLength = chn.nLoopEnd - chn.nLoopStart; const bool itEnvMode = sndFile.m_playBehaviour[kITEnvelopePositionHandling]; const bool updatePitchEnv = (chn.PitchEnv.flags & (ENV_ENABLED | ENV_FILTER)) == ENV_ENABLED; bool stopNote = false; SamplePosition inc = chn.increment * tickDuration; if(chn.dwFlags[CHN_PINGPONGFLAG]) inc.Negate(); for(uint32 i = 0; i < numTicks; i++) { bool updateInc = (chn.PitchEnv.flags & (ENV_ENABLED | ENV_FILTER)) == ENV_ENABLED; if(i >= portaStart) { chn.isFirstTick = false; const ModCommand &m = *sndFile.Patterns[state->m_nPattern].GetpModCommand(state->m_nRow, channel); auto command = m.command; if(m.volcmd == VOLCMD_TONEPORTAMENTO) { const auto [porta, clearEffectCommand] = sndFile.GetVolCmdTonePorta(m, 0); sndFile.TonePortamento(chn, porta); if(clearEffectCommand) command = CMD_NONE; } if(command == CMD_TONEPORTAMENTO) sndFile.TonePortamento(chn, m.param); else if(command == CMD_TONEPORTAVOL) sndFile.TonePortamento(chn, 0); updateInc = true; } int32 period = chn.nPeriod; if(itEnvMode) sndFile.IncrementEnvelopePositions(chn); if(updatePitchEnv) { sndFile.ProcessPitchFilterEnvelope(chn, period); updateInc = true; } if(!itEnvMode) sndFile.IncrementEnvelopePositions(chn); int vol = 0; sndFile.ProcessInstrumentFade(chn, vol); if(chn.dwFlags[CHN_ADLIB]) continue; if(updateInc || chnSettings[channel].incChanged) { if(chn.m_CalculateFreq || chn.m_ReCalculateFreqOnFirstTick) { chn.RecalcTuningFreq(1, 0, sndFile); if(!chn.m_CalculateFreq) chn.m_ReCalculateFreqOnFirstTick = false; else chn.m_CalculateFreq = false; } chn.increment = sndFile.GetChannelIncrement(chn, period, 0).first; chnSettings[channel].incChanged = false; inc = chn.increment * tickDuration; if(chn.dwFlags[CHN_PINGPONGFLAG]) inc.Negate(); } chn.position += inc; if(chn.position >= sampleEnd || (chn.position < loopStart && inc.IsNegative())) { if(!chn.dwFlags[CHN_LOOP]) { // Past sample end. stopNote = true; break; } // We exceeded the sample loop, go back to loop start. if(chn.dwFlags[CHN_PINGPONGLOOP]) { if(chn.position < loopStart) { chn.position = SamplePosition(chn.nLoopStart + chn.nLoopStart, 0) - chn.position; chn.dwFlags.flip(CHN_PINGPONGFLAG); inc.Negate(); } SmpLength posInt = chn.position.GetUInt() - chn.nLoopStart; SmpLength pingpongLength = loopLength * 2; if(sndFile.m_playBehaviour[kITPingPongMode]) pingpongLength--; posInt %= pingpongLength; bool forward = (posInt < loopLength); if(forward) chn.position.SetInt(chn.nLoopStart + posInt); else chn.position.SetInt(chn.nLoopEnd - (posInt - loopLength)); if(forward == chn.dwFlags[CHN_PINGPONGFLAG]) { chn.dwFlags.flip(CHN_PINGPONGFLAG); inc.Negate(); } } else { SmpLength posInt = chn.position.GetUInt(); if(posInt >= chn.nLoopEnd + loopLength) { const SmpLength overshoot = posInt - chn.nLoopEnd; posInt -= (overshoot / loopLength) * loopLength; } while(posInt >= chn.nLoopEnd) { posInt -= loopLength; } chn.position.SetInt(posInt); } } } if(stopNote) { chn.Stop(); chn.nPortamentoDest = 0; } chnSettings[channel].ticksToRender = 0; } }; // Get mod length in various cases. Parameters: // [in] adjustMode: See enmGetLengthResetMode for possible adjust modes. // [in] target: Time or position target which should be reached, or no target to get length of the first sub song. Use GetLengthTarget::StartPos to also specify a position from where the seeking should begin. // [out] See definition of type GetLengthType for the returned values. std::vector CSoundFile::GetLength(enmGetLengthResetMode adjustMode, GetLengthTarget target) { std::vector results; GetLengthType retval; // Are we trying to reach a certain pattern position? const bool hasSearchTarget = target.mode != GetLengthTarget::NoTarget && target.mode != GetLengthTarget::GetAllSubsongs; const bool adjustSamplePos = (adjustMode & eAdjustSamplePositions) == eAdjustSamplePositions; SEQUENCEINDEX sequence = target.sequence; if(sequence >= Order.GetNumSequences()) sequence = Order.GetCurrentSequenceIndex(); const ModSequence &orderList = Order(sequence); GetLengthMemory memory(*this); CSoundFile::PlayState &playState = *memory.state; // Temporary visited rows vector (so that GetLength() won't interfere with the player code if the module is playing at the same time) RowVisitor visitedRows(*this, sequence); ROWINDEX allowedPatternLoopComplexity = 32768; // If sequence starts with some non-existent patterns, find a better start while(target.startOrder < orderList.size() && !orderList.IsValidPat(target.startOrder)) { target.startOrder++; target.startRow = 0; } retval.startRow = playState.m_nNextRow = playState.m_nRow = target.startRow; retval.startOrder = playState.m_nNextOrder = playState.m_nCurrentOrder = target.startOrder; // Fast LUTs for commands that are too weird / complicated / whatever to emulate in sample position adjust mode. std::bitset forbiddenCommands; std::bitset forbiddenVolCommands; if(adjustSamplePos) { forbiddenCommands.set(CMD_ARPEGGIO); forbiddenCommands.set(CMD_PORTAMENTOUP); forbiddenCommands.set(CMD_PORTAMENTODOWN); forbiddenCommands.set(CMD_XFINEPORTAUPDOWN); forbiddenCommands.set(CMD_NOTESLIDEUP); forbiddenCommands.set(CMD_NOTESLIDEUPRETRIG); forbiddenCommands.set(CMD_NOTESLIDEDOWN); forbiddenCommands.set(CMD_NOTESLIDEDOWNRETRIG); forbiddenVolCommands.set(VOLCMD_PORTAUP); forbiddenVolCommands.set(VOLCMD_PORTADOWN); if(target.mode == GetLengthTarget::SeekPosition && target.pos.order < orderList.size()) { // If we know where to seek, we can directly rule out any channels on which a new note would be triggered right at the start. const PATTERNINDEX seekPat = orderList[target.pos.order]; if(Patterns.IsValidPat(seekPat) && Patterns[seekPat].IsValidRow(target.pos.row)) { const ModCommand *m = Patterns[seekPat].GetpModCommand(target.pos.row, 0); for(CHANNELINDEX i = 0; i < GetNumChannels(); i++, m++) { if(m->note == NOTE_NOTECUT || m->note == NOTE_KEYOFF || (m->note == NOTE_FADE && GetNumInstruments()) || (m->IsNote() && !m->IsPortamento())) { memory.chnSettings[i].ticksToRender = GetLengthMemory::IGNORE_CHANNEL; } } } } } if(adjustMode & eAdjust) playState.m_midiMacroEvaluationResults.emplace(); // If samples are being synced, force them to resync if tick duration changes uint32 oldTickDuration = 0; bool breakToRow = false; for (;;) { const bool ignoreRow = NextRow(playState, breakToRow).first; // Time target reached. if(target.mode == GetLengthTarget::SeekSeconds && memory.elapsedTime >= target.time) { retval.targetReached = true; break; } // Check if pattern is valid playState.m_nPattern = playState.m_nCurrentOrder < orderList.size() ? orderList[playState.m_nCurrentOrder] : orderList.GetInvalidPatIndex(); if(!Patterns.IsValidPat(playState.m_nPattern) && playState.m_nPattern != orderList.GetInvalidPatIndex() && target.mode == GetLengthTarget::SeekPosition && playState.m_nCurrentOrder == target.pos.order) { // Early test: Target is inside +++ or non-existing pattern retval.targetReached = true; break; } while(playState.m_nPattern >= Patterns.Size()) { // End of song? if((playState.m_nPattern == orderList.GetInvalidPatIndex()) || (playState.m_nCurrentOrder >= orderList.size())) { if(playState.m_nCurrentOrder == orderList.GetRestartPos()) break; else playState.m_nCurrentOrder = orderList.GetRestartPos(); } else { playState.m_nCurrentOrder++; } playState.m_nPattern = (playState.m_nCurrentOrder < orderList.size()) ? orderList[playState.m_nCurrentOrder] : orderList.GetInvalidPatIndex(); playState.m_nNextOrder = playState.m_nCurrentOrder; if((!Patterns.IsValidPat(playState.m_nPattern)) && visitedRows.Visit(playState.m_nCurrentOrder, 0, playState.Chn, ignoreRow)) { if(!hasSearchTarget) { retval.lastOrder = playState.m_nCurrentOrder; retval.lastRow = 0; } if(target.mode == GetLengthTarget::NoTarget || !visitedRows.GetFirstUnvisitedRow(playState.m_nNextOrder, playState.m_nRow, true)) { // We aren't searching for a specific row, or we couldn't find any more unvisited rows. break; } else { // We haven't found the target row yet, but we found some other unplayed row... continue searching from here. retval.duration = memory.elapsedTime; results.push_back(retval); retval.startRow = playState.m_nRow; retval.startOrder = playState.m_nNextOrder; memory.Reset(); playState.m_nCurrentOrder = playState.m_nNextOrder; playState.m_nPattern = orderList[playState.m_nCurrentOrder]; playState.m_nNextRow = playState.m_nRow; break; } } } if(playState.m_nNextOrder == ORDERINDEX_INVALID) { // GetFirstUnvisitedRow failed, so there is nothing more to play break; } // Skip non-existing patterns if(!Patterns.IsValidPat(playState.m_nPattern)) { // If there isn't even a tune, we should probably stop here. if(playState.m_nCurrentOrder == orderList.GetRestartPos()) { if(target.mode == GetLengthTarget::NoTarget || !visitedRows.GetFirstUnvisitedRow(playState.m_nNextOrder, playState.m_nRow, true)) { // We aren't searching for a specific row, or we couldn't find any more unvisited rows. break; } else { // We haven't found the target row yet, but we found some other unplayed row... continue searching from here. retval.duration = memory.elapsedTime; results.push_back(retval); retval.startRow = playState.m_nRow; retval.startOrder = playState.m_nNextOrder; memory.Reset(); playState.m_nNextRow = playState.m_nRow; continue; } } playState.m_nNextOrder = playState.m_nCurrentOrder + 1; continue; } // Should never happen if(playState.m_nRow >= Patterns[playState.m_nPattern].GetNumRows()) playState.m_nRow = 0; // Check whether target was reached. if(target.mode == GetLengthTarget::SeekPosition && playState.m_nCurrentOrder == target.pos.order && playState.m_nRow == target.pos.row) { retval.targetReached = true; break; } // If pattern loops are nested too deeply, they can cause an effectively infinite amount of loop evalations to be generated. // As we don't want the user to wait forever, we bail out if the pattern loops are too complex. const bool moduleTooComplex = target.mode != GetLengthTarget::SeekSeconds && visitedRows.ModuleTooComplex(allowedPatternLoopComplexity); if(moduleTooComplex) { memory.elapsedTime = std::numeric_limits::infinity(); // Decrease allowed complexity with each subsong, as this seems to be a malicious module if(allowedPatternLoopComplexity > 256) allowedPatternLoopComplexity /= 2; visitedRows.ResetComplexity(); } if(visitedRows.Visit(playState.m_nCurrentOrder, playState.m_nRow, playState.Chn, ignoreRow) || moduleTooComplex) { if(!hasSearchTarget) { retval.lastOrder = playState.m_nCurrentOrder; retval.lastRow = playState.m_nRow; } if(target.mode == GetLengthTarget::NoTarget || !visitedRows.GetFirstUnvisitedRow(playState.m_nNextOrder, playState.m_nRow, true)) { // We aren't searching for a specific row, or we couldn't find any more unvisited rows. break; } else { // We haven't found the target row yet, but we found some other unplayed row... continue searching from here. retval.duration = memory.elapsedTime; results.push_back(retval); retval.startRow = playState.m_nRow; retval.startOrder = playState.m_nNextOrder; memory.Reset(); playState.m_nNextRow = playState.m_nRow; continue; } } retval.endOrder = playState.m_nCurrentOrder; retval.endRow = playState.m_nRow; // Update next position SetupNextRow(playState, false); // Jumped to invalid pattern row? if(playState.m_nRow >= Patterns[playState.m_nPattern].GetNumRows()) { playState.m_nRow = 0; } if(ignoreRow) continue; // For various effects, we need to know first how many ticks there are in this row. const ModCommand *p = Patterns[playState.m_nPattern].GetpModCommand(playState.m_nRow, 0); const bool ignoreMutedChn = m_playBehaviour[kST3NoMutedChannels]; for(CHANNELINDEX nChn = 0; nChn < GetNumChannels(); nChn++, p++) { ModChannel &chn = playState.Chn[nChn]; if(p->IsEmpty() || (ignoreMutedChn && ChnSettings[nChn].dwFlags[CHN_MUTE])) // not even effects are processed on muted S3M channels { chn.rowCommand.Clear(); continue; } if(p->IsPcNote()) { #ifndef NO_PLUGINS if(playState.m_midiMacroEvaluationResults && p->instr > 0 && p->instr <= MAX_MIXPLUGINS) { playState.m_midiMacroEvaluationResults->pluginParameter[{static_cast(p->instr - 1), p->GetValueVolCol()}] = p->GetValueEffectCol() / PlugParamValue(ModCommand::maxColumnValue); } #endif // NO_PLUGINS chn.rowCommand.Clear(); continue; } chn.rowCommand = *p; switch(p->command) { case CMD_SPEED: SetSpeed(playState, p->param); break; case CMD_TEMPO: if(m_playBehaviour[kMODVBlankTiming]) { // ProTracker MODs with VBlank timing: All Fxx parameters set the tick count. if(p->param != 0) SetSpeed(playState, p->param); } break; case CMD_S3MCMDEX: if(!chn.rowCommand.param && (GetType() & (MOD_TYPE_S3M | MOD_TYPE_IT | MOD_TYPE_MPT))) chn.rowCommand.param = chn.nOldCmdEx; else chn.nOldCmdEx = static_cast(chn.rowCommand.param); if((p->param & 0xF0) == 0x60) { // Fine Pattern Delay playState.m_nFrameDelay += (p->param & 0x0F); } else if((p->param & 0xF0) == 0xE0 && !playState.m_nPatternDelay) { // Pattern Delay if(!(GetType() & MOD_TYPE_S3M) || (p->param & 0x0F) != 0) { // While Impulse Tracker *does* count S60 as a valid row delay (and thus ignores any other row delay commands on the right), // Scream Tracker 3 simply ignores such commands. playState.m_nPatternDelay = 1 + (p->param & 0x0F); } } break; case CMD_MODCMDEX: if((p->param & 0xF0) == 0xE0) { // Pattern Delay playState.m_nPatternDelay = 1 + (p->param & 0x0F); } break; } } const uint32 numTicks = playState.TicksOnRow(); const uint32 nonRowTicks = numTicks - std::max(playState.m_nPatternDelay, uint32(1)); playState.m_patLoopRow = ROWINDEX_INVALID; playState.m_breakRow = ROWINDEX_INVALID; playState.m_posJump = ORDERINDEX_INVALID; for(CHANNELINDEX nChn = 0; nChn < GetNumChannels(); nChn++) { ModChannel &chn = playState.Chn[nChn]; if(chn.rowCommand.IsEmpty()) continue; ModCommand::COMMAND command = chn.rowCommand.command; ModCommand::PARAM param = chn.rowCommand.param; ModCommand::NOTE note = chn.rowCommand.note; if(adjustMode & eAdjust) { if(chn.rowCommand.instr) { chn.nNewIns = chn.rowCommand.instr; chn.nLastNote = NOTE_NONE; memory.chnSettings[nChn].vol = 0xFF; } if(chn.rowCommand.IsNote()) { chn.nLastNote = note; chn.RestorePanAndFilter(); } // Update channel panning if(chn.rowCommand.IsNote() || chn.rowCommand.instr) { ModInstrument *pIns; if(chn.nNewIns > 0 && chn.nNewIns <= GetNumInstruments() && (pIns = Instruments[chn.nNewIns]) != nullptr) { if(pIns->dwFlags[INS_SETPANNING]) chn.SetInstrumentPan(pIns->nPan, *this); } const SAMPLEINDEX smp = GetSampleIndex(note, chn.nNewIns); if(smp > 0) { if(Samples[smp].uFlags[CHN_PANNING]) chn.SetInstrumentPan(Samples[smp].nPan, *this); } } switch(chn.rowCommand.volcmd) { case VOLCMD_VOLUME: memory.chnSettings[nChn].vol = chn.rowCommand.vol; break; case VOLCMD_VOLSLIDEUP: case VOLCMD_VOLSLIDEDOWN: if(chn.rowCommand.vol != 0) chn.nOldVolParam = chn.rowCommand.vol; break; case VOLCMD_TONEPORTAMENTO: if(chn.rowCommand.vol) { const auto [porta, clearEffectCommand] = GetVolCmdTonePorta(chn.rowCommand, 0); chn.portamentoSlide = porta; if(clearEffectCommand) command = CMD_NONE; } break; } } switch(command) { // Position Jump case CMD_POSITIONJUMP: PositionJump(playState, nChn); break; // Pattern Break case CMD_PATTERNBREAK: if(ROWINDEX row = PatternBreak(playState, nChn, param); row != ROWINDEX_INVALID) playState.m_breakRow = row; break; // Set Tempo case CMD_TEMPO: if(!m_playBehaviour[kMODVBlankTiming]) { TEMPO tempo(CalculateXParam(playState.m_nPattern, playState.m_nRow, nChn), 0); if ((adjustMode & eAdjust) && (GetType() & (MOD_TYPE_S3M | MOD_TYPE_IT | MOD_TYPE_MPT))) { if (tempo.GetInt()) chn.nOldTempo = static_cast(tempo.GetInt()); else tempo.Set(chn.nOldTempo); } if (tempo.GetInt() >= 0x20) playState.m_nMusicTempo = tempo; else { // Tempo Slide TEMPO tempoDiff((tempo.GetInt() & 0x0F) * nonRowTicks, 0); if ((tempo.GetInt() & 0xF0) == 0x10) { playState.m_nMusicTempo += tempoDiff; } else { if(tempoDiff < playState.m_nMusicTempo) playState.m_nMusicTempo -= tempoDiff; else playState.m_nMusicTempo.Set(0); } } TEMPO tempoMin = GetModSpecifications().GetTempoMin(), tempoMax = GetModSpecifications().GetTempoMax(); if(m_playBehaviour[kTempoClamp]) // clamp tempo correctly in compatible mode { tempoMax.Set(255); } Limit(playState.m_nMusicTempo, tempoMin, tempoMax); } break; case CMD_S3MCMDEX: switch(param & 0xF0) { case 0x90: if(param <= 0x91) chn.dwFlags.set(CHN_SURROUND, param == 0x91); break; case 0xA0: // High sample offset chn.nOldHiOffset = param & 0x0F; break; case 0xB0: // Pattern Loop PatternLoop(playState, chn, param & 0x0F); break; case 0xF0: // Active macro chn.nActiveMacro = param & 0x0F; break; } break; case CMD_MODCMDEX: switch(param & 0xF0) { case 0x60: // Pattern Loop PatternLoop(playState, chn, param & 0x0F); break; case 0xF0: // Active macro chn.nActiveMacro = param & 0x0F; break; } break; case CMD_XFINEPORTAUPDOWN: // ignore high offset in compatible mode if(((param & 0xF0) == 0xA0) && !m_playBehaviour[kFT2RestrictXCommand]) chn.nOldHiOffset = param & 0x0F; break; } // The following calculations are not interesting if we just want to get the song length. if(!(adjustMode & eAdjust)) continue; switch(command) { // Portamento Up/Down case CMD_PORTAMENTOUP: if(param) { // FT2 compatibility: Separate effect memory for all portamento commands // Test case: Porta-LinkMem.xm if(!m_playBehaviour[kFT2PortaUpDownMemory]) chn.nOldPortaDown = param; chn.nOldPortaUp = param; } break; case CMD_PORTAMENTODOWN: if(param) { // FT2 compatibility: Separate effect memory for all portamento commands // Test case: Porta-LinkMem.xm if(!m_playBehaviour[kFT2PortaUpDownMemory]) chn.nOldPortaUp = param; chn.nOldPortaDown = param; } break; // Tone-Portamento case CMD_TONEPORTAMENTO: if (param) chn.portamentoSlide = param; break; // Offset case CMD_OFFSET: if(param) chn.oldOffset = param << 8; break; // Volume Slide case CMD_VOLUMESLIDE: case CMD_TONEPORTAVOL: if (param) chn.nOldVolumeSlide = param; break; // Set Volume case CMD_VOLUME: memory.chnSettings[nChn].vol = param; break; // Global Volume case CMD_GLOBALVOLUME: if(!(GetType() & GLOBALVOL_7BIT_FORMATS) && param < 128) param *= 2; // IT compatibility 16. ST3 and IT ignore out-of-range values if(param <= 128) { playState.m_nGlobalVolume = param * 2; } else if(!(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_S3M))) { playState.m_nGlobalVolume = 256; } break; // Global Volume Slide case CMD_GLOBALVOLSLIDE: if(m_playBehaviour[kPerChannelGlobalVolSlide]) { // IT compatibility 16. Global volume slide params are stored per channel (FT2/IT) if (param) chn.nOldGlobalVolSlide = param; else param = chn.nOldGlobalVolSlide; } else { if (param) playState.Chn[0].nOldGlobalVolSlide = param; else param = playState.Chn[0].nOldGlobalVolSlide; } if (((param & 0x0F) == 0x0F) && (param & 0xF0)) { param >>= 4; if (!(GetType() & GLOBALVOL_7BIT_FORMATS)) param <<= 1; playState.m_nGlobalVolume += param << 1; } else if (((param & 0xF0) == 0xF0) && (param & 0x0F)) { param = (param & 0x0F) << 1; if (!(GetType() & GLOBALVOL_7BIT_FORMATS)) param <<= 1; playState.m_nGlobalVolume -= param; } else if (param & 0xF0) { param >>= 4; param <<= 1; if (!(GetType() & GLOBALVOL_7BIT_FORMATS)) param <<= 1; playState.m_nGlobalVolume += param * nonRowTicks; } else { param = (param & 0x0F) << 1; if (!(GetType() & GLOBALVOL_7BIT_FORMATS)) param <<= 1; playState.m_nGlobalVolume -= param * nonRowTicks; } Limit(playState.m_nGlobalVolume, 0, 256); break; case CMD_CHANNELVOLUME: if (param <= 64) chn.nGlobalVol = param; break; case CMD_CHANNELVOLSLIDE: { if (param) chn.nOldChnVolSlide = param; else param = chn.nOldChnVolSlide; int32 volume = chn.nGlobalVol; if((param & 0x0F) == 0x0F && (param & 0xF0)) volume += (param >> 4); // Fine Up else if((param & 0xF0) == 0xF0 && (param & 0x0F)) volume -= (param & 0x0F); // Fine Down else if(param & 0x0F) // Down volume -= (param & 0x0F) * nonRowTicks; else // Up volume += ((param & 0xF0) >> 4) * nonRowTicks; Limit(volume, 0, 64); chn.nGlobalVol = volume; } break; case CMD_PANNING8: Panning(chn, param, Pan8bit); break; case CMD_MODCMDEX: if(param < 0x10) { // LED filter for(CHANNELINDEX channel = 0; channel < GetNumChannels(); channel++) { playState.Chn[channel].dwFlags.set(CHN_AMIGAFILTER, !(param & 1)); } } [[fallthrough]]; case CMD_S3MCMDEX: if((param & 0xF0) == 0x80) { Panning(chn, (param & 0x0F), Pan4bit); } break; case CMD_VIBRATOVOL: if (param) chn.nOldVolumeSlide = param; param = 0; [[fallthrough]]; case CMD_VIBRATO: Vibrato(chn, param); break; case CMD_FINEVIBRATO: FineVibrato(chn, param); break; case CMD_TREMOLO: Tremolo(chn, param); break; case CMD_PANBRELLO: Panbrello(chn, param); break; case CMD_MIDI: case CMD_SMOOTHMIDI: if(param < 0x80) ProcessMIDIMacro(playState, nChn, false, m_MidiCfg.SFx[chn.nActiveMacro], chn.rowCommand.param, 0); else ProcessMIDIMacro(playState, nChn, false, m_MidiCfg.Zxx[param & 0x7F], chn.rowCommand.param, 0); break; default: break; } switch(chn.rowCommand.volcmd) { case VOLCMD_PANNING: Panning(chn, chn.rowCommand.vol, Pan6bit); break; case VOLCMD_VIBRATOSPEED: // FT2 does not automatically enable vibrato with the "set vibrato speed" command if(m_playBehaviour[kFT2VolColVibrato]) chn.nVibratoSpeed = chn.rowCommand.vol & 0x0F; else Vibrato(chn, chn.rowCommand.vol << 4); break; case VOLCMD_VIBRATODEPTH: Vibrato(chn, chn.rowCommand.vol); break; } // Process vibrato / tremolo / panbrello switch(chn.rowCommand.command) { case CMD_VIBRATO: case CMD_FINEVIBRATO: case CMD_VIBRATOVOL: if(adjustMode & eAdjust) { uint32 vibTicks = ((GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) && !m_SongFlags[SONG_ITOLDEFFECTS]) ? numTicks : nonRowTicks; uint32 inc = chn.nVibratoSpeed * vibTicks; if(m_playBehaviour[kITVibratoTremoloPanbrello]) inc *= 4; chn.nVibratoPos += static_cast(inc); } break; case CMD_TREMOLO: if(adjustMode & eAdjust) { uint32 tremTicks = ((GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) && !m_SongFlags[SONG_ITOLDEFFECTS]) ? numTicks : nonRowTicks; uint32 inc = chn.nTremoloSpeed * tremTicks; if(m_playBehaviour[kITVibratoTremoloPanbrello]) inc *= 4; chn.nTremoloPos += static_cast(inc); } break; case CMD_PANBRELLO: if(adjustMode & eAdjust) { // Panbrello effect is permanent in compatible mode, so actually apply panbrello for the last tick of this row chn.nPanbrelloPos += static_cast(chn.nPanbrelloSpeed * (numTicks - 1)); ProcessPanbrello(chn); } break; } if(m_playBehaviour[kST3EffectMemory] && param != 0) { UpdateS3MEffectMemory(chn, param); } } // Interpret F00 effect in XM files as "stop song" if(GetType() == MOD_TYPE_XM && playState.m_nMusicSpeed == uint16_max) { break; } playState.m_nCurrentRowsPerBeat = m_nDefaultRowsPerBeat; if(Patterns[playState.m_nPattern].GetOverrideSignature()) { playState.m_nCurrentRowsPerBeat = Patterns[playState.m_nPattern].GetRowsPerBeat(); } const uint32 tickDuration = GetTickDuration(playState); const uint32 rowDuration = tickDuration * numTicks; memory.elapsedTime += static_cast(rowDuration) / static_cast(m_MixerSettings.gdwMixingFreq); playState.m_lTotalSampleCount += rowDuration; if(adjustSamplePos) { // Super experimental and dirty sample seeking for(CHANNELINDEX nChn = 0; nChn < GetNumChannels(); nChn++) { if(memory.chnSettings[nChn].ticksToRender == GetLengthMemory::IGNORE_CHANNEL) continue; ModChannel &chn = playState.Chn[nChn]; const ModCommand &m = chn.rowCommand; if(!chn.nPeriod && m.IsEmpty()) continue; uint32 paramHi = m.param >> 4, paramLo = m.param & 0x0F; uint32 startTick = 0; bool porta = m.command == CMD_TONEPORTAMENTO || m.command == CMD_TONEPORTAVOL || m.volcmd == VOLCMD_TONEPORTAMENTO; bool stopNote = false; if(m.instr) chn.prevNoteOffset = 0; if(m.IsNote()) { if(porta && memory.chnSettings[nChn].incChanged) { // If there's a portamento, the current channel increment mustn't be 0 in NoteChange() chn.increment = GetChannelIncrement(chn, chn.nPeriod, 0).first; } int32 setPan = chn.nPan; chn.nNewNote = chn.nLastNote; if(chn.nNewIns != 0) InstrumentChange(chn, chn.nNewIns, porta); NoteChange(chn, m.note, porta); HandleDigiSamplePlayDirection(playState, nChn); memory.chnSettings[nChn].incChanged = true; if((m.command == CMD_MODCMDEX || m.command == CMD_S3MCMDEX) && (m.param & 0xF0) == 0xD0 && paramLo < numTicks) { startTick = paramLo; } else if(m.command == CMD_DELAYCUT && paramHi < numTicks) { startTick = paramHi; } if(playState.m_nPatternDelay > 1 && startTick != 0 && (GetType() & (MOD_TYPE_S3M | MOD_TYPE_IT | MOD_TYPE_MPT))) { startTick += (playState.m_nMusicSpeed + playState.m_nFrameDelay) * (playState.m_nPatternDelay - 1); } if(!porta) memory.chnSettings[nChn].ticksToRender = 0; // Panning commands have to be re-applied after a note change with potential pan change. if(m.command == CMD_PANNING8 || ((m.command == CMD_MODCMDEX || m.command == CMD_S3MCMDEX) && paramHi == 0x8) || m.volcmd == VOLCMD_PANNING) { chn.nPan = setPan; } } if(m.IsNote() || m_playBehaviour[kApplyOffsetWithoutNote]) { if(m.command == CMD_OFFSET) { ProcessSampleOffset(chn, nChn, playState); } else if(m.command == CMD_OFFSETPERCENTAGE) { SampleOffset(chn, Util::muldiv_unsigned(chn.nLength, m.param, 256)); } else if(m.command == CMD_REVERSEOFFSET && chn.pModSample != nullptr) { memory.RenderChannel(nChn, oldTickDuration); // Re-sync what we've got so far ReverseSampleOffset(chn, m.param); startTick = playState.m_nMusicSpeed - 1; } else if(m.volcmd == VOLCMD_OFFSET) { if(chn.pModSample != nullptr && m.vol <= std::size(chn.pModSample->cues)) { SmpLength offset; if(m.vol == 0) offset = chn.oldOffset; else offset = chn.oldOffset = chn.pModSample->cues[m.vol - 1]; SampleOffset(chn, offset); } } } if(m.note == NOTE_KEYOFF || m.note == NOTE_NOTECUT || (m.note == NOTE_FADE && GetNumInstruments()) || ((m.command == CMD_MODCMDEX || m.command == CMD_S3MCMDEX) && (m.param & 0xF0) == 0xC0 && paramLo < numTicks) || (m.command == CMD_DELAYCUT && paramLo != 0 && startTick + paramLo < numTicks) || m.command == CMD_KEYOFF) { stopNote = true; } if(m.command == CMD_VOLUME) { chn.nVolume = m.param * 4; } else if(m.volcmd == VOLCMD_VOLUME) { chn.nVolume = m.vol * 4; } if(chn.pModSample && !stopNote) { // Check if we don't want to emulate some effect and thus stop processing. if(m.command < MAX_EFFECTS) { if(forbiddenCommands[m.command]) { stopNote = true; } else if(m.command == CMD_MODCMDEX) { // Special case: Slides using extended commands switch(m.param & 0xF0) { case 0x10: case 0x20: stopNote = true; } } } if(m.volcmd < forbiddenVolCommands.size() && forbiddenVolCommands[m.volcmd]) { stopNote = true; } } if(stopNote) { chn.Stop(); memory.chnSettings[nChn].ticksToRender = 0; } else { if(oldTickDuration != tickDuration && oldTickDuration != 0) { memory.RenderChannel(nChn, oldTickDuration); // Re-sync what we've got so far } switch(m.command) { case CMD_TONEPORTAVOL: case CMD_VOLUMESLIDE: case CMD_VIBRATOVOL: if(m.param || (GetType() != MOD_TYPE_MOD)) { for(uint32 i = 0; i < numTicks; i++) { chn.isFirstTick = (i == 0); VolumeSlide(chn, m.param); } } break; case CMD_MODCMDEX: if((m.param & 0x0F) || (GetType() & (MOD_TYPE_XM | MOD_TYPE_MT2))) { chn.isFirstTick = true; switch(m.param & 0xF0) { case 0xA0: FineVolumeUp(chn, m.param & 0x0F, false); break; case 0xB0: FineVolumeDown(chn, m.param & 0x0F, false); break; } } break; case CMD_S3MCMDEX: if(m.param == 0x9E) { // Play forward memory.RenderChannel(nChn, oldTickDuration); // Re-sync what we've got so far chn.dwFlags.reset(CHN_PINGPONGFLAG); } else if(m.param == 0x9F) { // Reverse memory.RenderChannel(nChn, oldTickDuration); // Re-sync what we've got so far chn.dwFlags.set(CHN_PINGPONGFLAG); if(!chn.position.GetInt() && chn.nLength && (m.IsNote() || !chn.dwFlags[CHN_LOOP])) { chn.position.Set(chn.nLength - 1, SamplePosition::fractMax); } } else if((m.param & 0xF0) == 0x70) { if(m.param >= 0x73) chn.InstrumentControl(m.param, *this); } break; case CMD_DIGIREVERSESAMPLE: DigiBoosterSampleReverse(chn, m.param); break; case CMD_FINETUNE: case CMD_FINETUNE_SMOOTH: memory.RenderChannel(nChn, oldTickDuration); // Re-sync what we've got so far SetFinetune(nChn, playState, false); // TODO should render each tick individually for CMD_FINETUNE_SMOOTH for higher sync accuracy break; } chn.isFirstTick = true; switch(m.volcmd) { case VOLCMD_FINEVOLUP: FineVolumeUp(chn, m.vol, m_playBehaviour[kITVolColMemory]); break; case VOLCMD_FINEVOLDOWN: FineVolumeDown(chn, m.vol, m_playBehaviour[kITVolColMemory]); break; case VOLCMD_VOLSLIDEUP: case VOLCMD_VOLSLIDEDOWN: { // IT Compatibility: Volume column volume slides have their own memory // Test case: VolColMemory.it ModCommand::VOL vol = m.vol; if(vol == 0 && m_playBehaviour[kITVolColMemory]) { vol = chn.nOldVolParam; if(vol == 0) break; } if(m.volcmd == VOLCMD_VOLSLIDEUP) vol <<= 4; for(uint32 i = 0; i < numTicks; i++) { chn.isFirstTick = (i == 0); VolumeSlide(chn, vol); } } break; case VOLCMD_PLAYCONTROL: if(m.vol <= 1) chn.isPaused = (m.vol == 0); break; } if(chn.isPaused) continue; if(porta) { // Portamento needs immediate syncing, as the pitch changes on each tick uint32 portaTick = memory.chnSettings[nChn].ticksToRender + startTick + 1; memory.chnSettings[nChn].ticksToRender += numTicks; memory.RenderChannel(nChn, tickDuration, portaTick); } else { memory.chnSettings[nChn].ticksToRender += (numTicks - startTick); } } } } oldTickDuration = tickDuration; breakToRow = HandleNextRow(playState, orderList, false); } // Now advance the sample positions for sample seeking on channels that are still playing if(adjustSamplePos) { for(CHANNELINDEX nChn = 0; nChn < GetNumChannels(); nChn++) { if(memory.chnSettings[nChn].ticksToRender != GetLengthMemory::IGNORE_CHANNEL) { memory.RenderChannel(nChn, oldTickDuration); } } } if(retval.targetReached) { retval.lastOrder = playState.m_nCurrentOrder; retval.lastRow = playState.m_nRow; } retval.duration = memory.elapsedTime; results.push_back(retval); // Store final variables if(adjustMode & eAdjust) { if(retval.targetReached || target.mode == GetLengthTarget::NoTarget) { const auto midiMacroEvaluationResults = std::move(playState.m_midiMacroEvaluationResults); playState.m_midiMacroEvaluationResults.reset(); // Target found, or there is no target (i.e. play whole song)... m_PlayState = std::move(playState); m_PlayState.ResetGlobalVolumeRamping(); m_PlayState.m_nNextRow = m_PlayState.m_nRow; m_PlayState.m_nFrameDelay = m_PlayState.m_nPatternDelay = 0; m_PlayState.m_nTickCount = TICKS_ROW_FINISHED; m_PlayState.m_bPositionChanged = true; if(m_opl != nullptr) m_opl->Reset(); for(CHANNELINDEX n = 0; n < GetNumChannels(); n++) { auto &chn = m_PlayState.Chn[n]; if(chn.nLastNote != NOTE_NONE) { chn.nNewNote = chn.nLastNote; } if(memory.chnSettings[n].vol != 0xFF && !adjustSamplePos) { chn.nVolume = std::min(memory.chnSettings[n].vol, uint8(64)) * 4; } if(chn.pModSample != nullptr && chn.pModSample->uFlags[CHN_ADLIB] && m_opl) { m_opl->Patch(n, chn.pModSample->adlib); m_opl->NoteCut(n); } chn.pCurrentSample = nullptr; } #ifndef NO_PLUGINS // If there were any PC events or MIDI macros updating plugin parameters, update plugin parameters to their latest value. std::bitset plugSetProgram; for(const auto &[plugParam, value] : midiMacroEvaluationResults->pluginParameter) { PLUGINDEX plug = plugParam.first; IMixPlugin *plugin = m_MixPlugins[plug].pMixPlugin; if(plugin != nullptr) { if(!plugSetProgram[plug]) { // Used for bridged plugins to avoid sending out individual messages for each parameter. plugSetProgram.set(plug); plugin->BeginSetProgram(); } plugin->SetParameter(plugParam.second, value); } } if(plugSetProgram.any()) { for(PLUGINDEX i = 0; i < MAX_MIXPLUGINS; i++) { if(plugSetProgram[i]) { m_MixPlugins[i].pMixPlugin->EndSetProgram(); } } } // Do the same for dry/wet ratios for(const auto &[plug, dryWetRatio] : midiMacroEvaluationResults->pluginDryWetRatio) { m_MixPlugins[plug].fDryRatio = dryWetRatio; } #endif // NO_PLUGINS } else if(adjustMode != eAdjustOnSuccess) { // Target not found (e.g. when jumping to a hidden sub song), reset global variables... m_PlayState.m_nMusicSpeed = m_nDefaultSpeed; m_PlayState.m_nMusicTempo = m_nDefaultTempo; m_PlayState.m_nGlobalVolume = m_nDefaultGlobalVolume; } // When adjusting the playback status, we will also want to update the visited rows vector according to the current position. if(sequence != Order.GetCurrentSequenceIndex()) { Order.SetSequence(sequence); } } if(adjustMode & (eAdjust | eAdjustOnlyVisitedRows)) m_visitedRows.MoveVisitedRowsFrom(visitedRows); return results; } ////////////////////////////////////////////////////////////////////////////////////////////////// // Effects // Change sample or instrument number. void CSoundFile::InstrumentChange(ModChannel &chn, uint32 instr, bool bPorta, bool bUpdVol, bool bResetEnv) const { const ModInstrument *pIns = instr <= GetNumInstruments() ? Instruments[instr] : nullptr; const ModSample *pSmp = &Samples[instr]; const auto oldInsVol = chn.nInsVol; ModCommand::NOTE note = chn.nNewNote; if(note == NOTE_NONE && m_playBehaviour[kITInstrWithoutNote]) return; if(pIns != nullptr && ModCommand::IsNote(note)) { // Impulse Tracker ignores empty slots. // We won't ignore them if a plugin is assigned to this slot, so that VSTis still work as intended. // Test case: emptyslot.it, PortaInsNum.it, gxsmp.it, gxsmp2.it if(pIns->Keyboard[note - NOTE_MIN] == 0 && m_playBehaviour[kITEmptyNoteMapSlot] && !pIns->HasValidMIDIChannel()) { chn.pModInstrument = pIns; return; } if(pIns->NoteMap[note - NOTE_MIN] > NOTE_MAX) return; uint32 n = pIns->Keyboard[note - NOTE_MIN]; pSmp = ((n) && (n < MAX_SAMPLES)) ? &Samples[n] : nullptr; } else if(GetNumInstruments()) { // No valid instrument, or not a valid note. if (note >= NOTE_MIN_SPECIAL) return; if(m_playBehaviour[kITEmptyNoteMapSlot] && (pIns == nullptr || !pIns->HasValidMIDIChannel())) { // Impulse Tracker ignores empty slots. // We won't ignore them if a plugin is assigned to this slot, so that VSTis still work as intended. // Test case: emptyslot.it, PortaInsNum.it, gxsmp.it, gxsmp2.it chn.pModInstrument = nullptr; chn.nNewIns = 0; return; } pSmp = nullptr; } bool returnAfterVolumeAdjust = false; // instrumentChanged is used for IT carry-on env option bool instrumentChanged = (pIns != chn.pModInstrument); const bool sampleChanged = (chn.pModSample != nullptr) && (pSmp != chn.pModSample); const bool newTuning = (GetType() == MOD_TYPE_MPT && pIns && pIns->pTuning); if(!bPorta || instrumentChanged || sampleChanged) chn.microTuning = 0; // Playback behavior change for MPT: With portamento don't change sample if it is in // the same instrument as previous sample. if(bPorta && newTuning && pIns == chn.pModInstrument && sampleChanged) return; if(sampleChanged && bPorta) { // IT compatibility: No sample change (also within multi-sample instruments) during portamento when using Compatible Gxx. // Test case: PortaInsNumCompat.it, PortaSampleCompat.it, PortaCutCompat.it if(m_playBehaviour[kITPortamentoInstrument] && m_SongFlags[SONG_ITCOMPATGXX] && !chn.increment.IsZero()) { pSmp = chn.pModSample; } // Special XM hack (also applies to MOD / S3M, except when playing IT-style S3Ms, such as k_vision.s3m) // Test case: PortaSmpChange.mod, PortaSmpChange.s3m, PortaSwap.s3m if((!instrumentChanged && (GetType() & (MOD_TYPE_XM | MOD_TYPE_MT2)) && pIns) || (GetType() == MOD_TYPE_PLM) || (GetType() == MOD_TYPE_MOD && chn.IsSamplePlaying()) || (m_playBehaviour[kST3PortaSampleChange] && chn.IsSamplePlaying())) { // FT2 doesn't change the sample in this case, // but still uses the sample info from the old one (bug?) returnAfterVolumeAdjust = true; } } // IT compatibility: A lone instrument number should only reset sample properties to those of the corresponding sample in instrument mode. // C#5 01 ... <-- sample 1 // C-5 .. g02 <-- sample 2 // ... 01 ... <-- still sample 1, but with properties of sample 2 // In the above example, no sample change happens on the second row. In the third row, sample 1 keeps playing but with the // volume and panning properties of sample 2. // Test case: InstrAfterMultisamplePorta.it if(m_nInstruments && !instrumentChanged && sampleChanged && chn.pCurrentSample != nullptr && m_playBehaviour[kITMultiSampleInstrumentNumber] && !chn.rowCommand.IsNote()) { returnAfterVolumeAdjust = true; } // IT Compatibility: Envelope pickup after SCx cut (but don't do this when working with plugins, or else envelope carry stops working) // Test case: cut-carry.it if(!chn.IsSamplePlaying() && (GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) && (!pIns || !pIns->HasValidMIDIChannel())) { instrumentChanged = true; } // FT2 compatibility: new instrument + portamento = ignore new instrument number, but reload old instrument settings (the world of XM is upside down...) // And this does *not* happen if volume column portamento is used together with note delay... (handled in ProcessEffects(), where all the other note delay stuff is.) // Test case: porta-delay.xm, SamplePortaInInstrument.xm if((instrumentChanged || sampleChanged) && bPorta && m_playBehaviour[kFT2PortaIgnoreInstr] && (chn.pModInstrument != nullptr || chn.pModSample != nullptr)) { pIns = chn.pModInstrument; pSmp = chn.pModSample; instrumentChanged = false; } else { chn.pModInstrument = pIns; } // Update Volume if (bUpdVol && (!(GetType() & (MOD_TYPE_MOD | MOD_TYPE_S3M)) || ((pSmp != nullptr && pSmp->HasSampleData()) || chn.HasMIDIOutput()))) { if(pSmp) { if(!pSmp->uFlags[SMP_NODEFAULTVOLUME]) chn.nVolume = pSmp->nVolume; } else if(pIns && pIns->nMixPlug) { chn.nVolume = chn.GetVSTVolume(); } else { chn.nVolume = 0; } } if(returnAfterVolumeAdjust && sampleChanged && pSmp != nullptr) { // ProTracker applies new instrument's finetune but keeps the old sample playing. // Test case: PortaSwapPT.mod if(m_playBehaviour[kMODSampleSwap]) chn.nFineTune = pSmp->nFineTune; // ST3 does it similarly for middle-C speed. // Test case: PortaSwap.s3m, SampleSwap.s3m if(GetType() == MOD_TYPE_S3M && pSmp->HasSampleData()) chn.nC5Speed = pSmp->nC5Speed; } if(returnAfterVolumeAdjust) return; // Instrument adjust chn.nNewIns = 0; // IT Compatiblity: NNA is reset on every note change, not every instrument change (fixes s7xinsnum.it). if (pIns && ((!m_playBehaviour[kITNNAReset] && pSmp) || pIns->nMixPlug || instrumentChanged)) chn.nNNA = pIns->nNNA; // Update volume chn.UpdateInstrumentVolume(pSmp, pIns); // Update panning // FT2 compatibility: Only reset panning on instrument numbers, not notes (bUpdVol condition) // Test case: PanMemory.xm // IT compatibility: Sample and instrument panning is only applied on note change, not instrument change // Test case: PanReset.it if((bUpdVol || !(GetType() & (MOD_TYPE_XM | MOD_TYPE_MT2))) && !m_playBehaviour[kITPanningReset]) { ApplyInstrumentPanning(chn, pIns, pSmp); } // Reset envelopes if(bResetEnv) { // Blurb by Storlek (from the SchismTracker code): // Conditions experimentally determined to cause envelope reset in Impulse Tracker: // - no note currently playing (of course) // - note given, no portamento // - instrument number given, portamento, compat gxx enabled // - instrument number given, no portamento, after keyoff, old effects enabled // If someone can enlighten me to what the logic really is here, I'd appreciate it. // Seems like it's just a total mess though, probably to get XMs to play right. bool reset, resetAlways; // IT Compatibility: Envelope reset // Test case: EnvReset.it if(m_playBehaviour[kITEnvelopeReset]) { const bool insNumber = (instr != 0); reset = (!chn.nLength || (insNumber && bPorta && m_SongFlags[SONG_ITCOMPATGXX]) || (insNumber && !bPorta && chn.dwFlags[CHN_NOTEFADE | CHN_KEYOFF] && m_SongFlags[SONG_ITOLDEFFECTS])); // NOTE: IT2.14 with SB/GUS/etc. output is different. We are going after IT's WAV writer here. // For SB/GUS/etc. emulation, envelope carry should only apply when the NNA isn't set to "Note Cut". // Test case: CarryNNA.it resetAlways = (!chn.nFadeOutVol || instrumentChanged || chn.dwFlags[CHN_KEYOFF]); } else { reset = (!bPorta || !(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_DBM)) || m_SongFlags[SONG_ITCOMPATGXX] || !chn.nLength || (chn.dwFlags[CHN_NOTEFADE] && !chn.nFadeOutVol)); resetAlways = !(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_DBM)) || instrumentChanged || pIns == nullptr || chn.dwFlags[CHN_KEYOFF | CHN_NOTEFADE]; } if(reset) { chn.dwFlags.set(CHN_FASTVOLRAMP); if(pIns != nullptr) { if(resetAlways) { chn.ResetEnvelopes(); } else { if(!pIns->VolEnv.dwFlags[ENV_CARRY]) chn.VolEnv.Reset(); if(!pIns->PanEnv.dwFlags[ENV_CARRY]) chn.PanEnv.Reset(); if(!pIns->PitchEnv.dwFlags[ENV_CARRY]) chn.PitchEnv.Reset(); } } // IT Compatibility: Autovibrato reset if(!m_playBehaviour[kITVibratoTremoloPanbrello]) { chn.nAutoVibDepth = 0; chn.nAutoVibPos = 0; } } else if(pIns != nullptr && !pIns->VolEnv.dwFlags[ENV_ENABLED]) { if(m_playBehaviour[kITPortamentoInstrument]) { chn.VolEnv.Reset(); } else { chn.ResetEnvelopes(); } } } // Invalid sample ? if(pSmp == nullptr && (pIns == nullptr || !pIns->HasValidMIDIChannel())) { chn.pModSample = nullptr; chn.nInsVol = 0; return; } // Tone-Portamento doesn't reset the pingpong direction flag if(bPorta && pSmp == chn.pModSample && pSmp != nullptr) { // If channel length is 0, we cut a previous sample using SCx. In that case, we have to update sample length, loop points, etc... if(GetType() & (MOD_TYPE_S3M|MOD_TYPE_IT|MOD_TYPE_MPT) && chn.nLength != 0) return; // FT2 compatibility: Do not reset key-off status on portamento without instrument number // Test case: Off-Porta.xm if(GetType() != MOD_TYPE_XM || !m_playBehaviour[kITFT2DontResetNoteOffOnPorta] || chn.rowCommand.instr != 0) chn.dwFlags.reset(CHN_KEYOFF | CHN_NOTEFADE); chn.dwFlags = (chn.dwFlags & (CHN_CHANNELFLAGS | CHN_PINGPONGFLAG)); } else //if(!instrumentChanged || chn.rowCommand.instr != 0 || !IsCompatibleMode(TRK_FASTTRACKER2)) // SampleChange.xm? { chn.dwFlags.reset(CHN_KEYOFF | CHN_NOTEFADE); // IT compatibility: Don't change bidi loop direction when no sample nor instrument is changed. if((m_playBehaviour[kITPingPongNoReset] || !(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT))) && pSmp == chn.pModSample && !instrumentChanged) chn.dwFlags = (chn.dwFlags & (CHN_CHANNELFLAGS | CHN_PINGPONGFLAG)); else chn.dwFlags = (chn.dwFlags & CHN_CHANNELFLAGS); if(pIns) { // Copy envelope flags (we actually only need the "enabled" and "pitch" flag) chn.VolEnv.flags = pIns->VolEnv.dwFlags; chn.PanEnv.flags = pIns->PanEnv.dwFlags; chn.PitchEnv.flags = pIns->PitchEnv.dwFlags; // A cutoff frequency of 0 should not be reset just because the filter envelope is enabled. // Test case: FilterEnvReset.it if((pIns->PitchEnv.dwFlags & (ENV_ENABLED | ENV_FILTER)) == (ENV_ENABLED | ENV_FILTER) && !m_playBehaviour[kITFilterBehaviour]) { if(!chn.nCutOff) chn.nCutOff = 0x7F; } if(pIns->IsCutoffEnabled()) chn.nCutOff = pIns->GetCutoff(); if(pIns->IsResonanceEnabled()) chn.nResonance = pIns->GetResonance(); } } if(pSmp == nullptr) { chn.pModSample = nullptr; chn.nLength = 0; return; } if(bPorta && chn.nLength == 0 && (m_playBehaviour[kFT2PortaNoNote] || m_playBehaviour[kITPortaNoNote])) { // IT/FT2 compatibility: If the note just stopped on the previous tick, prevent it from restarting. // Test cases: PortaJustStoppedNote.xm, PortaJustStoppedNote.it chn.increment.Set(0); } // IT compatibility: Note-off with instrument number + Old Effects retriggers envelopes. // If the instrument changes, keep playing the previous sample, but load the new instrument's envelopes. // Test case: ResetEnvNoteOffOldFx.it if(chn.rowCommand.note == NOTE_KEYOFF && m_playBehaviour[kITInstrWithNoteOffOldEffects] && m_SongFlags[SONG_ITOLDEFFECTS] && sampleChanged) { if(chn.pModSample) { chn.dwFlags |= (chn.pModSample->uFlags & CHN_SAMPLEFLAGS); } chn.nInsVol = oldInsVol; chn.nVolume = pSmp->nVolume; if(pSmp->uFlags[CHN_PANNING]) chn.SetInstrumentPan(pSmp->nPan, *this); return; } chn.pModSample = pSmp; chn.nLength = pSmp->nLength; chn.nLoopStart = pSmp->nLoopStart; chn.nLoopEnd = pSmp->nLoopEnd; // ProTracker "oneshot" loops (if loop start is 0, play the whole sample once and then repeat until loop end) if(m_playBehaviour[kMODOneShotLoops] && chn.nLoopStart == 0) chn.nLoopEnd = pSmp->nLength; chn.dwFlags |= (pSmp->uFlags & CHN_SAMPLEFLAGS); // IT Compatibility: Autovibrato reset if(m_playBehaviour[kITVibratoTremoloPanbrello]) { chn.nAutoVibDepth = 0; chn.nAutoVibPos = 0; } if(newTuning) { chn.nC5Speed = pSmp->nC5Speed; chn.m_CalculateFreq = true; chn.nFineTune = 0; } else if(!bPorta || sampleChanged || !(GetType() & (MOD_TYPE_MOD | MOD_TYPE_XM))) { // Don't reset finetune changed by "set finetune" command. // Test case: finetune.xm, finetune.mod // But *do* change the finetune if we switch to a different sample, to fix // Miranda`s axe by Jamson (jam007.xm). chn.nC5Speed = pSmp->nC5Speed; chn.nFineTune = pSmp->nFineTune; } chn.nTranspose = UseFinetuneAndTranspose() ? pSmp->RelativeTone : 0; // FT2 compatibility: Don't reset portamento target with new instrument numbers. // Test case: Porta-Pickup.xm // ProTracker does the same. // Test case: PortaTarget.mod if(!m_playBehaviour[kFT2PortaTargetNoReset] && GetType() != MOD_TYPE_MOD) { chn.nPortamentoDest = 0; } chn.m_PortamentoFineSteps = 0; if(chn.dwFlags[CHN_SUSTAINLOOP]) { chn.nLoopStart = pSmp->nSustainStart; chn.nLoopEnd = pSmp->nSustainEnd; if(chn.dwFlags[CHN_PINGPONGSUSTAIN]) chn.dwFlags.set(CHN_PINGPONGLOOP); chn.dwFlags.set(CHN_LOOP); } if(chn.dwFlags[CHN_LOOP] && chn.nLoopEnd < chn.nLength) chn.nLength = chn.nLoopEnd; // Fix sample position on instrument change. This is needed for IT "on the fly" sample change. // XXX is this actually called? In ProcessEffects(), a note-on effect is emulated if there's an on the fly sample change! if(chn.position.GetUInt() >= chn.nLength) { if((GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT))) { chn.position.Set(0); } } } void CSoundFile::NoteChange(ModChannel &chn, int note, bool bPorta, bool bResetEnv, bool bManual, CHANNELINDEX channelHint) const { if(note < NOTE_MIN) return; const int origNote = note; const ModSample *pSmp = chn.pModSample; const ModInstrument *pIns = chn.pModInstrument; const bool newTuning = (GetType() == MOD_TYPE_MPT && pIns != nullptr && pIns->pTuning); // save the note that's actually used, as it's necessary to properly calculate PPS and stuff const int realnote = note; if((pIns) && (note - NOTE_MIN < (int)std::size(pIns->Keyboard))) { uint32 n = pIns->Keyboard[note - NOTE_MIN]; if((n) && (n < MAX_SAMPLES)) { pSmp = &Samples[n]; } else if(m_playBehaviour[kITEmptyNoteMapSlot] && !chn.HasMIDIOutput()) { // Impulse Tracker ignores empty slots. // We won't ignore them if a plugin is assigned to this slot, so that VSTis still work as intended. // Test case: emptyslot.it, PortaInsNum.it, gxsmp.it, gxsmp2.it return; } note = pIns->NoteMap[note - NOTE_MIN]; } // Key Off if(note > NOTE_MAX) { // Key Off (+ Invalid Note for XM - TODO is this correct?) if(note == NOTE_KEYOFF || !(GetType() & (MOD_TYPE_IT|MOD_TYPE_MPT))) { KeyOff(chn); // IT compatibility: Note-off + instrument releases sample sustain but does not release envelopes or fade the instrument // Test case: noteoff3.it, ResetEnvNoteOffOldFx2.it if(!bPorta && m_playBehaviour[kITInstrWithNoteOffOldEffects] && m_SongFlags[SONG_ITOLDEFFECTS] && chn.rowCommand.instr) chn.dwFlags.reset(CHN_NOTEFADE | CHN_KEYOFF); } else // Invalid Note -> Note Fade { if(/*note == NOTE_FADE && */ GetNumInstruments()) chn.dwFlags.set(CHN_NOTEFADE); } // Note Cut if (note == NOTE_NOTECUT) { if(chn.dwFlags[CHN_ADLIB] && GetType() == MOD_TYPE_S3M) { // OPL voices are not cut but enter the release portion of their envelope // In S3M we can still modify the volume after note-off, in legacy MPTM mode we can't chn.dwFlags.set(CHN_KEYOFF); } else { chn.dwFlags.set(CHN_NOTEFADE | CHN_FASTVOLRAMP); // IT compatibility: Stopping sample playback by setting sample increment to 0 rather than volume // Test case: NoteOffInstr.it if ((!(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT))) || (m_nInstruments != 0 && !m_playBehaviour[kITInstrWithNoteOff])) chn.nVolume = 0; if (m_playBehaviour[kITInstrWithNoteOff]) chn.increment.Set(0); chn.nFadeOutVol = 0; } } // IT compatibility tentative fix: Clear channel note memory (TRANCE_N.IT by A3F). if(m_playBehaviour[kITClearOldNoteAfterCut]) { chn.nNote = chn.nNewNote = NOTE_NONE; } return; } if(newTuning) { if(!bPorta || chn.nNote == NOTE_NONE) chn.nPortamentoDest = 0; else { chn.nPortamentoDest = pIns->pTuning->GetStepDistance(chn.nNote, chn.m_PortamentoFineSteps, static_cast(note), 0); //Here chn.nPortamentoDest means 'steps to slide'. chn.m_PortamentoFineSteps = -chn.nPortamentoDest; } } if(!bPorta && (GetType() & (MOD_TYPE_XM | MOD_TYPE_MED | MOD_TYPE_MT2))) { if(pSmp) { chn.nTranspose = pSmp->RelativeTone; chn.nFineTune = pSmp->nFineTune; } } // IT Compatibility: Update multisample instruments frequency even if instrument is not specified (fixes the guitars in spx-shuttledeparture.it) // Test case: freqreset-noins.it if(!bPorta && pSmp && m_playBehaviour[kITMultiSampleBehaviour]) chn.nC5Speed = pSmp->nC5Speed; if(bPorta && !chn.IsSamplePlaying()) { if(m_playBehaviour[kFT2PortaNoNote]) { // FT2 Compatibility: Ignore notes with portamento if there was no note playing. // Test case: 3xx-no-old-samp.xm chn.nPeriod = 0; return; } else if(m_playBehaviour[kITPortaNoNote]) { // IT Compatibility: Ignore portamento command if no note was playing (e.g. if a previous note has faded out). // Test case: Fade-Porta.it bPorta = false; } } if(UseFinetuneAndTranspose()) { note += chn.nTranspose; // RealNote = PatternNote + RelativeTone; (0..118, 0 = C-0, 118 = A#9) Limit(note, NOTE_MIN + 11, NOTE_MIN + 130); // 119 possible notes } else { Limit(note, NOTE_MIN, NOTE_MAX); } if(m_playBehaviour[kITRealNoteMapping]) { // need to memorize the original note for various effects (e.g. PPS) chn.nNote = static_cast(Clamp(realnote, NOTE_MIN, NOTE_MAX)); } else { chn.nNote = static_cast(note); } chn.m_CalculateFreq = true; chn.isPaused = false; if ((!bPorta) || (GetType() & (MOD_TYPE_S3M|MOD_TYPE_IT|MOD_TYPE_MPT))) chn.nNewIns = 0; uint32 period = GetPeriodFromNote(note, chn.nFineTune, chn.nC5Speed); chn.nPanbrelloOffset = 0; // IT compatibility: Sample and instrument panning is only applied on note change, not instrument change // Test case: PanReset.it if(m_playBehaviour[kITPanningReset]) ApplyInstrumentPanning(chn, pIns, pSmp); // IT compatibility: Pitch/Pan Separation can be overriden by panning commands, and shouldn't be affected by note-off commands // Test case: PitchPanReset.it if(m_playBehaviour[kITPitchPanSeparation] && pIns && pIns->nPPS) { if(!chn.nRestorePanOnNewNote) chn.nRestorePanOnNewNote = static_cast(chn.nPan + 1); ProcessPitchPanSeparation(chn.nPan, origNote, *pIns); } if(bResetEnv && !bPorta) { chn.nVolSwing = chn.nPanSwing = 0; chn.nResSwing = chn.nCutSwing = 0; if(pIns) { // IT Compatiblity: NNA is reset on every note change, not every instrument change (fixes spx-farspacedance.it). if(m_playBehaviour[kITNNAReset]) chn.nNNA = pIns->nNNA; if(!pIns->VolEnv.dwFlags[ENV_CARRY]) chn.VolEnv.Reset(); if(!pIns->PanEnv.dwFlags[ENV_CARRY]) chn.PanEnv.Reset(); if(!pIns->PitchEnv.dwFlags[ENV_CARRY]) chn.PitchEnv.Reset(); // Volume Swing if(pIns->nVolSwing) { chn.nVolSwing = static_cast(((mpt::random(AccessPRNG()) * pIns->nVolSwing) / 64 + 1) * (m_playBehaviour[kITSwingBehaviour] ? chn.nInsVol : ((chn.nVolume + 1) / 2)) / 199); } // Pan Swing if(pIns->nPanSwing) { chn.nPanSwing = static_cast(((mpt::random(AccessPRNG()) * pIns->nPanSwing * 4) / 128)); if(!m_playBehaviour[kITSwingBehaviour] && chn.nRestorePanOnNewNote == 0) { chn.nRestorePanOnNewNote = static_cast(chn.nPan + 1); } } // Cutoff Swing if(pIns->nCutSwing) { int32 d = ((int32)pIns->nCutSwing * (int32)(static_cast(mpt::random(AccessPRNG())) + 1)) / 128; chn.nCutSwing = static_cast((d * chn.nCutOff + 1) / 128); chn.nRestoreCutoffOnNewNote = chn.nCutOff + 1; } // Resonance Swing if(pIns->nResSwing) { int32 d = ((int32)pIns->nResSwing * (int32)(static_cast(mpt::random(AccessPRNG())) + 1)) / 128; chn.nResSwing = static_cast((d * chn.nResonance + 1) / 128); chn.nRestoreResonanceOnNewNote = chn.nResonance + 1; } } } if(!pSmp) return; if(period) { if((!bPorta) || (!chn.nPeriod)) chn.nPeriod = period; if(!newTuning) { // FT2 compatibility: Don't reset portamento target with new notes. // Test case: Porta-Pickup.xm // ProTracker does the same. // Test case: PortaTarget.mod // IT compatibility: Portamento target is completely cleared with new notes. // Test case: PortaReset.it if(bPorta || !(m_playBehaviour[kFT2PortaTargetNoReset] || m_playBehaviour[kITClearPortaTarget] || GetType() == MOD_TYPE_MOD)) { chn.nPortamentoDest = period; chn.portaTargetReached = false; } } if(!bPorta || (!chn.nLength && !(GetType() & MOD_TYPE_S3M))) { chn.pModSample = pSmp; chn.nLength = pSmp->nLength; chn.nLoopEnd = pSmp->nLength; chn.nLoopStart = 0; chn.position.Set(0); if((m_SongFlags[SONG_PT_MODE] || m_playBehaviour[kST3OffsetWithoutInstrument]) && !chn.rowCommand.instr) { chn.position.SetInt(std::min(chn.prevNoteOffset, chn.nLength - SmpLength(1))); } else { chn.prevNoteOffset = 0; } chn.dwFlags = (chn.dwFlags & CHN_CHANNELFLAGS) | (pSmp->uFlags & CHN_SAMPLEFLAGS); chn.dwFlags.reset(CHN_PORTAMENTO); if(chn.dwFlags[CHN_SUSTAINLOOP]) { chn.nLoopStart = pSmp->nSustainStart; chn.nLoopEnd = pSmp->nSustainEnd; chn.dwFlags.set(CHN_PINGPONGLOOP, chn.dwFlags[CHN_PINGPONGSUSTAIN]); chn.dwFlags.set(CHN_LOOP); if (chn.nLength > chn.nLoopEnd) chn.nLength = chn.nLoopEnd; } else if(chn.dwFlags[CHN_LOOP]) { chn.nLoopStart = pSmp->nLoopStart; chn.nLoopEnd = pSmp->nLoopEnd; if (chn.nLength > chn.nLoopEnd) chn.nLength = chn.nLoopEnd; } // ProTracker "oneshot" loops (if loop start is 0, play the whole sample once and then repeat until loop end) if(m_playBehaviour[kMODOneShotLoops] && chn.nLoopStart == 0) chn.nLoopEnd = chn.nLength = pSmp->nLength; if(chn.dwFlags[CHN_REVERSE] && chn.nLength > 0) { chn.dwFlags.set(CHN_PINGPONGFLAG); chn.position.SetInt(chn.nLength - 1); } // Handle "retrigger" waveform type if(chn.nVibratoType < 4) { // IT Compatibilty: Slightly different waveform offsets (why does MPT have two different offsets here with IT old effects enabled and disabled?) if(!m_playBehaviour[kITVibratoTremoloPanbrello] && (GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) && !m_SongFlags[SONG_ITOLDEFFECTS]) chn.nVibratoPos = 0x10; else if(GetType() == MOD_TYPE_MTM) chn.nVibratoPos = 0x20; else if(!(GetType() & (MOD_TYPE_DIGI | MOD_TYPE_DBM))) chn.nVibratoPos = 0; } // IT Compatibility: No "retrigger" waveform here if(!m_playBehaviour[kITVibratoTremoloPanbrello] && chn.nTremoloType < 4) { chn.nTremoloPos = 0; } } if(chn.position.GetUInt() >= chn.nLength) chn.position.SetInt(chn.nLoopStart); } else { bPorta = false; } if (!bPorta || (!(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_DBM))) || (chn.dwFlags[CHN_NOTEFADE] && !chn.nFadeOutVol) || (m_SongFlags[SONG_ITCOMPATGXX] && chn.rowCommand.instr != 0)) { if((GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_DBM)) && chn.dwFlags[CHN_NOTEFADE] && !chn.nFadeOutVol) { chn.ResetEnvelopes(); // IT Compatibility: Autovibrato reset if(!m_playBehaviour[kITVibratoTremoloPanbrello]) { chn.nAutoVibDepth = 0; chn.nAutoVibPos = 0; } chn.dwFlags.reset(CHN_NOTEFADE); chn.nFadeOutVol = 65536; } if ((!bPorta) || (!m_SongFlags[SONG_ITCOMPATGXX]) || (chn.rowCommand.instr)) { if ((!(GetType() & (MOD_TYPE_XM|MOD_TYPE_MT2))) || (chn.rowCommand.instr)) { chn.dwFlags.reset(CHN_NOTEFADE); chn.nFadeOutVol = 65536; } } } // IT compatibility: Don't reset key-off flag on porta notes unless Compat Gxx is enabled. // Test case: Off-Porta.it, Off-Porta-CompatGxx.it, Off-Porta.xm if(m_playBehaviour[kITFT2DontResetNoteOffOnPorta] && bPorta && (!m_SongFlags[SONG_ITCOMPATGXX] || chn.rowCommand.instr == 0)) chn.dwFlags.reset(CHN_EXTRALOUD); else chn.dwFlags.reset(CHN_EXTRALOUD | CHN_KEYOFF); // Enable Ramping if(!bPorta) { chn.nLeftVU = chn.nRightVU = 0xFF; chn.dwFlags.reset(CHN_FILTER); chn.dwFlags.set(CHN_FASTVOLRAMP); // IT compatibility 15. Retrigger is reset in RetrigNote (Tremor doesn't store anything here, so we just don't reset this as well) if(!m_playBehaviour[kITRetrigger] && !m_playBehaviour[kITTremor]) { // FT2 compatibility: Retrigger is reset in RetrigNote, tremor in ProcessEffects if(!m_playBehaviour[kFT2Retrigger] && !m_playBehaviour[kFT2Tremor]) { chn.nRetrigCount = 0; chn.nTremorCount = 0; } } if(bResetEnv) { chn.nAutoVibDepth = 0; chn.nAutoVibPos = 0; } chn.rightVol = chn.leftVol = 0; bool useFilter = !m_SongFlags[SONG_MPTFILTERMODE]; // Setup Initial Filter for this note if(pIns) { if(pIns->IsResonanceEnabled()) { chn.nResonance = pIns->GetResonance(); useFilter = true; } if(pIns->IsCutoffEnabled()) { chn.nCutOff = pIns->GetCutoff(); useFilter = true; } if(useFilter && (pIns->filterMode != FilterMode::Unchanged)) { chn.nFilterMode = pIns->filterMode; } } else { chn.nVolSwing = chn.nPanSwing = 0; chn.nCutSwing = chn.nResSwing = 0; } if((chn.nCutOff < 0x7F || m_playBehaviour[kITFilterBehaviour]) && useFilter) { int cutoff = SetupChannelFilter(chn, true); if(cutoff >= 0 && chn.dwFlags[CHN_ADLIB] && m_opl && channelHint != CHANNELINDEX_INVALID) m_opl->Volume(channelHint, chn.nCutOff / 2u, true); } if(chn.dwFlags[CHN_ADLIB] && m_opl && channelHint != CHANNELINDEX_INVALID) { // Test case: AdlibZeroVolumeNote.s3m if(m_playBehaviour[kOPLNoteOffOnNoteChange]) m_opl->NoteOff(channelHint); else if(m_playBehaviour[kOPLNoteStopWith0Hz]) m_opl->Frequency(channelHint, 0, true, false); } } // Special case for MPT if (bManual) chn.dwFlags.reset(CHN_MUTE); if((chn.dwFlags[CHN_MUTE] && (m_MixerSettings.MixerFlags & SNDMIX_MUTECHNMODE)) || (chn.pModSample != nullptr && chn.pModSample->uFlags[CHN_MUTE] && !bManual) || (chn.pModInstrument != nullptr && chn.pModInstrument->dwFlags[INS_MUTE] && !bManual)) { if (!bManual) chn.nPeriod = 0; } // Reset the Amiga resampler for this channel if(!bPorta) { chn.paulaState.Reset(); } } // Apply sample or instrument panning void CSoundFile::ApplyInstrumentPanning(ModChannel &chn, const ModInstrument *instr, const ModSample *smp) const { int32 newPan = int32_min; // Default instrument panning if(instr != nullptr && instr->dwFlags[INS_SETPANNING]) newPan = instr->nPan; // Default sample panning if(smp != nullptr && smp->uFlags[CHN_PANNING]) newPan = smp->nPan; if(newPan != int32_min) { chn.SetInstrumentPan(newPan, *this); // IT compatibility: Sample and instrument panning overrides channel surround status. // Test case: SmpInsPanSurround.it if(m_playBehaviour[kPanOverride] && !m_SongFlags[SONG_SURROUNDPAN]) { chn.dwFlags.reset(CHN_SURROUND); } } } CHANNELINDEX CSoundFile::GetNNAChannel(CHANNELINDEX nChn) const { // Check for empty channel for(CHANNELINDEX i = m_nChannels; i < MAX_CHANNELS; i++) { const ModChannel &c = m_PlayState.Chn[i]; // No sample and no plugin playing if(!c.nLength && !c.HasMIDIOutput()) return i; // Plugin channel with already released note if(!c.nLength && c.dwFlags[CHN_KEYOFF | CHN_NOTEFADE]) return i; // Stopped OPL channel if(c.dwFlags[CHN_ADLIB] && (!m_opl || !m_opl->IsActive(i))) return i; } uint32 vol = 0x800000; if(nChn < MAX_CHANNELS) { const ModChannel &srcChn = m_PlayState.Chn[nChn]; if(!srcChn.nFadeOutVol && srcChn.nLength) return CHANNELINDEX_INVALID; vol = (srcChn.nRealVolume << 9) | srcChn.nVolume; } // All channels are used: check for lowest volume CHANNELINDEX result = CHANNELINDEX_INVALID; uint32 envpos = 0; for(CHANNELINDEX i = m_nChannels; i < MAX_CHANNELS; i++) { const ModChannel &c = m_PlayState.Chn[i]; if(c.nLength && !c.nFadeOutVol) return i; // Use a combination of real volume [14 bit] (which includes volume envelopes, but also potentially global volume) and note volume [9 bit]. // Rationale: We need volume envelopes in case e.g. all NNA channels are playing at full volume but are looping on a 0-volume envelope node. // But if global volume is not applied to master and the global volume temporarily drops to 0, we would kill arbitrary channels. Hence, add the note volume as well. uint32 v = (c.nRealVolume << 9) | c.nVolume; if(c.dwFlags[CHN_LOOP]) v /= 2; if((v < vol) || ((v == vol) && (c.VolEnv.nEnvPosition > envpos))) { envpos = c.VolEnv.nEnvPosition; vol = v; result = i; } } return result; } CHANNELINDEX CSoundFile::CheckNNA(CHANNELINDEX nChn, uint32 instr, int note, bool forceCut) { ModChannel &srcChn = m_PlayState.Chn[nChn]; const ModInstrument *pIns = nullptr; if(!ModCommand::IsNote(static_cast(note))) return CHANNELINDEX_INVALID; // Always NNA cut - using if((!(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_MT2)) || !m_nInstruments || forceCut) && !srcChn.HasMIDIOutput()) { if(!srcChn.nLength || srcChn.dwFlags[CHN_MUTE] || !(srcChn.rightVol | srcChn.leftVol)) return CHANNELINDEX_INVALID; if(srcChn.dwFlags[CHN_ADLIB] && m_opl) { m_opl->NoteCut(nChn, false); return CHANNELINDEX_INVALID; } const CHANNELINDEX nnaChn = GetNNAChannel(nChn); if(nnaChn == CHANNELINDEX_INVALID) return CHANNELINDEX_INVALID; ModChannel &chn = m_PlayState.Chn[nnaChn]; // Copy Channel chn = srcChn; chn.dwFlags.reset(CHN_VIBRATO | CHN_TREMOLO | CHN_MUTE | CHN_PORTAMENTO); chn.nPanbrelloOffset = 0; chn.nMasterChn = nChn + 1; chn.nCommand = CMD_NONE; chn.rowCommand.Clear(); // Cut the note chn.nFadeOutVol = 0; chn.dwFlags.set(CHN_NOTEFADE | CHN_FASTVOLRAMP); // Stop this channel srcChn.nLength = 0; srcChn.position.Set(0); srcChn.nROfs = srcChn.nLOfs = 0; srcChn.rightVol = srcChn.leftVol = 0; return nnaChn; } if(instr > GetNumInstruments()) instr = 0; const ModSample *pSample = srcChn.pModSample; // If no instrument is given, assume previous instrument to still be valid. // Test case: DNA-NoInstr.it pIns = instr > 0 ? Instruments[instr] : srcChn.pModInstrument; auto dnaNote = note; if(pIns != nullptr) { auto smp = pIns->Keyboard[note - NOTE_MIN]; // IT compatibility: DCT = note uses pattern notes for comparison // Note: This is not applied in case kITRealNoteMapping is not set to keep playback of legacy modules simple (chn.nNote is translated note in that case) // Test case: dct_smp_note_test.it if(!m_playBehaviour[kITDCTBehaviour] || !m_playBehaviour[kITRealNoteMapping]) dnaNote = pIns->NoteMap[note - NOTE_MIN]; if(smp > 0 && smp < MAX_SAMPLES) { pSample = &Samples[smp]; } else if(m_playBehaviour[kITEmptyNoteMapSlot] && !pIns->HasValidMIDIChannel()) { // Impulse Tracker ignores empty slots. // We won't ignore them if a plugin is assigned to this slot, so that VSTis still work as intended. // Test case: emptyslot.it, PortaInsNum.it, gxsmp.it, gxsmp2.it return CHANNELINDEX_INVALID; } } if(srcChn.dwFlags[CHN_MUTE]) return CHANNELINDEX_INVALID; for(CHANNELINDEX i = nChn; i < MAX_CHANNELS; i++) { // Only apply to background channels, or the same pattern channel if(i < m_nChannels && i != nChn) continue; ModChannel &chn = m_PlayState.Chn[i]; bool applyDNAtoPlug = false; if((chn.nMasterChn == nChn + 1 || i == nChn) && chn.pModInstrument != nullptr) { bool applyDNA = false; // Duplicate Check Type switch(chn.pModInstrument->nDCT) { case DuplicateCheckType::None: break; // Note case DuplicateCheckType::Note: if(dnaNote != NOTE_NONE && chn.nNote == dnaNote && pIns == chn.pModInstrument) applyDNA = true; if(pIns && pIns->nMixPlug) applyDNAtoPlug = true; break; // Sample case DuplicateCheckType::Sample: // IT compatibility: DCT = sample only applies to same instrument // Test case: dct_smp_note_test.it if(pSample != nullptr && pSample == chn.pModSample && (pIns == chn.pModInstrument || !m_playBehaviour[kITDCTBehaviour])) applyDNA = true; break; // Instrument case DuplicateCheckType::Instrument: if(pIns == chn.pModInstrument) applyDNA = true; if(pIns && pIns->nMixPlug) applyDNAtoPlug = true; break; // Plugin case DuplicateCheckType::Plugin: if(pIns && (pIns->nMixPlug) && (pIns->nMixPlug == chn.pModInstrument->nMixPlug)) { applyDNAtoPlug = true; applyDNA = true; } break; } // Duplicate Note Action if(applyDNA) { #ifndef NO_PLUGINS if(applyDNAtoPlug && chn.nNote != NOTE_NONE) { switch(chn.pModInstrument->nDNA) { case DuplicateNoteAction::NoteCut: case DuplicateNoteAction::NoteOff: case DuplicateNoteAction::NoteFade: // Switch off duplicated note played on this plugin if(const auto oldNote = chn.GetPluginNote(m_playBehaviour[kITRealNoteMapping]); oldNote != NOTE_NONE) { SendMIDINote(i, oldNote + NOTE_MAX_SPECIAL, 0); chn.nArpeggioLastNote = NOTE_NONE; chn.nNote = NOTE_NONE; } break; } } #endif // NO_PLUGINS switch(chn.pModInstrument->nDNA) { // Cut case DuplicateNoteAction::NoteCut: KeyOff(chn); chn.nVolume = 0; if(chn.dwFlags[CHN_ADLIB] && m_opl) m_opl->NoteCut(i); break; // Note Off case DuplicateNoteAction::NoteOff: KeyOff(chn); if(chn.dwFlags[CHN_ADLIB] && m_opl) m_opl->NoteOff(i); break; // Note Fade case DuplicateNoteAction::NoteFade: chn.dwFlags.set(CHN_NOTEFADE); if(chn.dwFlags[CHN_ADLIB] && m_opl && !m_playBehaviour[kOPLwithNNA]) m_opl->NoteOff(i); break; } if(!chn.nVolume) { chn.nFadeOutVol = 0; chn.dwFlags.set(CHN_NOTEFADE | CHN_FASTVOLRAMP); } } } } // Do we need to apply New/Duplicate Note Action to a VSTi? bool applyNNAtoPlug = false; #ifndef NO_PLUGINS IMixPlugin *pPlugin = nullptr; if(srcChn.HasMIDIOutput() && ModCommand::IsNote(srcChn.nNote)) // instro sends to a midi chan { PLUGINDEX plugin = GetBestPlugin(m_PlayState, nChn, PrioritiseInstrument, RespectMutes); if(plugin > 0 && plugin <= MAX_MIXPLUGINS) { pPlugin = m_MixPlugins[plugin - 1].pMixPlugin; if(pPlugin) { // apply NNA to this plugin iff it is currently playing a note on this tracker channel // (and if it is playing a note, we know that would be the last note played on this chan). const auto oldNote = srcChn.GetPluginNote(m_playBehaviour[kITRealNoteMapping]); applyNNAtoPlug = (oldNote != NOTE_NONE) && pPlugin->IsNotePlaying(oldNote, nChn); } } } #endif // NO_PLUGINS // New Note Action if(!srcChn.IsSamplePlaying() && !applyNNAtoPlug) return CHANNELINDEX_INVALID; #ifndef NO_PLUGINS if(applyNNAtoPlug && pPlugin) { switch(srcChn.nNNA) { case NewNoteAction::NoteOff: case NewNoteAction::NoteCut: case NewNoteAction::NoteFade: // Switch off note played on this plugin, on this tracker channel and midi channel SendMIDINote(nChn, NOTE_KEYOFF, 0); srcChn.nArpeggioLastNote = NOTE_NONE; break; case NewNoteAction::Continue: break; } } #endif // NO_PLUGINS CHANNELINDEX nnaChn = GetNNAChannel(nChn); if(nnaChn == CHANNELINDEX_INVALID) return CHANNELINDEX_INVALID; ModChannel &chn = m_PlayState.Chn[nnaChn]; if(chn.dwFlags[CHN_ADLIB] && m_opl) m_opl->NoteCut(nnaChn); // Copy Channel chn = srcChn; chn.dwFlags.reset(CHN_VIBRATO | CHN_TREMOLO | CHN_PORTAMENTO); chn.nPanbrelloOffset = 0; chn.nMasterChn = nChn < GetNumChannels() ? nChn + 1 : 0; chn.nCommand = CMD_NONE; // Key Off the note switch(srcChn.nNNA) { case NewNoteAction::NoteOff: KeyOff(chn); if(chn.dwFlags[CHN_ADLIB] && m_opl) { m_opl->NoteOff(nChn); if(m_playBehaviour[kOPLwithNNA]) m_opl->MoveChannel(nChn, nnaChn); } break; case NewNoteAction::NoteCut: chn.nFadeOutVol = 0; chn.dwFlags.set(CHN_NOTEFADE); if(chn.dwFlags[CHN_ADLIB] && m_opl) m_opl->NoteCut(nChn); break; case NewNoteAction::NoteFade: chn.dwFlags.set(CHN_NOTEFADE); if(chn.dwFlags[CHN_ADLIB] && m_opl) { if(m_playBehaviour[kOPLwithNNA]) m_opl->MoveChannel(nChn, nnaChn); else m_opl->NoteOff(nChn); } break; case NewNoteAction::Continue: if(chn.dwFlags[CHN_ADLIB] && m_opl) m_opl->MoveChannel(nChn, nnaChn); break; } if(!chn.nVolume) { chn.nFadeOutVol = 0; chn.dwFlags.set(CHN_NOTEFADE | CHN_FASTVOLRAMP); } // Stop this channel srcChn.nLength = 0; srcChn.position.Set(0); srcChn.nROfs = srcChn.nLOfs = 0; return nnaChn; } bool CSoundFile::ProcessEffects() { m_PlayState.m_breakRow = ROWINDEX_INVALID; // Is changed if a break to row command is encountered m_PlayState.m_patLoopRow = ROWINDEX_INVALID; // Is changed if a pattern loop jump-back is executed m_PlayState.m_posJump = ORDERINDEX_INVALID; for(CHANNELINDEX nChn = 0; nChn < GetNumChannels(); nChn++) { ModChannel &chn = m_PlayState.Chn[nChn]; const uint32 tickCount = m_PlayState.m_nTickCount % (m_PlayState.m_nMusicSpeed + m_PlayState.m_nFrameDelay); uint32 instr = chn.rowCommand.instr; ModCommand::VOLCMD volcmd = chn.rowCommand.volcmd; uint32 vol = chn.rowCommand.vol; ModCommand::COMMAND cmd = chn.rowCommand.command; uint32 param = chn.rowCommand.param; bool bPorta = chn.rowCommand.IsPortamento(); uint32 nStartTick = 0; chn.isFirstTick = m_SongFlags[SONG_FIRSTTICK]; // Process parameter control note. if(chn.rowCommand.note == NOTE_PC) { #ifndef NO_PLUGINS const PLUGINDEX plug = chn.rowCommand.instr; const PlugParamIndex plugparam = chn.rowCommand.GetValueVolCol(); const PlugParamValue value = chn.rowCommand.GetValueEffectCol() / PlugParamValue(ModCommand::maxColumnValue); if(plug > 0 && plug <= MAX_MIXPLUGINS && m_MixPlugins[plug - 1].pMixPlugin) m_MixPlugins[plug-1].pMixPlugin->SetParameter(plugparam, value); #endif // NO_PLUGINS } // Process continuous parameter control note. // Row data is cleared after first tick so on following // ticks using channels m_nPlugParamValueStep to identify // the need for parameter control. The condition cmd == 0 // is to make sure that m_nPlugParamValueStep != 0 because // of NOTE_PCS, not because of macro. if(chn.rowCommand.note == NOTE_PCS || (cmd == CMD_NONE && chn.m_plugParamValueStep != 0)) { #ifndef NO_PLUGINS const bool isFirstTick = m_SongFlags[SONG_FIRSTTICK]; if(isFirstTick) chn.m_RowPlug = chn.rowCommand.instr; const PLUGINDEX plugin = chn.m_RowPlug; const bool hasValidPlug = (plugin > 0 && plugin <= MAX_MIXPLUGINS && m_MixPlugins[plugin - 1].pMixPlugin); if(hasValidPlug) { if(isFirstTick) chn.m_RowPlugParam = ModCommand::GetValueVolCol(chn.rowCommand.volcmd, chn.rowCommand.vol); const PlugParamIndex plugparam = chn.m_RowPlugParam; if(isFirstTick) { PlugParamValue targetvalue = ModCommand::GetValueEffectCol(chn.rowCommand.command, chn.rowCommand.param) / PlugParamValue(ModCommand::maxColumnValue); chn.m_plugParamTargetValue = targetvalue; chn.m_plugParamValueStep = (targetvalue - m_MixPlugins[plugin - 1].pMixPlugin->GetParameter(plugparam)) / PlugParamValue(m_PlayState.TicksOnRow()); } if(m_PlayState.m_nTickCount + 1 == m_PlayState.TicksOnRow()) { // On last tick, set parameter exactly to target value. m_MixPlugins[plugin - 1].pMixPlugin->SetParameter(plugparam, chn.m_plugParamTargetValue); } else m_MixPlugins[plugin - 1].pMixPlugin->ModifyParameter(plugparam, chn.m_plugParamValueStep); } #endif // NO_PLUGINS } // Apart from changing parameters, parameter control notes are intended to be 'invisible'. // To achieve this, clearing the note data so that rest of the process sees the row as empty row. if(ModCommand::IsPcNote(chn.rowCommand.note)) { chn.ClearRowCmd(); instr = 0; volcmd = VOLCMD_NONE; vol = 0; cmd = CMD_NONE; param = 0; bPorta = false; } // Process Invert Loop (MOD Effect, called every row if it's active) if(!m_SongFlags[SONG_FIRSTTICK]) { InvertLoop(m_PlayState.Chn[nChn]); } else { if(instr) m_PlayState.Chn[nChn].nEFxOffset = 0; } // Process special effects (note delay, pattern delay, pattern loop) if (cmd == CMD_DELAYCUT) { //:xy --> note delay until tick x, note cut at tick x+y nStartTick = (param & 0xF0) >> 4; const uint32 cutAtTick = nStartTick + (param & 0x0F); NoteCut(nChn, cutAtTick, m_playBehaviour[kITSCxStopsSample]); } else if ((cmd == CMD_MODCMDEX) || (cmd == CMD_S3MCMDEX)) { if ((!param) && (GetType() & (MOD_TYPE_S3M|MOD_TYPE_IT|MOD_TYPE_MPT))) param = chn.nOldCmdEx; else chn.nOldCmdEx = static_cast(param); // Note Delay ? if ((param & 0xF0) == 0xD0) { nStartTick = param & 0x0F; if(nStartTick == 0) { //IT compatibility 22. SD0 == SD1 if(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) nStartTick = 1; //ST3 ignores notes with SD0 completely else if(GetType() == MOD_TYPE_S3M) continue; } else if(nStartTick >= (m_PlayState.m_nMusicSpeed + m_PlayState.m_nFrameDelay) && m_playBehaviour[kITOutOfRangeDelay]) { // IT compatibility 08. Handling of out-of-range delay command. // Additional test case: tickdelay.it if(instr) { chn.nNewIns = static_cast(instr); } continue; } } else if(m_SongFlags[SONG_FIRSTTICK]) { // Pattern Loop ? if((param & 0xF0) == 0xE0) { // Pattern Delay // In Scream Tracker 3 / Impulse Tracker, only the first delay command on this row is considered. // Test cases: PatternDelays.it, PatternDelays.s3m, PatternDelays.xm // XXX In Scream Tracker 3, the "left" channels are evaluated before the "right" channels, which is not emulated here! if(!(GetType() & (MOD_TYPE_S3M | MOD_TYPE_IT | MOD_TYPE_MPT)) || !m_PlayState.m_nPatternDelay) { if(!(GetType() & (MOD_TYPE_S3M)) || (param & 0x0F) != 0) { // While Impulse Tracker *does* count S60 as a valid row delay (and thus ignores any other row delay commands on the right), // Scream Tracker 3 simply ignores such commands. m_PlayState.m_nPatternDelay = 1 + (param & 0x0F); } } } } } if(GetType() == MOD_TYPE_MTM && cmd == CMD_MODCMDEX && (param & 0xF0) == 0xD0) { // Apparently, retrigger and note delay have the same behaviour in MultiTracker: // They both restart the note at tick x, and if there is a note on the same row, // this note is started on the first tick. nStartTick = 0; param = 0x90 | (param & 0x0F); } if(nStartTick != 0 && chn.rowCommand.note == NOTE_KEYOFF && chn.rowCommand.volcmd == VOLCMD_PANNING && m_playBehaviour[kFT2PanWithDelayedNoteOff]) { // FT2 compatibility: If there's a delayed note off, panning commands are ignored. WTF! // Test case: PanOff.xm chn.rowCommand.volcmd = VOLCMD_NONE; } bool triggerNote = (m_PlayState.m_nTickCount == nStartTick); // Can be delayed by a note delay effect if(m_playBehaviour[kFT2OutOfRangeDelay] && nStartTick >= m_PlayState.m_nMusicSpeed) { // FT2 compatibility: Note delays greater than the song speed should be ignored. // However, EEx pattern delay is *not* considered at all. // Test case: DelayCombination.xm, PortaDelay.xm triggerNote = false; } else if(m_playBehaviour[kRowDelayWithNoteDelay] && nStartTick > 0 && tickCount == nStartTick) { // IT compatibility: Delayed notes (using SDx) that are on the same row as a Row Delay effect are retriggered. // ProTracker / Scream Tracker 3 / FastTracker 2 do the same. // Test case: PatternDelay-NoteDelay.it, PatternDelay-NoteDelay.xm, PatternDelaysRetrig.mod triggerNote = true; } // IT compatibility: Tick-0 vs non-tick-0 effect distinction is always based on tick delay. // Test case: SlideDelay.it if(m_playBehaviour[kITFirstTickHandling]) { chn.isFirstTick = tickCount == nStartTick; } chn.triggerNote = triggerNote; // FT2 compatibility: Note + portamento + note delay = no portamento // Test case: PortaDelay.xm if(m_playBehaviour[kFT2PortaDelay] && nStartTick != 0) { bPorta = false; } if(m_SongFlags[SONG_PT_MODE] && instr && !m_PlayState.m_nTickCount) { // Instrument number resets the stacked ProTracker offset. // Test case: ptoffset.mod chn.prevNoteOffset = 0; // ProTracker compatibility: Sample properties are always loaded on the first tick, even when there is a note delay. // Test case: InstrDelay.mod if(!triggerNote && chn.IsSamplePlaying()) { chn.nNewIns = static_cast(instr); if(instr <= GetNumSamples()) { chn.nVolume = Samples[instr].nVolume; chn.nFineTune = Samples[instr].nFineTune; } } } // Handles note/instrument/volume changes if(triggerNote) { ModCommand::NOTE note = chn.rowCommand.note; if(instr) chn.nNewIns = static_cast(instr); if(ModCommand::IsNote(note) && m_playBehaviour[kFT2Transpose]) { // Notes that exceed FT2's limit are completely ignored. // Test case: NoteLimit.xm int transpose = chn.nTranspose; if(instr && !bPorta) { // Refresh transpose // Test case: NoteLimit2.xm const SAMPLEINDEX sample = GetSampleIndex(note, instr); if(sample > 0) transpose = GetSample(sample).RelativeTone; } const int computedNote = note + transpose; if((computedNote < NOTE_MIN + 11 || computedNote > NOTE_MIN + 130)) { note = NOTE_NONE; } } else if((GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_J2B)) && GetNumInstruments() != 0 && ModCommand::IsNoteOrEmpty(static_cast(note))) { // IT compatibility: Invalid instrument numbers do nothing, but they are remembered for upcoming notes and do not trigger a note in that case. // Test case: InstrumentNumberChange.it INSTRUMENTINDEX instrToCheck = static_cast((instr != 0) ? instr : chn.nOldIns); if(instrToCheck != 0 && (instrToCheck > GetNumInstruments() || Instruments[instrToCheck] == nullptr)) { note = NOTE_NONE; instr = 0; } } // XM: FT2 ignores a note next to a K00 effect, and a fade-out seems to be done when no volume envelope is present (not exactly the Kxx behaviour) if(cmd == CMD_KEYOFF && param == 0 && m_playBehaviour[kFT2KeyOff]) { note = NOTE_NONE; instr = 0; } bool retrigEnv = note == NOTE_NONE && instr != 0; // Apparently, any note number in a pattern causes instruments to recall their original volume settings - no matter if there's a Note Off next to it or whatever. // Test cases: keyoff+instr.xm, delay.xm bool reloadSampleSettings = (m_playBehaviour[kFT2ReloadSampleSettings] && instr != 0); // ProTracker Compatibility: If a sample was stopped before, lone instrument numbers can retrigger it // Test case: PTSwapEmpty.mod, PTInstrVolume.mod, SampleSwap.s3m bool keepInstr = (GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) || m_playBehaviour[kST3SampleSwap] || (m_playBehaviour[kMODSampleSwap] && !chn.IsSamplePlaying() && (chn.pModSample == nullptr || !chn.pModSample->HasSampleData())); // Now it's time for some FT2 crap... if (GetType() & (MOD_TYPE_XM | MOD_TYPE_MT2)) { // XM: Key-Off + Sample == Note Cut (BUT: Only if no instr number or volume effect is present!) // Test case: NoteOffVolume.xm if(note == NOTE_KEYOFF && ((!instr && volcmd != VOLCMD_VOLUME && cmd != CMD_VOLUME) || !m_playBehaviour[kFT2KeyOff]) && (chn.pModInstrument == nullptr || !chn.pModInstrument->VolEnv.dwFlags[ENV_ENABLED])) { chn.dwFlags.set(CHN_FASTVOLRAMP); chn.nVolume = 0; note = NOTE_NONE; instr = 0; retrigEnv = false; // FT2 Compatbility: Start fading the note for notes with no delay. Only relevant when a volume command is encountered after the note-off. // Test case: NoteOffFadeNoEnv.xm if(m_SongFlags[SONG_FIRSTTICK] && m_playBehaviour[kFT2NoteOffFlags]) chn.dwFlags.set(CHN_NOTEFADE); } else if(m_playBehaviour[kFT2RetrigWithNoteDelay] && !m_SongFlags[SONG_FIRSTTICK]) { // FT2 Compatibility: Some special hacks for rogue note delays... (EDx with x > 0) // Apparently anything that is next to a note delay behaves totally unpredictable in FT2. Swedish tracker logic. :) retrigEnv = true; // Portamento + Note Delay = No Portamento // Test case: porta-delay.xm bPorta = false; if(note == NOTE_NONE) { // If there's a note delay but no real note, retrig the last note. // Test case: delay2.xm, delay3.xm note = static_cast(chn.nNote - chn.nTranspose); } else if(note >= NOTE_MIN_SPECIAL) { // Gah! Even Note Off + Note Delay will cause envelopes to *retrigger*! How stupid is that? // ... Well, and that is actually all it does if there's an envelope. No fade out, no nothing. *sigh* // Test case: OffDelay.xm note = NOTE_NONE; keepInstr = false; reloadSampleSettings = true; } else if(instr || !m_playBehaviour[kFT2NoteDelayWithoutInstr]) { // Normal note (only if there is an instrument, test case: DelayVolume.xm) keepInstr = true; reloadSampleSettings = true; } } } if((retrigEnv && !m_playBehaviour[kFT2ReloadSampleSettings]) || reloadSampleSettings) { const ModSample *oldSample = nullptr; // Reset default volume when retriggering envelopes if(GetNumInstruments()) { oldSample = chn.pModSample; } else if (instr <= GetNumSamples()) { // Case: Only samples are used; no instruments. oldSample = &Samples[instr]; } if(oldSample != nullptr) { if(!oldSample->uFlags[SMP_NODEFAULTVOLUME] && (GetType() != MOD_TYPE_S3M || oldSample->HasSampleData())) chn.nVolume = oldSample->nVolume; if(reloadSampleSettings) { // Also reload panning chn.SetInstrumentPan(oldSample->nPan, *this); } } } // FT2 compatibility: Instrument number disables tremor effect // Test case: TremorInstr.xm, TremoRecover.xm if(m_playBehaviour[kFT2Tremor] && instr != 0) { chn.nTremorCount = 0x20; } // IT compatibility: Envelope retriggering with instrument number based on Old Effects and Compatible Gxx flags: // OldFX CompatGxx Env Behaviour // ----- --------- ------------- // off off never reset // on off reset on instrument without portamento // off on reset on instrument with portamento // on on always reset // Test case: ins-xx.it, ins-ox.it, ins-oc.it, ins-xc.it, ResetEnvNoteOffOldFx.it, ResetEnvNoteOffOldFx2.it, noteoff3.it if(GetNumInstruments() && m_playBehaviour[kITInstrWithNoteOffOldEffects] && instr && !ModCommand::IsNote(note)) { if((bPorta && m_SongFlags[SONG_ITCOMPATGXX]) || (!bPorta && m_SongFlags[SONG_ITOLDEFFECTS])) { chn.ResetEnvelopes(); chn.dwFlags.set(CHN_FASTVOLRAMP); chn.nFadeOutVol = 65536; } } if(retrigEnv) //Case: instrument with no note data. { //IT compatibility: Instrument with no note. if(m_playBehaviour[kITInstrWithoutNote] || GetType() == MOD_TYPE_PLM) { // IT compatibility: Completely retrigger note after sample end to also reset portamento. // Test case: PortaResetAfterRetrigger.it bool triggerAfterSmpEnd = m_playBehaviour[kITMultiSampleInstrumentNumber] && !chn.IsSamplePlaying(); if(GetNumInstruments()) { // Instrument mode if(instr <= GetNumInstruments() && (chn.pModInstrument != Instruments[instr] || triggerAfterSmpEnd)) note = chn.nNote; } else { // Sample mode if(instr < MAX_SAMPLES && (chn.pModSample != &Samples[instr] || triggerAfterSmpEnd)) note = chn.nNote; } } if(GetNumInstruments() && (GetType() & (MOD_TYPE_XM | MOD_TYPE_MT2 | MOD_TYPE_MED))) { chn.ResetEnvelopes(); chn.dwFlags.set(CHN_FASTVOLRAMP); chn.dwFlags.reset(CHN_NOTEFADE); chn.nAutoVibDepth = 0; chn.nAutoVibPos = 0; chn.nFadeOutVol = 65536; // FT2 Compatbility: Reset key-off status with instrument number // Test case: NoteOffInstrChange.xm if(m_playBehaviour[kFT2NoteOffFlags]) chn.dwFlags.reset(CHN_KEYOFF); } if (!keepInstr) instr = 0; } // Note Cut/Off/Fade => ignore instrument if (note >= NOTE_MIN_SPECIAL) { // IT compatibility: Default volume of sample is recalled if instrument number is next to a note-off. // Test case: NoteOffInstr.it, noteoff2.it if(m_playBehaviour[kITInstrWithNoteOff] && instr) { const SAMPLEINDEX smp = GetSampleIndex(chn.nLastNote, instr); if(smp > 0 && !Samples[smp].uFlags[SMP_NODEFAULTVOLUME]) chn.nVolume = Samples[smp].nVolume; } // IT compatibility: Note-off with instrument number + Old Effects retriggers envelopes. // Test case: ResetEnvNoteOffOldFx.it if(!m_playBehaviour[kITInstrWithNoteOffOldEffects] || !m_SongFlags[SONG_ITOLDEFFECTS]) instr = 0; } if(ModCommand::IsNote(note)) { chn.nNewNote = chn.nLastNote = note; // New Note Action ? if(!bPorta) { CheckNNA(nChn, instr, note, false); } chn.RestorePanAndFilter(); } // Instrument Change ? if(instr) { const ModSample *oldSample = chn.pModSample; //const ModInstrument *oldInstrument = chn.pModInstrument; InstrumentChange(chn, instr, bPorta, true); if(chn.pModSample != nullptr && chn.pModSample->uFlags[CHN_ADLIB] && m_opl) { m_opl->Patch(nChn, chn.pModSample->adlib); } // IT compatibility: Keep new instrument number for next instrument-less note even if sample playback is stopped // Test case: StoppedInstrSwap.it if(GetType() == MOD_TYPE_MOD) { // Test case: PortaSwapPT.mod if(!bPorta || !m_playBehaviour[kMODSampleSwap]) chn.nNewIns = 0; } else { if(!m_playBehaviour[kITInstrWithNoteOff] || ModCommand::IsNote(note)) chn.nNewIns = 0; } if(m_playBehaviour[kITPortamentoSwapResetsPos]) { // Test cases: PortaInsNum.it, PortaSample.it if(ModCommand::IsNote(note) && oldSample != chn.pModSample) { //const bool newInstrument = oldInstrument != chn.pModInstrument && chn.pModInstrument->Keyboard[chn.nNewNote - NOTE_MIN] != 0; chn.position.Set(0); } } else if((GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) && oldSample != chn.pModSample && ModCommand::IsNote(note)) { // Special IT case: portamento+note causes sample change -> ignore portamento bPorta = false; } else if(m_playBehaviour[kST3SampleSwap] && oldSample != chn.pModSample && (bPorta || !ModCommand::IsNote(note)) && chn.position.GetUInt() > chn.nLength) { // ST3 with SoundBlaster does sample swapping and continues playing the new sample where the old sample was stopped. // If the new sample is shorter than that, it is stopped, even if it could be looped. // This also applies to portamento between different samples. // Test case: SampleSwap.s3m chn.nLength = 0; } else if(m_playBehaviour[kMODSampleSwap] && !chn.IsSamplePlaying()) { // If channel was paused and is resurrected by a lone instrument number, reset the sample position. // Test case: PTSwapEmpty.mod chn.position.Set(0); } } // New Note ? if (note != NOTE_NONE) { const bool instrChange = (!instr) && (chn.nNewIns) && ModCommand::IsNote(note); if(instrChange) { InstrumentChange(chn, chn.nNewIns, bPorta, chn.pModSample == nullptr && chn.pModInstrument == nullptr, !(GetType() & (MOD_TYPE_XM|MOD_TYPE_MT2))); chn.nNewIns = 0; } if(chn.pModSample != nullptr && chn.pModSample->uFlags[CHN_ADLIB] && m_opl && (instrChange || !m_opl->IsActive(nChn))) { m_opl->Patch(nChn, chn.pModSample->adlib); } NoteChange(chn, note, bPorta, !(GetType() & (MOD_TYPE_XM | MOD_TYPE_MT2)), false, nChn); HandleDigiSamplePlayDirection(m_PlayState, nChn); if ((bPorta) && (GetType() & (MOD_TYPE_XM|MOD_TYPE_MT2)) && (instr)) { chn.dwFlags.set(CHN_FASTVOLRAMP); chn.ResetEnvelopes(); chn.nAutoVibDepth = 0; chn.nAutoVibPos = 0; } if(chn.dwFlags[CHN_ADLIB] && m_opl && ((note == NOTE_NOTECUT || note == NOTE_KEYOFF) || (note == NOTE_FADE && !m_playBehaviour[kOPLFlexibleNoteOff]))) { if(m_playBehaviour[kOPLNoteStopWith0Hz]) m_opl->Frequency(nChn, 0, true, false); m_opl->NoteOff(nChn); } } // Tick-0 only volume commands if (volcmd == VOLCMD_VOLUME) { if (vol > 64) vol = 64; chn.nVolume = vol << 2; chn.dwFlags.set(CHN_FASTVOLRAMP); } else if (volcmd == VOLCMD_PANNING) { Panning(chn, vol, Pan6bit); } #ifndef NO_PLUGINS if (m_nInstruments) ProcessMidiOut(nChn); #endif // NO_PLUGINS } if(m_playBehaviour[kST3NoMutedChannels] && ChnSettings[nChn].dwFlags[CHN_MUTE]) // not even effects are processed on muted S3M channels continue; // Volume Column Effect (except volume & panning) /* A few notes, paraphrased from ITTECH.TXT by Storlek (creator of schismtracker): Ex/Fx/Gx are shared with Exx/Fxx/Gxx; Ex/Fx are 4x the 'normal' slide value Gx is linked with Ex/Fx if Compat Gxx is off, just like Gxx is with Exx/Fxx Gx values: 1, 4, 8, 16, 32, 64, 96, 128, 255 Ax/Bx/Cx/Dx values are used directly (i.e. D9 == D09), and are NOT shared with Dxx (value is stored into nOldVolParam and used by A0/B0/C0/D0) Hx uses the same value as Hxx and Uxx, and affects the *depth* so... hxx = (hx | (oldhxx & 0xf0)) ??? TODO is this done correctly? */ bool doVolumeColumn = m_PlayState.m_nTickCount >= nStartTick; // FT2 compatibility: If there's a note delay, volume column effects are NOT executed // on the first tick and, if there's an instrument number, on the delayed tick. // Test case: VolColDelay.xm, PortaDelay.xm if(m_playBehaviour[kFT2VolColDelay] && nStartTick != 0) { doVolumeColumn = m_PlayState.m_nTickCount != 0 && (m_PlayState.m_nTickCount != nStartTick || (chn.rowCommand.instr == 0 && volcmd != VOLCMD_TONEPORTAMENTO)); } if(volcmd > VOLCMD_PANNING && doVolumeColumn) { if(volcmd == VOLCMD_TONEPORTAMENTO) { const auto [porta, clearEffectCommand] = GetVolCmdTonePorta(chn.rowCommand, nStartTick); if(clearEffectCommand) cmd = CMD_NONE; TonePortamento(chn, porta); } else { // FT2 Compatibility: FT2 ignores some volume commands with parameter = 0. if(m_playBehaviour[kFT2VolColMemory] && vol == 0) { switch(volcmd) { case VOLCMD_VOLUME: case VOLCMD_PANNING: case VOLCMD_VIBRATODEPTH: break; case VOLCMD_PANSLIDELEFT: // FT2 Compatibility: Pan slide left with zero parameter causes panning to be set to full left on every non-row tick. // Test case: PanSlideZero.xm if(!m_SongFlags[SONG_FIRSTTICK]) { chn.nPan = 0; } [[fallthrough]]; default: // no memory here. volcmd = VOLCMD_NONE; } } else if(!m_playBehaviour[kITVolColMemory]) { // IT Compatibility: Effects in the volume column don't have an unified memory. // Test case: VolColMemory.it if(vol) chn.nOldVolParam = static_cast(vol); else vol = chn.nOldVolParam; } switch(volcmd) { case VOLCMD_VOLSLIDEUP: case VOLCMD_VOLSLIDEDOWN: // IT Compatibility: Volume column volume slides have their own memory // Test case: VolColMemory.it if(vol == 0 && m_playBehaviour[kITVolColMemory]) { vol = chn.nOldVolParam; if(vol == 0) break; } else { chn.nOldVolParam = static_cast(vol); } VolumeSlide(chn, static_cast(volcmd == VOLCMD_VOLSLIDEUP ? (vol << 4) : vol)); break; case VOLCMD_FINEVOLUP: // IT Compatibility: Fine volume slides in the volume column are only executed on the first tick, not on multiples of the first tick in case of pattern delay // Test case: FineVolColSlide.it if(m_PlayState.m_nTickCount == nStartTick || !m_playBehaviour[kITVolColMemory]) { // IT Compatibility: Volume column volume slides have their own memory // Test case: VolColMemory.it FineVolumeUp(chn, static_cast(vol), m_playBehaviour[kITVolColMemory]); } break; case VOLCMD_FINEVOLDOWN: // IT Compatibility: Fine volume slides in the volume column are only executed on the first tick, not on multiples of the first tick in case of pattern delay // Test case: FineVolColSlide.it if(m_PlayState.m_nTickCount == nStartTick || !m_playBehaviour[kITVolColMemory]) { // IT Compatibility: Volume column volume slides have their own memory // Test case: VolColMemory.it FineVolumeDown(chn, static_cast(vol), m_playBehaviour[kITVolColMemory]); } break; case VOLCMD_VIBRATOSPEED: // FT2 does not automatically enable vibrato with the "set vibrato speed" command if(m_playBehaviour[kFT2VolColVibrato]) chn.nVibratoSpeed = vol & 0x0F; else Vibrato(chn, vol << 4); break; case VOLCMD_VIBRATODEPTH: Vibrato(chn, vol); break; case VOLCMD_PANSLIDELEFT: PanningSlide(chn, static_cast(vol), !m_playBehaviour[kFT2VolColMemory]); break; case VOLCMD_PANSLIDERIGHT: PanningSlide(chn, static_cast(vol << 4), !m_playBehaviour[kFT2VolColMemory]); break; case VOLCMD_PORTAUP: // IT compatibility (one of the first testcases - link effect memory) PortamentoUp(nChn, static_cast(vol << 2), m_playBehaviour[kITVolColFinePortamento]); break; case VOLCMD_PORTADOWN: // IT compatibility (one of the first testcases - link effect memory) PortamentoDown(nChn, static_cast(vol << 2), m_playBehaviour[kITVolColFinePortamento]); break; case VOLCMD_OFFSET: if(triggerNote && chn.pModSample && vol <= std::size(chn.pModSample->cues)) { SmpLength offset; if(vol == 0) offset = chn.oldOffset; else offset = chn.oldOffset = chn.pModSample->cues[vol - 1]; SampleOffset(chn, offset); } break; case VOLCMD_PLAYCONTROL: if(vol <= 1) chn.isPaused = (vol == 0); break; } } } // Effects if(cmd != CMD_NONE) switch (cmd) { // Set Volume case CMD_VOLUME: if(m_SongFlags[SONG_FIRSTTICK]) { chn.nVolume = (param < 64) ? param * 4 : 256; chn.dwFlags.set(CHN_FASTVOLRAMP); } break; // Portamento Up case CMD_PORTAMENTOUP: if ((!param) && (GetType() & MOD_TYPE_MOD)) break; PortamentoUp(nChn, static_cast(param)); break; // Portamento Down case CMD_PORTAMENTODOWN: if ((!param) && (GetType() & MOD_TYPE_MOD)) break; PortamentoDown(nChn, static_cast(param)); break; // Volume Slide case CMD_VOLUMESLIDE: if (param || (GetType() != MOD_TYPE_MOD)) VolumeSlide(chn, static_cast(param)); break; // Tone-Portamento case CMD_TONEPORTAMENTO: TonePortamento(chn, static_cast(param)); break; // Tone-Portamento + Volume Slide case CMD_TONEPORTAVOL: if ((param) || (GetType() != MOD_TYPE_MOD)) VolumeSlide(chn, static_cast(param)); TonePortamento(chn, 0); break; // Vibrato case CMD_VIBRATO: Vibrato(chn, param); break; // Vibrato + Volume Slide case CMD_VIBRATOVOL: if ((param) || (GetType() != MOD_TYPE_MOD)) VolumeSlide(chn, static_cast(param)); Vibrato(chn, 0); break; // Set Speed case CMD_SPEED: if(m_SongFlags[SONG_FIRSTTICK]) SetSpeed(m_PlayState, param); break; // Set Tempo case CMD_TEMPO: if(m_playBehaviour[kMODVBlankTiming]) { // ProTracker MODs with VBlank timing: All Fxx parameters set the tick count. if(m_SongFlags[SONG_FIRSTTICK] && param != 0) SetSpeed(m_PlayState, param); break; } { param = CalculateXParam(m_PlayState.m_nPattern, m_PlayState.m_nRow, nChn); if (GetType() & (MOD_TYPE_S3M|MOD_TYPE_IT|MOD_TYPE_MPT)) { if (param) chn.nOldTempo = static_cast(param); else param = chn.nOldTempo; } TEMPO t(param, 0); LimitMax(t, GetModSpecifications().GetTempoMax()); SetTempo(t); } break; // Set Offset case CMD_OFFSET: if(triggerNote) { // FT2 compatibility: Portamento + Offset = Ignore offset // Test case: porta-offset.xm if(bPorta && GetType() == MOD_TYPE_XM) break; ProcessSampleOffset(chn, nChn, m_PlayState); } break; // Disorder Tracker 2 percentage offset case CMD_OFFSETPERCENTAGE: if(triggerNote) { SampleOffset(chn, Util::muldiv_unsigned(chn.nLength, param, 256)); } break; // Arpeggio case CMD_ARPEGGIO: // IT compatibility 01. Don't ignore Arpeggio if no note is playing (also valid for ST3) if(m_PlayState.m_nTickCount) break; if((!chn.nPeriod || !chn.nNote) && (chn.pModInstrument == nullptr || !chn.pModInstrument->HasValidMIDIChannel()) // Plugin arpeggio && !m_playBehaviour[kITArpeggio] && (GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT))) break; if (!param && (GetType() & (MOD_TYPE_XM | MOD_TYPE_MOD))) break; // Only important when editing MOD/XM files (000 effects are removed when loading files where this means "no effect") chn.nCommand = CMD_ARPEGGIO; if (param) chn.nArpeggio = static_cast(param); break; // Retrig case CMD_RETRIG: if (GetType() & (MOD_TYPE_XM|MOD_TYPE_MT2)) { if (!(param & 0xF0)) param |= chn.nRetrigParam & 0xF0; if (!(param & 0x0F)) param |= chn.nRetrigParam & 0x0F; param |= 0x100; // increment retrig count on first row } // IT compatibility 15. Retrigger if(m_playBehaviour[kITRetrigger]) { if (param) chn.nRetrigParam = static_cast(param & 0xFF); RetrigNote(nChn, chn.nRetrigParam, (volcmd == VOLCMD_OFFSET) ? vol + 1 : 0); } else { // XM Retrig if (param) chn.nRetrigParam = static_cast(param & 0xFF); else param = chn.nRetrigParam; RetrigNote(nChn, param, (volcmd == VOLCMD_OFFSET) ? vol + 1 : 0); } break; // Tremor case CMD_TREMOR: if(!m_SongFlags[SONG_FIRSTTICK]) { break; } // IT compatibility 12. / 13. Tremor (using modified DUMB's Tremor logic here because of old effects - http://dumb.sf.net/) if(m_playBehaviour[kITTremor]) { if(param && !m_SongFlags[SONG_ITOLDEFFECTS]) { // Old effects have different length interpretation (+1 for both on and off) if(param & 0xF0) param -= 0x10; if(param & 0x0F) param -= 0x01; chn.nTremorParam = static_cast(param); } chn.nTremorCount |= 0x80; // set on/off flag } else if(m_playBehaviour[kFT2Tremor]) { // XM Tremor. Logic is being processed in sndmix.cpp chn.nTremorCount |= 0x80; // set on/off flag } chn.nCommand = CMD_TREMOR; if(param) chn.nTremorParam = static_cast(param); break; // Set Global Volume case CMD_GLOBALVOLUME: // IT compatibility: Only apply global volume on first tick (and multiples) // Test case: GlobalVolFirstTick.it if(!m_SongFlags[SONG_FIRSTTICK]) break; // ST3 applies global volume on tick 1 and does other weird things, but we won't emulate this for now. // if(((GetType() & MOD_TYPE_S3M) && m_nTickCount != 1) // || (!(GetType() & MOD_TYPE_S3M) && !m_SongFlags[SONG_FIRSTTICK])) // { // break; // } // FT2 compatibility: On channels that are "left" of the global volume command, the new global volume is not applied // until the second tick of the row. Since we apply global volume on the mix buffer rather than note volumes, this // cannot be fixed for now. // Test case: GlobalVolume.xm // if(IsCompatibleMode(TRK_FASTTRACKER2) && m_SongFlags[SONG_FIRSTTICK] && m_nMusicSpeed > 1) // { // break; // } if (!(GetType() & GLOBALVOL_7BIT_FORMATS)) param *= 2; // IT compatibility 16. ST3 and IT ignore out-of-range values. // Test case: globalvol-invalid.it if(param <= 128) { m_PlayState.m_nGlobalVolume = param * 2; } else if(!(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_S3M))) { m_PlayState.m_nGlobalVolume = 256; } break; // Global Volume Slide case CMD_GLOBALVOLSLIDE: //IT compatibility 16. Saving last global volume slide param per channel (FT2/IT) if(m_playBehaviour[kPerChannelGlobalVolSlide]) GlobalVolSlide(static_cast(param), chn.nOldGlobalVolSlide); else GlobalVolSlide(static_cast(param), m_PlayState.Chn[0].nOldGlobalVolSlide); break; // Set 8-bit Panning case CMD_PANNING8: if(m_SongFlags[SONG_FIRSTTICK]) { Panning(chn, param, Pan8bit); } break; // Panning Slide case CMD_PANNINGSLIDE: PanningSlide(chn, static_cast(param)); break; // Tremolo case CMD_TREMOLO: Tremolo(chn, param); break; // Fine Vibrato case CMD_FINEVIBRATO: FineVibrato(chn, param); break; // MOD/XM Exx Extended Commands case CMD_MODCMDEX: ExtendedMODCommands(nChn, static_cast(param)); break; // S3M/IT Sxx Extended Commands case CMD_S3MCMDEX: ExtendedS3MCommands(nChn, static_cast(param)); break; // Key Off case CMD_KEYOFF: // This is how Key Off is supposed to sound... (in FT2 at least) if(m_playBehaviour[kFT2KeyOff]) { if (m_PlayState.m_nTickCount == param) { // XM: Key-Off + Sample == Note Cut if(chn.pModInstrument == nullptr || !chn.pModInstrument->VolEnv.dwFlags[ENV_ENABLED]) { if(param == 0 && (chn.rowCommand.instr || chn.rowCommand.volcmd != VOLCMD_NONE)) // FT2 is weird.... { chn.dwFlags.set(CHN_NOTEFADE); } else { chn.dwFlags.set(CHN_FASTVOLRAMP); chn.nVolume = 0; } } KeyOff(chn); } } // This is how it's NOT supposed to sound... else { if(m_SongFlags[SONG_FIRSTTICK]) KeyOff(chn); } break; // Extra-fine porta up/down case CMD_XFINEPORTAUPDOWN: switch(param & 0xF0) { case 0x10: ExtraFinePortamentoUp(chn, param & 0x0F); break; case 0x20: ExtraFinePortamentoDown(chn, param & 0x0F); break; // ModPlug XM Extensions (ignore in compatible mode) case 0x50: case 0x60: case 0x70: case 0x90: case 0xA0: if(!m_playBehaviour[kFT2RestrictXCommand]) ExtendedS3MCommands(nChn, static_cast(param)); break; } break; case CMD_FINETUNE: case CMD_FINETUNE_SMOOTH: if(m_SongFlags[SONG_FIRSTTICK] || cmd == CMD_FINETUNE_SMOOTH) { SetFinetune(nChn, m_PlayState, cmd == CMD_FINETUNE_SMOOTH); #ifndef NO_PLUGINS if(IMixPlugin *plugin = GetChannelInstrumentPlugin(m_PlayState.Chn[nChn]); plugin != nullptr) plugin->MidiPitchBendRaw(chn.GetMIDIPitchBend(), nChn); #endif // NO_PLUGINS } break; // Set Channel Global Volume case CMD_CHANNELVOLUME: if(!m_SongFlags[SONG_FIRSTTICK]) break; if (param <= 64) { chn.nGlobalVol = param; chn.dwFlags.set(CHN_FASTVOLRAMP); } break; // Channel volume slide case CMD_CHANNELVOLSLIDE: ChannelVolSlide(chn, static_cast(param)); break; // Panbrello (IT) case CMD_PANBRELLO: Panbrello(chn, param); break; // Set Envelope Position case CMD_SETENVPOSITION: if(m_SongFlags[SONG_FIRSTTICK]) { chn.VolEnv.nEnvPosition = param; // FT2 compatibility: FT2 only sets the position of the panning envelope if the volume envelope's sustain flag is set // Test case: SetEnvPos.xm if(!m_playBehaviour[kFT2SetPanEnvPos] || chn.VolEnv.flags[ENV_SUSTAIN]) { chn.PanEnv.nEnvPosition = param; chn.PitchEnv.nEnvPosition = param; } } break; // Position Jump case CMD_POSITIONJUMP: PositionJump(m_PlayState, nChn); break; // Pattern Break case CMD_PATTERNBREAK: if(ROWINDEX row = PatternBreak(m_PlayState, nChn, static_cast(param)); row != ROWINDEX_INVALID) { m_PlayState.m_breakRow = row; if(m_SongFlags[SONG_PATTERNLOOP]) { //If song is set to loop and a pattern break occurs we should stay on the same pattern. //Use nPosJump to force playback to "jump to this pattern" rather than move to next, as by default. m_PlayState.m_posJump = m_PlayState.m_nCurrentOrder; } } break; // IMF / PTM Note Slides case CMD_NOTESLIDEUP: case CMD_NOTESLIDEDOWN: case CMD_NOTESLIDEUPRETRIG: case CMD_NOTESLIDEDOWNRETRIG: // Note that this command seems to be a bit buggy in Polytracker... Luckily, no tune seems to seriously use this // (Vic uses it e.g. in Spaceman or Perfect Reason to slide effect samples, noone will notice the difference :) NoteSlide(chn, param, cmd == CMD_NOTESLIDEUP || cmd == CMD_NOTESLIDEUPRETRIG, cmd == CMD_NOTESLIDEUPRETRIG || cmd == CMD_NOTESLIDEDOWNRETRIG); break; // PTM Reverse sample + offset (executed on every tick) case CMD_REVERSEOFFSET: ReverseSampleOffset(chn, static_cast(param)); break; #ifndef NO_PLUGINS // DBM: Toggle DSP Echo case CMD_DBMECHO: if(m_PlayState.m_nTickCount == 0) { uint32 echoType = (param >> 4), enable = (param & 0x0F); if(echoType > 2 || enable > 1) { break; } CHANNELINDEX firstChn = nChn, lastChn = nChn; if(echoType == 1) { firstChn = 0; lastChn = m_nChannels - 1; } for(CHANNELINDEX c = firstChn; c <= lastChn; c++) { ChnSettings[c].dwFlags.set(CHN_NOFX, enable == 1); m_PlayState.Chn[c].dwFlags.set(CHN_NOFX, enable == 1); } } break; #endif // NO_PLUGINS // Digi Booster sample reverse case CMD_DIGIREVERSESAMPLE: DigiBoosterSampleReverse(chn, static_cast(param)); break; } if(m_playBehaviour[kST3EffectMemory] && param != 0) { UpdateS3MEffectMemory(chn, static_cast(param)); } if(chn.rowCommand.instr) { // Not necessarily consistent with actually playing instrument for IT compatibility chn.nOldIns = chn.rowCommand.instr; } } // for(...) end // Navigation Effects if(m_SongFlags[SONG_FIRSTTICK]) { if(HandleNextRow(m_PlayState, Order(), true)) m_SongFlags.set(SONG_BREAKTOROW); } return true; } bool CSoundFile::HandleNextRow(PlayState &state, const ModSequence &order, bool honorPatternLoop) const { const bool doPatternLoop = (state.m_patLoopRow != ROWINDEX_INVALID); const bool doBreakRow = (state.m_breakRow != ROWINDEX_INVALID); const bool doPosJump = (state.m_posJump != ORDERINDEX_INVALID); bool breakToRow = false; // Pattern Break / Position Jump only if no loop running // Exception: FastTracker 2 in all cases, Impulse Tracker in case of position jump // Test case for FT2 exception: PatLoop-Jumps.xm, PatLoop-Various.xm // Test case for IT: exception: LoopBreak.it, sbx-priority.it if((doBreakRow || doPosJump) && (!doPatternLoop || m_playBehaviour[kFT2PatternLoopWithJumps] || (m_playBehaviour[kITPatternLoopWithJumps] && doPosJump) || (m_playBehaviour[kITPatternLoopWithJumpsOld] && doPosJump))) { if(!doPosJump) state.m_posJump = state.m_nCurrentOrder + 1; if(!doBreakRow) state.m_breakRow = 0; breakToRow = true; if(state.m_posJump >= order.size()) state.m_posJump = order.GetRestartPos(); // IT / FT2 compatibility: don't reset loop count on pattern break. // Test case: gm-trippy01.it, PatLoop-Break.xm, PatLoop-Weird.xm, PatLoop-Break.mod if(state.m_posJump != state.m_nCurrentOrder && !m_playBehaviour[kITPatternLoopBreak] && !m_playBehaviour[kFT2PatternLoopWithJumps] && GetType() != MOD_TYPE_MOD) { for(CHANNELINDEX i = 0; i < GetNumChannels(); i++) { state.Chn[i].nPatternLoopCount = 0; } } state.m_nNextRow = state.m_breakRow; if(!honorPatternLoop || !m_SongFlags[SONG_PATTERNLOOP]) state.m_nNextOrder = state.m_posJump; } else if(doPatternLoop) { // Pattern Loop state.m_nNextOrder = state.m_nCurrentOrder; state.m_nNextRow = state.m_patLoopRow; // FT2 skips the first row of the pattern loop if there's a pattern delay, ProTracker sometimes does it too (didn't quite figure it out yet). // But IT and ST3 don't do this. // Test cases: PatLoopWithDelay.it, PatLoopWithDelay.s3m if(state.m_nPatternDelay && (GetType() != MOD_TYPE_IT || !m_playBehaviour[kITPatternLoopWithJumps]) && GetType() != MOD_TYPE_S3M) { state.m_nNextRow++; } // IT Compatibility: If the restart row is past the end of the current pattern // (e.g. when continued from a previous pattern without explicit SB0 effect), continue the next pattern. // Test case: LoopStartAfterPatternEnd.it if(state.m_patLoopRow >= Patterns[state.m_nPattern].GetNumRows()) { state.m_nNextOrder++; state.m_nNextRow = 0; } } return breakToRow; } //////////////////////////////////////////////////////////// // Channels effects // Update the effect memory of all S3M effects that use the last non-zero effect parameter as memory (Dxy, Exx, Fxx, Ixy, Jxy, Kxy, Lxy, Qxy, Rxy, Sxy) // Test case: ParamMemory.s3m void CSoundFile::UpdateS3MEffectMemory(ModChannel &chn, ModCommand::PARAM param) const { chn.nOldVolumeSlide = param; // Dxy / Kxy / Lxy chn.nOldPortaUp = param; // Exx / Fxx chn.nOldPortaDown = param; // Exx / Fxx chn.nTremorParam = param; // Ixy chn.nArpeggio = param; // Jxy chn.nRetrigParam = param; // Qxy chn.nTremoloDepth = (param & 0x0F) << 2; // Rxy chn.nTremoloSpeed = (param >> 4) & 0x0F; // Rxy chn.nOldCmdEx = param; // Sxy } // Calculate full parameter for effects that support parameter extension at the given pattern location. // maxCommands sets the maximum number of XParam commands to look at for this effect // extendedRows returns how many extended rows are used (i.e. a value of 0 means the command is not extended). uint32 CSoundFile::CalculateXParam(PATTERNINDEX pat, ROWINDEX row, CHANNELINDEX chn, uint32 *extendedRows) const { if(extendedRows != nullptr) *extendedRows = 0; if(!Patterns.IsValidPat(pat)) { #ifdef MPT_BUILD_FUZZER // Ending up in this situation implies a logic error std::abort(); #else return 0; #endif } ROWINDEX maxCommands = 4; const ModCommand *m = Patterns[pat].GetpModCommand(row, chn); const auto startCmd = m->command; uint32 val = m->param; switch(m->command) { case CMD_OFFSET: // 24 bit command maxCommands = 2; break; case CMD_TEMPO: case CMD_PATTERNBREAK: case CMD_POSITIONJUMP: case CMD_FINETUNE: case CMD_FINETUNE_SMOOTH: // 16 bit command maxCommands = 1; break; default: return val; } const bool xmTempoFix = m->command == CMD_TEMPO && GetType() == MOD_TYPE_XM; ROWINDEX numRows = std::min(Patterns[pat].GetNumRows() - row - 1, maxCommands); uint32 extRows = 0; while(numRows > 0) { m += Patterns[pat].GetNumChannels(); if(m->command != CMD_XPARAM) break; if(xmTempoFix && val < 256) { // With XM, 0x20 is the lowest tempo. Anything below changes ticks per row. val -= 0x20; } val = (val << 8) | m->param; numRows--; extRows++; } // Always return a full-precision value for finetune if((startCmd == CMD_FINETUNE || startCmd == CMD_FINETUNE_SMOOTH) && !extRows) val <<= 8; if(extendedRows != nullptr) *extendedRows = extRows; return val; } void CSoundFile::PositionJump(PlayState &state, CHANNELINDEX chn) const { state.m_nextPatStartRow = 0; // FT2 E60 bug state.m_posJump = static_cast(CalculateXParam(state.m_nPattern, state.m_nRow, chn)); // see https://forum.openmpt.org/index.php?topic=2769.0 - FastTracker resets Dxx if Bxx is called _after_ Dxx // Test case: PatternJump.mod if((GetType() & (MOD_TYPE_MOD | MOD_TYPE_XM)) && state.m_breakRow != ROWINDEX_INVALID) { state.m_breakRow = 0; } } ROWINDEX CSoundFile::PatternBreak(PlayState &state, CHANNELINDEX chn, uint8 param) const { if(param >= 64 && (GetType() & MOD_TYPE_S3M)) { // ST3 ignores invalid pattern breaks. return ROWINDEX_INVALID; } state.m_nextPatStartRow = 0; // FT2 E60 bug return static_cast(CalculateXParam(state.m_nPattern, state.m_nRow, chn)); } void CSoundFile::PortamentoUp(CHANNELINDEX nChn, ModCommand::PARAM param, const bool doFinePortamentoAsRegular) { ModChannel &chn = m_PlayState.Chn[nChn]; if(param) { // FT2 compatibility: Separate effect memory for all portamento commands // Test case: Porta-LinkMem.xm if(!m_playBehaviour[kFT2PortaUpDownMemory]) chn.nOldPortaDown = param; chn.nOldPortaUp = param; } else { param = chn.nOldPortaUp; } const bool doFineSlides = !doFinePortamentoAsRegular && !(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)); // Process MIDI pitch bend for instrument plugins MidiPortamento(nChn, param, doFineSlides); if(GetType() == MOD_TYPE_MPT && chn.pModInstrument && chn.pModInstrument->pTuning) { // Portamento for instruments with custom tuning if(param >= 0xF0 && !doFinePortamentoAsRegular) PortamentoFineMPT(chn, param - 0xF0); else if(param >= 0xE0 && !doFinePortamentoAsRegular) PortamentoExtraFineMPT(chn, param - 0xE0); else PortamentoMPT(chn, param); return; } else if(GetType() == MOD_TYPE_PLM) { // A normal portamento up or down makes a follow-up tone portamento go the same direction. chn.nPortamentoDest = 1; } if (doFineSlides && param >= 0xE0) { if (param & 0x0F) { if ((param & 0xF0) == 0xF0) { FinePortamentoUp(chn, param & 0x0F); return; } else if ((param & 0xF0) == 0xE0 && GetType() != MOD_TYPE_DBM) { ExtraFinePortamentoUp(chn, param & 0x0F); return; } } if(GetType() != MOD_TYPE_DBM) { // DBM only has fine slides, no extra-fine slides. return; } } // Regular Slide if(!chn.isFirstTick || (m_PlayState.m_nMusicSpeed == 1 && m_playBehaviour[kSlidesAtSpeed1]) || (GetType() & (MOD_TYPE_669 | MOD_TYPE_OKT)) || (GetType() == MOD_TYPE_MED && m_SongFlags[SONG_FASTVOLSLIDES])) { DoFreqSlide(chn, chn.nPeriod, param * 4); } } void CSoundFile::PortamentoDown(CHANNELINDEX nChn, ModCommand::PARAM param, const bool doFinePortamentoAsRegular) { ModChannel &chn = m_PlayState.Chn[nChn]; if(param) { // FT2 compatibility: Separate effect memory for all portamento commands // Test case: Porta-LinkMem.xm if(!m_playBehaviour[kFT2PortaUpDownMemory]) chn.nOldPortaUp = param; chn.nOldPortaDown = param; } else { param = chn.nOldPortaDown; } const bool doFineSlides = !doFinePortamentoAsRegular && !(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)); // Process MIDI pitch bend for instrument plugins MidiPortamento(nChn, -static_cast(param), doFineSlides); if(GetType() == MOD_TYPE_MPT && chn.pModInstrument && chn.pModInstrument->pTuning) { // Portamento for instruments with custom tuning if(param >= 0xF0 && !doFinePortamentoAsRegular) PortamentoFineMPT(chn, -static_cast(param - 0xF0)); else if(param >= 0xE0 && !doFinePortamentoAsRegular) PortamentoExtraFineMPT(chn, -static_cast(param - 0xE0)); else PortamentoMPT(chn, -static_cast(param)); return; } else if(GetType() == MOD_TYPE_PLM) { // A normal portamento up or down makes a follow-up tone portamento go the same direction. chn.nPortamentoDest = 65535; } if(doFineSlides && param >= 0xE0) { if (param & 0x0F) { if ((param & 0xF0) == 0xF0) { FinePortamentoDown(chn, param & 0x0F); return; } else if ((param & 0xF0) == 0xE0 && GetType() != MOD_TYPE_DBM) { ExtraFinePortamentoDown(chn, param & 0x0F); return; } } if(GetType() != MOD_TYPE_DBM) { // DBM only has fine slides, no extra-fine slides. return; } } if(!chn.isFirstTick || (m_PlayState.m_nMusicSpeed == 1 && m_playBehaviour[kSlidesAtSpeed1]) || (GetType() & (MOD_TYPE_669 | MOD_TYPE_OKT)) || (GetType() == MOD_TYPE_MED && m_SongFlags[SONG_FASTVOLSLIDES])) { DoFreqSlide(chn, chn.nPeriod, param * -4); } } // Send portamento commands to plugins void CSoundFile::MidiPortamento(CHANNELINDEX nChn, int param, bool doFineSlides) { int actualParam = std::abs(param); int pitchBend = 0; // Old MIDI Pitch Bends: // - Applied on every tick // - No fine pitch slides (they are interpreted as normal slides) // New MIDI Pitch Bends: // - Behaviour identical to sample pitch bends if the instrument's PWD parameter corresponds to the actual VSTi setting. if(doFineSlides && actualParam >= 0xE0 && !m_playBehaviour[kOldMIDIPitchBends]) { if(m_PlayState.Chn[nChn].isFirstTick) { // Extra fine slide... pitchBend = (actualParam & 0x0F) * mpt::signum(param); if(actualParam >= 0xF0) { // ... or just a fine slide! pitchBend *= 4; } } } else if(!m_PlayState.Chn[nChn].isFirstTick || m_playBehaviour[kOldMIDIPitchBends]) { // Regular slide pitchBend = param * 4; } if(pitchBend) { #ifndef NO_PLUGINS IMixPlugin *plugin = GetChannelInstrumentPlugin(m_PlayState.Chn[nChn]); if(plugin != nullptr) { int8 pwd = 13; // Early OpenMPT legacy... Actually it's not *exactly* 13, but close enough... if(m_PlayState.Chn[nChn].pModInstrument != nullptr) { pwd = m_PlayState.Chn[nChn].pModInstrument->midiPWD; } plugin->MidiPitchBend(pitchBend, pwd, nChn); } #endif // NO_PLUGINS } } void CSoundFile::FinePortamentoUp(ModChannel &chn, ModCommand::PARAM param) const { MPT_ASSERT(!chn.HasCustomTuning()); if(GetType() == MOD_TYPE_XM) { // FT2 compatibility: E1x / E2x / X1x / X2x memory is not linked // Test case: Porta-LinkMem.xm if(param) chn.nOldFinePortaUpDown = (chn.nOldFinePortaUpDown & 0x0F) | (param << 4); else param = (chn.nOldFinePortaUpDown >> 4); } else if(GetType() == MOD_TYPE_MT2) { if(param) chn.nOldFinePortaUpDown = param; else param = chn.nOldFinePortaUpDown; } if(chn.isFirstTick && chn.nPeriod && param) DoFreqSlide(chn, chn.nPeriod, param * 4); } void CSoundFile::FinePortamentoDown(ModChannel &chn, ModCommand::PARAM param) const { MPT_ASSERT(!chn.HasCustomTuning()); if(GetType() == MOD_TYPE_XM) { // FT2 compatibility: E1x / E2x / X1x / X2x memory is not linked // Test case: Porta-LinkMem.xm if(param) chn.nOldFinePortaUpDown = (chn.nOldFinePortaUpDown & 0xF0) | (param & 0x0F); else param = (chn.nOldFinePortaUpDown & 0x0F); } else if(GetType() == MOD_TYPE_MT2) { if(param) chn.nOldFinePortaUpDown = param; else param = chn.nOldFinePortaUpDown; } if(chn.isFirstTick && chn.nPeriod && param) { DoFreqSlide(chn, chn.nPeriod, param * -4); if(chn.nPeriod > 0xFFFF && !m_playBehaviour[kPeriodsAreHertz] && (!m_SongFlags[SONG_LINEARSLIDES] || GetType() == MOD_TYPE_XM)) chn.nPeriod = 0xFFFF; } } void CSoundFile::ExtraFinePortamentoUp(ModChannel &chn, ModCommand::PARAM param) const { MPT_ASSERT(!chn.HasCustomTuning()); if(GetType() == MOD_TYPE_XM) { // FT2 compatibility: E1x / E2x / X1x / X2x memory is not linked // Test case: Porta-LinkMem.xm if(param) chn.nOldExtraFinePortaUpDown = (chn.nOldExtraFinePortaUpDown & 0x0F) | (param << 4); else param = (chn.nOldExtraFinePortaUpDown >> 4); } else if(GetType() == MOD_TYPE_MT2) { if(param) chn.nOldFinePortaUpDown = param; else param = chn.nOldFinePortaUpDown; } if(chn.isFirstTick && chn.nPeriod && param) DoFreqSlide(chn, chn.nPeriod, param); } void CSoundFile::ExtraFinePortamentoDown(ModChannel &chn, ModCommand::PARAM param) const { MPT_ASSERT(!chn.HasCustomTuning()); if(GetType() == MOD_TYPE_XM) { // FT2 compatibility: E1x / E2x / X1x / X2x memory is not linked // Test case: Porta-LinkMem.xm if(param) chn.nOldExtraFinePortaUpDown = (chn.nOldExtraFinePortaUpDown & 0xF0) | (param & 0x0F); else param = (chn.nOldExtraFinePortaUpDown & 0x0F); } else if(GetType() == MOD_TYPE_MT2) { if(param) chn.nOldFinePortaUpDown = param; else param = chn.nOldFinePortaUpDown; } if(chn.isFirstTick && chn.nPeriod && param) { DoFreqSlide(chn, chn.nPeriod, -static_cast(param)); if(chn.nPeriod > 0xFFFF && !m_playBehaviour[kPeriodsAreHertz] && (!m_SongFlags[SONG_LINEARSLIDES] || GetType() == MOD_TYPE_XM)) chn.nPeriod = 0xFFFF; } } void CSoundFile::SetFinetune(CHANNELINDEX channel, PlayState &playState, bool isSmooth) const { ModChannel &chn = playState.Chn[channel]; int16 newTuning = mpt::saturate_cast(static_cast(CalculateXParam(playState.m_nPattern, playState.m_nRow, channel, nullptr)) - 0x8000); if(isSmooth) { const int32 ticksLeft = playState.TicksOnRow() - playState.m_nTickCount; if(ticksLeft > 1) { const int32 step = (newTuning - chn.microTuning) / ticksLeft; newTuning = mpt::saturate_cast(chn.microTuning + step); } } chn.microTuning = newTuning; } // Implemented for IMF / PTM / OKT compatibility, can't actually save this in any formats // Slide up / down every x ticks by y semitones // Oktalyzer: Slide down on first tick only, or on every tick void CSoundFile::NoteSlide(ModChannel &chn, uint32 param, bool slideUp, bool retrig) const { if(m_SongFlags[SONG_FIRSTTICK]) { if(param & 0xF0) chn.noteSlideParam = static_cast(param & 0xF0) | (chn.noteSlideParam & 0x0F); if(param & 0x0F) chn.noteSlideParam = (chn.noteSlideParam & 0xF0) | static_cast(param & 0x0F); chn.noteSlideCounter = (chn.noteSlideParam >> 4); } bool doTrigger = false; if(GetType() == MOD_TYPE_OKT) doTrigger = ((chn.noteSlideParam & 0xF0) == 0x10) || m_SongFlags[SONG_FIRSTTICK]; else doTrigger = !m_SongFlags[SONG_FIRSTTICK] && (--chn.noteSlideCounter == 0); if(doTrigger) { const uint8 speed = (chn.noteSlideParam >> 4), steps = (chn.noteSlideParam & 0x0F); chn.noteSlideCounter = speed; // update it const int32 delta = (slideUp ? steps : -steps); if(chn.HasCustomTuning()) chn.m_PortamentoFineSteps += delta * chn.pModInstrument->pTuning->GetFineStepCount(); else chn.nPeriod = GetPeriodFromNote(delta + GetNoteFromPeriod(chn.nPeriod, chn.nFineTune, chn.nC5Speed), chn.nFineTune, chn.nC5Speed); if(retrig) chn.position.Set(0); } } std::pair CSoundFile::GetVolCmdTonePorta(const ModCommand &m, uint32 startTick) const { if(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_AMS | MOD_TYPE_DMF | MOD_TYPE_DBM | MOD_TYPE_IMF | MOD_TYPE_PSM | MOD_TYPE_J2B | MOD_TYPE_ULT | MOD_TYPE_OKT | MOD_TYPE_MT2 | MOD_TYPE_MDL)) { return {ImpulseTrackerPortaVolCmd[m.vol & 0x0F], false}; } else { bool clearEffectColumn = false; uint16 vol = m.vol; if(m.command == CMD_TONEPORTAMENTO && GetType() == MOD_TYPE_XM) { // Yes, FT2 is *that* weird. If there is a Mx command in the volume column // and a normal 3xx command, the 3xx command is ignored but the Mx command's // effectiveness is doubled. // Test case: TonePortamentoMemory.xm clearEffectColumn = true; vol *= 2; } // FT2 compatibility: If there's a portamento and a note delay, execute the portamento, but don't update the parameter // Test case: PortaDelay.xm if(m_playBehaviour[kFT2PortaDelay] && startTick != 0) return {uint16(0), clearEffectColumn}; else return {static_cast(vol * 16), clearEffectColumn}; } } // Portamento Slide void CSoundFile::TonePortamento(ModChannel &chn, uint16 param) const { chn.dwFlags.set(CHN_PORTAMENTO); //IT compatibility 03: Share effect memory with portamento up/down if((!m_SongFlags[SONG_ITCOMPATGXX] && m_playBehaviour[kITPortaMemoryShare]) || GetType() == MOD_TYPE_PLM) { if(param == 0) param = chn.nOldPortaUp; chn.nOldPortaUp = chn.nOldPortaDown = static_cast(param); } if(param) chn.portamentoSlide = param; if(chn.HasCustomTuning()) { //Behavior: Param tells number of finesteps(or 'fullsteps'(notes) with glissando) //to slide per row(not per tick). if(chn.portamentoSlide == 0) return; const int32 oldPortamentoTickSlide = (m_PlayState.m_nTickCount != 0) ? chn.m_PortamentoTickSlide : 0; int32 delta = chn.portamentoSlide; if(chn.nPortamentoDest < 0) delta = -delta; chn.m_PortamentoTickSlide = static_cast((m_PlayState.m_nTickCount + 1.0) * delta / m_PlayState.m_nMusicSpeed); if(chn.dwFlags[CHN_GLISSANDO]) { chn.m_PortamentoTickSlide *= chn.pModInstrument->pTuning->GetFineStepCount() + 1; //With glissando interpreting param as notes instead of finesteps. } const int32 slide = chn.m_PortamentoTickSlide - oldPortamentoTickSlide; if(std::abs(chn.nPortamentoDest) <= std::abs(slide)) { if(chn.nPortamentoDest != 0) { chn.m_PortamentoFineSteps += chn.nPortamentoDest; chn.nPortamentoDest = 0; chn.m_CalculateFreq = true; } } else { chn.m_PortamentoFineSteps += slide; chn.nPortamentoDest -= slide; chn.m_CalculateFreq = true; } return; } bool doPorta = !chn.isFirstTick || (GetType() & (MOD_TYPE_DBM | MOD_TYPE_669)) || (m_PlayState.m_nMusicSpeed == 1 && m_playBehaviour[kSlidesAtSpeed1]) || (GetType() == MOD_TYPE_MED && m_SongFlags[SONG_FASTVOLSLIDES]); int32 delta = chn.portamentoSlide; if(GetType() == MOD_TYPE_PLM && delta >= 0xF0) { delta -= 0xF0; doPorta = chn.isFirstTick; } if(chn.nPeriod && chn.nPortamentoDest && doPorta) { delta *= (GetType() == MOD_TYPE_669) ? 2 : 4; if(!PeriodsAreFrequencies()) delta = -delta; if(chn.nPeriod < chn.nPortamentoDest || chn.portaTargetReached) { DoFreqSlide(chn, chn.nPeriod, delta, true); if(chn.nPeriod > chn.nPortamentoDest) chn.nPeriod = chn.nPortamentoDest; } else if(chn.nPeriod > chn.nPortamentoDest) { DoFreqSlide(chn, chn.nPeriod, -delta, true); if(chn.nPeriod < chn.nPortamentoDest) chn.nPeriod = chn.nPortamentoDest; // FT2 compatibility: Reaching portamento target from below forces subsequent portamentos on the same note to use the logic for reaching the note from above instead. // Test case: PortaResetDirection.xm if(chn.nPeriod == chn.nPortamentoDest && m_playBehaviour[kFT2PortaResetDirection]) chn.portaTargetReached = true; } } // IT compatibility 23. Portamento with no note // ProTracker also disables portamento once the target is reached. // Test case: PortaTarget.mod if(chn.nPeriod == chn.nPortamentoDest && (m_playBehaviour[kITPortaTargetReached] || GetType() == MOD_TYPE_MOD)) chn.nPortamentoDest = 0; } void CSoundFile::Vibrato(ModChannel &chn, uint32 param) const { if (param & 0x0F) chn.nVibratoDepth = (param & 0x0F) * 4; if (param & 0xF0) chn.nVibratoSpeed = (param >> 4) & 0x0F; chn.dwFlags.set(CHN_VIBRATO); } void CSoundFile::FineVibrato(ModChannel &chn, uint32 param) const { if (param & 0x0F) chn.nVibratoDepth = param & 0x0F; if (param & 0xF0) chn.nVibratoSpeed = (param >> 4) & 0x0F; chn.dwFlags.set(CHN_VIBRATO); // ST3 compatibility: Do not distinguish between vibrato types in effect memory // Test case: VibratoTypeChange.s3m if(m_playBehaviour[kST3VibratoMemory] && (param & 0x0F)) { chn.nVibratoDepth *= 4u; } } void CSoundFile::Panbrello(ModChannel &chn, uint32 param) const { if (param & 0x0F) chn.nPanbrelloDepth = param & 0x0F; if (param & 0xF0) chn.nPanbrelloSpeed = (param >> 4) & 0x0F; } void CSoundFile::Panning(ModChannel &chn, uint32 param, PanningType panBits) const { // No panning in ProTracker mode if(m_playBehaviour[kMODIgnorePanning]) { return; } // IT Compatibility (and other trackers as well): panning disables surround (unless panning in rear channels is enabled, which is not supported by the original trackers anyway) if (!m_SongFlags[SONG_SURROUNDPAN] && (panBits == Pan8bit || m_playBehaviour[kPanOverride])) { chn.dwFlags.reset(CHN_SURROUND); } if(panBits == Pan4bit) { // 0...15 panning chn.nPan = (param * 256 + 8) / 15; } else if(panBits == Pan6bit) { // 0...64 panning if(param > 64) param = 64; chn.nPan = param * 4; } else { if(!(GetType() & (MOD_TYPE_S3M | MOD_TYPE_DSM | MOD_TYPE_AMF0 | MOD_TYPE_AMF | MOD_TYPE_MTM))) { // Real 8-bit panning chn.nPan = param; } else { // 7-bit panning + surround if(param <= 0x80) { chn.nPan = param << 1; } else if(param == 0xA4) { chn.dwFlags.set(CHN_SURROUND); chn.nPan = 0x80; } } } chn.dwFlags.set(CHN_FASTVOLRAMP); chn.nRestorePanOnNewNote = 0; //IT compatibility 20. Set pan overrides random pan if(m_playBehaviour[kPanOverride]) { chn.nPanSwing = 0; chn.nPanbrelloOffset = 0; } } void CSoundFile::VolumeSlide(ModChannel &chn, ModCommand::PARAM param) const { if (param) chn.nOldVolumeSlide = param; else param = chn.nOldVolumeSlide; if((GetType() & (MOD_TYPE_MOD | MOD_TYPE_XM | MOD_TYPE_MT2 | MOD_TYPE_MED | MOD_TYPE_DIGI | MOD_TYPE_STP | MOD_TYPE_DTM))) { // MOD / XM nibble priority if((param & 0xF0) != 0) { param &= 0xF0; } else { param &= 0x0F; } } int newVolume = chn.nVolume; if(!(GetType() & (MOD_TYPE_MOD | MOD_TYPE_XM | MOD_TYPE_AMF0 | MOD_TYPE_MED | MOD_TYPE_DIGI))) { if ((param & 0x0F) == 0x0F) //Fine upslide or slide -15 { if (param & 0xF0) //Fine upslide { FineVolumeUp(chn, (param >> 4), false); return; } else //Slide -15 { if(chn.isFirstTick && !m_SongFlags[SONG_FASTVOLSLIDES]) { newVolume -= 0x0F * 4; } } } else if ((param & 0xF0) == 0xF0) //Fine downslide or slide +15 { if (param & 0x0F) //Fine downslide { FineVolumeDown(chn, (param & 0x0F), false); return; } else //Slide +15 { if(chn.isFirstTick && !m_SongFlags[SONG_FASTVOLSLIDES]) { newVolume += 0x0F * 4; } } } } if(!chn.isFirstTick || m_SongFlags[SONG_FASTVOLSLIDES] || (m_PlayState.m_nMusicSpeed == 1 && GetType() == MOD_TYPE_DBM)) { // IT compatibility: Ignore slide commands with both nibbles set. if (param & 0x0F) { if(!(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) || (param & 0xF0) == 0) newVolume -= (int)((param & 0x0F) * 4); } else { newVolume += (int)((param & 0xF0) >> 2); } if (GetType() == MOD_TYPE_MOD) chn.dwFlags.set(CHN_FASTVOLRAMP); } newVolume = Clamp(newVolume, 0, 256); chn.nVolume = newVolume; } void CSoundFile::PanningSlide(ModChannel &chn, ModCommand::PARAM param, bool memory) const { if(memory) { // FT2 compatibility: Use effect memory (lxx and rxx in XM shouldn't use effect memory). // Test case: PanSlideMem.xm if(param) chn.nOldPanSlide = param; else param = chn.nOldPanSlide; } if((GetType() & (MOD_TYPE_XM | MOD_TYPE_MT2))) { // XM nibble priority if((param & 0xF0) != 0) { param &= 0xF0; } else { param &= 0x0F; } } int32 nPanSlide = 0; if(!(GetType() & (MOD_TYPE_XM | MOD_TYPE_MT2))) { if (((param & 0x0F) == 0x0F) && (param & 0xF0)) { if(m_SongFlags[SONG_FIRSTTICK]) { param = (param & 0xF0) / 4u; nPanSlide = - (int)param; } } else if (((param & 0xF0) == 0xF0) && (param & 0x0F)) { if(m_SongFlags[SONG_FIRSTTICK]) { nPanSlide = (param & 0x0F) * 4u; } } else if(!m_SongFlags[SONG_FIRSTTICK]) { if (param & 0x0F) { // IT compatibility: Ignore slide commands with both nibbles set. if(!(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) || (param & 0xF0) == 0) nPanSlide = (int)((param & 0x0F) * 4u); } else { nPanSlide = -(int)((param & 0xF0) / 4u); } } } else { if(!m_SongFlags[SONG_FIRSTTICK]) { if (param & 0xF0) { nPanSlide = (int)((param & 0xF0) / 4u); } else { nPanSlide = -(int)((param & 0x0F) * 4u); } // FT2 compatibility: FT2's panning slide is like IT's fine panning slide (not as deep) if(m_playBehaviour[kFT2PanSlide]) nPanSlide /= 4; } } if (nPanSlide) { nPanSlide += chn.nPan; nPanSlide = Clamp(nPanSlide, 0, 256); chn.nPan = nPanSlide; chn.nRestorePanOnNewNote = 0; } } void CSoundFile::FineVolumeUp(ModChannel &chn, ModCommand::PARAM param, bool volCol) const { if(GetType() == MOD_TYPE_XM) { // FT2 compatibility: EAx / EBx memory is not linked // Test case: FineVol-LinkMem.xm if(param) chn.nOldFineVolUpDown = (param << 4) | (chn.nOldFineVolUpDown & 0x0F); else param = (chn.nOldFineVolUpDown >> 4); } else if(volCol) { if(param) chn.nOldVolParam = param; else param = chn.nOldVolParam; } else { if(param) chn.nOldFineVolUpDown = param; else param = chn.nOldFineVolUpDown; } if(chn.isFirstTick) { chn.nVolume += param * 4; if(chn.nVolume > 256) chn.nVolume = 256; if(GetType() & MOD_TYPE_MOD) chn.dwFlags.set(CHN_FASTVOLRAMP); } } void CSoundFile::FineVolumeDown(ModChannel &chn, ModCommand::PARAM param, bool volCol) const { if(GetType() == MOD_TYPE_XM) { // FT2 compatibility: EAx / EBx memory is not linked // Test case: FineVol-LinkMem.xm if(param) chn.nOldFineVolUpDown = param | (chn.nOldFineVolUpDown & 0xF0); else param = (chn.nOldFineVolUpDown & 0x0F); } else if(volCol) { if(param) chn.nOldVolParam = param; else param = chn.nOldVolParam; } else { if(param) chn.nOldFineVolUpDown = param; else param = chn.nOldFineVolUpDown; } if(chn.isFirstTick) { chn.nVolume -= param * 4; if(chn.nVolume < 0) chn.nVolume = 0; if(GetType() & MOD_TYPE_MOD) chn.dwFlags.set(CHN_FASTVOLRAMP); } } void CSoundFile::Tremolo(ModChannel &chn, uint32 param) const { if (param & 0x0F) chn.nTremoloDepth = (param & 0x0F) << 2; if (param & 0xF0) chn.nTremoloSpeed = (param >> 4) & 0x0F; chn.dwFlags.set(CHN_TREMOLO); } void CSoundFile::ChannelVolSlide(ModChannel &chn, ModCommand::PARAM param) const { int32 nChnSlide = 0; if (param) chn.nOldChnVolSlide = param; else param = chn.nOldChnVolSlide; if (((param & 0x0F) == 0x0F) && (param & 0xF0)) { if(m_SongFlags[SONG_FIRSTTICK]) nChnSlide = param >> 4; } else if (((param & 0xF0) == 0xF0) && (param & 0x0F)) { if(m_SongFlags[SONG_FIRSTTICK]) nChnSlide = - (int)(param & 0x0F); } else { if(!m_SongFlags[SONG_FIRSTTICK]) { if (param & 0x0F) { if(!(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_J2B | MOD_TYPE_DBM)) || (param & 0xF0) == 0) nChnSlide = -(int)(param & 0x0F); } else { nChnSlide = (int)((param & 0xF0) >> 4); } } } if (nChnSlide) { nChnSlide += chn.nGlobalVol; nChnSlide = Clamp(nChnSlide, 0, 64); chn.nGlobalVol = nChnSlide; } } void CSoundFile::ExtendedMODCommands(CHANNELINDEX nChn, ModCommand::PARAM param) { ModChannel &chn = m_PlayState.Chn[nChn]; uint8 command = param & 0xF0; param &= 0x0F; switch(command) { // E0x: Set Filter case 0x00: for(CHANNELINDEX channel = 0; channel < GetNumChannels(); channel++) { m_PlayState.Chn[channel].dwFlags.set(CHN_AMIGAFILTER, !(param & 1)); } break; // E1x: Fine Portamento Up case 0x10: if ((param) || (GetType() & (MOD_TYPE_XM|MOD_TYPE_MT2))) FinePortamentoUp(chn, param); break; // E2x: Fine Portamento Down case 0x20: if ((param) || (GetType() & (MOD_TYPE_XM|MOD_TYPE_MT2))) FinePortamentoDown(chn, param); break; // E3x: Set Glissando Control case 0x30: chn.dwFlags.set(CHN_GLISSANDO, param != 0); break; // E4x: Set Vibrato WaveForm case 0x40: chn.nVibratoType = param & 0x07; break; // E5x: Set FineTune case 0x50: if(!m_SongFlags[SONG_FIRSTTICK]) break; if(GetType() & (MOD_TYPE_MOD | MOD_TYPE_DIGI | MOD_TYPE_AMF0 | MOD_TYPE_MED)) { chn.nFineTune = MOD2XMFineTune(param); if(chn.nPeriod && chn.rowCommand.IsNote()) chn.nPeriod = GetPeriodFromNote(chn.nNote, chn.nFineTune, chn.nC5Speed); } else if(GetType() == MOD_TYPE_MTM) { if(chn.rowCommand.IsNote() && chn.pModSample != nullptr) { // Effect is permanent in MultiTracker const_cast(chn.pModSample)->nFineTune = param; chn.nFineTune = param; if(chn.nPeriod) chn.nPeriod = GetPeriodFromNote(chn.nNote, chn.nFineTune, chn.nC5Speed); } } else if(chn.rowCommand.IsNote()) { chn.nFineTune = MOD2XMFineTune(param - 8); if(chn.nPeriod) chn.nPeriod = GetPeriodFromNote(chn.nNote, chn.nFineTune, chn.nC5Speed); } break; // E6x: Pattern Loop case 0x60: if(m_SongFlags[SONG_FIRSTTICK]) PatternLoop(m_PlayState, chn, param & 0x0F); break; // E7x: Set Tremolo WaveForm case 0x70: chn.nTremoloType = param & 0x07; break; // E8x: Set 4-bit Panning case 0x80: if(m_SongFlags[SONG_FIRSTTICK]) { Panning(chn, param, Pan4bit); } break; // E9x: Retrig case 0x90: RetrigNote(nChn, param); break; // EAx: Fine Volume Up case 0xA0: if ((param) || (GetType() & (MOD_TYPE_XM|MOD_TYPE_MT2))) FineVolumeUp(chn, param, false); break; // EBx: Fine Volume Down case 0xB0: if ((param) || (GetType() & (MOD_TYPE_XM|MOD_TYPE_MT2))) FineVolumeDown(chn, param, false); break; // ECx: Note Cut case 0xC0: NoteCut(nChn, param, false); break; // EDx: Note Delay // EEx: Pattern Delay case 0xF0: if(GetType() == MOD_TYPE_MOD) // MOD: Invert Loop { chn.nEFxSpeed = param; if(m_SongFlags[SONG_FIRSTTICK]) InvertLoop(chn); } else // XM: Set Active Midi Macro { chn.nActiveMacro = param; } break; } } void CSoundFile::ExtendedS3MCommands(CHANNELINDEX nChn, ModCommand::PARAM param) { ModChannel &chn = m_PlayState.Chn[nChn]; uint8 command = param & 0xF0; param &= 0x0F; switch(command) { // S0x: Set Filter // S1x: Set Glissando Control case 0x10: chn.dwFlags.set(CHN_GLISSANDO, param != 0); break; // S2x: Set FineTune case 0x20: if(!m_SongFlags[SONG_FIRSTTICK]) break; if(chn.HasCustomTuning()) { chn.nFineTune = param - 8; chn.m_CalculateFreq = true; } else if(GetType() != MOD_TYPE_669) { chn.nC5Speed = S3MFineTuneTable[param]; chn.nFineTune = MOD2XMFineTune(param); if(chn.nPeriod) chn.nPeriod = GetPeriodFromNote(chn.nNote, chn.nFineTune, chn.nC5Speed); } else if(chn.pModSample != nullptr) { chn.nC5Speed = chn.pModSample->nC5Speed + param * 80; } break; // S3x: Set Vibrato Waveform case 0x30: if(GetType() == MOD_TYPE_S3M) { chn.nVibratoType = param & 0x03; } else { // IT compatibility: Ignore waveform types > 3 if(m_playBehaviour[kITVibratoTremoloPanbrello]) chn.nVibratoType = (param < 0x04) ? param : 0; else chn.nVibratoType = param & 0x07; } break; // S4x: Set Tremolo Waveform case 0x40: if(GetType() == MOD_TYPE_S3M) { chn.nTremoloType = param & 0x03; } else { // IT compatibility: Ignore waveform types > 3 if(m_playBehaviour[kITVibratoTremoloPanbrello]) chn.nTremoloType = (param < 0x04) ? param : 0; else chn.nTremoloType = param & 0x07; } break; // S5x: Set Panbrello Waveform case 0x50: // IT compatibility: Ignore waveform types > 3 if(m_playBehaviour[kITVibratoTremoloPanbrello]) { chn.nPanbrelloType = (param < 0x04) ? param : 0; chn.nPanbrelloPos = 0; } else { chn.nPanbrelloType = param & 0x07; } break; // S6x: Pattern Delay for x frames case 0x60: if(m_SongFlags[SONG_FIRSTTICK] && m_PlayState.m_nTickCount == 0) { // Tick delays are added up. // Scream Tracker 3 does actually not support this command. // We'll use the same behaviour as for Impulse Tracker, as we can assume that // most S3Ms that make use of this command were made with Impulse Tracker. // MPT added this command to the XM format through the X6x effect, so we will use // the same behaviour here as well. // Test cases: PatternDelays.it, PatternDelays.s3m, PatternDelays.xm m_PlayState.m_nFrameDelay += param; } break; // S7x: Envelope Control / Instrument Control case 0x70: if(!m_SongFlags[SONG_FIRSTTICK]) break; switch(param) { case 0: case 1: case 2: { for (CHANNELINDEX i = m_nChannels; i < MAX_CHANNELS; i++) { ModChannel &bkChn = m_PlayState.Chn[i]; if (bkChn.nMasterChn == nChn + 1) { if (param == 1) { KeyOff(bkChn); if(bkChn.dwFlags[CHN_ADLIB] && m_opl) m_opl->NoteOff(i); } else if (param == 2) { bkChn.dwFlags.set(CHN_NOTEFADE); if(bkChn.dwFlags[CHN_ADLIB] && m_opl) m_opl->NoteOff(i); } else { bkChn.dwFlags.set(CHN_NOTEFADE); bkChn.nFadeOutVol = 0; if(bkChn.dwFlags[CHN_ADLIB] && m_opl) m_opl->NoteCut(i); } #ifndef NO_PLUGINS const ModInstrument *pIns = bkChn.pModInstrument; IMixPlugin *pPlugin; if(pIns != nullptr && pIns->nMixPlug && (pPlugin = m_MixPlugins[pIns->nMixPlug - 1].pMixPlugin) != nullptr) { pPlugin->MidiCommand(*pIns, bkChn.nNote + NOTE_MAX_SPECIAL, 0, nChn); } #endif // NO_PLUGINS } } } break; default: // S73-S7E chn.InstrumentControl(param, *this); break; } break; // S8x: Set 4-bit Panning case 0x80: if(m_SongFlags[SONG_FIRSTTICK]) { Panning(chn, param, Pan4bit); } break; // S9x: Sound Control case 0x90: ExtendedChannelEffect(chn, param); break; // SAx: Set 64k Offset case 0xA0: if(m_SongFlags[SONG_FIRSTTICK]) { chn.nOldHiOffset = static_cast(param); if (!m_playBehaviour[kITHighOffsetNoRetrig] && chn.rowCommand.IsNote()) { SmpLength pos = param << 16; if (pos < chn.nLength) chn.position.SetInt(pos); } } break; // SBx: Pattern Loop case 0xB0: if(m_SongFlags[SONG_FIRSTTICK]) PatternLoop(m_PlayState, chn, param & 0x0F); break; // SCx: Note Cut case 0xC0: if(param == 0) { //IT compatibility 22. SC0 == SC1 if(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) param = 1; // ST3 doesn't cut notes with SC0 else if(GetType() == MOD_TYPE_S3M) return; } // S3M/IT compatibility: Note Cut really cuts notes and does not just mute them (so that following volume commands could restore the sample) // Test case: scx.it NoteCut(nChn, param, m_playBehaviour[kITSCxStopsSample] || GetType() == MOD_TYPE_S3M); break; // SDx: Note Delay // SEx: Pattern Delay for x rows // SFx: S3M: Not used, IT: Set Active Midi Macro case 0xF0: if(GetType() != MOD_TYPE_S3M) { chn.nActiveMacro = static_cast(param); } break; } } void CSoundFile::ExtendedChannelEffect(ModChannel &chn, uint32 param) { // S9x and X9x commands (S3M/XM/IT only) if(!m_SongFlags[SONG_FIRSTTICK]) return; switch(param & 0x0F) { // S90: Surround Off case 0x00: chn.dwFlags.reset(CHN_SURROUND); break; // S91: Surround On case 0x01: chn.dwFlags.set(CHN_SURROUND); chn.nPan = 128; break; //////////////////////////////////////////////////////////// // ModPlug Extensions // S98: Reverb Off case 0x08: chn.dwFlags.reset(CHN_REVERB); chn.dwFlags.set(CHN_NOREVERB); break; // S99: Reverb On case 0x09: chn.dwFlags.reset(CHN_NOREVERB); chn.dwFlags.set(CHN_REVERB); break; // S9A: 2-Channels surround mode case 0x0A: m_SongFlags.reset(SONG_SURROUNDPAN); break; // S9B: 4-Channels surround mode case 0x0B: m_SongFlags.set(SONG_SURROUNDPAN); break; // S9C: IT Filter Mode case 0x0C: m_SongFlags.reset(SONG_MPTFILTERMODE); break; // S9D: MPT Filter Mode case 0x0D: m_SongFlags.set(SONG_MPTFILTERMODE); break; // S9E: Go forward case 0x0E: chn.dwFlags.reset(CHN_PINGPONGFLAG); break; // S9F: Go backward (and set playback position to the end if sample just started) case 0x0F: if(chn.position.IsZero() && chn.nLength && (chn.rowCommand.IsNote() || !chn.dwFlags[CHN_LOOP])) { chn.position.Set(chn.nLength - 1, SamplePosition::fractMax); } chn.dwFlags.set(CHN_PINGPONGFLAG); break; } } void CSoundFile::InvertLoop(ModChannel &chn) { // EFx implementation for MOD files (PT 1.1A and up: Invert Loop) // This effect trashes samples. Thanks to 8bitbubsy for making this work. :) if(GetType() != MOD_TYPE_MOD || chn.nEFxSpeed == 0) return; ModSample *pModSample = const_cast(chn.pModSample); if(pModSample == nullptr || !pModSample->HasSampleData() || !pModSample->uFlags[CHN_LOOP | CHN_SUSTAINLOOP]) return; chn.nEFxDelay += ModEFxTable[chn.nEFxSpeed & 0x0F]; if(chn.nEFxDelay < 128) return; chn.nEFxDelay = 0; const SmpLength loopStart = pModSample->uFlags[CHN_LOOP] ? pModSample->nLoopStart : pModSample->nSustainStart; const SmpLength loopEnd = pModSample->uFlags[CHN_LOOP] ? pModSample->nLoopEnd : pModSample->nSustainEnd; if(++chn.nEFxOffset >= loopEnd - loopStart) chn.nEFxOffset = 0; // TRASH IT!!! (Yes, the sample!) const uint8 bps = pModSample->GetBytesPerSample(); uint8 *begin = mpt::byte_cast(pModSample->sampleb()) + (loopStart + chn.nEFxOffset) * bps; for(auto &sample : mpt::as_span(begin, bps)) { sample = ~sample; } pModSample->PrecomputeLoops(*this, false); } // Process a MIDI Macro. // Parameters: // playState: The playback state to operate on. // nChn: Mod channel to apply macro on // isSmooth: If true, internal macros are interpolated between two rows // macro: MIDI Macro string to process // param: Parameter for parametric macros (Zxx / \xx parameter) // plugin: Plugin to send MIDI message to (if not specified but needed, it is autodetected) void CSoundFile::ProcessMIDIMacro(PlayState &playState, CHANNELINDEX nChn, bool isSmooth, const MIDIMacroConfigData::Macro ¯o, uint8 param, PLUGINDEX plugin) { playState.m_midiMacroScratchSpace.resize(macro.Length() + 1); auto out = mpt::as_span(playState.m_midiMacroScratchSpace); ParseMIDIMacro(playState, nChn, isSmooth, macro, out, param, plugin); // Macro string has been parsed and translated, now send the message(s)... uint32 outSize = static_cast(out.size()); uint32 sendPos = 0; uint8 runningStatus = 0; while(sendPos < out.size()) { uint32 sendLen = 0; if(out[sendPos] == 0xF0) { // SysEx start if((outSize - sendPos >= 4) && (out[sendPos + 1] == 0xF0 || out[sendPos + 1] == 0xF1)) { // Internal macro (normal (F0F0) or extended (F0F1)), 4 bytes long sendLen = 4; } else { // SysEx message, find end of message for(uint32 i = sendPos + 1; i < outSize; i++) { if(out[i] == 0xF7) { // Found end of SysEx message sendLen = i - sendPos + 1; break; } } if(sendLen == 0) { // Didn't find end, so "invent" end of SysEx message out[outSize++] = 0xF7; sendLen = outSize - sendPos; } } } else if(!(out[sendPos] & 0x80)) { // Missing status byte? Try inserting running status if(runningStatus != 0) { sendPos--; out[sendPos] = runningStatus; } else { // No running status to re-use; skip this byte sendPos++; } continue; } else { // Other MIDI messages sendLen = std::min(static_cast(MIDIEvents::GetEventLength(out[sendPos])), outSize - sendPos); } if(sendLen == 0) break; if(out[sendPos] < 0xF0) { runningStatus = out[sendPos]; } const auto midiMsg = out.subspan(sendPos, sendLen); SendMIDIData(playState, nChn, isSmooth, midiMsg, plugin); sendPos += sendLen; } } void CSoundFile::ParseMIDIMacro(PlayState &playState, CHANNELINDEX nChn, bool isSmooth, const mpt::span macro, mpt::span &out, uint8 param, PLUGINDEX plugin) const { ModChannel &chn = playState.Chn[nChn]; const ModInstrument *pIns = chn.pModInstrument; const uint8 lastZxxParam = chn.lastZxxParam; // always interpolate based on original value in case z appears multiple times in macro string uint8 updateZxxParam = 0xFF; // avoid updating lastZxxParam immediately if macro contains both internal and external MIDI message bool firstNibble = true; size_t outPos = 0; // output buffer position, which also equals the number of complete bytes for(size_t pos = 0; pos < macro.size() && outPos < out.size(); pos++) { bool isNibble = false; // did we parse a nibble or a byte value? uint8 data = 0; // data that has just been parsed // Parse next macro byte... See Impulse Tracker's MIDI.TXT for detailed information on each possible character. if(macro[pos] >= '0' && macro[pos] <= '9') { isNibble = true; data = static_cast(macro[pos] - '0'); } else if(macro[pos] >= 'A' && macro[pos] <= 'F') { isNibble = true; data = static_cast(macro[pos] - 'A' + 0x0A); } else if(macro[pos] == 'c') { // MIDI channel isNibble = true; data = 0xFF; #ifndef NO_PLUGINS const PLUGINDEX plug = (plugin != 0) ? plugin : GetBestPlugin(playState, nChn, PrioritiseChannel, EvenIfMuted); if(plug > 0 && plug <= MAX_MIXPLUGINS) { auto midiPlug = dynamic_cast(m_MixPlugins[plug - 1u].pMixPlugin); if(midiPlug) data = midiPlug->GetMidiChannel(playState.Chn[nChn], nChn); } #endif // NO_PLUGINS if(data == 0xFF) { // Fallback if no plugin was found if(pIns) data = pIns->GetMIDIChannel(playState.Chn[nChn], nChn); else data = 0; } } else if(macro[pos] == 'n') { // Last triggered note if(ModCommand::IsNote(chn.nLastNote)) { data = chn.nLastNote - NOTE_MIN; } } else if(macro[pos] == 'v') { // Velocity // This is "almost" how IT does it - apparently, IT seems to lag one row behind on global volume or channel volume changes. const int swing = (m_playBehaviour[kITSwingBehaviour] || m_playBehaviour[kMPTOldSwingBehaviour]) ? chn.nVolSwing : 0; const int vol = Util::muldiv((chn.nVolume + swing) * m_PlayState.m_nGlobalVolume, chn.nGlobalVol * chn.nInsVol, 1 << 20); data = static_cast(Clamp(vol / 2, 1, 127)); //data = (unsigned char)std::min((chn.nVolume * chn.nGlobalVol * m_nGlobalVolume) >> (1 + 6 + 8), 127); } else if(macro[pos] == 'u') { // Calculated volume // Same note as with velocity applies here, but apparently also for instrument / sample volumes? const int vol = Util::muldiv(chn.nCalcVolume * m_PlayState.m_nGlobalVolume, chn.nGlobalVol * chn.nInsVol, 1 << 26); data = static_cast(Clamp(vol / 2, 1, 127)); //data = (unsigned char)std::min((chn.nCalcVolume * chn.nGlobalVol * m_nGlobalVolume) >> (7 + 6 + 8), 127); } else if(macro[pos] == 'x') { // Pan set data = static_cast(std::min(static_cast(chn.nPan / 2), 127)); } else if(macro[pos] == 'y') { // Calculated pan data = static_cast(std::min(static_cast(chn.nRealPan / 2), 127)); } else if(macro[pos] == 'a') { // High byte of bank select if(pIns && pIns->wMidiBank) { data = static_cast(((pIns->wMidiBank - 1) >> 7) & 0x7F); } } else if(macro[pos] == 'b') { // Low byte of bank select if(pIns && pIns->wMidiBank) { data = static_cast((pIns->wMidiBank - 1) & 0x7F); } } else if(macro[pos] == 'o') { // Offset (ignoring high offset) data = static_cast((chn.oldOffset >> 8) & 0xFF); } else if(macro[pos] == 'h') { // Host channel number data = static_cast((nChn >= GetNumChannels() ? (chn.nMasterChn - 1) : nChn) & 0x7F); } else if(macro[pos] == 'm') { // Loop direction (judging from the character, it was supposed to be loop type, though) data = chn.dwFlags[CHN_PINGPONGFLAG] ? 1 : 0; } else if(macro[pos] == 'p') { // Program select if(pIns && pIns->nMidiProgram) { data = static_cast((pIns->nMidiProgram - 1) & 0x7F); } } else if(macro[pos] == 'z') { // Zxx parameter data = param; if(isSmooth && chn.lastZxxParam < 0x80 && (outPos < 3 || out[outPos - 3] != 0xF0 || out[outPos - 2] < 0xF0)) { // Interpolation for external MIDI messages - interpolation for internal messages // is handled separately to allow for more than 7-bit granularity where it's possible data = static_cast(CalculateSmoothParamChange(playState, lastZxxParam, data)); chn.lastZxxParam = data; updateZxxParam = 0x80; } else if(updateZxxParam == 0xFF) { updateZxxParam = data; } } else if(macro[pos] == 's') { // SysEx Checksum (not an original Impulse Tracker macro variable, but added for convenience) auto startPos = outPos; while(startPos > 0 && out[--startPos] != 0xF0); if(outPos - startPos < 5 || out[startPos] != 0xF0) { continue; } for(auto p = startPos + 5u; p != outPos; p++) { data += out[p]; } data = (~data + 1) & 0x7F; } else { // Unrecognized byte (e.g. space char) continue; } // Append parsed data if(isNibble) // parsed a nibble (constant or 'c' variable) { if(firstNibble) { out[outPos] = data; } else { out[outPos] = (out[outPos] << 4) | data; outPos++; } firstNibble = !firstNibble; } else // parsed a byte (variable) { if(!firstNibble) // From MIDI.TXT: '9n' is exactly the same as '09 n' or '9 n' -- so finish current byte first { outPos++; } out[outPos++] = data; firstNibble = true; } } if(!firstNibble) { // Finish current byte outPos++; } if(updateZxxParam < 0x80) chn.lastZxxParam = updateZxxParam; out = out.first(outPos); } // Calculate smooth MIDI macro slide parameter for current tick. float CSoundFile::CalculateSmoothParamChange(const PlayState &playState, float currentValue, float param) { MPT_ASSERT(playState.TicksOnRow() > playState.m_nTickCount); const uint32 ticksLeft = playState.TicksOnRow() - playState.m_nTickCount; if(ticksLeft > 1) { // Slide param const float step = (param - currentValue) / static_cast(ticksLeft); return (currentValue + step); } else { // On last tick, set exact value. return param; } } // Process exactly one MIDI message parsed by ProcessMIDIMacro. Returns bytes sent on success, 0 on (parse) failure. void CSoundFile::SendMIDIData(PlayState &playState, CHANNELINDEX nChn, bool isSmooth, const mpt::span macro, PLUGINDEX plugin) { if(macro.size() < 1) return; // Don't do anything that modifies state outside of the playState itself. const bool localOnly = playState.m_midiMacroEvaluationResults.has_value(); if(macro[0] == 0xFA || macro[0] == 0xFC || macro[0] == 0xFF) { // Start Song, Stop Song, MIDI Reset - both interpreted internally and sent to plugins for(CHANNELINDEX chn = 0; chn < GetNumChannels(); chn++) { playState.Chn[chn].nCutOff = 0x7F; playState.Chn[chn].nResonance = 0x00; } } ModChannel &chn = playState.Chn[nChn]; if(macro.size() == 4 && macro[0] == 0xF0 && (macro[1] == 0xF0 || macro[1] == 0xF1)) { // Internal device. const bool isExtended = (macro[1] == 0xF1); const uint8 macroCode = macro[2]; const uint8 param = macro[3]; if(macroCode == 0x00 && !isExtended && param < 0x80) { // F0.F0.00.xx: Set CutOff if(!isSmooth) chn.nCutOff = param; else chn.nCutOff = mpt::saturate_round(CalculateSmoothParamChange(playState, chn.nCutOff, param)); chn.nRestoreCutoffOnNewNote = 0; int cutoff = SetupChannelFilter(chn, !chn.dwFlags[CHN_FILTER]); if(cutoff >= 0 && chn.dwFlags[CHN_ADLIB] && m_opl && !localOnly) { // Cutoff doubles as modulator intensity for FM instruments m_opl->Volume(nChn, static_cast(cutoff / 4), true); } } else if(macroCode == 0x01 && !isExtended && param < 0x80) { // F0.F0.01.xx: Set Resonance if(!isSmooth) chn.nResonance = param; else chn.nResonance = mpt::saturate_round(CalculateSmoothParamChange(playState, chn.nResonance, param)); chn.nRestoreResonanceOnNewNote = 0; SetupChannelFilter(chn, !chn.dwFlags[CHN_FILTER]); } else if(macroCode == 0x02 && !isExtended) { // F0.F0.02.xx: Set filter mode (high nibble determines filter mode) if(param < 0x20) { chn.nFilterMode = static_cast(param >> 4); SetupChannelFilter(chn, !chn.dwFlags[CHN_FILTER]); } #ifndef NO_PLUGINS } else if(macroCode == 0x03 && !isExtended) { // F0.F0.03.xx: Set plug dry/wet PLUGINDEX plug = (plugin != 0) ? plugin : GetBestPlugin(playState, nChn, PrioritiseChannel, EvenIfMuted); if(plug > 0 && plug <= MAX_MIXPLUGINS && param < 0x80) { plug--; if(IMixPlugin* pPlugin = m_MixPlugins[plug].pMixPlugin; pPlugin) { const float newRatio = (127 - param) / 127.0f; if(localOnly) playState.m_midiMacroEvaluationResults->pluginDryWetRatio[plug] = newRatio; else if(!isSmooth) pPlugin->SetDryRatio(newRatio); else pPlugin->SetDryRatio(CalculateSmoothParamChange(playState, m_MixPlugins[plug].fDryRatio, newRatio)); } } } else if((macroCode & 0x80) || isExtended) { // F0.F0.{80|n}.xx / F0.F1.n.xx: Set VST effect parameter n to xx PLUGINDEX plug = (plugin != 0) ? plugin : GetBestPlugin(playState, nChn, PrioritiseChannel, EvenIfMuted); if(plug > 0 && plug <= MAX_MIXPLUGINS && param < 0x80) { plug--; if(IMixPlugin *pPlugin = m_MixPlugins[plug].pMixPlugin; pPlugin) { const PlugParamIndex plugParam = isExtended ? (0x80 + macroCode) : (macroCode & 0x7F); const PlugParamValue value = param / 127.0f; if(localOnly) playState.m_midiMacroEvaluationResults->pluginParameter[{plug, plugParam}] = value; else if(!isSmooth) pPlugin->SetParameter(plugParam, value); else pPlugin->SetParameter(plugParam, CalculateSmoothParamChange(playState, pPlugin->GetParameter(plugParam), value)); } } #endif // NO_PLUGINS } } else if(!localOnly) { #ifndef NO_PLUGINS // Not an internal device. Pass on to appropriate plugin. const CHANNELINDEX plugChannel = (nChn < GetNumChannels()) ? nChn + 1 : chn.nMasterChn; if(plugChannel > 0 && plugChannel <= GetNumChannels()) // XXX do we need this? I guess it might be relevant for previewing notes in the pattern... Or when using this mechanism for volume/panning! { PLUGINDEX plug = 0; if(!chn.dwFlags[CHN_NOFX]) { plug = (plugin != 0) ? plugin : GetBestPlugin(playState, nChn, PrioritiseChannel, EvenIfMuted); } if(plug > 0 && plug <= MAX_MIXPLUGINS) { if(IMixPlugin *pPlugin = m_MixPlugins[plug - 1].pMixPlugin; pPlugin != nullptr) { if(macro[0] == 0xF0) { pPlugin->MidiSysexSend(mpt::byte_cast(macro)); } else { size_t len = std::min(static_cast(MIDIEvents::GetEventLength(macro[0])), macro.size()); uint32 curData = 0; memcpy(&curData, macro.data(), len); pPlugin->MidiSend(curData); } } } } #else MPT_UNREFERENCED_PARAMETER(plugin); #endif // NO_PLUGINS } } void CSoundFile::SendMIDINote(CHANNELINDEX chn, uint16 note, uint16 volume) { #ifndef NO_PLUGINS auto &channel = m_PlayState.Chn[chn]; const ModInstrument *pIns = channel.pModInstrument; // instro sends to a midi chan if (pIns && pIns->HasValidMIDIChannel()) { PLUGINDEX plug = pIns->nMixPlug; if(plug > 0 && plug <= MAX_MIXPLUGINS) { IMixPlugin *pPlug = m_MixPlugins[plug - 1].pMixPlugin; if (pPlug != nullptr) { pPlug->MidiCommand(*pIns, note, volume, chn); if(note < NOTE_MIN_SPECIAL) channel.nLeftVU = channel.nRightVU = 0xFF; } } } #endif // NO_PLUGINS } void CSoundFile::ProcessSampleOffset(ModChannel &chn, CHANNELINDEX nChn, const PlayState &playState) const { const ModCommand &m = chn.rowCommand; uint32 extendedRows = 0; SmpLength offset = CalculateXParam(playState.m_nPattern, playState.m_nRow, nChn, &extendedRows), highOffset = 0; if(!extendedRows) { // No X-param (normal behaviour) const bool isPercentageOffset = (m.volcmd == VOLCMD_OFFSET && m.vol == 0); offset <<= 8; if(offset) chn.oldOffset = offset; else if(m.volcmd != VOLCMD_OFFSET) offset = chn.oldOffset; if(!isPercentageOffset) highOffset = static_cast(chn.nOldHiOffset) << 16; } if(m.volcmd == VOLCMD_OFFSET) { if(m.vol == 0) offset = Util::muldivr_unsigned(chn.nLength, offset, 256u << (8u * std::max(uint32(1), extendedRows))); // o00 + Oxx = Percentage Offset else if(m.vol <= std::size(ModSample().cues) && chn.pModSample != nullptr) offset += chn.pModSample->cues[m.vol - 1]; // Offset relative to cue point chn.oldOffset = offset; } SampleOffset(chn, offset + highOffset); } void CSoundFile::SampleOffset(ModChannel &chn, SmpLength param) const { // ST3 compatibility: Instrument-less note recalls previous note's offset // Test case: OxxMemory.s3m if(m_playBehaviour[kST3OffsetWithoutInstrument]) chn.prevNoteOffset = 0; chn.prevNoteOffset += param; if(param >= chn.nLoopEnd && (GetType() & (MOD_TYPE_S3M | MOD_TYPE_MTM)) && chn.dwFlags[CHN_LOOP] && chn.nLoopEnd > 0) { // Offset wrap-around // Note that ST3 only does this in GUS mode. SoundBlaster stops the sample entirely instead. // Test case: OffsetLoopWraparound.s3m param = (param - chn.nLoopStart) % (chn.nLoopEnd - chn.nLoopStart) + chn.nLoopStart; } if(GetType() == MOD_TYPE_MDL && chn.dwFlags[CHN_16BIT]) { // Digitrakker really uses byte offsets, not sample offsets. WTF! param /= 2u; } if(chn.rowCommand.IsNote() || m_playBehaviour[kApplyOffsetWithoutNote]) { // IT compatibility: If this note is not mapped to a sample, ignore it. // Test case: empty_sample_offset.it if(chn.pModInstrument != nullptr && chn.rowCommand.IsNote()) { SAMPLEINDEX smp = chn.pModInstrument->Keyboard[chn.rowCommand.note - NOTE_MIN]; if(smp == 0 || smp > GetNumSamples()) return; } if(m_SongFlags[SONG_PT_MODE]) { // ProTracker compatbility: PT1/2-style funky 9xx offset command // Test case: ptoffset.mod chn.position.Set(chn.prevNoteOffset); chn.prevNoteOffset += param; } else { chn.position.Set(param); } if (chn.position.GetUInt() >= chn.nLength || (chn.dwFlags[CHN_LOOP] && chn.position.GetUInt() >= chn.nLoopEnd)) { // Offset beyond sample size if(m_playBehaviour[kFT2ST3OffsetOutOfRange] || GetType() == MOD_TYPE_MTM) { // FT2 Compatibility: Don't play note if offset is beyond sample length // ST3 Compatibility: Don't play note if offset is beyond sample length (non-looped samples only) // Test cases: 3xx-no-old-samp.xm, OffsetPastSampleEnd.s3m chn.dwFlags.set(CHN_FASTVOLRAMP); chn.nPeriod = 0; } else if(!(GetType() & (MOD_TYPE_XM | MOD_TYPE_MT2 | MOD_TYPE_MOD))) { // IT Compatibility: Offset if(m_playBehaviour[kITOffset]) { if(m_SongFlags[SONG_ITOLDEFFECTS]) chn.position.Set(chn.nLength); // Old FX: Clip to end of sample else chn.position.Set(0); // Reset to beginning of sample } else { chn.position.Set(chn.nLoopStart); if(m_SongFlags[SONG_ITOLDEFFECTS] && chn.nLength > 4) { chn.position.Set(chn.nLength - 2); } } } else if(GetType() == MOD_TYPE_MOD && chn.dwFlags[CHN_LOOP]) { chn.position.Set(chn.nLoopStart); } } } else if ((param < chn.nLength) && (GetType() & (MOD_TYPE_MTM | MOD_TYPE_DMF | MOD_TYPE_MDL | MOD_TYPE_PLM))) { // Some trackers can also call offset effects without notes next to them... chn.position.Set(param); } } void CSoundFile::ReverseSampleOffset(ModChannel &chn, ModCommand::PARAM param) const { if(chn.pModSample != nullptr && chn.pModSample->nLength > 0) { chn.dwFlags.set(CHN_PINGPONGFLAG); chn.dwFlags.reset(CHN_LOOP); chn.nLength = chn.pModSample->nLength; // If there was a loop, extend sample to whole length. chn.position.Set((chn.nLength - 1) - std::min(SmpLength(param) << 8, chn.nLength - SmpLength(1)), 0); } } void CSoundFile::DigiBoosterSampleReverse(ModChannel &chn, ModCommand::PARAM param) const { if(chn.isFirstTick && chn.pModSample != nullptr && chn.pModSample->nLength > 0) { chn.dwFlags.set(CHN_PINGPONGFLAG); chn.nLength = chn.pModSample->nLength; // If there was a loop, extend sample to whole length. chn.position.Set(chn.nLength - 1, 0); chn.dwFlags.set(CHN_LOOP | CHN_PINGPONGLOOP, param > 0); if(param > 0) { chn.nLoopStart = 0; chn.nLoopEnd = chn.nLength; // TODO: When the sample starts playing in forward direction again, the loop should be updated to the normal sample loop. } } } void CSoundFile::HandleDigiSamplePlayDirection(PlayState &state, CHANNELINDEX chn) const { // Digi Booster mixes two channels into one Paula channel, and when a note is triggered on one of them it resets the reverse play flag on the other. if(GetType() == MOD_TYPE_DIGI) { state.Chn[chn].dwFlags.reset(CHN_PINGPONGFLAG); const CHANNELINDEX otherChn = chn ^ 1; if(otherChn < GetNumChannels()) state.Chn[otherChn].dwFlags.reset(CHN_PINGPONGFLAG); } } void CSoundFile::RetrigNote(CHANNELINDEX nChn, int param, int offset) { // Retrig: bit 8 is set if it's the new XM retrig ModChannel &chn = m_PlayState.Chn[nChn]; int retrigSpeed = param & 0x0F; uint8 retrigCount = chn.nRetrigCount; bool doRetrig = false; // IT compatibility 15. Retrigger if(m_playBehaviour[kITRetrigger]) { if(m_PlayState.m_nTickCount == 0 && chn.rowCommand.note) { chn.nRetrigCount = param & 0x0F; } else if(!chn.nRetrigCount || !--chn.nRetrigCount) { chn.nRetrigCount = param & 0x0F; doRetrig = true; } } else if(m_playBehaviour[kFT2Retrigger] && (param & 0x100)) { // Buggy-like-hell FT2 Rxy retrig! // Test case: retrig.xm if(m_SongFlags[SONG_FIRSTTICK]) { // Here are some really stupid things FT2 does on the first tick. // Test case: RetrigTick0.xm if(chn.rowCommand.instr > 0 && chn.rowCommand.IsNoteOrEmpty()) retrigCount = 1; if(chn.rowCommand.volcmd == VOLCMD_VOLUME && chn.rowCommand.vol != 0) { // I guess this condition simply checked if the volume byte was != 0 in FT2. chn.nRetrigCount = retrigCount; return; } } if(retrigCount >= retrigSpeed) { if(!m_SongFlags[SONG_FIRSTTICK] || !chn.rowCommand.IsNote()) { doRetrig = true; retrigCount = 0; } } } else { // old routines if (GetType() & (MOD_TYPE_S3M|MOD_TYPE_IT|MOD_TYPE_MPT)) { if(!retrigSpeed) retrigSpeed = 1; if(retrigCount && !(retrigCount % retrigSpeed)) doRetrig = true; retrigCount++; } else if(GetType() == MOD_TYPE_MOD) { // ProTracker-style retrigger // Test case: PTRetrigger.mod const auto tick = m_PlayState.m_nTickCount % m_PlayState.m_nMusicSpeed; if(!tick && chn.rowCommand.IsNote()) return; if(retrigSpeed && !(tick % retrigSpeed)) doRetrig = true; } else if(GetType() == MOD_TYPE_MTM) { // In MultiTracker, E9x retriggers the last note at exactly the x-th tick of the row doRetrig = m_PlayState.m_nTickCount == static_cast(param & 0x0F) && retrigSpeed != 0; } else { int realspeed = retrigSpeed; // FT2 bug: if a retrig (Rxy) occurs together with a volume command, the first retrig interval is increased by one tick if((param & 0x100) && (chn.rowCommand.volcmd == VOLCMD_VOLUME) && (chn.rowCommand.param & 0xF0)) realspeed++; if(!m_SongFlags[SONG_FIRSTTICK] || (param & 0x100)) { if(!realspeed) realspeed = 1; if(!(param & 0x100) && m_PlayState.m_nMusicSpeed && !(m_PlayState.m_nTickCount % realspeed)) doRetrig = true; retrigCount++; } else if(GetType() & (MOD_TYPE_XM | MOD_TYPE_MT2)) retrigCount = 0; if (retrigCount >= realspeed) { if(m_PlayState.m_nTickCount || ((param & 0x100) && !chn.rowCommand.note)) doRetrig = true; } if(m_playBehaviour[kFT2Retrigger] && param == 0) { // E90 = Retrig instantly, and only once doRetrig = (m_PlayState.m_nTickCount == 0); } } } // IT compatibility: If a sample is shorter than the retrig time (i.e. it stops before the retrig counter hits zero), it is not retriggered. // Test case: retrig-short.it if(chn.nLength == 0 && m_playBehaviour[kITShortSampleRetrig] && !chn.HasMIDIOutput()) return; // ST3 compatibility: No retrig after Note Cut // Test case: RetrigAfterNoteCut.s3m if(m_playBehaviour[kST3RetrigAfterNoteCut] && !chn.nFadeOutVol) return; if(doRetrig) { uint32 dv = (param >> 4) & 0x0F; int vol = chn.nVolume; if(dv) { // FT2 compatibility: Retrig + volume will not change volume of retrigged notes if(!m_playBehaviour[kFT2Retrigger] || !(chn.rowCommand.volcmd == VOLCMD_VOLUME)) { if(retrigTable1[dv]) vol = (vol * retrigTable1[dv]) / 16; else vol += ((int)retrigTable2[dv]) * 4; } Limit(vol, 0, 256); chn.dwFlags.set(CHN_FASTVOLRAMP); } uint32 note = chn.nNewNote; int32 oldPeriod = chn.nPeriod; // ST3 doesn't retrigger OPL notes // Test case: RetrigSlide.s3m const bool oplRealRetrig = chn.dwFlags[CHN_ADLIB] && m_playBehaviour[kOPLRealRetrig]; if(note >= NOTE_MIN && note <= NOTE_MAX && chn.nLength && (GetType() != MOD_TYPE_S3M || oplRealRetrig)) CheckNNA(nChn, 0, note, true); bool resetEnv = false; if(GetType() & (MOD_TYPE_XM | MOD_TYPE_MT2)) { if(chn.rowCommand.instr && param < 0x100) { InstrumentChange(chn, chn.rowCommand.instr, false, false); resetEnv = true; } if(param < 0x100) resetEnv = true; } const bool fading = chn.dwFlags[CHN_NOTEFADE]; const auto oldPrevNoteOffset = chn.prevNoteOffset; chn.prevNoteOffset = 0; // Retriggered notes should not use previous offset (test case: OxxMemoryWithRetrig.s3m) // IT compatibility: Really weird combination of envelopes and retrigger (see Storlek's q.it testcase) // Test cases: retrig.it, RetrigSlide.s3m const bool itS3Mstyle = m_playBehaviour[kITRetrigger] || (GetType() == MOD_TYPE_S3M && chn.nLength && !oplRealRetrig); NoteChange(chn, note, itS3Mstyle, resetEnv, false, nChn); if(!chn.rowCommand.instr) chn.prevNoteOffset = oldPrevNoteOffset; // XM compatibility: Prevent NoteChange from resetting the fade flag in case an instrument number + note-off is present. // Test case: RetrigFade.xm if(fading && GetType() == MOD_TYPE_XM) chn.dwFlags.set(CHN_NOTEFADE); chn.nVolume = vol; if(m_nInstruments) { chn.rowCommand.note = static_cast(note); // No retrig without note... #ifndef NO_PLUGINS ProcessMidiOut(nChn); //Send retrig to Midi #endif // NO_PLUGINS } if((GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) && chn.rowCommand.note == NOTE_NONE && oldPeriod != 0) chn.nPeriod = oldPeriod; if(!(GetType() & (MOD_TYPE_S3M | MOD_TYPE_IT | MOD_TYPE_MPT))) retrigCount = 0; // IT compatibility: see previous IT compatibility comment =) if(itS3Mstyle) chn.position.Set(0); offset--; if(chn.pModSample != nullptr && offset >= 0 && offset <= static_cast(std::size(chn.pModSample->cues))) { if(offset == 0) offset = chn.oldOffset; else offset = chn.oldOffset = chn.pModSample->cues[offset - 1]; SampleOffset(chn, offset); } } // buggy-like-hell FT2 Rxy retrig! if(m_playBehaviour[kFT2Retrigger] && (param & 0x100)) retrigCount++; // Now we can also store the retrig value for IT... if(!m_playBehaviour[kITRetrigger]) chn.nRetrigCount = retrigCount; } // Execute a frequency slide on given channel. // Positive amounts increase the frequency, negative amounts decrease it. // The period or frequency that is read and written is in the period variable, chn.nPeriod is not touched. void CSoundFile::DoFreqSlide(ModChannel &chn, int32 &period, int32 amount, bool isTonePorta) const { if(!period || !amount) return; MPT_ASSERT(!chn.HasCustomTuning()); if(GetType() == MOD_TYPE_669) { // Like other oldskool trackers, Composer 669 doesn't have linear slides... // But the slides are done in Hertz rather than periods, meaning that they // are more effective in the lower notes (rather than the higher notes). period += amount * 20; } else if(GetType() == MOD_TYPE_FAR) { period += (amount * 36318 / 1024); } else if(m_SongFlags[SONG_LINEARSLIDES] && GetType() != MOD_TYPE_XM) { // IT Linear slides const auto oldPeriod = period; uint32 n = std::abs(amount); LimitMax(n, 255u * 4u); // Note: IT ignores the lower 2 bits when abs(mount) > 16 (it either uses the fine *or* the regular table, not both) // This means that vibratos are slightly less accurate in this range than they could be. // Other code paths will *either* have an amount that's a multiple of 4 *or* it's less than 16. if(amount > 0) { if(n < 16) period = Util::muldivr(period, GetFineLinearSlideUpTable(this, n), 65536); else period = Util::muldivr(period, GetLinearSlideUpTable(this, n / 4u), 65536); } else { if(n < 16) period = Util::muldivr(period, GetFineLinearSlideDownTable(this, n), 65536); else period = Util::muldivr(period, GetLinearSlideDownTable(this, n / 4u), 65536); } if(period == oldPeriod) { const bool incPeriod = m_playBehaviour[kPeriodsAreHertz] == (amount > 0); if(incPeriod && period < Util::MaxValueOfType(period)) period++; else if(!incPeriod && period > 1) period--; } } else if(!m_SongFlags[SONG_LINEARSLIDES] && m_playBehaviour[kPeriodsAreHertz]) { // IT Amiga slides if(amount < 0) { // Go down period = mpt::saturate_cast(Util::mul32to64_unsigned(1712 * 8363, period) / (Util::mul32to64_unsigned(period, -amount) + 1712 * 8363)); } else if(amount > 0) { // Go up const auto periodDiv = 1712 * 8363 - Util::mul32to64(period, amount); if(periodDiv <= 0) { if(isTonePorta) { period = int32_max; return; } else { period = 0; chn.nFadeOutVol = 0; chn.dwFlags.set(CHN_NOTEFADE | CHN_FASTVOLRAMP); } return; } period = mpt::saturate_cast(Util::mul32to64_unsigned(1712 * 8363, period) / periodDiv); } } else { period -= amount; } if(period < 1) { period = 1; if(GetType() == MOD_TYPE_S3M && !isTonePorta) { chn.nFadeOutVol = 0; chn.dwFlags.set(CHN_NOTEFADE | CHN_FASTVOLRAMP); } } } void CSoundFile::NoteCut(CHANNELINDEX nChn, uint32 nTick, bool cutSample) { if (m_PlayState.m_nTickCount == nTick) { ModChannel &chn = m_PlayState.Chn[nChn]; if(cutSample) { chn.increment.Set(0); chn.nFadeOutVol = 0; chn.dwFlags.set(CHN_NOTEFADE); } else { chn.nVolume = 0; } chn.dwFlags.set(CHN_FASTVOLRAMP); // instro sends to a midi chan SendMIDINote(nChn, /*chn.nNote+*/NOTE_MAX_SPECIAL, 0); if(chn.dwFlags[CHN_ADLIB] && m_opl) { m_opl->NoteCut(nChn, false); } } } void CSoundFile::KeyOff(ModChannel &chn) const { const bool keyIsOn = !chn.dwFlags[CHN_KEYOFF]; chn.dwFlags.set(CHN_KEYOFF); if(chn.pModInstrument != nullptr && !chn.VolEnv.flags[ENV_ENABLED]) { chn.dwFlags.set(CHN_NOTEFADE); } if (!chn.nLength) return; if (chn.dwFlags[CHN_SUSTAINLOOP] && chn.pModSample && keyIsOn) { const ModSample *pSmp = chn.pModSample; if(pSmp->uFlags[CHN_LOOP]) { if (pSmp->uFlags[CHN_PINGPONGLOOP]) chn.dwFlags.set(CHN_PINGPONGLOOP); else chn.dwFlags.reset(CHN_PINGPONGLOOP | CHN_PINGPONGFLAG); chn.dwFlags.set(CHN_LOOP); chn.nLength = pSmp->nLength; chn.nLoopStart = pSmp->nLoopStart; chn.nLoopEnd = pSmp->nLoopEnd; if (chn.nLength > chn.nLoopEnd) chn.nLength = chn.nLoopEnd; if(chn.position.GetUInt() > chn.nLength) { // Test case: SusAfterLoop.it chn.position.Set(chn.nLoopStart + ((chn.position.GetInt() - chn.nLoopStart) % (chn.nLoopEnd - chn.nLoopStart))); } } else { chn.dwFlags.reset(CHN_LOOP | CHN_PINGPONGLOOP | CHN_PINGPONGFLAG); chn.nLength = pSmp->nLength; } } if (chn.pModInstrument) { const ModInstrument *pIns = chn.pModInstrument; if((pIns->VolEnv.dwFlags[ENV_LOOP] || (GetType() & (MOD_TYPE_XM | MOD_TYPE_MT2 | MOD_TYPE_MDL))) && pIns->nFadeOut != 0) { chn.dwFlags.set(CHN_NOTEFADE); } if (pIns->VolEnv.nReleaseNode != ENV_RELEASE_NODE_UNSET && chn.VolEnv.nEnvValueAtReleaseJump == NOT_YET_RELEASED) { chn.VolEnv.nEnvValueAtReleaseJump = mpt::saturate_cast(pIns->VolEnv.GetValueFromPosition(chn.VolEnv.nEnvPosition, 256)); chn.VolEnv.nEnvPosition = pIns->VolEnv[pIns->VolEnv.nReleaseNode].tick; } } } ////////////////////////////////////////////////////////// // CSoundFile: Global Effects void CSoundFile::SetSpeed(PlayState &playState, uint32 param) const { #ifdef MODPLUG_TRACKER // FT2 appears to be decrementing the tick count before checking for zero, // so it effectively counts down 65536 ticks with speed = 0 (song speed is a 16-bit variable in FT2) if(GetType() == MOD_TYPE_XM && !param) { playState.m_nMusicSpeed = uint16_max; } #endif // MODPLUG_TRACKER if(param > 0) playState.m_nMusicSpeed = param; if(GetType() == MOD_TYPE_STM && param > 0) { playState.m_nMusicSpeed = std::max(param >> 4, uint32(1)); playState.m_nMusicTempo = ConvertST2Tempo(static_cast(param)); } } // Convert a ST2 tempo byte to classic tempo and speed combination TEMPO CSoundFile::ConvertST2Tempo(uint8 tempo) { static constexpr uint8 ST2TempoFactor[] = { 140, 50, 25, 15, 10, 7, 6, 4, 3, 3, 2, 2, 2, 2, 1, 1 }; static constexpr uint32 st2MixingRate = 23863; // Highest possible setting in ST2 // This underflows at tempo 06...0F, and the resulting tick lengths depend on the mixing rate. // Note: ST2.3 uses the constant 50 below, earlier versions use 49 but they also play samples at a different speed. int32 samplesPerTick = st2MixingRate / (50 - ((ST2TempoFactor[tempo >> 4u] * (tempo & 0x0F)) >> 4u)); if(samplesPerTick <= 0) samplesPerTick += 65536; return TEMPO().SetRaw(Util::muldivrfloor(st2MixingRate, 5 * TEMPO::fractFact, samplesPerTick * 2)); } void CSoundFile::SetTempo(TEMPO param, bool setFromUI) { const CModSpecifications &specs = GetModSpecifications(); // Anything lower than the minimum tempo is considered to be a tempo slide const TEMPO minTempo = (GetType() & (MOD_TYPE_MDL | MOD_TYPE_MED | MOD_TYPE_MOD)) ? TEMPO(1, 0) : TEMPO(32, 0); if(setFromUI) { // Set tempo from UI - ignore slide commands and such. m_PlayState.m_nMusicTempo = Clamp(param, specs.GetTempoMin(), specs.GetTempoMax()); } else if(param >= minTempo && m_SongFlags[SONG_FIRSTTICK] == !m_playBehaviour[kMODTempoOnSecondTick]) { // ProTracker sets the tempo after the first tick. // Note: The case of one tick per row is handled in ProcessRow() instead. // Test case: TempoChange.mod m_PlayState.m_nMusicTempo = std::min(param, specs.GetTempoMax()); } else if(param < minTempo && !m_SongFlags[SONG_FIRSTTICK]) { // Tempo Slide TEMPO tempDiff(param.GetInt() & 0x0F, 0); if((param.GetInt() & 0xF0) == 0x10) m_PlayState.m_nMusicTempo += tempDiff; else m_PlayState.m_nMusicTempo -= tempDiff; TEMPO tempoMin = specs.GetTempoMin(), tempoMax = specs.GetTempoMax(); if(m_playBehaviour[kTempoClamp]) // clamp tempo correctly in compatible mode { tempoMax.Set(255); } Limit(m_PlayState.m_nMusicTempo, tempoMin, tempoMax); } } void CSoundFile::PatternLoop(PlayState &state, ModChannel &chn, ModCommand::PARAM param) const { if(m_playBehaviour[kST3NoMutedChannels] && chn.dwFlags[CHN_MUTE | CHN_SYNCMUTE]) return; // not even effects are processed on muted S3M channels if(!param) { // Loop Start chn.nPatternLoop = state.m_nRow; return; } // Loop Repeat if(chn.nPatternLoopCount) { // There's a loop left chn.nPatternLoopCount--; if(!chn.nPatternLoopCount) { // IT compatibility 10. Pattern loops (+ same fix for S3M files) // When finishing a pattern loop, the next loop without a dedicated SB0 starts on the first row after the previous loop. if(m_playBehaviour[kITPatternLoopTargetReset] || (GetType() == MOD_TYPE_S3M)) chn.nPatternLoop = state.m_nRow + 1; return; } } else { // First time we get into the loop => Set loop count. // IT compatibility 10. Pattern loops (+ same fix for XM / MOD / S3M files) if(!m_playBehaviour[kITFT2PatternLoop] && !(GetType() & (MOD_TYPE_MOD | MOD_TYPE_S3M))) { auto p = std::cbegin(state.Chn); for(CHANNELINDEX i = 0; i < GetNumChannels(); i++, p++) { // Loop on other channel if(p != &chn && p->nPatternLoopCount) return; } } chn.nPatternLoopCount = param; } state.m_nextPatStartRow = chn.nPatternLoop; // Nasty FT2 E60 bug emulation! const auto loopTarget = chn.nPatternLoop; if(loopTarget != ROWINDEX_INVALID) { // FT2 compatibility: E6x overwrites jump targets of Dxx effects that are located left of the E6x effect. // Test cases: PatLoop-Jumps.xm, PatLoop-Various.xm if(state.m_breakRow != ROWINDEX_INVALID && m_playBehaviour[kFT2PatternLoopWithJumps]) state.m_breakRow = loopTarget; state.m_patLoopRow = loopTarget; // IT compatibility: SBx is prioritized over Position Jump (Bxx) effects that are located left of the SBx effect. // Test case: sbx-priority.it, LoopBreak.it if(m_playBehaviour[kITPatternLoopWithJumps]) state.m_posJump = ORDERINDEX_INVALID; } if(GetType() == MOD_TYPE_S3M) { // ST3 doesn't have per-channel pattern loop memory, so spam all changes to other channels as well. for(CHANNELINDEX i = 0; i < GetNumChannels(); i++) { state.Chn[i].nPatternLoop = chn.nPatternLoop; state.Chn[i].nPatternLoopCount = chn.nPatternLoopCount; } } } void CSoundFile::GlobalVolSlide(ModCommand::PARAM param, uint8 &nOldGlobalVolSlide) { int32 nGlbSlide = 0; if (param) nOldGlobalVolSlide = param; else param = nOldGlobalVolSlide; if((GetType() & (MOD_TYPE_XM | MOD_TYPE_MT2))) { // XM nibble priority if((param & 0xF0) != 0) { param &= 0xF0; } else { param &= 0x0F; } } if (((param & 0x0F) == 0x0F) && (param & 0xF0)) { if(m_SongFlags[SONG_FIRSTTICK]) nGlbSlide = (param >> 4) * 2; } else if (((param & 0xF0) == 0xF0) && (param & 0x0F)) { if(m_SongFlags[SONG_FIRSTTICK]) nGlbSlide = - (int)((param & 0x0F) * 2); } else { if(!m_SongFlags[SONG_FIRSTTICK]) { if (param & 0xF0) { // IT compatibility: Ignore slide commands with both nibbles set. if(!(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_IMF | MOD_TYPE_J2B | MOD_TYPE_MID | MOD_TYPE_AMS | MOD_TYPE_DBM)) || (param & 0x0F) == 0) nGlbSlide = (int)((param & 0xF0) >> 4) * 2; } else { nGlbSlide = -(int)((param & 0x0F) * 2); } } } if (nGlbSlide) { if(!(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_IMF | MOD_TYPE_J2B | MOD_TYPE_MID | MOD_TYPE_AMS | MOD_TYPE_DBM))) nGlbSlide *= 2; nGlbSlide += m_PlayState.m_nGlobalVolume; Limit(nGlbSlide, 0, 256); m_PlayState.m_nGlobalVolume = nGlbSlide; } } ////////////////////////////////////////////////////// // Note/Period/Frequency functions // Find lowest note which has same or lower period as a given period (i.e. the note has the same or higher frequency) uint32 CSoundFile::GetNoteFromPeriod(uint32 period, int32 nFineTune, uint32 nC5Speed) const { if(!period) return 0; if(m_playBehaviour[kFT2Periods]) { // FT2's "RelocateTon" function actually rounds up and down, while GetNoteFromPeriod normally just truncates. nFineTune += 64; } // This essentially implements std::lower_bound, with the difference that we don't need an iterable container. uint32 minNote = NOTE_MIN, maxNote = NOTE_MAX, count = maxNote - minNote + 1; const bool periodIsFreq = PeriodsAreFrequencies(); while(count > 0) { const uint32 step = count / 2, midNote = minNote + step; uint32 n = GetPeriodFromNote(midNote, nFineTune, nC5Speed); if((n > period && !periodIsFreq) || (n < period && periodIsFreq) || !n) { minNote = midNote + 1; count -= step + 1; } else { count = step; } } return minNote; } uint32 CSoundFile::GetPeriodFromNote(uint32 note, int32 nFineTune, uint32 nC5Speed) const { if (note == NOTE_NONE || (note >= NOTE_MIN_SPECIAL)) return 0; note -= NOTE_MIN; if(!UseFinetuneAndTranspose()) { if(GetType() & (MOD_TYPE_MDL | MOD_TYPE_DTM)) { // MDL uses non-linear slides, but their effectiveness does not depend on the middle-C frequency. return (FreqS3MTable[note % 12u] << 4) >> (note / 12); } if(!nC5Speed) nC5Speed = 8363; if(PeriodsAreFrequencies()) { // Compute everything in Hertz rather than periods. uint32 freq = Util::muldiv_unsigned(nC5Speed, LinearSlideUpTable[(note % 12u) * 16u] << (note / 12u), 65536 << 5); LimitMax(freq, static_cast(int32_max)); return freq; } else if(m_SongFlags[SONG_LINEARSLIDES]) { return (FreqS3MTable[note % 12u] << 5) >> (note / 12); } else { LimitMax(nC5Speed, uint32_max >> (note / 12u)); //(a*b)/c return Util::muldiv_unsigned(8363, (FreqS3MTable[note % 12u] << 5), nC5Speed << (note / 12u)); //8363 * freq[note%12] / nC5Speed * 2^(5-note/12) } } else if(GetType() & (MOD_TYPE_XM | MOD_TYPE_MTM)) { if (note < 12) note = 12; note -= 12; if(GetType() == MOD_TYPE_MTM) { nFineTune *= 16; } else if(m_playBehaviour[kFT2FinetunePrecision]) { // FT2 Compatibility: The lower three bits of the finetune are truncated. // Test case: Finetune-Precision.xm nFineTune &= ~7; } if(m_SongFlags[SONG_LINEARSLIDES]) { int l = ((NOTE_MAX - note) << 6) - (nFineTune / 2); if (l < 1) l = 1; return static_cast(l); } else { int finetune = nFineTune; uint32 rnote = (note % 12) << 3; uint32 roct = note / 12; int rfine = finetune / 16; int i = rnote + rfine + 8; Limit(i , 0, 103); uint32 per1 = XMPeriodTable[i]; if(finetune < 0) { rfine--; finetune = -finetune; } else rfine++; i = rnote+rfine+8; if (i < 0) i = 0; if (i >= 104) i = 103; uint32 per2 = XMPeriodTable[i]; rfine = finetune & 0x0F; per1 *= 16-rfine; per2 *= rfine; return ((per1 + per2) << 1) >> roct; } } else { nFineTune = XM2MODFineTune(nFineTune); if ((nFineTune) || (note < 24) || (note >= 24 + std::size(ProTrackerPeriodTable))) return (ProTrackerTunedPeriods[nFineTune * 12u + note % 12u] << 5) >> (note / 12u); else return (ProTrackerPeriodTable[note - 24] << 2); } } // Converts period value to sample frequency. Return value is fixed point, with FREQ_FRACBITS fractional bits. uint32 CSoundFile::GetFreqFromPeriod(uint32 period, uint32 c5speed, int32 nPeriodFrac) const { if (!period) return 0; if (GetType() & (MOD_TYPE_XM | MOD_TYPE_MTM)) { if(m_playBehaviour[kFT2Periods]) { // FT2 compatibility: Period is a 16-bit value in FT2, and it overflows happily. // Test case: FreqWraparound.xm period &= 0xFFFF; } if(m_SongFlags[SONG_LINEARSLIDES]) { uint32 octave; if(m_playBehaviour[kFT2Periods]) { // Under normal circumstances, this calculation returns the same values as the non-compatible one. // However, once the 12 octaves are exceeded (through portamento slides), the octave shift goes // crazy in FT2, meaning that the frequency wraps around randomly... // The entries in FT2's conversion table are four times as big, hence we have to do an additional shift by two bits. // Test case: FreqWraparound.xm // 12 octaves * (12 * 64) LUT entries = 9216, add 767 for rounding uint32 div = ((9216u + 767u - period) / 768); octave = ((14 - div) & 0x1F); } else { octave = (period / 768) + 2; } return (XMLinearTable[period % 768] << (FREQ_FRACBITS + 2)) >> octave; } else { if(!period) period = 1; return ((8363 * 1712L) << FREQ_FRACBITS) / period; } } else if(UseFinetuneAndTranspose()) { return ((3546895L * 4) << FREQ_FRACBITS) / period; } else if(GetType() == MOD_TYPE_669) { // We only really use c5speed for the finetune pattern command. All samples in 669 files have the same middle-C speed (imported as 8363 Hz). return (period + c5speed - 8363) << FREQ_FRACBITS; } else if(GetType() & (MOD_TYPE_MDL | MOD_TYPE_DTM)) { LimitMax(period, Util::MaxValueOfType(period) >> 8); if (!c5speed) c5speed = 8363; return Util::muldiv_unsigned(c5speed, (1712L << 7) << FREQ_FRACBITS, (period << 8) + nPeriodFrac); } else { LimitMax(period, Util::MaxValueOfType(period) >> 8); if(PeriodsAreFrequencies()) { // Input is already a frequency in Hertz, not a period. static_assert(FREQ_FRACBITS <= 8, "Check this shift operator"); return uint32(((uint64(period) << 8) + nPeriodFrac) >> (8 - FREQ_FRACBITS)); } else if(m_SongFlags[SONG_LINEARSLIDES]) { if(!c5speed) c5speed = 8363; return Util::muldiv_unsigned(c5speed, (1712L << 8) << FREQ_FRACBITS, (period << 8) + nPeriodFrac); } else { return Util::muldiv_unsigned(8363, (1712L << 8) << FREQ_FRACBITS, (period << 8) + nPeriodFrac); } } } PLUGINDEX CSoundFile::GetBestPlugin(const PlayState &playState, CHANNELINDEX nChn, PluginPriority priority, PluginMutePriority respectMutes) const { if (nChn >= MAX_CHANNELS) //Check valid channel number { return 0; } //Define search source order PLUGINDEX plugin = 0; switch (priority) { case ChannelOnly: plugin = GetChannelPlugin(playState, nChn, respectMutes); break; case InstrumentOnly: plugin = GetActiveInstrumentPlugin(playState.Chn[nChn], respectMutes); break; case PrioritiseInstrument: plugin = GetActiveInstrumentPlugin(playState.Chn[nChn], respectMutes); if(!plugin || plugin > MAX_MIXPLUGINS) { plugin = GetChannelPlugin(playState, nChn, respectMutes); } break; case PrioritiseChannel: plugin = GetChannelPlugin(playState, nChn, respectMutes); if(!plugin || plugin > MAX_MIXPLUGINS) { plugin = GetActiveInstrumentPlugin(playState.Chn[nChn], respectMutes); } break; } return plugin; // 0 Means no plugin found. } PLUGINDEX CSoundFile::GetChannelPlugin(const PlayState &playState, CHANNELINDEX nChn, PluginMutePriority respectMutes) const { const ModChannel &channel = playState.Chn[nChn]; PLUGINDEX plugin; if((respectMutes == RespectMutes && channel.dwFlags[CHN_MUTE | CHN_SYNCMUTE]) || channel.dwFlags[CHN_NOFX]) { plugin = 0; } else { // If it looks like this is an NNA channel, we need to find the master channel. // This ensures we pick up the right ChnSettings. if(channel.nMasterChn > 0) { nChn = channel.nMasterChn - 1; } if(nChn < MAX_BASECHANNELS) { plugin = ChnSettings[nChn].nMixPlugin; } else { plugin = 0; } } return plugin; } PLUGINDEX CSoundFile::GetActiveInstrumentPlugin(const ModChannel &chn, PluginMutePriority respectMutes) { // Unlike channel settings, pModInstrument is copied from the original chan to the NNA chan, // so we don't need to worry about finding the master chan. PLUGINDEX plug = 0; if(chn.pModInstrument != nullptr) { // TODO this looks fishy. Shouldn't it check the mute status of the instrument itself?! if(respectMutes == RespectMutes && chn.pModSample && chn.pModSample->uFlags[CHN_MUTE]) { plug = 0; } else { plug = chn.pModInstrument->nMixPlug; } } return plug; } // Retrieve the plugin that is associated with the channel's current instrument. // No plugin is returned if the channel is muted or if the instrument doesn't have a MIDI channel set up, // As this is meant to be used with instrument plugins. IMixPlugin *CSoundFile::GetChannelInstrumentPlugin(const ModChannel &chn) const { #ifndef NO_PLUGINS if(chn.dwFlags[CHN_MUTE | CHN_SYNCMUTE]) { // Don't process portamento on muted channels. Note that this might have a side-effect // on other channels which trigger notes on the same MIDI channel of the same plugin, // as those won't be pitch-bent anymore. return nullptr; } if(chn.HasMIDIOutput()) { const ModInstrument *pIns = chn.pModInstrument; // Instrument sends to a MIDI channel if(pIns->nMixPlug != 0 && pIns->nMixPlug <= MAX_MIXPLUGINS) { return m_MixPlugins[pIns->nMixPlug - 1].pMixPlugin; } } #else MPT_UNREFERENCED_PARAMETER(chn); #endif // NO_PLUGINS return nullptr; } #ifdef MODPLUG_TRACKER void CSoundFile::HandlePatternTransitionEvents() { // MPT sequence override if(m_PlayState.m_nSeqOverride != ORDERINDEX_INVALID && m_PlayState.m_nSeqOverride < Order().size()) { if(m_SongFlags[SONG_PATTERNLOOP]) { m_PlayState.m_nPattern = Order()[m_PlayState.m_nSeqOverride]; } m_PlayState.m_nCurrentOrder = m_PlayState.m_nSeqOverride; m_PlayState.m_nSeqOverride = ORDERINDEX_INVALID; } // Channel mutes for (CHANNELINDEX chan = 0; chan < GetNumChannels(); chan++) { if (m_bChannelMuteTogglePending[chan]) { if(GetpModDoc()) { GetpModDoc()->MuteChannel(chan, !GetpModDoc()->IsChannelMuted(chan)); } m_bChannelMuteTogglePending[chan] = false; } } } #endif // MODPLUG_TRACKER // Update time signatures (global or pattern-specific). Don't forget to call this when changing the RPB/RPM settings anywhere! void CSoundFile::UpdateTimeSignature() { if(!Patterns.IsValidIndex(m_PlayState.m_nPattern) || !Patterns[m_PlayState.m_nPattern].GetOverrideSignature()) { m_PlayState.m_nCurrentRowsPerBeat = m_nDefaultRowsPerBeat; m_PlayState.m_nCurrentRowsPerMeasure = m_nDefaultRowsPerMeasure; } else { m_PlayState.m_nCurrentRowsPerBeat = Patterns[m_PlayState.m_nPattern].GetRowsPerBeat(); m_PlayState.m_nCurrentRowsPerMeasure = Patterns[m_PlayState.m_nPattern].GetRowsPerMeasure(); } } void CSoundFile::PortamentoMPT(ModChannel &chn, int param) { //Behavior: Modifies portamento by param-steps on every tick. //Note that step meaning depends on tuning. chn.m_PortamentoFineSteps += param; chn.m_CalculateFreq = true; } void CSoundFile::PortamentoFineMPT(ModChannel &chn, int param) { //Behavior: Divides portamento change between ticks/row. For example //if Ticks/row == 6, and param == +-6, portamento goes up/down by one tuning-dependent //fine step every tick. if(m_PlayState.m_nTickCount == 0) chn.nOldFinePortaUpDown = 0; const int tickParam = static_cast((m_PlayState.m_nTickCount + 1.0) * param / m_PlayState.m_nMusicSpeed); chn.m_PortamentoFineSteps += (param >= 0) ? tickParam - chn.nOldFinePortaUpDown : tickParam + chn.nOldFinePortaUpDown; if(m_PlayState.m_nTickCount + 1 == m_PlayState.m_nMusicSpeed) chn.nOldFinePortaUpDown = static_cast(std::abs(param)); else chn.nOldFinePortaUpDown = static_cast(std::abs(tickParam)); chn.m_CalculateFreq = true; } void CSoundFile::PortamentoExtraFineMPT(ModChannel &chn, int param) { // This kinda behaves like regular fine portamento. // It changes the pitch by n finetune steps on the first tick. if(chn.isFirstTick) { chn.m_PortamentoFineSteps += param; chn.m_CalculateFreq = true; } } OPENMPT_NAMESPACE_END