tracktion-engine 3.0-10-g034fdde4aa5
Tracktion Engine — High level data model for audio applications

« « « Anklang Documentation
Loading...
Searching...
No Matches
tracktion_LoopingMidiNode.cpp
Go to the documentation of this file.
1 /*
2 ,--. ,--. ,--. ,--.
3 ,-' '-.,--.--.,--,--.,---.| |,-.,-' '-.`--' ,---. ,--,--, Copyright 2024
4 '-. .-'| .--' ,-. | .--'| /'-. .-',--.| .-. || \ Tracktion Software
5 | | | | \ '-' \ `--.| \ \ | | | |' '-' '| || | Corporation
6 `---' `--' `--`--'`---'`--'`--' `---' `--' `---' `--''--' www.tracktion.com
7
8 Tracktion Engine uses a GPL/commercial licence - see LICENCE.md for details.
9*/
10
12
13
14namespace tracktion { inline namespace engine
15{
16
17//==============================================================================
18using EditBeatPosition = double;
19using ClipBeatPosition = double;
20using SequenceBeatPosition = double;
21using BlockBeatPosition = double;
22
23using EditBeatDuration = double;
24using ClipBeatDuration = double;
25using BlockBeatDuration = double;
26
27using EditBeatRange = juce::Range<double>;
28using ClipBeatRange = juce::Range<double>;
29using SequenceBeatRange = juce::Range<double>;
30using BlockBeatRange = juce::Range<double>;
31
32namespace chocMidiHelpers
33{
34 //==============================================================================
36 {
37 void addToBuffer (int channel, juce::Array<juce::MidiMessage>& dest) const
38 {
39 if (value)
40 dest.add (juce::MidiMessage::pitchWheel (channel, static_cast<int> (*value)));
41 }
42
43 void update (uint32_t v)
44 {
45 value = v;
46 }
47
48 private:
50 };
51
53 {
54 void addToBuffer (int channel, juce::Array<juce::MidiMessage>& dest) const
55 {
56 std::for_each (std::begin (values), std::end (values),
57 [&, index = 0] (const auto& v) mutable
58 {
59 if (v)
60 dest.add (juce::MidiMessage::controllerEvent (channel, index, *v));
61
62 ++index;
63 });
64 }
65
66 void update (int controller, uint8_t value)
67 {
68 values[controller] = value;
69 }
70
71 private:
72 std::optional<uint8_t> values[128];
73 };
74
76 {
77 void addToBuffer (int channel, double time, juce::Array<juce::MidiMessage>& dest) const
78 {
79 if (! value)
80 return;
81
82 if (bankLSB && bankMSB)
83 {
84 dest.add (juce::MidiMessage::controllerEvent (channel, 0x00, *bankMSB).withTimeStamp (time));
85 dest.add (juce::MidiMessage::controllerEvent (channel, 0x20, *bankLSB).withTimeStamp (time));
86 }
87
88 dest.add (juce::MidiMessage::programChange (channel, *value).withTimeStamp (time));
89 }
90
91 // Returns true if this is a bank number change, and false otherwise.
92 bool trySetBank (uint8_t controller, uint8_t v)
93 {
94 switch (controller)
95 {
96 case 0x00: bankMSB = v; return true;
97 case 0x20: bankLSB = v; return true;
98 }
99
100 return false;
101 }
102
103 void setProgram (uint8_t v)
104 {
105 value = v;
106 }
107
108 private:
109 std::optional<uint8_t> value, bankLSB, bankMSB;
110 };
111
113 {
114 // If the effective parameter number has changed since the last time this function was called,
115 // this will emit the current parameter in full (MSB and LSB).
116 // This should be called before each data message (entry, increment, decrement: 0x06, 0x26, 0x60, 0x61)
117 // to ensure that the data message operates on the correct parameter number.
118 void sendIfNecessary (int channel, double time, juce::Array<juce::MidiMessage>& dest)
119 {
120 const auto newestMsb = newestKind == Kind::rpn ? newestRpnMsb : newestNrpnMsb;
121 const auto newestLsb = newestKind == Kind::rpn ? newestRpnLsb : newestNrpnLsb;
122
123 auto lastSent = std::tie (lastSentKind, lastSentMsb, lastSentLsb);
124 const auto newest = std::tie (newestKind, newestMsb, newestLsb);
125
126 if (lastSent == newest || ! newestMsb || ! newestLsb)
127 return;
128
129 dest.add (juce::MidiMessage::controllerEvent (channel, newestKind == Kind::rpn ? 0x65 : 0x63, *newestMsb).withTimeStamp (time));
130 dest.add (juce::MidiMessage::controllerEvent (channel, newestKind == Kind::rpn ? 0x64 : 0x62, *newestLsb).withTimeStamp (time));
131
132 lastSent = newest;
133 }
134
135 bool trySetProgramNumber (uint8_t controller, uint8_t value)
136 {
137 switch (controller)
138 {
139 case 0x65: newestRpnMsb = value; newestKind = Kind::rpn; return true;
140 case 0x64: newestRpnLsb = value; newestKind = Kind::rpn; return true;
141 case 0x63: newestNrpnMsb = value; newestKind = Kind::nrpn; return true;
142 case 0x62: newestNrpnLsb = value; newestKind = Kind::nrpn; return true;
143 }
144
145 return false;
146 }
147
148 private:
149 enum class Kind { rpn, nrpn };
150
151 std::optional<uint8_t> newestRpnLsb, newestRpnMsb, newestNrpnLsb, newestNrpnMsb;
152 std::optional<uint8_t> lastSentLsb, lastSentMsb;
153 Kind lastSentKind = Kind::rpn, newestKind = Kind::rpn;
154 };
155
156 inline void createControllerUpdatesForTime (const choc::midi::Sequence& sequence,
157 uint8_t channel, double time,
159 {
160 ProgramChange programChange;
161 ControllerValues controllerValues;
162 PitchWheel pitchWheel;
163 ParameterNumberState parameterNumberState;
164
165 for (const auto& event : sequence)
166 {
167 if (! event.message.isShortMessage())
168 continue;
169
170 const auto& mm = event.message;
171
172 if (! (mm.getChannel1to16() == channel && event.timeStamp <= time))
173 continue;
174
175 if (mm.isController())
176 {
177 const auto num = mm.getControllerNumber();
178
179 if (parameterNumberState.trySetProgramNumber (num, mm.getControllerValue()))
180 continue;
181
182 if (programChange.trySetBank (num, mm.getControllerValue()))
183 continue;
184
185 constexpr int passthroughs[] { 0x06, 0x26, 0x60, 0x61 };
186
187 if (std::find (std::begin (passthroughs), std::end (passthroughs), num) != std::end (passthroughs))
188 {
189 parameterNumberState.sendIfNecessary (channel, event.timeStamp, dest);
190 dest.add (toMidiMessage (event));
191 }
192 else
193 {
194 controllerValues.update (num, mm.getControllerValue());
195 }
196 }
197 else if (mm.isProgramChange())
198 {
199 programChange.setProgram (mm.getProgramChangeNumber());
200 }
201 else if (mm.isPitchWheel())
202 {
203 pitchWheel.update (mm.getPitchWheelValue());
204 }
205 }
206
207 pitchWheel.addToBuffer (channel, dest);
208
209 controllerValues.addToBuffer (channel, dest);
210
211 // Also emits bank change messages if necessary.
212 programChange.addToBuffer (channel, time, dest);
213
214 // Set the parameter number to its final state.
215 parameterNumberState.sendIfNecessary (channel, time, dest);
216 }
217}
218
219//==============================================================================
220//==============================================================================
221namespace MidiHelpers
222{
224 inline juce::MidiMessageSequence createLoopSection (juce::MidiMessageSequence sourceSequence,
225 juce::Range<double> loopRange)
226 {
228
229 jassert (! loopRange.isEmpty());
230 sourceSequence.updateMatchedPairs();
231
232 for (int i = 0; i < sourceSequence.getNumEvents(); ++i)
233 {
234 if (auto meh = sourceSequence.getEventPointer (i))
235 {
236 if (meh->noteOffObject != nullptr
237 && meh->message.isNoteOn())
238 {
239 juce::Range<double> noteRange (meh->message.getTimeStamp(),
240 meh->noteOffObject->message.getTimeStamp());
241
242 // Note before loop start
243 if (noteRange.getEnd() < loopRange.getStart())
244 continue;
245
246 // Past the end of the sequence
247 if (noteRange.getStart() >= loopRange.getEnd())
248 break;
249
250 // Crop note start to loop start
251 if (noteRange.getStart() < loopRange.getStart())
252 noteRange = noteRange.withStart (loopRange.getStart());
253
254 // Crop note end to loop end
255 if (noteRange.getEnd() > loopRange.getEnd())
256 noteRange = noteRange.withEnd (loopRange.getEnd());
257
258 // If the size of the note ends up as 0 don't add it.
259 // This can confuse some synths
260 if (noteRange.isEmpty())
261 continue;
262
263 res.addEvent ({ meh->message, noteRange.getStart() });
264 res.addEvent ({ meh->noteOffObject->message, noteRange.getEnd() });
265 }
266 else if (! meh->message.isNoteOff())
267 {
268 if (meh->message.getTimeStamp() >= loopRange.getEnd())
269 break;
270
271 if (meh->message.getTimeStamp() < loopRange.getStart())
272 continue;
273
274 res.addEvent (meh->message);
275 }
276 }
277 }
278
279 res.updateMatchedPairs();
280 res.sort();
281
282 return res;
283 }
284
286 inline std::vector<juce::MidiMessageSequence> createLoopSection (const std::vector<juce::MidiMessageSequence>& sourceSequences,
287 juce::Range<double> loopRange)
288 {
290 jassert (! loopRange.isEmpty());
291
292 for (auto& seq : sourceSequences)
293 res.push_back (createLoopSection (seq, loopRange));
294
295 return res;
296 }
297
298 inline void applyQuantisationToSequence (const QuantisationType& q, juce::MidiMessageSequence& ms, bool canQuantiseNoteOffs)
299 {
300 if (! q.isEnabled())
301 return;
302
303 const bool quantiseNoteOffs = canQuantiseNoteOffs && q.isQuantisingNoteOffs();
304
305 for (int i = ms.getNumEvents(); --i >= 0;)
306 {
307 auto* e = ms.getEventPointer (i);
308 auto& m = e->message;
309
310 if (m.isNoteOn())
311 {
312 const auto noteOnTime = (q.roundBeatToNearest (BeatPosition::fromBeats (m.getTimeStamp()))).inBeats();
313
314 if (auto noteOff = e->noteOffObject)
315 {
316 auto& mOff = noteOff->message;
317
318 if (quantiseNoteOffs)
319 {
320 auto noteOffTime = (q.roundBeatUp (BeatPosition::fromBeats (mOff.getTimeStamp()))).inBeats();
321
322 static constexpr double beatsToBumpUpBy = 1.0 / 512.0;
323
324 if (noteOffTime <= noteOnTime) // Don't want note on and off time the same
325 noteOffTime = q.roundBeatUp (BeatPosition::fromBeats (noteOnTime + beatsToBumpUpBy)).inBeats();
326
327 mOff.setTimeStamp (noteOffTime);
328 }
329 else
330 {
331 // nudge the note-up backwards just a bit to make sure the ordering is correct
332 mOff.setTimeStamp (noteOnTime + (mOff.getTimeStamp() - m.getTimeStamp()) - 0.00001);
333 }
334 }
335
336 m.setTimeStamp (noteOnTime);
337 }
338 else if (m.isNoteOff() && quantiseNoteOffs)
339 {
340 m.setTimeStamp ((q.roundBeatUp (BeatPosition::fromBeats (m.getTimeStamp()))).inBeats());
341 }
342 }
343 }
344
345 inline void applyGrooveToSequence (const GrooveTemplate& groove, float grooveStrength, juce::MidiMessageSequence& ms)
346 {
347 for (auto mh : ms)
348 {
349 auto& m = mh->message;
350
351 if (m.isNoteOn() || m.isNoteOff())
352 m.setTimeStamp (groove.beatsTimeToGroovyTime (BeatPosition::fromBeats (m.getTimeStamp()), grooveStrength).inBeats());
353 }
354 }
355
356 inline choc::midi::Sequence& addSequence (choc::midi::Sequence& dest, const juce::MidiMessageSequence& src, double timeStampOffset)
357 {
358 for (auto meh : src)
359 {
360 dest.events.push_back ({ meh->message.getTimeStamp() + timeStampOffset,
361 { meh->message.getRawData(), (size_t) meh->message.getRawDataSize() } });
362 }
363
364 return dest;
365 }
366
367 inline void createNoteOffMap (std::vector<std::pair<size_t, size_t>>& noteOffMap,
368 const choc::midi::Sequence& seq)
369 {
370 noteOffMap.clear();
371 const auto seqLen = seq.events.size();
372
373 for (size_t i = 0; i < seqLen; ++i)
374 {
375 const auto& m = seq.events[i].message;
376
377 if (! m.isShortMessage())
378 continue;
379
380 if (m.isNoteOn())
381 {
382 const auto note = m.getNoteNumber();
383 const auto chan = m.getChannel0to15();
384
385 for (size_t j = i + 1; j < seqLen; ++j)
386 {
387 const auto& m2 = seq.events[j].message;
388
389 if (! m2.isShortMessage())
390 continue;
391
392 if (m2.getNoteNumber() == note
393 && m2.getChannel0to15() == chan
394 && m2.isNoteOff())
395 {
396 noteOffMap.emplace_back (std::make_pair (i, j));
397 break;
398 }
399 }
400 }
401 }
402 }
403
404 inline choc::midi::Sequence::Event* getNoteOff (size_t noteOnIndex,
405 choc::midi::Sequence& ms,
406 const std::vector<std::pair<size_t, size_t>>& noteOffMap)
407 {
408 auto found = std::find_if (noteOffMap.begin(), noteOffMap.end(),
409 [noteOnIndex] (const auto& m) { return m.first == noteOnIndex; });
410
411 if (found != noteOffMap.end())
412 return &ms.events[found->second];
413
414 return {};
415 }
416
417 inline const choc::midi::Sequence::Event* getNoteOff (size_t noteOnIndex,
418 const choc::midi::Sequence& ms,
419 const std::vector<std::pair<size_t, size_t>>& noteOffMap)
420 {
421 auto found = std::find_if (noteOffMap.begin(), noteOffMap.end(),
422 [noteOnIndex] (const auto& m) { return m.first == noteOnIndex; });
423
424 if (found != noteOffMap.end())
425 return &ms.events[found->second];
426
427 return {};
428 }
429
430 inline std::optional<size_t> getNoteOffIndex (size_t noteOnIndex,
431 const std::vector<std::pair<size_t, size_t>>& noteOffMap)
432 {
433 auto found = std::find_if (noteOffMap.begin(), noteOffMap.end(),
434 [noteOnIndex] (const auto& m) { return m.first == noteOnIndex; });
435
436 if (found != noteOffMap.end())
437 return found->second;
438
439 return {};
440 }
441
442
443 inline void applyQuantisationToSequence (const QuantisationType& q, bool canQuantiseNoteOffs,
444 choc::midi::Sequence& ms, const std::vector<std::pair<size_t, size_t>>& noteOffMap)
445 {
446 if (! q.isEnabled())
447 return;
448
449 const bool quantiseNoteOffs = canQuantiseNoteOffs && q.isQuantisingNoteOffs();
450
451 size_t index = ms.events.size();
452 std::for_each (ms.events.rbegin(), ms.events.rend(),
453 [&] (auto& e)
454 {
455 --index;
456
457 if (! e.message.isShortMessage())
458 return;
459
460 const auto& m = e.message;
461
462 if (m.isNoteOn())
463 {
464 const auto noteOnTime = q.roundBeatToNearest (BeatPosition::fromBeats (e.timeStamp)).inBeats();
465
466 if (auto noteOff = getNoteOff (index, ms, noteOffMap))
467 {
468 if (quantiseNoteOffs)
469 {
470 auto noteOffTime = (q.roundBeatUp (BeatPosition::fromBeats (noteOff->timeStamp))).inBeats();
471
472 static constexpr double beatsToBumpUpBy = 1.0 / 512.0;
473
474 if (noteOffTime <= noteOnTime) // Don't want note on and off time the same
475 noteOffTime = q.roundBeatUp (BeatPosition::fromBeats (noteOnTime + beatsToBumpUpBy)).inBeats();
476
477 noteOff->timeStamp = noteOffTime;
478 }
479 else
480 {
481 // nudge the note-up backwards just a bit to make sure the ordering is correct
482 noteOff->timeStamp = (noteOnTime + (noteOff->timeStamp - e.timeStamp) - 0.00001);
483 }
484 }
485
486 e.timeStamp = noteOnTime;
487 }
488 else if (m.isNoteOff() && quantiseNoteOffs)
489 {
490 e.timeStamp = q.roundBeatUp (BeatPosition::fromBeats (e.timeStamp)).inBeats();
491 }
492 });
493 }
494
495 inline void applyGrooveToSequence (const GrooveTemplate& groove, float grooveStrength, choc::midi::Sequence& ms)
496 {
497 for (auto& e : ms)
498 if (e.message.isNoteOn() || e.message.isNoteOff())
499 e.timeStamp = groove.beatsTimeToGroovyTime (BeatPosition::fromBeats (e.timeStamp), grooveStrength).inBeats();
500 }
501
502 inline void createMessagesForTime (MidiMessageArray& destBuffer,
503 const choc::midi::Sequence& sourceSequence,
504 const std::vector<std::pair<size_t, size_t>>& noteOffMap,
505 double time,
506 juce::Range<int> channelNumbers,
507 LiveClipLevel& clipLevel,
508 bool useMPEChannelMode, MidiMessageArray::MPESourceID midiSourceID,
509 juce::Array<juce::MidiMessage>& controllerMessagesScratchBuffer)
510 {
511 if (useMPEChannelMode)
512 {
513 const auto indexOfTime = [&]() -> size_t
514 {
515 const auto numEvents = sourceSequence.events.size();
516
517 for (size_t i = 0; i < numEvents; ++i)
518 if (sourceSequence.events[i].timeStamp >= time)
519 return i;
520
521 return {};
522 }();
523
524 controllerMessagesScratchBuffer.clearQuick();
525
526 for (int i = channelNumbers.getStart(); i < channelNumbers.getEnd(); ++i)
527 MPEStartTrimmer::reconstructExpression (controllerMessagesScratchBuffer, sourceSequence, indexOfTime, i);
528
529 for (auto& m : controllerMessagesScratchBuffer)
530 destBuffer.addMidiMessage (m, 0.0001, midiSourceID);
531 }
532 else
533 {
534 {
535 controllerMessagesScratchBuffer.clearQuick();
536
537 for (int i = channelNumbers.getStart(); i < channelNumbers.getEnd(); ++i)
538 chocMidiHelpers::createControllerUpdatesForTime (sourceSequence, (uint8_t) i, time, controllerMessagesScratchBuffer);
539
540 for (auto& m : controllerMessagesScratchBuffer)
541 destBuffer.addMidiMessage (m, midiSourceID);
542 }
543
544 if (! clipLevel.isMute())
545 {
546 auto volScale = clipLevel.getGain();
547
548 for (size_t i = 0; i < sourceSequence.events.size(); ++i)
549 {
550 auto e = sourceSequence.events[i];
551 const auto& m = e.message;
552
553 if (! m.isNoteOn())
554 continue;
555
556 if (auto noteOffEvent = getNoteOff (i, sourceSequence, noteOffMap))
557 {
558 if (e.timeStamp >= time)
559 break;
560
561 // don't play very short notes or ones that have already finished
562 if (noteOffEvent->timeStamp > time + 0.0001)
563 {
564 auto data = m.data();
565 juce::MidiMessage m2 ((int) data[0], (int) data[1], (int) data[2], e.timeStamp);
566 m2.multiplyVelocity (volScale);
567
568 // give these a tiny offset to make sure they're played after the controller updates
569 destBuffer.addMidiMessage (m2, 0.0001, midiSourceID);
570 }
571 }
572 }
573 }
574 }
575 }
576
577 inline ActiveNoteList getNotesOnAtTime (const choc::midi::Sequence& sourceSequence,
578 const std::vector<std::pair<size_t, size_t>>& noteOffMap,
579 double time,
580 juce::Range<int> channelNumbers, LiveClipLevel& clipLevel)
581 {
582 ActiveNoteList noteList;
583
584 if (clipLevel.isMute())
585 return {};
586
587 for (size_t i = 0; i < sourceSequence.events.size(); ++i)
588 {
589 const auto& e = sourceSequence.events[i];
590 const auto& m = e.message;
591
592 if (! m.isNoteOn())
593 continue;
594
595 if (! channelNumbers.contains ((int) m.getChannel1to16()))
596 continue;
597
598 if (auto noteOffEvent = getNoteOff (i, sourceSequence, noteOffMap))
599 {
600 if (e.timeStamp >= time)
601 break;
602
603 // don't play very short notes or ones that have already finished
604 if (noteOffEvent->timeStamp > time + 0.0001)
605 noteList.startNote ((int) m.getChannel1to16(), (int) m.getNoteNumber());
606 }
607 }
608
609 return noteList;
610 }
611
612 inline void clipSequenceToRange (choc::midi::Sequence& sequence, const juce::Range<double> clipRange,
614 {
615 if (clipRange.isEmpty())
616 return;
617
618 // Use a special number that won't be in use to signify an event to remove
619 // We have to do it like this to avoid allocating a new sequence
620 constexpr auto timeStampToRemoveFlag = std::numeric_limits<double>::lowest();
621
622 // First adjust all the note times
623 for (auto& m : sequence)
624 if (m.message.isShortMessage())
625 if (auto& sm = m.message; sm.isNoteOn() || sm.isNoteOff())
626 m.timeStamp = clipRange.clipValue (m.timeStamp);
627
628 // Then change the timestamps of an zero or negative length notes
629 for (int i = (int) sequence.events.size(); --i >= 0;)
630 {
631 auto index = static_cast<size_t> (i);
632 const auto& e = sequence.events[index];
633 const auto& m = e.message;
634
635 if (! m.isNoteOn())
636 continue;
637
638 if (auto noteOffIndex = getNoteOffIndex (index, noteOffMap))
639 {
640 if (*noteOffIndex < index)
641 continue;
642
643 const auto noteLength = sequence.events[*noteOffIndex].timeStamp - e.timeStamp;
644
645 if (noteLength > 0.0)
646 continue;
647
648 sequence.events[*noteOffIndex].timeStamp = timeStampToRemoveFlag;
649 sequence.events[index].timeStamp = timeStampToRemoveFlag;
650 }
651 }
652
653 // Finally, erase any events with the flagged timestamp
654 sequence.events.erase (std::remove_if (sequence.events.begin(), sequence.events.end(),
655 [timeStampToRemoveFlag] (const auto& e) { return juce::approximatelyEqual (e.timeStamp, timeStampToRemoveFlag); }),
656 sequence.events.end());
657 }
658}
659
660//==============================================================================
661//==============================================================================
663{
664public:
665 MidiGenerator() = default;
666 virtual ~MidiGenerator() = default;
667
668 //==============================================================================
669 virtual void createMessagesForTime (MidiMessageArray& destBuffer,
670 double time,
672 juce::Range<int> channelNumbers,
674 bool useMPEChannelMode, MidiMessageArray::MPESourceID,
675 juce::Array<juce::MidiMessage>& controllerMessagesScratchBuffer)
676 {
677 juce::ignoreUnused (destBuffer, time, channelNumbers, useMPEChannelMode, controllerMessagesScratchBuffer);
678 }
679
680 virtual ActiveNoteList getNotesOnAtTime (double /*time*/, juce::Range<int> /*channelNumbers*/, LiveClipLevel&)
681 {
682 jassertfalse; // You shouldn't be calling the default implementation of this!
683 return {};
684 }
685
686 //==============================================================================
687 virtual void cacheSequence (double /*offset*/, std::optional<juce::Range<double>> /*clipRange*/) {}
688
689 virtual void setTime (double) = 0;
690 virtual bool advance() = 0;
691
692 virtual bool exhausted() = 0;
693 virtual juce::MidiMessage getEvent() = 0;
694};
695
696
697//==============================================================================
699{
700 EventGenerator (const choc::midi::Sequence& seq,
701 const std::vector<std::pair<size_t, size_t>>& noteOffs)
702 : sequence (seq), noteOffMap (noteOffs)
703 {
704 }
705
706 void createMessagesForTime (MidiMessageArray& destBuffer,
707 SequenceBeatPosition time,
708 ActiveNoteList& activeNoteList,
709 juce::Range<int> channelNumbers,
710 LiveClipLevel& clipLevel,
711 bool useMPEChannelMode, MidiMessageArray::MPESourceID midiSourceID,
712 juce::Array<juce::MidiMessage>& controllerMessagesScratchBuffer) override
713 {
714 thread_local MidiMessageArray scratchBuffer, cleanedBufferToMerge;
715 scratchBuffer.clear();
716 cleanedBufferToMerge.clear();
717
718 MidiHelpers::createMessagesForTime (scratchBuffer,
719 sequence, noteOffMap,
720 time,
721 channelNumbers,
722 clipLevel,
723 useMPEChannelMode, midiSourceID,
724 controllerMessagesScratchBuffer);
725
726 // This isn't quite right as there could be notes that are turned on in the original buffer after the scratch buffer?
727 for (const auto& e : scratchBuffer)
728 {
729 if (e.isNoteOn())
730 {
731 if (! activeNoteList.isNoteActive (e.getChannel(), e.getNoteNumber()))
732 {
733 cleanedBufferToMerge.add (e);
734 activeNoteList.startNote (e.getChannel(), e.getNoteNumber());
735 }
736 }
737 else if (e.isNoteOff())
738 {
739 if (activeNoteList.isNoteActive (e.getChannel(), e.getNoteNumber()))
740 {
741 activeNoteList.clearNote (e.getChannel(), e.getNoteNumber());
742 cleanedBufferToMerge.add (e);
743 }
744 }
745 else
746 {
747 cleanedBufferToMerge.add (e);
748 }
749 }
750
751 destBuffer.mergeFrom (cleanedBufferToMerge);
752 }
753
754 ActiveNoteList getNotesOnAtTime (SequenceBeatPosition time, juce::Range<int> channelNumbers, LiveClipLevel& clipLevel) override
755 {
756 return MidiHelpers::getNotesOnAtTime (sequence, noteOffMap,
757 time,
758 channelNumbers,
759 clipLevel);
760 }
761
762 void setTime (SequenceBeatPosition pos) override
763 {
764 auto numEvents = sequence.events.size();
765
766 // Set the index to the start of the range
767 if (numEvents != 0)
768 {
769 currentIndex = std::clamp<size_t> (currentIndex, 0, numEvents - 1);
770
771 if (sequence.events[currentIndex].timeStamp >= pos)
772 {
773 while (currentIndex > 0 && sequence.events[currentIndex - 1].timeStamp >= pos)
774 --currentIndex;
775 }
776 else
777 {
778 while (currentIndex < numEvents && sequence.events[currentIndex].timeStamp < pos)
779 ++currentIndex;
780 }
781 }
782 }
783
784 juce::MidiMessage getEvent() override
785 {
786 [[ maybe_unused ]] auto numEvents = sequence.events.size();
787 jassert (currentIndex < numEvents);
788
789 return toMidiMessage (sequence.events[currentIndex]);
790 }
791
792 bool advance() override
793 {
794 ++currentIndex;
795 return ! exhausted();
796 }
797
798 bool exhausted() override
799 {
800 return currentIndex >= sequence.events.size();
801 }
802
803 const choc::midi::Sequence& sequence;
804 const std::vector<std::pair<size_t, size_t>>& noteOffMap;
805 size_t currentIndex = 0;
806};
807
808
809//==============================================================================
810//==============================================================================
812{
813public:
816 const GrooveTemplate& grooveTemplate, float grooveStrength_)
817 : sequences (std::move (seq)),
818 quantisation (std::move (qt)),
819 groove (grooveTemplate),
820 grooveStrength (grooveStrength_)
821 {
822 // Cache the sequence at 0.0 time to reserve the required storage
823 cacheSequence (0.0, {});
824
825 // Reserve the scratch space for the note on/off map
826 size_t maxNumEvents = 0, maxNumNoteOns = 0;
827
828 for (auto& sequence : sequences)
829 {
830 size_t squenceNumEvents = 0, squenceNumNoteOns = 0;
831
832 for (auto meh : sequence)
833 {
834 ++squenceNumEvents;
835
836 if (meh->message.isNoteOn())
837 ++squenceNumNoteOns;
838 }
839
840 maxNumEvents = std::max (squenceNumEvents, maxNumEvents);
841 maxNumNoteOns = std::max (squenceNumNoteOns, maxNumNoteOns);
842 }
843
844 noteOffMap.reserve (maxNumNoteOns);
845 currentSequence.events.reserve (maxNumEvents);
846 }
847
848 void createMessagesForTime (MidiMessageArray& destBuffer,
849 EditBeatPosition editBeatPosition,
850 ActiveNoteList& noteList,
851 juce::Range<int> channelNumbers,
852 LiveClipLevel& clipLevel,
853 bool useMPEChannelMode, MidiMessageArray::MPESourceID midiSourceID,
854 juce::Array<juce::MidiMessage>& controllerMessagesScratchBuffer) override
855 {
856 generator.createMessagesForTime (destBuffer,
857 editBeatPosition,
858 noteList,
859 channelNumbers,
860 clipLevel,
861 useMPEChannelMode, midiSourceID,
862 controllerMessagesScratchBuffer);
863 }
864
865 ActiveNoteList getNotesOnAtTime (EditBeatPosition time, juce::Range<int> channelNumbers, LiveClipLevel& clipLevel) override
866 {
867 return MidiHelpers::getNotesOnAtTime (currentSequence, noteOffMap,
868 time,
869 channelNumbers,
870 clipLevel);
871 }
872
873 void setTime (EditBeatPosition editBeatPosition) override
874 {
875 generator.setTime (editBeatPosition);
876 }
877
878 void cacheSequence (double offsetBeats, std::optional<juce::Range<double>> clipRange) override
879 {
880 // Create a new sequence by:
881 // - Iterating the current sequence
882 // - Adding the offset timestamp to get Edit times
883 // - Applying the quantisation
884 // - Applying the groove
885 // - Sorting so events are in order
886 // - Setting the sequence to be iterated
887 // - Updating the offset used
888
889 if (sequences.size() > 0)
890 if (++currentSequenceIndex >= sequences.size())
891 currentSequenceIndex = 0;
892
893 // Create the cached sequence (without allocating)
894 currentSequence.events.clear();
895
896 if (currentSequenceIndex < sequences.size())
897 MidiHelpers::addSequence (currentSequence, sequences[currentSequenceIndex], offsetBeats);
898
899 jassert (std::is_sorted (currentSequence.begin(), currentSequence.end()));
900 MidiHelpers::createNoteOffMap (noteOffMap, currentSequence);
901 MidiHelpers::applyQuantisationToSequence (quantisation, false, currentSequence, noteOffMap);
902
903 if (! groove.isEmpty())
904 MidiHelpers::applyGrooveToSequence (groove, grooveStrength, currentSequence);
905
906 currentSequence.sortEvents();
907
908 if (clipRange)
909 {
910 MidiHelpers::createNoteOffMap (noteOffMap, currentSequence);
911 MidiHelpers::clipSequenceToRange (currentSequence, *clipRange, noteOffMap);
912 }
913
914 MidiHelpers::createNoteOffMap (noteOffMap, currentSequence);
915
916 cachedSequenceOffset = offsetBeats;
917 }
918
919 juce::MidiMessage getEvent() override
920 {
921 auto e = generator.getEvent();
922 e.addToTimeStamp (-cachedSequenceOffset);
923 return e;
924 }
925
926 bool advance() override
927 {
928 return generator.advance();
929 }
930
931 bool exhausted() override
932 {
933 return generator.exhausted();
934 }
935
936private:
938
939 choc::midi::Sequence currentSequence;
941 EventGenerator generator { currentSequence, noteOffMap };
942
943 const QuantisationType quantisation;
944 const GrooveTemplate groove;
945 float grooveStrength = 0.0;
946
947 size_t currentSequenceIndex = 0;
948 double cachedSequenceOffset = 0.0;
949};
950
951//==============================================================================
952//==============================================================================
954{
955public:
958 EditBeatRange clipRangeToUse,
959 ClipBeatRange loopTimesToUse)
960 : generator (std::move (gen)),
961 activeNoteList (std::move (anl)),
962 clipRange (clipRangeToUse),
963 loopTimes (loopTimesToUse)
964 {
965 assert (activeNoteList);
966 }
967
968 void createMessagesForTime (MidiMessageArray& destBuffer,
969 EditBeatPosition editBeatPosition,
970 ActiveNoteList& noteList,
971 juce::Range<int> channelNumbers,
972 LiveClipLevel& clipLevel,
973 bool useMPEChannelMode, MidiMessageArray::MPESourceID midiSourceID,
974 juce::Array<juce::MidiMessage>& controllerMessagesScratchBuffer) override
975 {
976 // Ensure the correct sequence is cached
977 setTime (editBeatPosition);
978
979 generator->createMessagesForTime (destBuffer,
980 editBeatPositionToSequenceBeatPosition (editBeatPosition),
981 noteList,
982 channelNumbers,
983 clipLevel,
984 useMPEChannelMode, midiSourceID,
985 controllerMessagesScratchBuffer);
986 }
987
988 ActiveNoteList getNotesOnAtTime (EditBeatPosition editBeatPosition, juce::Range<int> channelNumbers, LiveClipLevel& clipLevel) override
989 {
990 return generator->getNotesOnAtTime (editBeatPositionToSequenceBeatPosition (editBeatPosition),
991 channelNumbers,
992 clipLevel);
993 }
994
995 void setTime (EditBeatPosition editBeatPosition) override
996 {
997 const ClipBeatPosition clipPos = editBeatPosition - clipRange.getStart();
998
999 if (loopTimes.isEmpty())
1000 {
1001 generator->setTime (clipPos);
1002 }
1003 else
1004 {
1005 const SequenceBeatPosition sequencePos = loopTimes.getStart() + std::fmod (clipPos, loopTimes.getLength());
1006
1007 setLoopIndex (static_cast<int> (clipPos / loopTimes.getLength()));
1008 generator->setTime (sequencePos);
1009 }
1010 }
1011
1012 juce::MidiMessage getEvent() override
1013 {
1014 const auto offsetBeats = clipRange.getStart() - loopTimes.getStart() + (loopIndex * loopTimes.getLength());
1015
1016 auto e = generator->getEvent();
1017 e.addToTimeStamp (offsetBeats);
1018 return e;
1019 }
1020
1021 bool advance() override
1022 {
1023 generator->advance();
1024
1025 if (exhausted() && ! loopTimes.isEmpty())
1026 {
1027 setLoopIndex (loopIndex + 1);
1028 generator->setTime (0.0);
1029 }
1030
1031 return exhausted();
1032 }
1033
1034 bool exhausted() override
1035 {
1036 return generator->exhausted();
1037 }
1038
1039private:
1040 //==============================================================================
1042 std::shared_ptr<ActiveNoteList> activeNoteList;
1043 const EditBeatRange clipRange;
1044 const ClipBeatRange loopTimes;
1045 int loopIndex = 0;
1046
1047 SequenceBeatPosition editBeatPositionToSequenceBeatPosition (EditBeatPosition editBeatPosition) const
1048 {
1049 const ClipBeatPosition clipPos = editBeatPosition - clipRange.getStart();
1050
1051 if (loopTimes.isEmpty())
1052 return clipPos;
1053
1054 const SequenceBeatPosition sequencePos = loopTimes.getStart() + std::fmod (clipPos, loopTimes.getLength());
1055
1056 return sequencePos;
1057 }
1058
1059 void setLoopIndex (int newLoopIndex)
1060 {
1061 if (newLoopIndex == loopIndex)
1062 return;
1063
1064 loopIndex = newLoopIndex;
1065 const auto sequenceOffset = clipRange.getStart() + (loopIndex * loopTimes.getLength());
1066 generator->cacheSequence (sequenceOffset, loopTimes + sequenceOffset);
1067 }
1068};
1069
1070
1071//==============================================================================
1073{
1074public:
1076 ClipBeatDuration offsetToUse,
1077 std::shared_ptr<BeatDuration> dynamicOffsetToUse)
1078 : generator (std::move (gen)),
1079 clipOffset (offsetToUse),
1080 dynamicOffset (std::move (dynamicOffsetToUse))
1081 {
1082 }
1083
1084 void createMessagesForTime (MidiMessageArray& destBuffer,
1085 EditBeatPosition editBeatPosition,
1086 ActiveNoteList& noteList,
1087 juce::Range<int> channelNumbers,
1088 LiveClipLevel& clipLevel,
1089 bool useMPEChannelMode, MidiMessageArray::MPESourceID midiSourceID,
1090 juce::Array<juce::MidiMessage>& controllerMessagesScratchBuffer) override
1091 {
1092 generator->createMessagesForTime (destBuffer,
1093 editBeatPosition + getOffset(),
1094 noteList,
1095 channelNumbers,
1096 clipLevel,
1097 useMPEChannelMode, midiSourceID,
1098 controllerMessagesScratchBuffer);
1099 }
1100
1101 ActiveNoteList getNotesOnAtTime (EditBeatPosition editBeatPosition, juce::Range<int> channelNumbers, LiveClipLevel& clipLevel) override
1102 {
1103 return generator->getNotesOnAtTime (editBeatPosition + getOffset(),
1104 channelNumbers,
1105 clipLevel);
1106 }
1107
1108 void setTime (EditBeatPosition editBeatPosition) override
1109 {
1110 generator->setTime (editBeatPosition + getOffset());
1111 }
1112
1113 juce::MidiMessage getEvent() override
1114 {
1115 auto e = generator->getEvent();
1116 e.addToTimeStamp (-getOffset());
1117 return e;
1118 }
1119
1120 bool advance() override
1121 {
1122 return generator->advance();
1123 }
1124
1125 bool exhausted() override
1126 {
1127 return generator->exhausted();
1128 }
1129
1130private:
1131 //==============================================================================
1133 const ClipBeatDuration clipOffset;
1134 std::shared_ptr<BeatDuration> dynamicOffset;
1135
1136 ClipBeatDuration getOffset() const
1137 {
1138 return clipOffset - dynamicOffset->inBeats();
1139 }
1140};
1141
1142//==============================================================================
1143//==============================================================================
1145{
1146public:
1148 BeatRange editRangeToUse,
1149 BeatRange loopRangeToUse,
1150 BeatDuration offsetToUse,
1151 const QuantisationType& quantisation_,
1152 const GrooveTemplate* groove_,
1153 float grooveStrength_)
1154 : sequences (std::move (sequencesToUse)),
1155 editRange (editRangeToUse),
1156 loopRange (loopRangeToUse),
1157 offset (offsetToUse),
1158 quantisation (quantisation_),
1159 groove (groove_ != nullptr ? *groove_ : GrooveTemplate()),
1160 grooveStrength (grooveStrength_)
1161 {
1162 assert (sequences.size() > 0);
1163 }
1164
1165 void initialise (std::shared_ptr<ActiveNoteList> noteListToUse,
1166 bool clipPropertiesHaveChanged, size_t lastSequencesHash,
1167 std::shared_ptr<BeatDuration> dynamicOffsetBeatsToUse)
1168 {
1169 if (isInitialised())
1170 return;
1171
1172 assert (sequences.size() > 0);
1173 dynamicOffsetBeats = std::move (dynamicOffsetBeatsToUse);
1174 shouldCreateMessagesForTime = clipPropertiesHaveChanged || noteListToUse == nullptr;
1175 activeNoteList = noteListToUse ? std::move (noteListToUse)
1177
1178 const EditBeatRange clipRangeRaw { editRange.getStart().inBeats(), editRange.getEnd().inBeats() };
1179 const ClipBeatRange loopRangeRaw { loopRange.getStart().inBeats(), loopRange.getEnd().inBeats() };
1180
1181 if (! loopRangeRaw.isEmpty())
1182 sequences = MidiHelpers::createLoopSection (std::move (sequences), loopRangeRaw);
1183
1184 sequencesHash = std::hash<std::vector<juce::MidiMessageSequence>>{} (sequences);
1185
1186 if (sequencesHash != lastSequencesHash || clipPropertiesHaveChanged)
1187 shouldSendNoteOffsForNotesNoLongerPlaying = true;
1188
1189 auto cachingGenerator = std::make_unique<CachingMidiEventGenerator> (std::move (sequences),
1190 std::move (quantisation), std::move (groove), grooveStrength);
1191 auto loopedGenerator = std::make_unique<LoopedMidiEventGenerator> (std::move (cachingGenerator),
1192 activeNoteList, clipRangeRaw, loopRangeRaw);
1193 generator = std::make_unique<OffsetMidiEventGenerator> (std::move (loopedGenerator),
1194 offset.inBeats(), dynamicOffsetBeats);
1195
1196 controllerMessagesScratchBuffer.ensureStorageAllocated (32);
1197 initialised = true;
1198 }
1199
1200 const std::shared_ptr<ActiveNoteList>& getActiveNoteList() const
1201 {
1202 return activeNoteList;
1203 }
1204
1205 void processSection (MidiMessageArray& destBuffer, choc::buffer::FrameCount numSamples,
1206 BeatRange sectionEditBeatRange,
1207 TimeRange sectionEditTimeRange,
1208 LiveClipLevel& clipLevel,
1209 juce::Range<int> channelNumbers,
1210 bool useMPEChannelMode,
1211 MidiMessageArray::MPESourceID midiSourceID,
1212 bool isPlaying,
1213 bool isContiguousWithPreviousBlock,
1214 bool lastBlockOfLoop)
1215 {
1216 const auto secondsPerBeat = sectionEditTimeRange.getLength() / sectionEditBeatRange.getLength().inBeats();
1217 const auto blockStartBeatRelativeToClip = sectionEditBeatRange.getStart() - (editRange.getStart() + *dynamicOffsetBeats);
1218
1219 const auto volScale = clipLevel.getGain();
1220 const auto isLastBlockOfClip = sectionEditBeatRange.containsInclusive ((editRange.getEnd() + *dynamicOffsetBeats));
1221 const double beatDurationOfOneSample = sectionEditBeatRange.getLength().inBeats() / numSamples;
1222 const auto timePositionOfLastSample = sectionEditTimeRange.getLength() > 0_td
1223 ? (sectionEditTimeRange.getLength() - sectionEditTimeRange.getLength() / numSamples).inSeconds()
1224 : 0.0;
1225
1226 const auto clipIntersection = sectionEditBeatRange.getIntersectionWith (editRange + *dynamicOffsetBeats);
1227
1228 if (clipIntersection.isEmpty())
1229 {
1230 if (activeNoteList->areAnyNotesActive())
1231 MidiNodeHelpers::createNoteOffs (*activeNoteList,
1232 destBuffer,
1233 midiSourceID,
1234 timePositionOfLastSample,
1235 isPlaying);
1236
1237 return;
1238 }
1239
1240 // This turns notes off that are no longer playing due to a change in the sequence
1241 // It is only called when the sequence changes
1242 if (shouldSendNoteOffsForNotesNoLongerPlaying)
1243 {
1244 // Find the currently playing notes from the sequence
1245 // Send note offs for any active notes, not in that sequence
1246 // This should also catch note numbers being moved
1247 const auto currentlyPlayingNoteList = generator->getNotesOnAtTime (clipIntersection.getStart().inBeats(), channelNumbers, clipLevel);
1248
1249 if (activeNoteList->areAnyNotesActive())
1250 activeNoteList->iterate ([&] (auto chan, auto note)
1251 {
1252 if (currentlyPlayingNoteList.isNoteActive (chan, note))
1253 return;
1254
1255 destBuffer.addMidiMessage (juce::MidiMessage::noteOff (chan, note), 0.0, midiSourceID);
1256 activeNoteList->clearNote (chan, note);
1257 });
1258
1259 shouldSendNoteOffsForNotesNoLongerPlaying = false;
1260 }
1261
1262 if (! isContiguousWithPreviousBlock
1263 || blockStartBeatRelativeToClip <= 0.00001_bd)
1264 {
1265 MidiNodeHelpers::createNoteOffs (*activeNoteList,
1266 destBuffer,
1267 midiSourceID,
1268 0.0,
1269 isPlaying);
1270 shouldCreateMessagesForTime = true;
1271 }
1272
1273 if (shouldCreateMessagesForTime)
1274 {
1275 generator->createMessagesForTime (destBuffer, clipIntersection.getStart().inBeats(),
1276 *activeNoteList,
1277 channelNumbers, clipLevel, useMPEChannelMode, midiSourceID,
1278 controllerMessagesScratchBuffer);
1279 shouldCreateMessagesForTime = false;
1280
1281 // Ensure generator is initialised
1282 generator->setTime (clipIntersection.getStart().inBeats());
1283 }
1284
1285 // Iterate notes in blocks
1286 {
1287 for (;;)
1288 {
1289 if (generator->exhausted())
1290 break;
1291
1292 auto e = generator->getEvent();
1293 const EditBeatPosition editBeatPosition = e.getTimeStamp();
1294
1295 // Ensure we stop at the clip end
1296 if (editBeatPosition >= clipIntersection.getEnd().inBeats())
1297 break;
1298
1299 BlockBeatPosition blockBeatPosition = editBeatPosition - sectionEditBeatRange.getStart().inBeats();
1300
1301 // This time correction is due to rounding errors accumulating and casuing events to be slightly negative in a block
1302 if (blockBeatPosition < -0.000001)
1303 {
1304 generator->advance();
1305 continue;
1306 }
1307
1308 blockBeatPosition = std::max (blockBeatPosition, 0.0);
1309
1310 // Note-offs that are on the end boundry need to be nudged back by 1 sample so they're not lost (which leads to stuck notes)
1311 if (e.isNoteOff() && juce::isWithin (editBeatPosition, clipIntersection.getEnd().inBeats(), beatDurationOfOneSample))
1312 blockBeatPosition = blockBeatPosition - beatDurationOfOneSample;
1313
1314 e.multiplyVelocity (volScale);
1315 const auto eventTimeSeconds = blockBeatPosition * secondsPerBeat.inSeconds();
1316 destBuffer.addMidiMessage (e, eventTimeSeconds, midiSourceID);
1317
1318 // Update note list
1319 if (e.isNoteOn())
1320 activeNoteList->startNote (e.getChannel(), e.getNoteNumber());
1321 else if (e.isNoteOff())
1322 activeNoteList->clearNote (e.getChannel(), e.getNoteNumber());
1323
1324 generator->advance();
1325 }
1326 }
1327
1328 if (lastBlockOfLoop)
1329 MidiNodeHelpers::createNoteOffs (*activeNoteList,
1330 destBuffer,
1331 midiSourceID,
1332 timePositionOfLastSample,
1333 isPlaying);
1334
1335 if (isLastBlockOfClip)
1336 {
1337 const auto endOfClipBeats = (editRange.getEnd() + *dynamicOffsetBeats) - sectionEditBeatRange.getStart();
1338
1339 // If the section ends right at the end of the clip, we need to nudge the note-offs back so they get played in this buffer
1340 auto eventTimeSeconds = (endOfClipBeats.inBeats() - beatDurationOfOneSample) * secondsPerBeat.inSeconds();
1341
1342 MidiNodeHelpers::createNoteOffs (*activeNoteList,
1343 destBuffer,
1344 midiSourceID,
1345 eventTimeSeconds,
1346 isPlaying);
1347 }
1348 }
1349
1350 bool hasSameContentAs (const GeneratorAndNoteList& o) const
1351 {
1352 return editRange.getStart() == o.editRange.getStart()
1353 && loopRange == o.loopRange
1354 && offset == o.offset
1355 && quantisation == o.quantisation
1356 && groove == o.groove
1357 && grooveStrength == o.grooveStrength;
1358 }
1359
1360 size_t getSequencesHash() const
1361 {
1362 return sequencesHash;
1363 }
1364
1365 bool isInitialised() const
1366 {
1367 return initialised;
1368 }
1369
1370private:
1371 std::shared_ptr<ActiveNoteList> activeNoteList;
1373 std::shared_ptr<BeatDuration> dynamicOffsetBeats;
1374
1376 size_t sequencesHash = 0;
1377 const BeatRange editRange, loopRange;
1378 const BeatDuration offset;
1379 QuantisationType quantisation;
1380 GrooveTemplate groove;
1381 float grooveStrength = 0.0f;
1382 bool initialised = false;
1383
1384 bool shouldCreateMessagesForTime = false, shouldSendNoteOffsForNotesNoLongerPlaying = false;
1385 juce::Array<juce::MidiMessage> controllerMessagesScratchBuffer;
1386};
1387
1388
1389//==============================================================================
1390//==============================================================================
1391LoopingMidiNode::LoopingMidiNode (std::vector<juce::MidiMessageSequence> sequences,
1392 juce::Range<int> midiChannelNumbers,
1393 bool useMPE,
1394 BeatRange editTimeRange,
1395 BeatRange sequenceLoopRange,
1396 BeatDuration sequenceOffset,
1397 LiveClipLevel liveClipLevel,
1398 ProcessState& processStateToUse,
1399 EditItemID editItemIDToUse,
1400 const QuantisationType& quantisation,
1401 const GrooveTemplate* groove,
1402 float grooveStrength,
1403 std::function<bool()> shouldBeMuted)
1404 : TracktionEngineNode (processStateToUse),
1405 channelNumbers (midiChannelNumbers),
1406 useMPEChannelMode (useMPE),
1407 editRange (editTimeRange),
1408 clipLevel (liveClipLevel),
1409 editItemID (editItemIDToUse),
1410 shouldBeMutedDelegate (std::move (shouldBeMuted)),
1411 wasMute (liveClipLevel.isMute())
1412{
1413 jassert (! sequences.empty());
1414 // -1 from the channel numbers end here as Range end is exclusive
1415 jassert (channelNumbers.getStart() > 0 && (channelNumbers.getEnd() - 1) <= 16);
1416
1417 // Create this now but don't initialise it until we know if we have to
1418 // steal an old node's ActiveNoteList, this happens in prepareToPlay
1419 generatorAndNoteList = std::make_unique<GeneratorAndNoteList> (std::move (sequences),
1420 editTimeRange,
1421 sequenceLoopRange,
1422 sequenceOffset,
1423 quantisation,
1424 groove,
1425 grooveStrength);
1426}
1427
1429{
1430 return generatorAndNoteList->getActiveNoteList();
1431}
1432
1433//==============================================================================
1435{
1436 if (juce::approximatelyEqual (dynamicOffsetBeats->inBeats(), newOffset.inBeats()))
1437 return;
1438
1439 (*dynamicOffsetBeats) = newOffset;
1440}
1441
1442void LoopingMidiNode::killActiveNotes (MidiMessageArray& dest, double timestampForNoteOffs)
1443{
1444 MidiNodeHelpers::createNoteOffs (*generatorAndNoteList->getActiveNoteList(),
1445 dest,
1446 midiSourceID,
1447 timestampForNoteOffs,
1448 false);
1449}
1450
1451//==============================================================================
1453{
1455 props.hasMidi = true;
1456 props.nodeID = (size_t) editItemID.getRawID();
1457 return props;
1458}
1459
1461{
1462 if (generatorAndNoteList->isInitialised())
1463 {
1464 // We shouldn't be getting initialised twice if the graph to replace is only preset on subsequent times
1465 jassert (info.nodeGraphToReplace == nullptr);
1466 return;
1467 }
1468
1469 std::shared_ptr<ActiveNoteList> activeNoteList;
1470 bool clipPropertiesHaveChanged = false;
1471 size_t lastSequencesHash = 0;
1472
1473 if (auto oldNode = findNodeWithIDIfNonZero<LoopingMidiNode> (info.nodeGraphToReplace, getNodeProperties().nodeID))
1474 {
1475 midiSourceID = oldNode->midiSourceID;
1476 activeNoteList = oldNode->generatorAndNoteList->getActiveNoteList();
1477 clipPropertiesHaveChanged = ! generatorAndNoteList->hasSameContentAs (*oldNode->generatorAndNoteList);
1478 lastSequencesHash = oldNode->generatorAndNoteList->getSequencesHash();
1479 dynamicOffsetBeats = oldNode->dynamicOffsetBeats;
1480 }
1481
1482 generatorAndNoteList->initialise (activeNoteList, clipPropertiesHaveChanged, lastSequencesHash, dynamicOffsetBeats);
1483}
1484
1486{
1487 return true;
1488}
1489
1491{
1492 SCOPED_REALTIME_CHECK
1493 if (shouldBeMutedDelegate && shouldBeMutedDelegate())
1494 return;
1495
1496 const auto isPlaying = getPlayHead().isPlaying();
1497
1498 if (const bool mute = clipLevel.isMute(); mute)
1499 {
1500 if (mute != wasMute)
1501 {
1502 wasMute = mute;
1503 MidiNodeHelpers::createNoteOffs (*generatorAndNoteList->getActiveNoteList(),
1504 pc.buffers.midi,
1505 midiSourceID,
1506 (getEditTimeRange().getLength() - 10us).inSeconds(),
1507 isPlaying);
1508 }
1509
1510 return;
1511 }
1512
1513 generatorAndNoteList->processSection (pc.buffers.midi, pc.numSamples,
1516 clipLevel,
1517 channelNumbers,
1518 useMPEChannelMode,
1519 midiSourceID,
1520 isPlaying,
1521 getPlayHeadState().isContiguousWithPreviousBlock(),
1522 getPlayHeadState().isLastBlockOfLoop());
1523}
1524
1525}} // namespace tracktion { inline namespace engine
assert
T begin(T... args)
void ensureStorageAllocated(int minNumElements)
void clearQuick()
void add(const ElementType &newElement)
MidiEventHolder * addEvent(const MidiMessage &newMessage, double timeAdjustment=0)
void updateMatchedPairs() noexcept
MidiEventHolder * getEventPointer(int index) const noexcept
int getNumEvents() const noexcept
static MidiMessage pitchWheel(int channel, int position) noexcept
static MidiMessage controllerEvent(int channel, int controllerType, int value) noexcept
static MidiMessage noteOff(int channel, int noteNumber, float velocity) noexcept
void addToTimeStamp(double delta) noexcept
static MidiMessage programChange(int channel, int programNumber) noexcept
constexpr ValueType getStart() const noexcept
constexpr bool isEmpty() const noexcept
constexpr ValueType getEnd() const noexcept
ValueType clipValue(const ValueType value) const noexcept
constexpr ValueType getLength() const noexcept
constexpr bool contains(const ValueType position) const noexcept
bool isReadyToProcess() override
Should return true when this node is ready to be processed.
tracktion::graph::NodeProperties getNodeProperties() override
Should return the properties of the node.
const std::shared_ptr< ActiveNoteList > & getActiveNoteList() const
Returns the ActiveNoteList in use for this Node.
void prepareToPlay(const tracktion::graph::PlaybackInitialisationInfo &) override
Called once before playback begins for each node.
void process(ProcessContext &) override
Called when the node is to be processed.
void killActiveNotes(MidiMessageArray &, double timestampForNoteOffs)
Iterates the ActiveNoteList adding note-off events for the active notes and then resets them.
void setDynamicOffsetBeats(BeatDuration) override
Sets an offset to be applied to all times in this node, effectively shifting it forwards or backwards...
Base class for Nodes that provides information about the current process call.
TimeRange getEditTimeRange() const
Returns the edit time range of the current process block.
BeatRange getEditBeatRange() const
Returns the edit beat range of the current process block.
tracktion::graph::PlayHeadState & getPlayHeadState()
Returns the PlayHeadState in use.
tracktion::graph::PlayHead & getPlayHead()
Returns the PlayHead in use.
Struct to describe a single iteration of a process call.
bool isPlaying() const noexcept
Returns true is the play head is currently playing.
T data(T... args)
T empty(T... args)
T end(T... args)
T find(T... args)
T fmod(T... args)
T for_each(T... args)
T is_pointer_v
T is_sorted(T... args)
#define jassert(expression)
#define jassertfalse
T lowest(T... args)
T make_pair(T... args)
typedef double
T max(T... args)
bool isWithin(Type a, Type b, Type tolerance) noexcept
constexpr bool approximatelyEqual(Type a, Type b, Tolerance< Type > tolerance=Tolerance< Type >{} .withAbsolute(std::numeric_limits< Type >::min()) .withRelative(std::numeric_limits< Type >::epsilon()))
void ignoreUnused(Types &&...) noexcept
juce::MidiMessage toMidiMessage(const choc::midi::Sequence::Event &e)
Converts a choc::midi event to a juce::MidiMessage.
T remove_if(T... args)
T reserve(T... args)
T size(T... args)
typedef uint32_t
Represents a duration in beats.
constexpr double inBeats() const
Returns the position as a number of beats.
ID for objects of type EditElement - e.g.
Provides a thread-safe way to share a clip's levels with an audio engine without worrying about the C...
float getGain() const noexcept
Returns the clip's absolute gain.
static void reconstructExpression(juce::Array< juce::MidiMessage > &mpeMessagesToAddAtStart, const juce::MidiMessageSequence &data, int trimIndex, int channel)
Reconstruct note expression for a particular channel.
Holds the state of a process call.
Holds some really basic properties of a node.
Passed into Nodes when they are being initialised, to give them useful contextual information that th...
typedef size_t
T tie(T... args)
time