diff --git a/modules/tracktion_engine/playback/graph/tracktion_LoopingMidiNode.cpp b/modules/tracktion_engine/playback/graph/tracktion_LoopingMidiNode.cpp index 44a929d2938..af62f197b47 100644 --- a/modules/tracktion_engine/playback/graph/tracktion_LoopingMidiNode.cpp +++ b/modules/tracktion_engine/playback/graph/tracktion_LoopingMidiNode.cpp @@ -711,9 +711,13 @@ struct EventGenerator : public MidiGenerator bool useMPEChannelMode, MPESourceID midiSourceID, juce::Array& controllerMessagesScratchBuffer) override { - thread_local MidiMessageArray scratchBuffer, cleanedBufferToMerge; + ActiveNoteList originalState; + activeNoteList.iterate([&](int chan, int note) { + originalState.startNote(chan, note); + }); + + thread_local MidiMessageArray scratchBuffer; scratchBuffer.clear(); - cleanedBufferToMerge.clear(); MidiHelpers::createMessagesForTime (scratchBuffer, sequence, noteOffMap, @@ -723,32 +727,29 @@ struct EventGenerator : public MidiGenerator useMPEChannelMode, midiSourceID, controllerMessagesScratchBuffer); - // This isn't quite right as there could be notes that are turned on in the original buffer after the scratch buffer? for (const auto& e : scratchBuffer) { if (e.isNoteOn()) { - if (! activeNoteList.isNoteActive (e.getChannel(), e.getNoteNumber())) + if (!originalState.isNoteActive(e.getChannel(), e.getNoteNumber())) { - cleanedBufferToMerge.add (e); - activeNoteList.startNote (e.getChannel(), e.getNoteNumber()); + destBuffer.addMidiMessage(e, midiSourceID); + activeNoteList.startNote(e.getChannel(), e.getNoteNumber()); } } else if (e.isNoteOff()) { - if (activeNoteList.isNoteActive (e.getChannel(), e.getNoteNumber())) + if (originalState.isNoteActive(e.getChannel(), e.getNoteNumber())) { - activeNoteList.clearNote (e.getChannel(), e.getNoteNumber()); - cleanedBufferToMerge.add (e); + destBuffer.addMidiMessage(e, midiSourceID); + activeNoteList.clearNote(e.getChannel(), e.getNoteNumber()); } } else { - cleanedBufferToMerge.add (e); + destBuffer.addMidiMessage(e, midiSourceID); } } - - destBuffer.mergeFrom (cleanedBufferToMerge); } ActiveNoteList getNotesOnAtTime (SequenceBeatPosition time, juce::Range channelNumbers, LiveClipLevel& clipLevel) override @@ -956,11 +957,15 @@ class LoopedMidiEventGenerator : public MidiGenerator LoopedMidiEventGenerator (std::unique_ptr gen, std::shared_ptr anl, EditBeatRange clipRangeToUse, - ClipBeatRange loopTimesToUse) + ClipBeatRange loopTimesToUse, + juce::Range channels, + LiveClipLevel& level) : generator (std::move (gen)), activeNoteList (std::move (anl)), clipRange (clipRangeToUse), - loopTimes (loopTimesToUse) + loopTimes (loopTimesToUse), + channelNumbers (channels), + clipLevel (level) { assert (activeNoteList); } @@ -1025,10 +1030,10 @@ class LoopedMidiEventGenerator : public MidiGenerator if (exhausted() && ! loopTimes.isEmpty()) { setLoopIndex (loopIndex + 1); - generator->setTime (0.0); + generator->setTime (loopTimes.getStart()); } - return exhausted(); + return !exhausted(); } bool exhausted() override @@ -1044,6 +1049,9 @@ class LoopedMidiEventGenerator : public MidiGenerator const ClipBeatRange loopTimes; int loopIndex = 0; + const juce::Range channelNumbers; + LiveClipLevel& clipLevel; + SequenceBeatPosition editBeatPositionToSequenceBeatPosition (EditBeatPosition editBeatPosition) const { const ClipBeatPosition clipPos = editBeatPosition - clipRange.getStart(); @@ -1063,7 +1071,19 @@ class LoopedMidiEventGenerator : public MidiGenerator loopIndex = newLoopIndex; const auto sequenceOffset = clipRange.getStart() + (loopIndex * loopTimes.getLength()); - generator->cacheSequence (sequenceOffset, loopTimes + sequenceOffset); + generator->cacheSequence(sequenceOffset, loopTimes + sequenceOffset); + + // Get notes that would be active at the start of the new loop + ActiveNoteList notesAtLoopStart = generator->getNotesOnAtTime(loopTimes.getStart(), + channelNumbers, + clipLevel); + + activeNoteList->reset(); + + // Track which notes we're preserving vs adding new + notesAtLoopStart.iterate([&](int channel, int note) { + activeNoteList->startNote(channel, note); + }); } }; @@ -1150,14 +1170,18 @@ class GeneratorAndNoteList BeatDuration offsetToUse, const QuantisationType& quantisation_, const GrooveTemplate* groove_, - float grooveStrength_) + float grooveStrength_, + juce::Range channelNumbers_, + LiveClipLevel& clipLevelToUse) : sequences (std::move (sequencesToUse)), editRange (editRangeToUse), loopRange (loopRangeToUse), offset (offsetToUse), quantisation (quantisation_), groove (groove_ != nullptr ? *groove_ : GrooveTemplate()), - grooveStrength (grooveStrength_) + grooveStrength (grooveStrength_), + channelNumbers (channelNumbers_), + clipLevel (clipLevelToUse) { assert (sequences.size() > 0); } @@ -1170,10 +1194,13 @@ class GeneratorAndNoteList return; assert (sequences.size() > 0); - dynamicOffsetBeats = std::move (dynamicOffsetBeatsToUse); - shouldCreateMessagesForTime = clipPropertiesHaveChanged || noteListToUse == nullptr; + dynamicOffsetBeats = !dynamicOffsetBeatsToUse ? std::move (dynamicOffsetBeatsToUse) + : std::make_shared(); activeNoteList = noteListToUse ? std::move (noteListToUse) : std::make_shared(); + + // If the clip properties have changed, we need to recreate messages + shouldCreateMessagesForTime = clipPropertiesHaveChanged || activeNoteList->areAnyNotesActive() == false; const EditBeatRange clipRangeRaw { editRange.getStart().inBeats(), editRange.getEnd().inBeats() }; const ClipBeatRange loopRangeRaw { loopRange.getStart().inBeats(), loopRange.getEnd().inBeats() }; @@ -1183,13 +1210,14 @@ class GeneratorAndNoteList sequencesHash = std::hash>{} (sequences); - if (sequencesHash != lastSequencesHash || clipPropertiesHaveChanged) - shouldSendNoteOffsForNotesNoLongerPlaying = true; + // Force note-offs for all active notes if the sequence has changed + shouldSendNoteOffsForNotesNoLongerPlaying = (sequencesHash != lastSequencesHash) || clipPropertiesHaveChanged; auto cachingGenerator = std::make_unique (std::move (sequences), std::move (quantisation), std::move (groove), grooveStrength); auto loopedGenerator = std::make_unique (std::move (cachingGenerator), - activeNoteList, clipRangeRaw, loopRangeRaw); + activeNoteList, clipRangeRaw, loopRangeRaw, + channelNumbers, clipLevel); generator = std::make_unique (std::move (loopedGenerator), offset.inBeats(), dynamicOffsetBeats); @@ -1262,12 +1290,25 @@ class GeneratorAndNoteList if (! isContiguousWithPreviousBlock || blockStartBeatRelativeToClip <= 0.00001_bd) { - MidiNodeHelpers::createNoteOffs (*activeNoteList, - destBuffer, - midiSourceID, - 0.0, - isPlaying); - shouldCreateMessagesForTime = true; + // Only send note-offs when actually recreating the node - avoid turning off notes unnecessarily + if (! isContiguousWithPreviousBlock && activeNoteList->areAnyNotesActive()) + { + // Get the notes that would be active at this clip intersection + auto notesAtCurrentPosition = generator->getNotesOnAtTime(clipIntersection.getStart().inBeats(), + channelNumbers, + clipLevel); + + // Turn off any notes that don't appear in the current position + activeNoteList->iterate([&](int chan, int note) { + if (!notesAtCurrentPosition.isNoteActive(chan, note)) { + destBuffer.addMidiMessage(juce::MidiMessage::noteOff(chan, note), 0.0, midiSourceID); + activeNoteList->clearNote(chan, note); + } + }); + } + + // Only create messages at clip start, otherwise use the existing note state + shouldCreateMessagesForTime = blockStartBeatRelativeToClip <= 0.00001_bd; } if (shouldCreateMessagesForTime) @@ -1383,6 +1424,9 @@ class GeneratorAndNoteList bool shouldCreateMessagesForTime = false, shouldSendNoteOffsForNotesNoLongerPlaying = false; juce::Array controllerMessagesScratchBuffer; + + const juce::Range channelNumbers; + LiveClipLevel& clipLevel; }; @@ -1422,7 +1466,9 @@ LoopingMidiNode::LoopingMidiNode (std::vector sequenc sequenceOffset, quantisation, groove, - grooveStrength); + grooveStrength, + channelNumbers, + clipLevel); } const std::shared_ptr& LoopingMidiNode::getActiveNoteList() const