@@ -711,9 +711,13 @@ struct EventGenerator : public MidiGenerator
711
711
bool useMPEChannelMode, MPESourceID midiSourceID,
712
712
juce::Array<juce::MidiMessage>& controllerMessagesScratchBuffer) override
713
713
{
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;
715
720
scratchBuffer.clear ();
716
- cleanedBufferToMerge.clear ();
717
721
718
722
MidiHelpers::createMessagesForTime (scratchBuffer,
719
723
sequence, noteOffMap,
@@ -723,32 +727,29 @@ struct EventGenerator : public MidiGenerator
723
727
useMPEChannelMode, midiSourceID,
724
728
controllerMessagesScratchBuffer);
725
729
726
- // This isn't quite right as there could be notes that are turned on in the original buffer after the scratch buffer?
727
730
for (const auto & e : scratchBuffer)
728
731
{
729
732
if (e.isNoteOn ())
730
733
{
731
- if (! activeNoteList .isNoteActive (e.getChannel (), e.getNoteNumber ()))
734
+ if (!originalState .isNoteActive (e.getChannel (), e.getNoteNumber ()))
732
735
{
733
- cleanedBufferToMerge. add (e );
734
- activeNoteList.startNote (e.getChannel (), e.getNoteNumber ());
736
+ destBuffer. addMidiMessage (e, midiSourceID );
737
+ activeNoteList.startNote (e.getChannel (), e.getNoteNumber ());
735
738
}
736
739
}
737
740
else if (e.isNoteOff ())
738
741
{
739
- if (activeNoteList .isNoteActive (e.getChannel (), e.getNoteNumber ()))
742
+ if (originalState .isNoteActive (e.getChannel (), e.getNoteNumber ()))
740
743
{
741
- activeNoteList. clearNote (e. getChannel (), e. getNoteNumber () );
742
- cleanedBufferToMerge. add (e );
744
+ destBuffer. addMidiMessage (e, midiSourceID );
745
+ activeNoteList. clearNote (e. getChannel (), e. getNoteNumber () );
743
746
}
744
747
}
745
748
else
746
749
{
747
- cleanedBufferToMerge. add (e );
750
+ destBuffer. addMidiMessage (e, midiSourceID );
748
751
}
749
752
}
750
-
751
- destBuffer.mergeFrom (cleanedBufferToMerge);
752
753
}
753
754
754
755
ActiveNoteList getNotesOnAtTime (SequenceBeatPosition time, juce::Range<int > channelNumbers, LiveClipLevel& clipLevel) override
@@ -956,11 +957,15 @@ class LoopedMidiEventGenerator : public MidiGenerator
956
957
LoopedMidiEventGenerator (std::unique_ptr<MidiGenerator> gen,
957
958
std::shared_ptr<ActiveNoteList> anl,
958
959
EditBeatRange clipRangeToUse,
959
- ClipBeatRange loopTimesToUse)
960
+ ClipBeatRange loopTimesToUse,
961
+ juce::Range<int > channels,
962
+ LiveClipLevel& level)
960
963
: generator (std::move (gen)),
961
964
activeNoteList (std::move (anl)),
962
965
clipRange (clipRangeToUse),
963
- loopTimes (loopTimesToUse)
966
+ loopTimes (loopTimesToUse),
967
+ channelNumbers (channels),
968
+ clipLevel (level)
964
969
{
965
970
assert (activeNoteList);
966
971
}
@@ -1025,10 +1030,10 @@ class LoopedMidiEventGenerator : public MidiGenerator
1025
1030
if (exhausted () && ! loopTimes.isEmpty ())
1026
1031
{
1027
1032
setLoopIndex (loopIndex + 1 );
1028
- generator->setTime (0.0 );
1033
+ generator->setTime (loopTimes. getStart () );
1029
1034
}
1030
1035
1031
- return exhausted ();
1036
+ return ! exhausted ();
1032
1037
}
1033
1038
1034
1039
bool exhausted () override
@@ -1044,6 +1049,9 @@ class LoopedMidiEventGenerator : public MidiGenerator
1044
1049
const ClipBeatRange loopTimes;
1045
1050
int loopIndex = 0 ;
1046
1051
1052
+ const juce::Range<int > channelNumbers;
1053
+ LiveClipLevel& clipLevel;
1054
+
1047
1055
SequenceBeatPosition editBeatPositionToSequenceBeatPosition (EditBeatPosition editBeatPosition) const
1048
1056
{
1049
1057
const ClipBeatPosition clipPos = editBeatPosition - clipRange.getStart ();
@@ -1063,7 +1071,19 @@ class LoopedMidiEventGenerator : public MidiGenerator
1063
1071
1064
1072
loopIndex = newLoopIndex;
1065
1073
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
+ });
1067
1087
}
1068
1088
};
1069
1089
@@ -1150,14 +1170,18 @@ class GeneratorAndNoteList
1150
1170
BeatDuration offsetToUse,
1151
1171
const QuantisationType& quantisation_,
1152
1172
const GrooveTemplate* groove_,
1153
- float grooveStrength_)
1173
+ float grooveStrength_,
1174
+ juce::Range<int > channelNumbers_,
1175
+ LiveClipLevel& clipLevelToUse)
1154
1176
: sequences (std::move (sequencesToUse)),
1155
1177
editRange (editRangeToUse),
1156
1178
loopRange (loopRangeToUse),
1157
1179
offset (offsetToUse),
1158
1180
quantisation (quantisation_),
1159
1181
groove (groove_ != nullptr ? *groove_ : GrooveTemplate()),
1160
- grooveStrength (grooveStrength_)
1182
+ grooveStrength (grooveStrength_),
1183
+ channelNumbers (channelNumbers_),
1184
+ clipLevel (clipLevelToUse)
1161
1185
{
1162
1186
assert (sequences.size () > 0 );
1163
1187
}
@@ -1170,10 +1194,13 @@ class GeneratorAndNoteList
1170
1194
return ;
1171
1195
1172
1196
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>() ;
1175
1199
activeNoteList = noteListToUse ? std::move (noteListToUse)
1176
1200
: std::make_shared<ActiveNoteList>();
1201
+
1202
+ // If the clip properties have changed, we need to recreate messages
1203
+ shouldCreateMessagesForTime = clipPropertiesHaveChanged || activeNoteList->areAnyNotesActive () == false ;
1177
1204
1178
1205
const EditBeatRange clipRangeRaw { editRange.getStart ().inBeats (), editRange.getEnd ().inBeats () };
1179
1206
const ClipBeatRange loopRangeRaw { loopRange.getStart ().inBeats (), loopRange.getEnd ().inBeats () };
@@ -1183,13 +1210,14 @@ class GeneratorAndNoteList
1183
1210
1184
1211
sequencesHash = std::hash<std::vector<juce::MidiMessageSequence>>{} (sequences);
1185
1212
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 ;
1188
1215
1189
1216
auto cachingGenerator = std::make_unique<CachingMidiEventGenerator> (std::move (sequences),
1190
1217
std::move (quantisation), std::move (groove), grooveStrength);
1191
1218
auto loopedGenerator = std::make_unique<LoopedMidiEventGenerator> (std::move (cachingGenerator),
1192
- activeNoteList, clipRangeRaw, loopRangeRaw);
1219
+ activeNoteList, clipRangeRaw, loopRangeRaw,
1220
+ channelNumbers, clipLevel);
1193
1221
generator = std::make_unique<OffsetMidiEventGenerator> (std::move (loopedGenerator),
1194
1222
offset.inBeats (), dynamicOffsetBeats);
1195
1223
@@ -1262,12 +1290,25 @@ class GeneratorAndNoteList
1262
1290
if (! isContiguousWithPreviousBlock
1263
1291
|| blockStartBeatRelativeToClip <= 0 .00001_bd)
1264
1292
{
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;
1271
1312
}
1272
1313
1273
1314
if (shouldCreateMessagesForTime)
@@ -1383,6 +1424,9 @@ class GeneratorAndNoteList
1383
1424
1384
1425
bool shouldCreateMessagesForTime = false , shouldSendNoteOffsForNotesNoLongerPlaying = false ;
1385
1426
juce::Array<juce::MidiMessage> controllerMessagesScratchBuffer;
1427
+
1428
+ const juce::Range<int > channelNumbers;
1429
+ LiveClipLevel& clipLevel;
1386
1430
};
1387
1431
1388
1432
@@ -1422,7 +1466,9 @@ LoopingMidiNode::LoopingMidiNode (std::vector<juce::MidiMessageSequence> sequenc
1422
1466
sequenceOffset,
1423
1467
quantisation,
1424
1468
groove,
1425
- grooveStrength);
1469
+ grooveStrength,
1470
+ channelNumbers,
1471
+ clipLevel);
1426
1472
}
1427
1473
1428
1474
const std::shared_ptr<ActiveNoteList>& LoopingMidiNode::getActiveNoteList () const
0 commit comments