Skip to content

Commit 4f2e025

Browse files
committed
Fix note retriggering when changing clips during first loop in Clip Launcher
1 parent cd002bb commit 4f2e025

File tree

1 file changed

+77
-31
lines changed

1 file changed

+77
-31
lines changed

modules/tracktion_engine/playback/graph/tracktion_LoopingMidiNode.cpp

+77-31
Original file line numberDiff line numberDiff line change
@@ -711,9 +711,13 @@ struct EventGenerator : public MidiGenerator
711711
bool useMPEChannelMode, MPESourceID midiSourceID,
712712
juce::Array<juce::MidiMessage>& controllerMessagesScratchBuffer) override
713713
{
714-
thread_local MidiMessageArray scratchBuffer, cleanedBufferToMerge;
714+
ActiveNoteList originalState;
715+
activeNoteList.iterate([&](int chan, int note) {
716+
originalState.startNote(chan, note);
717+
});
718+
719+
thread_local MidiMessageArray scratchBuffer;
715720
scratchBuffer.clear();
716-
cleanedBufferToMerge.clear();
717721

718722
MidiHelpers::createMessagesForTime (scratchBuffer,
719723
sequence, noteOffMap,
@@ -723,32 +727,29 @@ struct EventGenerator : public MidiGenerator
723727
useMPEChannelMode, midiSourceID,
724728
controllerMessagesScratchBuffer);
725729

726-
// This isn't quite right as there could be notes that are turned on in the original buffer after the scratch buffer?
727730
for (const auto& e : scratchBuffer)
728731
{
729732
if (e.isNoteOn())
730733
{
731-
if (! activeNoteList.isNoteActive (e.getChannel(), e.getNoteNumber()))
734+
if (!originalState.isNoteActive(e.getChannel(), e.getNoteNumber()))
732735
{
733-
cleanedBufferToMerge.add (e);
734-
activeNoteList.startNote (e.getChannel(), e.getNoteNumber());
736+
destBuffer.addMidiMessage(e, midiSourceID);
737+
activeNoteList.startNote(e.getChannel(), e.getNoteNumber());
735738
}
736739
}
737740
else if (e.isNoteOff())
738741
{
739-
if (activeNoteList.isNoteActive (e.getChannel(), e.getNoteNumber()))
742+
if (originalState.isNoteActive(e.getChannel(), e.getNoteNumber()))
740743
{
741-
activeNoteList.clearNote (e.getChannel(), e.getNoteNumber());
742-
cleanedBufferToMerge.add (e);
744+
destBuffer.addMidiMessage(e, midiSourceID);
745+
activeNoteList.clearNote(e.getChannel(), e.getNoteNumber());
743746
}
744747
}
745748
else
746749
{
747-
cleanedBufferToMerge.add (e);
750+
destBuffer.addMidiMessage(e, midiSourceID);
748751
}
749752
}
750-
751-
destBuffer.mergeFrom (cleanedBufferToMerge);
752753
}
753754

754755
ActiveNoteList getNotesOnAtTime (SequenceBeatPosition time, juce::Range<int> channelNumbers, LiveClipLevel& clipLevel) override
@@ -956,11 +957,15 @@ class LoopedMidiEventGenerator : public MidiGenerator
956957
LoopedMidiEventGenerator (std::unique_ptr<MidiGenerator> gen,
957958
std::shared_ptr<ActiveNoteList> anl,
958959
EditBeatRange clipRangeToUse,
959-
ClipBeatRange loopTimesToUse)
960+
ClipBeatRange loopTimesToUse,
961+
juce::Range<int> channels,
962+
LiveClipLevel& level)
960963
: generator (std::move (gen)),
961964
activeNoteList (std::move (anl)),
962965
clipRange (clipRangeToUse),
963-
loopTimes (loopTimesToUse)
966+
loopTimes (loopTimesToUse),
967+
channelNumbers (channels),
968+
clipLevel (level)
964969
{
965970
assert (activeNoteList);
966971
}
@@ -1025,10 +1030,10 @@ class LoopedMidiEventGenerator : public MidiGenerator
10251030
if (exhausted() && ! loopTimes.isEmpty())
10261031
{
10271032
setLoopIndex (loopIndex + 1);
1028-
generator->setTime (0.0);
1033+
generator->setTime (loopTimes.getStart());
10291034
}
10301035

1031-
return exhausted();
1036+
return !exhausted();
10321037
}
10331038

10341039
bool exhausted() override
@@ -1044,6 +1049,9 @@ class LoopedMidiEventGenerator : public MidiGenerator
10441049
const ClipBeatRange loopTimes;
10451050
int loopIndex = 0;
10461051

1052+
const juce::Range<int> channelNumbers;
1053+
LiveClipLevel& clipLevel;
1054+
10471055
SequenceBeatPosition editBeatPositionToSequenceBeatPosition (EditBeatPosition editBeatPosition) const
10481056
{
10491057
const ClipBeatPosition clipPos = editBeatPosition - clipRange.getStart();
@@ -1063,7 +1071,19 @@ class LoopedMidiEventGenerator : public MidiGenerator
10631071

10641072
loopIndex = newLoopIndex;
10651073
const auto sequenceOffset = clipRange.getStart() + (loopIndex * loopTimes.getLength());
1066-
generator->cacheSequence (sequenceOffset, loopTimes + sequenceOffset);
1074+
generator->cacheSequence(sequenceOffset, loopTimes + sequenceOffset);
1075+
1076+
// Get notes that would be active at the start of the new loop
1077+
ActiveNoteList notesAtLoopStart = generator->getNotesOnAtTime(loopTimes.getStart(),
1078+
channelNumbers,
1079+
clipLevel);
1080+
1081+
activeNoteList->reset();
1082+
1083+
// Track which notes we're preserving vs adding new
1084+
notesAtLoopStart.iterate([&](int channel, int note) {
1085+
activeNoteList->startNote(channel, note);
1086+
});
10671087
}
10681088
};
10691089

@@ -1150,14 +1170,18 @@ class GeneratorAndNoteList
11501170
BeatDuration offsetToUse,
11511171
const QuantisationType& quantisation_,
11521172
const GrooveTemplate* groove_,
1153-
float grooveStrength_)
1173+
float grooveStrength_,
1174+
juce::Range<int> channelNumbers_,
1175+
LiveClipLevel& clipLevelToUse)
11541176
: sequences (std::move (sequencesToUse)),
11551177
editRange (editRangeToUse),
11561178
loopRange (loopRangeToUse),
11571179
offset (offsetToUse),
11581180
quantisation (quantisation_),
11591181
groove (groove_ != nullptr ? *groove_ : GrooveTemplate()),
1160-
grooveStrength (grooveStrength_)
1182+
grooveStrength (grooveStrength_),
1183+
channelNumbers (channelNumbers_),
1184+
clipLevel (clipLevelToUse)
11611185
{
11621186
assert (sequences.size() > 0);
11631187
}
@@ -1170,10 +1194,13 @@ class GeneratorAndNoteList
11701194
return;
11711195

11721196
assert (sequences.size() > 0);
1173-
dynamicOffsetBeats = std::move (dynamicOffsetBeatsToUse);
1174-
shouldCreateMessagesForTime = clipPropertiesHaveChanged || noteListToUse == nullptr;
1197+
dynamicOffsetBeats = !dynamicOffsetBeatsToUse ? std::move (dynamicOffsetBeatsToUse)
1198+
: std::make_shared<BeatDuration>();
11751199
activeNoteList = noteListToUse ? std::move (noteListToUse)
11761200
: std::make_shared<ActiveNoteList>();
1201+
1202+
// If the clip properties have changed, we need to recreate messages
1203+
shouldCreateMessagesForTime = clipPropertiesHaveChanged || activeNoteList->areAnyNotesActive() == false;
11771204

11781205
const EditBeatRange clipRangeRaw { editRange.getStart().inBeats(), editRange.getEnd().inBeats() };
11791206
const ClipBeatRange loopRangeRaw { loopRange.getStart().inBeats(), loopRange.getEnd().inBeats() };
@@ -1183,13 +1210,14 @@ class GeneratorAndNoteList
11831210

11841211
sequencesHash = std::hash<std::vector<juce::MidiMessageSequence>>{} (sequences);
11851212

1186-
if (sequencesHash != lastSequencesHash || clipPropertiesHaveChanged)
1187-
shouldSendNoteOffsForNotesNoLongerPlaying = true;
1213+
// Force note-offs for all active notes if the sequence has changed
1214+
shouldSendNoteOffsForNotesNoLongerPlaying = (sequencesHash != lastSequencesHash) || clipPropertiesHaveChanged;
11881215

11891216
auto cachingGenerator = std::make_unique<CachingMidiEventGenerator> (std::move (sequences),
11901217
std::move (quantisation), std::move (groove), grooveStrength);
11911218
auto loopedGenerator = std::make_unique<LoopedMidiEventGenerator> (std::move (cachingGenerator),
1192-
activeNoteList, clipRangeRaw, loopRangeRaw);
1219+
activeNoteList, clipRangeRaw, loopRangeRaw,
1220+
channelNumbers, clipLevel);
11931221
generator = std::make_unique<OffsetMidiEventGenerator> (std::move (loopedGenerator),
11941222
offset.inBeats(), dynamicOffsetBeats);
11951223

@@ -1262,12 +1290,25 @@ class GeneratorAndNoteList
12621290
if (! isContiguousWithPreviousBlock
12631291
|| blockStartBeatRelativeToClip <= 0.00001_bd)
12641292
{
1265-
MidiNodeHelpers::createNoteOffs (*activeNoteList,
1266-
destBuffer,
1267-
midiSourceID,
1268-
0.0,
1269-
isPlaying);
1270-
shouldCreateMessagesForTime = true;
1293+
// Only send note-offs when actually recreating the node - avoid turning off notes unnecessarily
1294+
if (! isContiguousWithPreviousBlock && activeNoteList->areAnyNotesActive())
1295+
{
1296+
// Get the notes that would be active at this clip intersection
1297+
auto notesAtCurrentPosition = generator->getNotesOnAtTime(clipIntersection.getStart().inBeats(),
1298+
channelNumbers,
1299+
clipLevel);
1300+
1301+
// Turn off any notes that don't appear in the current position
1302+
activeNoteList->iterate([&](int chan, int note) {
1303+
if (!notesAtCurrentPosition.isNoteActive(chan, note)) {
1304+
destBuffer.addMidiMessage(juce::MidiMessage::noteOff(chan, note), 0.0, midiSourceID);
1305+
activeNoteList->clearNote(chan, note);
1306+
}
1307+
});
1308+
}
1309+
1310+
// Only create messages at clip start, otherwise use the existing note state
1311+
shouldCreateMessagesForTime = blockStartBeatRelativeToClip <= 0.00001_bd;
12711312
}
12721313

12731314
if (shouldCreateMessagesForTime)
@@ -1383,6 +1424,9 @@ class GeneratorAndNoteList
13831424

13841425
bool shouldCreateMessagesForTime = false, shouldSendNoteOffsForNotesNoLongerPlaying = false;
13851426
juce::Array<juce::MidiMessage> controllerMessagesScratchBuffer;
1427+
1428+
const juce::Range<int> channelNumbers;
1429+
LiveClipLevel& clipLevel;
13861430
};
13871431

13881432

@@ -1422,7 +1466,9 @@ LoopingMidiNode::LoopingMidiNode (std::vector<juce::MidiMessageSequence> sequenc
14221466
sequenceOffset,
14231467
quantisation,
14241468
groove,
1425-
grooveStrength);
1469+
grooveStrength,
1470+
channelNumbers,
1471+
clipLevel);
14261472
}
14271473

14281474
const std::shared_ptr<ActiveNoteList>& LoopingMidiNode::getActiveNoteList() const

0 commit comments

Comments
 (0)