Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 22 additions & 13 deletions modules/juce_audio_basics/mpe/juce_MPEInstrument.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -365,18 +365,15 @@ void MPEInstrument::noteOn (int midiChannel,
const ScopedLock sl (lock);
updateNoteTotalPitchbend (newNote);

if (auto* alreadyPlayingNote = getNotePtr (midiChannel, midiNoteNumber))
{
// pathological case: second note-on received for same note -> retrigger it
alreadyPlayingNote->keyState = MPENote::off;
alreadyPlayingNote->noteOffVelocity = MPEValue::from7BitInt (64); // some reasonable number

// This is commented out so that we can legato mods correctly between voices that share the same note.
// listeners.call ([=] (Listener& l) { l.noteReleased (*alreadyPlayingNote); });

notes.remove (alreadyPlayingNote);
}

// Minimal Audio patch: a second note-on for a note that is already playing stacks a new
// note on top of it. Stock JUCE retriggers here (releases the old note, starts the new one).
// Our previous patch kept the old voice sounding but dropped its note from `notes`, so the
// instrument only ever knew about the newest instance: a single note-off then released
// every voice playing that note at once, and the older instances could never be released
// individually. Now every instance stays tracked and gets its own instanceID; note-offs
// release them oldest first (see noteOff), and MPESynthesiser::noteReleased stops only
// the voice that belongs to the released instance.
newNote.instanceID = ++lastNoteInstanceID;
notes.add (newNote);
listeners.call ([&] (Listener& l) { l.noteAdded (newNote); });
}
Expand All @@ -391,6 +388,9 @@ void MPEInstrument::noteOff (int midiChannel,
if (notes.isEmpty() || ! isUsingChannel (midiChannel))
return;

// Minimal Audio patch: getNotePtr returns the oldest note for this channel / number, so
// stacked instances of the same note (see noteOn) are released in the order they were
// started, one per note-off.
if (auto* note = getNotePtr (midiChannel, midiNoteNumber))
{
note->keyState = (note->keyState == MPENote::keyDownAndSustained) ? MPENote::sustained : MPENote::off;
Expand Down Expand Up @@ -994,13 +994,22 @@ class MPEInstrumentTests final : public UnitTest
expectNote (test.getNote (3, 2), 100, 0, 8192, 64, MPENote::keyDown);
}
{
// pathological case: second note-on for same note should retrigger it
// Minimal Audio patch: a second note-on for the same note stacks a new
// instance instead of retriggering it, and each note-off releases the
// oldest instance still playing
UnitTestInstrument test;
test.setZoneLayout (testLayout);
test.noteOn (3, 0, MPEValue::from7BitInt (100));
test.noteOn (3, 0, MPEValue::from7BitInt (60));
expectEquals (test.getNumPlayingNotes(), 2);
expectNote (test.getNote (3, 0), 100, 0, 8192, 64, MPENote::keyDown);

test.noteOff (3, 0, MPEValue::from7BitInt (33));
expectEquals (test.getNumPlayingNotes(), 1);
expectNote (test.getNote (3, 0), 60, 0, 8192, 64, MPENote::keyDown);

test.noteOff (3, 0, MPEValue::from7BitInt (33));
expectEquals (test.getNumPlayingNotes(), 0);
}
}

Expand Down
1 change: 1 addition & 0 deletions modules/juce_audio_basics/mpe/juce_MPEInstrument.h
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,7 @@ class JUCE_API MPEInstrument
private:
//==============================================================================
Array<MPENote> notes;
uint32 lastNoteInstanceID = 0;
MPEZoneLayout zoneLayout;
ListenerList<Listener> listeners;

Expand Down
8 changes: 8 additions & 0 deletions modules/juce_audio_basics/mpe/juce_MPENote.h
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,14 @@ struct JUCE_API MPENote
*/
uint16 noteID = 0;

/** Minimal Audio patch: identifies one particular note-on. Stacked instances of the same
note (see MPEInstrument::noteOn) share a noteID, so noteID alone can't say which of
them a note-off belongs to; this can. Assigned by MPEInstrument on note-on, 0 for
notes constructed by hand. Unlike noteID it takes no part in equality or expression
matching.
*/
uint32 instanceID = 0;

/** The MIDI channel which this note uses.
This should never change during the lifetime of an MPENote object.
*/
Expand Down
31 changes: 26 additions & 5 deletions modules/juce_audio_basics/mpe/juce_MPESynthesiser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,17 @@ void MPESynthesiser::noteAdded (MPENote newNote)
startVoice (voice, newNote);
}

// Minimal Audio patch: stacked instances of a note share a noteID and all receive the same
// expression / key-state updates (matched on noteID, as in stock JUCE). The incoming note
// carries the instanceID of whichever instance the instrument looked up, so keep the voice's
// own instanceID or a later release could stop the wrong voice.
void MPESynthesiser::updatePlayingNote (MPESynthesiserVoice& voice, MPENote changedNote)
{
const auto instanceID = voice.currentlyPlayingNote.instanceID;
voice.currentlyPlayingNote = changedNote;
voice.currentlyPlayingNote.instanceID = instanceID;
}

void MPESynthesiser::notePressureChanged (MPENote changedNote)
{
const ScopedLock sl (voicesLock);
Expand All @@ -83,7 +94,7 @@ void MPESynthesiser::notePressureChanged (MPENote changedNote)
{
if (voice->isCurrentlyPlayingNote (changedNote))
{
voice->currentlyPlayingNote = changedNote;
updatePlayingNote (*voice, changedNote);
voice->notePressureChanged();
}
}
Expand All @@ -97,7 +108,7 @@ void MPESynthesiser::notePitchbendChanged (MPENote changedNote)
{
if (voice->isCurrentlyPlayingNote (changedNote))
{
voice->currentlyPlayingNote = changedNote;
updatePlayingNote (*voice, changedNote);
voice->notePitchbendChanged();
}
}
Expand All @@ -111,7 +122,7 @@ void MPESynthesiser::noteTimbreChanged (MPENote changedNote)
{
if (voice->isCurrentlyPlayingNote (changedNote))
{
voice->currentlyPlayingNote = changedNote;
updatePlayingNote (*voice, changedNote);
voice->noteTimbreChanged();
}
}
Expand All @@ -125,7 +136,7 @@ void MPESynthesiser::noteKeyStateChanged (MPENote changedNote)
{
if (voice->isCurrentlyPlayingNote (changedNote))
{
voice->currentlyPlayingNote = changedNote;
updatePlayingNote (*voice, changedNote);
voice->noteKeyStateChanged();
}
}
Expand All @@ -135,12 +146,22 @@ void MPESynthesiser::noteReleased (MPENote finishedNote)
{
const ScopedLock sl (voicesLock);

// Minimal Audio patch: instances of the same note stack (see MPEInstrument::noteOn) and
// share a noteID. Stock JUCE (and our previous patch) stopped every voice playing that
// noteID here, so releasing one instance silenced all of them; now only the voice playing
// the released instance stops. Notes built by hand carry no instanceID and keep the
// stock noteID match.
for (auto i = voices.size(); --i >= 0;)
{
auto* voice = voices.getUnchecked (i);

if (voice->isCurrentlyPlayingNote (finishedNote))
stopVoice (voice, finishedNote, true);
{
const auto playingInstanceID = voice->getCurrentlyPlayingNote().instanceID;

if (finishedNote.instanceID == 0 || playingInstanceID == finishedNote.instanceID)
stopVoice (voice, finishedNote, true);
}
}
}

Expand Down
3 changes: 3 additions & 0 deletions modules/juce_audio_basics/mpe/juce_MPESynthesiser.h
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,9 @@ class JUCE_API MPESynthesiser : public MPESynthesiserBase
CriticalSection voicesLock;

private:
//==============================================================================
void updatePlayingNote (MPESynthesiserVoice& voice, MPENote changedNote);

//==============================================================================
std::atomic<bool> shouldStealVoices { false };
uint32 lastNoteOnCounter = 0;
Expand Down
Loading