Skip to content

Commit

Permalink
[Fix] Avoid plugin notes from channels where we can't find a new NNA …
Browse files Browse the repository at this point in the history
…channel with NNA=continue or force-reused NNA channels to linger on forever. The latter should in theory only have happened with notes triggered via CModDoc::PlayNote, which may force the first NNA channel to be used if no others are available.

[Imp] Allow NNA channels associated with MIDI-capable plugins to be reused even if they have NNA=continue set. The longer the NNA channel plays, the more likely it will be replaced.

git-svn-id: https://source.openmpt.org/svn/openmpt/trunk/OpenMPT@22602 56274372-70c3-4bfc-bfc3-4c3a0b034d27
  • Loading branch information
sagamusix committed Dec 21, 2024
1 parent 233d250 commit 8910daa
Show file tree
Hide file tree
Showing 7 changed files with 68 additions and 6 deletions.
5 changes: 4 additions & 1 deletion mptrack/Moddoc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1073,6 +1073,7 @@ CHANNELINDEX CModDoc::PlayNote(PlayNoteParam &params, NoteToChannelMap *noteChan
// Find a channel to play on
channel = FindAvailableChannel();
ModChannel &chn = m_SndFile.m_PlayState.Chn[channel];
m_SndFile.StopOldNNA(chn, channel);

// reset channel properties; in theory the chan is completely unused anyway.
chn.Reset(ModChannel::resetTotal, m_SndFile, CHANNELINDEX_INVALID, CHN_MUTE);
Expand Down Expand Up @@ -1112,7 +1113,9 @@ CHANNELINDEX CModDoc::PlayNote(PlayNoteParam &params, NoteToChannelMap *noteChan
}

m_SndFile.NoteChange(chn, note, false, true, true, channel);
if(params.m_volume >= 0) chn.nVolume = std::min(params.m_volume, 256);
if(params.m_volume >= 0)
chn.nVolume = std::min(params.m_volume, 256);
chn.nnaChannelAge = 0;

// Handle sample looping.
// Changed line to fix http://forum.openmpt.org/index.php?topic=1700.0
Expand Down
6 changes: 5 additions & 1 deletion soundlib/ModChannel.h
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,11 @@ struct ModChannel
int16 nVolSwing, nPanSwing;
int16 nCutSwing, nResSwing;
uint16 volSlideDownRemain, volSlideDownTotal;
uint16 nRestorePanOnNewNote; // If > 0, nPan should be set to nRestorePanOnNewNote - 1 on new note. Used to recover from pan swing and IT sample / instrument panning. High bit set = surround
union
{
uint16 nRestorePanOnNewNote; // If > 0, nPan should be set to nRestorePanOnNewNote - 1 on new note. Used to recover from pan swing and IT sample / instrument panning. High bit set = surround
uint16 nnaChannelAge; // If channel is moved to background (NNA), this counts up how old it is
};
uint16 nnaGeneration; // For PlaybackTest implementation
CHANNELINDEX nMasterChn;
SAMPLEINDEX swapSampleIndex; // Sample to swap to when current sample (loop) has finished playing
Expand Down
44 changes: 40 additions & 4 deletions soundlib/Snd_fx.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2271,7 +2271,10 @@ CHANNELINDEX CSoundFile::GetNNAChannel(CHANNELINDEX nChn) const
// Less priority to looped samples
if(c.dwFlags[CHN_LOOP])
v /= 2;
if((v < vol) || ((v == vol) && (c.VolEnv.nEnvPosition > envpos)))
// Less priority for channels potentially held for plugin notes with NNA=continue the older they get
if(!c.nLength && c.nMasterChn)
v -= std::min(static_cast<uint32>(c.nnaChannelAge) * c.nnaChannelAge, static_cast<uint32>(int32_max / 16)) * 16;
if((v < vol) || ((v == vol) && (c.VolEnv.nEnvPosition > envpos || !c.VolEnv.flags[ENV_ENABLED])))
{
envpos = c.VolEnv.nEnvPosition;
vol = v;
Expand Down Expand Up @@ -2326,6 +2329,7 @@ CHANNELINDEX CSoundFile::CheckNNA(CHANNELINDEX nChn, uint32 instr, int note, boo
if(nnaChn == CHANNELINDEX_INVALID)
return CHANNELINDEX_INVALID;
ModChannel &chn = m_PlayState.Chn[nnaChn];
StopOldNNA(chn, nnaChn);
// Copy Channel
chn = srcChn;
chn.dwFlags.reset(CHN_VIBRATO | CHN_TREMOLO | CHN_MUTE | CHN_PORTAMENTO);
Expand All @@ -2336,6 +2340,7 @@ CHANNELINDEX CSoundFile::CheckNNA(CHANNELINDEX nChn, uint32 instr, int note, boo
// Cut the note
chn.nFadeOutVol = 0;
chn.dwFlags.set(CHN_NOTEFADE | CHN_FASTVOLRAMP);
chn.nnaChannelAge = 0;
chn.nnaGeneration = ++srcChn.nnaGeneration;
// Stop this channel
srcChn.nLength = 0;
Expand Down Expand Up @@ -2477,6 +2482,8 @@ CHANNELINDEX CSoundFile::CheckNNA(CHANNELINDEX nChn, uint32 instr, int note, boo
if(!srcChn.IsSamplePlaying() && !applyNNAtoPlug)
return CHANNELINDEX_INVALID;

const CHANNELINDEX nnaChn = GetNNAChannel(nChn);

#ifndef NO_PLUGINS
if(applyNNAtoPlug)
{
Expand All @@ -2491,25 +2498,29 @@ CHANNELINDEX CSoundFile::CheckNNA(CHANNELINDEX nChn, uint32 instr, int note, boo
srcChn.lastMidiNoteWithoutArp = NOTE_NONE;
break;
case NewNoteAction::Continue:
// If there's no NNA channels available, avoid the note lingering on forever
if(nnaChn == CHANNELINDEX_INVALID)
SendMIDINote(nChn, NOTE_KEYOFF, 0, m_playBehaviour[kMIDINotesFromChannelPlugin] ? pPlugin : nullptr);
else
pPlugin->MoveChannel(nChn, nnaChn);
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);
StopOldNNA(chn, 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;
chn.nnaChannelAge = 0;
chn.nnaGeneration = ++srcChn.nnaGeneration;

// Key Off the note
Expand Down Expand Up @@ -2564,6 +2575,31 @@ CHANNELINDEX CSoundFile::CheckNNA(CHANNELINDEX nChn, uint32 instr, int note, boo
}


void CSoundFile::StopOldNNA(ModChannel &chn, CHANNELINDEX channel)
{
if(chn.dwFlags[CHN_ADLIB] && m_opl)
m_opl->NoteCut(channel);

#ifndef NO_PLUGINS
// Is a plugin note still associated with this old NNA channel? Stop it first.
if(chn.HasMIDIOutput() && ModCommand::IsNote(chn.nNote) && !chn.dwFlags[CHN_KEYOFF] && chn.lastMidiNoteWithoutArp != NOTE_NONE)
{
const PLUGINDEX plugin = GetBestPlugin(m_PlayState.Chn[channel], channel, PrioritiseInstrument, RespectMutes);
if(plugin > 0 && plugin <= MAX_MIXPLUGINS)
{
IMixPlugin *nnaPlugin = m_MixPlugins[plugin - 1].pMixPlugin;
// 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).
if(nnaPlugin && (chn.lastMidiNoteWithoutArp != NOTE_NONE) && nnaPlugin->IsNotePlaying(chn.lastMidiNoteWithoutArp, channel))
{
SendMIDINote(channel, chn.lastMidiNoteWithoutArp | IMixPlugin::MIDI_NOTE_OFF, 0, m_playBehaviour[kMIDINotesFromChannelPlugin] ? nnaPlugin : nullptr);
}
}
}
#endif // NO_PLUGINS
}


bool CSoundFile::ProcessEffects()
{
m_PlayState.m_breakRow = ROWINDEX_INVALID; // Is changed if a break to row command is encountered
Expand Down
1 change: 1 addition & 0 deletions soundlib/Sndfile.h
Original file line number Diff line number Diff line change
Expand Up @@ -1006,6 +1006,7 @@ class CSoundFile
void SetupNextRow(PlayState &playState, const bool patternLoop) const;
CHANNELINDEX GetNNAChannel(CHANNELINDEX nChn) const;
CHANNELINDEX CheckNNA(CHANNELINDEX nChn, uint32 instr, int note, bool forceCut);
void StopOldNNA(ModChannel &chn, CHANNELINDEX channel);
void NoteChange(ModChannel &chn, int note, bool bPorta = false, bool bResetEnv = true, bool bManual = false, CHANNELINDEX channelHint = CHANNELINDEX_INVALID) const;
void InstrumentChange(ModChannel &chn, uint32 instr, bool bPorta = false, bool bUpdVol = true, bool bResetEnv = true) const;
void ApplyInstrumentPanning(ModChannel &chn, const ModInstrument *instr, const ModSample *smp) const;
Expand Down
3 changes: 3 additions & 0 deletions soundlib/Sndmix.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2169,6 +2169,9 @@ bool CSoundFile::ReadNote()
chn.nLength = 0;
chn.nROfs = chn.nLOfs = 0;
}
// Increment age of NNA channels
if(chn.nMasterChn && chn.nnaChannelAge < Util::MaxValueOfType(chn.nnaChannelAge))
chn.nnaChannelAge++;
// Check for unused channel
if(chn.dwFlags[CHN_MUTE] || (nChn >= GetNumChannels() && !chn.nLength))
{
Expand Down
13 changes: 13 additions & 0 deletions soundlib/plugins/PlugInterface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1003,6 +1003,19 @@ bool IMidiPlugin::IsNotePlaying(uint8 note, CHANNELINDEX trackerChn)
}


void IMidiPlugin::MoveChannel(CHANNELINDEX from, CHANNELINDEX to)
{
if(from >= std::size(m_MidiCh[GetMidiChannel(from)].noteOnMap[0]) || to >= std::size(m_MidiCh[GetMidiChannel(from)].noteOnMap[0]))
return;

for(auto &noteOnMap : m_MidiCh[GetMidiChannel(from)].noteOnMap)
{
noteOnMap[to] = noteOnMap[from];
noteOnMap[from] = 0;
}
}


void IMidiPlugin::ReceiveMidi(mpt::const_byte_span midiData)
{
if(midiData.empty())
Expand Down
2 changes: 2 additions & 0 deletions soundlib/plugins/PlugInterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ class IMixPlugin
virtual void MidiCommand(const ModInstrument &/*instr*/, uint16 /*note*/, uint16 /*vol*/, CHANNELINDEX /*trackChannel*/) { }
virtual void HardAllNotesOff() { }
virtual bool IsNotePlaying(uint8 /*note*/, CHANNELINDEX /*trackerChn*/) { return false; }
virtual void MoveChannel(CHANNELINDEX /*from*/, CHANNELINDEX /*to*/) { }

// Modify parameter by given amount. Only needs to be re-implemented if plugin architecture allows this to be performed atomically.
virtual void ModifyParameter(PlugParamIndex nIndex, PlugParamValue diff, PlayState &playState, CHANNELINDEX chn);
Expand Down Expand Up @@ -286,6 +287,7 @@ class IMidiPlugin : public IMixPlugin
void MidiVibrato(int32 depth, int8 pwd, CHANNELINDEX trackerChn) override;
void MidiCommand(const ModInstrument &instr, uint16 note, uint16 vol, CHANNELINDEX trackChannel) override;
bool IsNotePlaying(uint8 note, CHANNELINDEX trackerChn) override;
void MoveChannel(CHANNELINDEX from, CHANNELINDEX to) override;

// Get the MIDI channel currently associated with a given tracker channel
virtual uint8 GetMidiChannel(const ModChannel &chn, CHANNELINDEX trackChannel) const;
Expand Down

0 comments on commit 8910daa

Please sign in to comment.