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

« « « Anklang Documentation
Loading...
Searching...
No Matches
tracktion_MidiClip.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
11namespace tracktion { inline namespace engine
12{
13
14static std::unique_ptr<MidiList> createLoopRangeDefinesAllRepetitionsSequence (MidiClip& clip, MidiList& sourceSequence)
15{
16 const auto loopStartBeats = clip.getLoopStartBeats();
17 const auto loopLengthBeats = clip.getLoopLengthBeats();
18
19 const auto extraLoops = juce::roundToInt (std::ceil (clip.getOffsetInBeats() / loopLengthBeats));
20 const auto loopTimes = juce::roundToInt (std::ceil (clip.getLengthInBeats() / loopLengthBeats)) + extraLoops;
21
22 const auto& notes = sourceSequence.getNotes();
23 const auto& sysex = sourceSequence.getSysexEvents();
24 const auto& controllers = sourceSequence.getControllerEvents();
25
26 auto v = MidiList::createMidiList();
27
28 for (int i = 0; i < loopTimes; ++i)
29 {
30 const auto loopPos = loopLengthBeats * i;
31 const auto nextLoopPos = loopLengthBeats * (i + 1);
32
33 // add the midi notes
34 for (auto note : notes)
35 {
36 auto start = (note->getStartBeat() - loopStartBeats) + loopPos;
37 auto length = note->getLengthBeats();
38
39 if (start < loopPos)
40 {
41 length = length - (loopPos - start);
42 start = start + (loopPos - start);
43 }
44
45 if (start + length > nextLoopPos)
46 length = length - ((start + length) - nextLoopPos);
47
48 if (start >= loopPos && start < nextLoopPos && length > BeatDuration())
49 v.addChild (MidiNote::createNote (*note, toPosition (start), length), -1, nullptr);
50 }
51
52 // add the sysex
53 for (auto oldEvent : sysex)
54 {
55 const auto start = (oldEvent->getBeatPosition() - loopStartBeats) + loopPos;
56
57 if (start >= loopPos && start < nextLoopPos)
58 v.addChild (MidiSysexEvent::createSysexEvent (*oldEvent, toPosition (start)), -1, nullptr);
59 }
60
61 // add the controller
62 for (auto oldEvent : controllers)
63 {
64 const auto start = (oldEvent->getBeatPosition() - loopStartBeats) + loopPos;
65
66 if (start >= loopPos && start < nextLoopPos)
67 v.addChild (MidiControllerEvent::createControllerEvent (*oldEvent, toPosition (start)), -1, nullptr);
68 }
69 }
70
71 std::unique_ptr<MidiList> destSequence (new MidiList (v, nullptr));
72 destSequence->setMidiChannel (sourceSequence.getMidiChannel());
73
74 return destSequence;
75}
76
77static std::unique_ptr<MidiList> createLoopRangeDefinesSubsequentRepetitionsSequence (MidiClip& clip, MidiList& sourceSequence)
78{
79 // TODO: what's the point of this function?
80 return createLoopRangeDefinesAllRepetitionsSequence (clip, sourceSequence);
81}
82
83//==============================================================================
84MidiClip::MidiClip (const juce::ValueTree& v, EditItemID id, ClipOwner& targetParent)
85 : Clip (v, targetParent, id, Type::midi)
86{
87 auto um = getUndoManager();
88
89 quantisation = std::make_unique<QuantisationType> (state.getOrCreateChildWithName (IDs::QUANTISATION, um), um);
90
91 if (state.hasProperty (IDs::quantisation))
92 quantisation->setType (state.getProperty (IDs::quantisation));
93
94 level->dbGain.referTo (state, IDs::volDb, um, 0.0f);
95 level->mute.referTo (state, IDs::mute, um, false);
96 sendPatch.referTo (state, IDs::sendProgramChange, um, true);
97 sendBankChange.referTo (state, IDs::sendBankChange, um, true);
98 mpeMode.referTo (state, IDs::mpeMode, um, false);
99 grooveStrength.referTo (state, IDs::grooveStrength, um, 0.1f);
100
101 loopStartBeats.referTo (state, IDs::loopStartBeats, um, BeatPosition());
102 loopLengthBeats.referTo (state, IDs::loopLengthBeats, um, BeatDuration());
103 originalLength.referTo (state, IDs::originalLength, um, BeatDuration());
104
105 useClipLaunchQuantisation.referTo (state, IDs::useClipLaunchQuantisation, um);
106
107 proxyAllowed.referTo (state, IDs::proxyAllowed, um, true);
108 currentTake.referTo (state, IDs::currentTake, um);
109
110 auto grooveTree = state.getOrCreateChildWithName (IDs::GROOVE, um);
111 grooveTemplate.referTo (grooveTree, IDs::current, um, {});
112
113 const int loopType = edit.engine.getEngineBehaviour().getDefaultLoopedSequenceType();
114 loopedSequenceType.referTo (state, IDs::loopedSequenceType, um,
115 static_cast<LoopedSequenceType> (juce::jlimit (0, 1, loopType)));
116 jassert (loopType == int (LoopedSequenceType::loopRangeDefinesAllRepetitions)
117 || loopType == int (LoopedSequenceType::loopRangeDefinesSubsequentRepetitions));
118
119 auto pgen = state.getChildWithName (IDs::PATTERNGENERATOR);
120
121 if (pgen.isValid())
122 patternGenerator = std::make_unique<PatternGenerator> (*this, pgen);
123}
124
125MidiClip::~MidiClip()
126{
127 notifyListenersOfDeletion();
128}
129
130//==============================================================================
131void MidiClip::initialise()
132{
133 Clip::initialise();
134
135 auto um = getUndoManager();
136 auto takesTree = state.getChildWithName (IDs::TAKES);
137
138 if (takesTree.isValid() && takesTree.getNumChildren() > 0)
139 {
140 channelSequence.clearQuick (true);
141
142 for (int i = 0; i < takesTree.getNumChildren(); ++i)
143 {
144 auto sequence = takesTree.getChild (i);
145
146 if (! sequence.isValid())
147 continue;
148
149 channelSequence.add (new MidiList (sequence, um));
150 }
151
152 if (state.getChildWithName (IDs::COMPS).isValid())
153 callBlocking ([this] { getCompManager(); });
154 }
155 else
156 {
157 auto list = state.getChildWithName (IDs::SEQUENCE);
158
159 if (list.isValid())
160 channelSequence.add (new MidiList (list, um));
161 else
162 state.addChild (MidiList::createMidiList(), -1, um);
163
164 currentTake = 0;
165 }
166
167 if (grooveTemplate.get().isNotEmpty())
168 {
169 auto g = edit.engine.getGrooveTemplateManager().getTemplateByName (grooveTemplate);
170
171 auto grooveTree = state.getChildWithName (IDs::GROOVE);
172
173 if (g == nullptr && grooveTree.getNumChildren() > 0)
174 {
175 auto grooveXml = grooveTree.getChild (0).createXml();
176
177 GrooveTemplate gt (grooveXml.get());
178
179 if (! gt.isEmpty())
180 edit.engine.getGrooveTemplateManager().updateTemplate (-1, gt);
181 }
182 }
183
184 speedRatio = 1.0; // not used in MIDI clips
185}
186
187void MidiClip::rescale (TimePosition pivotTimeInEdit, double factor)
188{
189 getSequence().rescale (factor, getUndoManager());
190 setLoopRangeBeats ({ loopStartBeats * factor, (loopStartBeats + loopLengthBeats) * factor });
191 Clip::rescale (pivotTimeInEdit, factor);
192}
193
194void MidiClip::cloneFrom (Clip* c)
195{
196 if (auto other = dynamic_cast<MidiClip*> (c))
197 {
198 Clip::cloneFrom (other);
199
200 level->dbGain .setValue (other->level->dbGain, nullptr);
201 level->mute .setValue (other->level->mute, nullptr);
202 currentTake .setValue (other->currentTake, nullptr);
203 mpeMode .setValue (other->mpeMode, nullptr);
204 originalLength .setValue (other->originalLength, nullptr);
205 sendPatch .setValue (other->sendPatch, nullptr);
206 sendBankChange .setValue (other->sendBankChange, nullptr);
207 grooveTemplate .setValue (other->grooveTemplate, nullptr);
208 grooveStrength .setValue (other->grooveStrength, nullptr);
209
210 quantisation->setType (other->quantisation->getType (false));
211 quantisation->setProportion (other->quantisation->getProportion());
212
213 juce::UndoManager* um = nullptr;
214
215 for (int i = 0; i < other->channelSequence.size(); i++)
216 {
217 MidiList* cs = i < channelSequence.size() ? channelSequence[i]
218 : channelSequence.add (new MidiList());
219 cs->copyFrom (*other->channelSequence[i], um);
220 }
221
222 while (channelSequence.size() > other->channelSequence.size())
223 channelSequence.removeLast();
224 }
225}
226
227//==============================================================================
228juce::String MidiClip::getSelectableDescription()
229{
230 return TRANS("MIDI Clip") + " - \"" + getName() + "\"";
231}
232
233juce::Colour MidiClip::getDefaultColour() const
234{
235 return juce::Colours::red.withHue (3.0f / 9.0f);
236}
237
238bool MidiClip::usesGrooveStrength() const
239{
240 auto gt = edit.engine.getGrooveTemplateManager().getTemplateByName (getGrooveTemplate());
241
242 if (gt != nullptr && gt->isEmpty())
243 gt = nullptr;
244
245 if (gt != nullptr)
246 return gt->isParameterized();
247
248 return false;
249}
250
251MidiList& MidiClip::getSequence() const noexcept
252{
253 if (! hasValidSequence())
254 {
255 jassertfalse; // This shouldn't get hit, let Dave know if it does
256 static MidiList dummyList;
257 return dummyList;
258 }
259
260 jassert (channelSequence.size() > 0);
261 return *channelSequence.getUnchecked (currentTake);
262}
263
264MidiList& MidiClip::getSequenceLooped()
265{
267 TRACKTION_ASSERT_MESSAGE_THREAD
268
269 if (! isLooping())
270 return getSequence();
271
272 if (cachedLoopedSequence == nullptr)
273 cachedLoopedSequence = createSequenceLooped (getSequence());
274
275 return *cachedLoopedSequence;
276}
277
278std::unique_ptr<MidiList> MidiClip::createSequenceLooped (MidiList& sourceSequence)
279{
280 switch (loopedSequenceType)
281 {
282 case LoopedSequenceType::loopRangeDefinesSubsequentRepetitions:
283 return createLoopRangeDefinesSubsequentRepetitionsSequence (*this, sourceSequence);
284
285 case LoopedSequenceType::loopRangeDefinesAllRepetitions:
286 default:
287 return createLoopRangeDefinesAllRepetitionsSequence (*this, sourceSequence);
288 }
289}
290
291MidiClip::ScopedEventsList::ScopedEventsList (MidiClip& c, SelectedMidiEvents* e)
292 : clip (c)
293{
294 clip.setSelectedEvents (e);
295}
296
297MidiClip::ScopedEventsList::~ScopedEventsList()
298{
299 clip.setSelectedEvents (nullptr);
300}
301
302//==============================================================================
304{
305 auto offsetNeededInBeats = edit.tempoSequence.toBeats (getPosition().getStart())
306 - edit.tempoSequence.toBeats (newStartTime);
307
308 setStart (newStartTime, false, false);
309
310 if (offsetNeededInBeats > BeatDuration())
311 getSequence().moveAllBeatPositions (offsetNeededInBeats, getUndoManager());
312}
313
314void MidiClip::trimBeyondEnds (bool beyondStart, bool beyondEnd, juce::UndoManager* um)
315{
316 if (beyondStart)
317 {
318 auto& sequence = getSequence();
319 auto startBeats = getContentBeatAtTime (getPosition().getStart());
320 sequence.trimOutside (startBeats, BeatPosition::fromBeats (Edit::maximumLength), um);
321 sequence.moveAllBeatPositions (-toDuration (getContentBeatAtTime (getPosition().getStart())), um);
322 setOffset ({});
323 }
324
325 if (beyondEnd)
326 {
327 auto endBeats = getContentBeatAtTime (getPosition().getEnd());
328 getSequence().trimOutside ({}, endBeats, um);
329 }
330}
331
333 const BeatPosition maxEndBeat, juce::UndoManager& um)
334{
335 // this looks rather convoluted but must account for various edge cases in order to behave in a similar way to Live
336 auto noteStartBeat = note.getQuantisedStartBeat (*this);
337 auto nextNoteStartBeat = noteStartBeat;
338
339 auto nextNote = notesToUse[notesToUse.indexOf (&note) + 1];
340
341 while (nextNote != nullptr)
342 {
343 nextNoteStartBeat = nextNote->getQuantisedStartBeat (*this);
344
345 if (nextNoteStartBeat != noteStartBeat)
346 break;
347
348 nextNote = notesToUse[notesToUse.indexOf (nextNote) + 1];
349 }
350
351 auto& sequence = getSequence();
352
353 if (nextNote == nullptr)
354 {
355 if (auto lastInSequence = sequence.getNotes().getLast())
356 {
357 if (*lastInSequence == note)
358 {
359 const auto separation = maxEndBeat - noteStartBeat;
360 note.setStartAndLength (note.getStartBeat(), separation, &um);
361 return;
362 }
363 }
364 }
365
366 const auto diff = nextNoteStartBeat - noteStartBeat;
367
368 if (diff > BeatDuration())
369 note.setStartAndLength (note.getStartBeat(), diff, &um);
370}
371
372//==============================================================================
373MidiList* MidiClip::getMidiListForState (const juce::ValueTree& v)
374{
375 for (int i = channelSequence.size(); --i >= 0;)
376 if (auto ml = channelSequence.getUnchecked (i))
377 if (ml->state == v)
378 return ml;
379
380 return {};
381}
382
383//==============================================================================
384void MidiClip::setQuantisation (const QuantisationType& newType)
385{
386 if (newType.getType (false) != quantisation->getType (false))
387 *quantisation = newType;
388}
389
390//==============================================================================
392{
393 if (! launchHandle)
394 launchHandle = std::make_shared<LaunchHandle>();
395
396 return launchHandle;
397}
398
400{
401 if (! launchQuantisation)
402 launchQuantisation = std::make_unique<LaunchQuantisation> (state, edit);
403
404 return launchQuantisation.get();
405}
406
408{
409 if (! followActions)
411
412 return followActions.get();
413}
414
415//==============================================================================
416MidiCompManager& MidiClip::getCompManager()
417{
419
420 if (midiCompManager == nullptr)
421 {
422 auto ptr = edit.engine.getCompFactory().getCompManager (*this);
423 midiCompManager = dynamic_cast<MidiCompManager*> (ptr.get());
424 jassert (midiCompManager != nullptr);
425 midiCompManager->initialise();
426 }
427
428 return *midiCompManager;
429}
430
431void MidiClip::scaleVerticallyToFit()
432{
433 int maxNote = 0;
434 int minNote = 256;
435
436 for (auto n : getSequence().getNotes())
437 {
438 maxNote = std::max (maxNote, n->getNoteNumber() + 3);
439 minNote = std::min (minNote, n->getNoteNumber() - 3);
440 }
441
442 if (minNote < maxNote)
443 {
444 const double newVisProp = (maxNote - minNote) / 128.0;
445
446 if (newVisProp < getAudioTrack()->getMidiVisibleProportion())
447 getAudioTrack()->setMidiVerticalPos (newVisProp, 1.0 - (maxNote / 128.0));
448 }
449}
450
451void MidiClip::addTake (juce::MidiMessageSequence& ms,
452 MidiList::NoteAutomationType automationType)
453{
454 auto um = getUndoManager();
455 auto takesTree = state.getChildWithName (IDs::TAKES);
456
457 if (! takesTree.isValid())
458 {
459 takesTree = state.getOrCreateChildWithName (IDs::TAKES, um);
460 auto seq = state.getChildWithName (IDs::SEQUENCE);
461
462 if (seq.isValid())
463 {
464 seq.getParent().removeChild (seq, um);
465 takesTree.addChild (seq, -1, um);
466 }
467 }
468
469 auto seq = MidiList::createMidiList();
470 takesTree.addChild (seq, -1, um);
471
472 if (auto take = getMidiListForState (seq))
473 {
474 auto chan = getSequence().getMidiChannel();
475 currentTake = channelSequence.size() - 1;
476 take->setMidiChannel (chan);
477
478 if (automationType == MidiList::NoteAutomationType::none)
479 {
480 for (int i = ms.getNumEvents(); --i >= 0;)
481 ms.getEventPointer (i)->message.setChannel (chan.getChannelNumber());
482
483 take->importMidiSequence (ms, &edit, getPosition().getStartOfSource(), getUndoManager());
484 }
485 else if (automationType == MidiList::NoteAutomationType::expression)
486 {
487 take->importFromEditTimeSequenceWithNoteExpression (ms, &edit, getPosition().getStartOfSource(), getUndoManager());
488 }
489
490 changed();
491 }
492 else
493 {
495 }
496}
497
499{
500 if (auto current = channelSequence[currentTake])
501 {
502 auto um = getUndoManager();
503 auto take = current->state;
504 auto takes = state.getChildWithName (IDs::TAKES);
505
506 for (int i = takes.getNumChildren(); --i >= 0;)
507 {
508 auto t = takes.getChild (i);
509 t.getParent().removeChild (t, um);
510 }
511
512 auto exisiting = state.getChildWithName (IDs::SEQUENCE);
513 jassert (! exisiting.isValid());
514
515 if (exisiting.isValid())
516 state.removeChild (exisiting, um);
517
518 state.addChild (take, -1, um);
519
520 if (auto newList = channelSequence.getFirst())
521 {
522 jassert (newList->state == take);
523 newList->setCompList (false);
524 channelSequence.minimiseStorageOverheads();
525 }
526
527 currentTake = 0;
528 midiCompManager = nullptr;
529
530 state.removeChild (state.getChildWithName (IDs::TAKES), um);
531 state.removeChild (state.getChildWithName (IDs::COMPS), um);
532
534 }
535}
536
537int MidiClip::getNumTakes (bool includeComps)
538{
539 if (includeComps)
540 return channelSequence.size();
541
542 return hasAnyTakes() ? getCompManager().getNumTakes() : 0;
543}
544
546{
548 int numTakes = 0;
549
550 if (midiCompManager != nullptr)
551 {
552 for (int i = 0; i < channelSequence.size(); ++i)
553 {
554 if (! midiCompManager->isTakeComp (i))
555 {
556 s.add (juce::String (i + 1) + ". " + TRANS("Take") + " #" + juce::String (i + 1));
557 ++numTakes;
558 }
559 else
560 {
561 s.add (juce::String (i + 1) + ". " + TRANS("Comp") + " #" + juce::String (i + 1 - numTakes));
562 }
563 }
564 }
565 else
566 {
567 for (int i = 0; i < channelSequence.size(); ++i)
568 s.add ("Take #" + juce::String (i + 1));
569 }
570
571 return s;
572}
573
574void MidiClip::setCurrentTake (int takeIndex)
575{
576 if (currentTake != takeIndex && juce::isPositiveAndBelow (takeIndex, channelSequence.size()))
577 currentTake = takeIndex;
578}
579
581{
582 if (hasAnyTakes())
583 return getCompManager().isCurrentTakeComp();
584
585 return false;
586}
587
588void MidiClip::deleteAllUnusedTakesConfirmingWithUser()
589{
590 if (edit.engine.getUIBehaviour()
591 .showOkCancelAlertBox (TRANS("Delete Unused Takes"),
592 TRANS("This will permanently delete all unused MIDI takes in this clip.")
593 + "\n\n"
594 + TRANS("Are you sure you want to do this?"),
595 TRANS("Delete")))
596 clearTakes();
597}
598
600{
602 Clip::Array newClips;
603
604 if (Track::Ptr t = getTrack())
605 {
606 const bool shouldBeShowingTakes = isShowingTakes();
607
608 if (shouldBeShowingTakes)
609 Clip::setShowingTakes (false);
610
611 auto clipNode = state.createCopy();
612
613 if (! clipNode.isValid())
614 return newClips;
615
616 clipNode.removeChild (clipNode.getChildWithName (IDs::TAKES), nullptr);
617 clipNode.removeChild (clipNode.getChildWithName (IDs::COMPS), nullptr);
618
619 int trackIndex = t->getIndexInEditTrackList();
620 auto allTracks = getAllTracks (t->edit);
621
622 for (int i = 0; i < channelSequence.size(); ++i)
623 {
624 auto srcList = channelSequence[i];
625
626 if (srcList == nullptr || getCompManager().isTakeComp (i))
627 continue;
628
629 AudioTrack::Ptr targetTrack (dynamic_cast<AudioTrack*> (allTracks[toNewTracks ? ++trackIndex : trackIndex]));
630
631 if (toNewTracks || targetTrack == nullptr)
632 targetTrack = t->edit.insertNewAudioTrack (TrackInsertPoint (t->getParentTrack(), t.get()), nullptr);
633
634 if (targetTrack != nullptr)
635 {
636 if (auto mc = targetTrack->insertMIDIClip (TRANS("Take") + " #" + juce::String (i + 1),
637 getEditTimeRange(), nullptr))
638 {
639 mc->getSequence().copyFrom (*srcList, nullptr);
640 newClips.add (mc.get());
641 }
642 else
643 {
645 }
646 }
647
648 t = targetTrack;
649 }
650
651 if (shouldBeShowingTakes)
653 }
654
655 return newClips;
656}
657
658//==============================================================================
659void MidiClip::mergeInMidiSequence (juce::MidiMessageSequence& ms,
660 MidiList::NoteAutomationType automationType)
661{
662 auto& take = getSequence();
663
664 if (automationType == MidiList::NoteAutomationType::none)
665 {
666 auto chan = take.getMidiChannel();
667
668 for (int i = ms.getNumEvents(); --i >= 0;)
669 ms.getEventPointer (i)->message.setChannel (chan.getChannelNumber());
670
671 take.importMidiSequence (ms, &edit, getPosition().getStartOfSource(), getUndoManager());
672 }
673 else if (automationType == MidiList::NoteAutomationType::expression)
674 {
675 take.importFromEditTimeSequenceWithNoteExpression (ms, &edit, getPosition().getStartOfSource(), getUndoManager());
676 }
677
678 changed();
679}
680
681//==============================================================================
683{
684 return canContainMIDI (co);
685}
686
687AudioTrack* MidiClip::getAudioTrack() const
688{
689 return dynamic_cast<AudioTrack*> (getTrack());
690}
691
692//==============================================================================
694{
695 if (! isLooping())
696 originalLength = getLengthInBeats();
697
698 auto& ts = edit.tempoSequence;
699 auto pos = getPosition();
700 auto newStartBeat = BeatPosition::fromBeats (pos.getOffset().inSeconds() * ts.getBeatsPerSecondAt (pos.getStart()));
701 setLoopRangeBeats ({ newStartBeat, newStartBeat + originalLength.get() });
702
703 auto endTime = ts.toTime (getStartBeat() + originalLength.get() * num);
704 setLength (endTime - pos.getStart(), true);
705 setOffset ({});
706}
707
709{
710 if (isLooping())
711 {
712 auto pos = getPosition();
713
714 auto offsetB = loopStartBeats.get() + getOffsetInBeats();
715 auto lengthB = getLoopLengthBeats();
716
717 pos.time = pos.time.withEnd (std::min (getTimeOfRelativeBeat (lengthB), pos.getEnd()));
718 pos.offset = getTimeOfRelativeBeat (toDuration (offsetB)) - pos.getStart(); // TODO: is this correct? Needs testing..
719
720 setLoopRange ({});
721 setPosition (pos);
722 }
723}
724
725void MidiClip::setLoopRangeBeats (BeatRange newRangeBeats)
726{
727 jassert (newRangeBeats.getStart() >= BeatPosition());
728
729 auto newStartBeat = std::max (BeatPosition(), newRangeBeats.getStart());
730 auto newLengthBeat = std::max (BeatDuration(), newRangeBeats.getLength());
731
732 if (loopStartBeats != newStartBeat || loopLengthBeats != newLengthBeat)
733 {
735
736 if (! isLooping())
737 originalLength = getLengthInBeats();
738
739 loopStartBeats = newStartBeat;
740 loopLengthBeats = newLengthBeat;
741 }
742}
743
744void MidiClip::setLoopRange (TimeRange newRange)
745{
746 jassert (newRange.getStart() >= TimePosition());
747
748 auto& ts = edit.tempoSequence;
749 auto pos = getPosition();
750 auto newStartBeat = BeatPosition::fromBeats (newRange.getStart().inSeconds() * ts.getBeatsPerSecondAt (pos.getStart()));
751 auto newLengthBeats = ts.toBeats (pos.getStart() + newRange.getLength()) - ts.toBeats (pos.getStart());
752
753 setLoopRangeBeats ({ newStartBeat, newStartBeat + newLengthBeats });
754}
755
757{
758 return TimePosition::fromSeconds (loopStartBeats.get().inBeats() / edit.tempoSequence.getBeatsPerSecondAt (getPosition().getStart()));
759}
760
762{
763 return TimeDuration::fromSeconds (loopLengthBeats.get().inBeats() / edit.tempoSequence.getBeatsPerSecondAt (getPosition().getStart()));
764}
765
766//==============================================================================
767void MidiClip::setSendingBankChanges (bool sendBank)
768{
769 if (sendBank != sendBankChange)
770 sendBankChange = ! sendBankChange;
771}
772
773LiveClipLevel MidiClip::getLiveClipLevel()
774{
775 return { level };
776}
777
778void MidiClip::valueTreePropertyChanged (juce::ValueTree& tree, const juce::Identifier& id)
779{
780 if (tree == state)
781 {
782 if (id == IDs::mute)
783 {
784 jassert (parent != nullptr); // Should have been set by now..
785
786 if (auto track = getTrack())
787 if (auto p = track->getParentFolderTrack())
788 p->setDirtyClips();
789
790 clearCachedLoopSequence();
791 }
792 else if (id == IDs::volDb || id == IDs::mpeMode
793 || id == IDs::loopStartBeats || id == IDs::loopLengthBeats
794 || id == IDs::loopedSequenceType
795 || id == IDs::grooveStrength)
796 {
797 clearCachedLoopSequence();
798 }
799 else if (id == IDs::currentTake)
800 {
801 currentTake.forceUpdateOfCachedValue();
802
803 for (SelectionManager::Iterator sm; sm.next();)
804 if (sm->isSelected (getSelectedEvents()))
805 sm->deselectAll();
806
807 clearCachedLoopSequence();
808 }
809 else if (id == IDs::launchQuantisation || id == IDs::useClipLaunchQuantisation)
810 {
811 changed();
812 }
813 else
814 {
815 if (id == IDs::length || id == IDs::offset)
816 clearCachedLoopSequence();
817
818 Clip::valueTreePropertyChanged (tree, id);
819 }
820 }
821 else if (tree.hasType (IDs::NOTE)
822 || tree.hasType (IDs::CONTROL)
823 || tree.hasType (IDs::SYSEX)
824 || tree.hasType (IDs::QUANTISATION)
825 || (tree.hasType (IDs::SEQUENCE) && id == IDs::channelNumber)
826 || (tree.hasType (IDs::GROOVE) && id == IDs::current))
827 {
828 clearCachedLoopSequence();
829 }
830 else
831 {
832 Clip::valueTreePropertyChanged (tree, id);
833 }
834}
835
836void MidiClip::valueTreeChildAdded (juce::ValueTree& p, juce::ValueTree& c)
837{
838 if (p.hasType (IDs::SEQUENCE))
839 clearCachedLoopSequence();
840 else if ((p == state || p.getParent() == state) && c.hasType (IDs::SEQUENCE))
841 channelSequence.add (new MidiList (c, getUndoManager()));
842
843 if (c.hasType (IDs::PATTERNGENERATOR))
844 patternGenerator = std::make_unique<PatternGenerator> (*this, c);
845}
846
847void MidiClip::valueTreeChildRemoved (juce::ValueTree& p, juce::ValueTree& c, int)
848{
849 if (p.hasType (IDs::SEQUENCE))
850 {
851 clearCachedLoopSequence();
852 }
853 else if ((p == state || p.getParent() == state) && c.hasType (IDs::SEQUENCE))
854 {
855 for (int i = channelSequence.size(); --i >= 0;)
856 if (auto ml = channelSequence.getUnchecked (i))
857 if (ml->state == c)
858 channelSequence.remove (i);
859 }
860 else if (p == state && c.hasType (IDs::COMPS))
861 {
862 midiCompManager = nullptr;
863 clearCachedLoopSequence();
864 }
865 else if (p == state && c.hasType (IDs::PATTERNGENERATOR))
866 {
867 patternGenerator = nullptr;
868 }
869}
870
871void MidiClip::clearCachedLoopSequence()
872{
873 cachedLoopedSequence = nullptr;
874 changed();
875}
876
877//==============================================================================
879{
880 if (! state.getChildWithName (IDs::PATTERNGENERATOR).isValid())
881 state.addChild (juce::ValueTree (IDs::PATTERNGENERATOR), -1, &edit.getUndoManager());
882
883 jassert (patternGenerator != nullptr);
884 return patternGenerator.get();
885}
886
888{
889 if (patternGenerator != nullptr)
890 patternGenerator->refreshPatternIfNeeded();
891
892 // for the midi editor to redraw
894}
895
896//==============================================================================
897//==============================================================================
898void mergeInMidiSequence (MidiClip& mc, juce::MidiMessageSequence ms, TimeDuration startTime,
899 MidiList::NoteAutomationType automationType)
900{
901 ms.addTimeToMessages (startTime.inSeconds());
902
903 const auto start = TimePosition::fromSeconds (ms.getStartTime());
904 const auto end = TimePosition::fromSeconds (ms.getEndTime());
905
906 auto pos = mc.getPosition();
907
908 if (pos.getStart() > start)
909 mc.extendStart (std::max (0_tp, start - 0.1s));
910
911 if (pos.getEnd() < end)
912 mc.setEnd (end + 0.1s, true);
913
914 mc.mergeInMidiSequence (ms, automationType);
915}
916
917}} // namespace tracktion { inline namespace engine
T ceil(T... args)
int indexOf(ParameterType elementToLookFor) const
void forceUpdateOfCachedValue()
Colour withHue(float newHue) const noexcept
bool isValid() const noexcept
void addTimeToMessages(double deltaTime) noexcept
double getStartTime() const noexcept
double getEndTime() const noexcept
MidiEventHolder * getEventPointer(int index) const noexcept
int getNumEvents() const noexcept
void add(String stringToAdd)
bool hasType(const Identifier &typeName) const noexcept
void removeChild(const ValueTree &child, UndoManager *undoManager)
bool isValid() const noexcept
void addChild(const ValueTree &child, int index, UndoManager *undoManager)
ValueTree getParent() const noexcept
ValueTree createCopy() const
ValueTree getChildWithName(const Identifier &type) const
ValueTree getOrCreateChildWithName(const Identifier &type, UndoManager *undoManager)
void sendPropertyChangeMessage(const Identifier &property)
double getMidiVisibleProportion() const
vertical scales for displaying the midi note editor
Base class for items that can contain clips.
A clip in an edit.
void changed() override
This should be called to send a change notification to any SelectableListeners that are registered wi...
void setOffset(TimeDuration newOffset)
Sets the offset of the clip, i.e.
juce::ValueTree state
The ValueTree of the Clip state.
void setStart(TimePosition newStart, bool preserveSync, bool keepLength)
Sets the start time of the clip.
Track * getTrack() const override
Returns the parent Track this clip is on (if any).
BeatPosition getContentBeatAtTime(TimePosition) const
Returns the beat number (with offset) at the given time.
void setLength(TimeDuration newLength, bool preserveSync)
Sets the length of the clip.
virtual void setShowingTakes(bool shouldShow)
Sets whether the clip should be showing takes.
virtual void setSpeedRatio(double)
Sets a speed ratio i.e.
virtual bool isShowingTakes() const
Returns true if the clip is showing takes.
juce::UndoManager * getUndoManager() const
Returns the UndoManager.
ClipPosition getPosition() const override
Returns the ClipPosition on the parent Track.
void setEnd(TimePosition newEnd, bool preserveSync)
Sets the end of the clip.
void setPosition(ClipPosition newPosition)
Sets the position of the clip.
bool isCurrentTakeComp() const
Returns true if the current take is a comp.
int getNumTakes() const
Returns the number of takes that are not comps.
static constexpr double maximumLength
The maximum length an Edit can be.
void pitchTempoTrackChanged() override
Called when there are pitch or tempo changes made which might require clips to adjust timing informat...
TimeDuration getLoopLength() const override
Returns the length of loop in seconds.
PatternGenerator * getPatternGenerator() override
Returns the PatternGenerator for this clip if it has one.
TimePosition getLoopStart() const override
Returns the start time of the loop start point.
BeatDuration getLoopLengthBeats() const override
Returns the length of loop in beats.
LaunchQuantisation * getLaunchQuantisation() override
Some clip types can be launched, if that's possible, this returns a quantisation that can be used for...
void setLoopRange(TimeRange) override
Sets the loop range the clip should use in seconds.
int getNumTakes(bool includeComps) override
Returns the total number of takes.
void extendStart(TimePosition newStartTime)
This will extend the start time backwards, moving the notes along if this takes the offset below 0....
bool isLooping() const override
Returns true if this clip is currently looping.
void clearTakes() override
Clears any takes this clip has.
FollowActions * getFollowActions() override
Some clip types can be launched, if that's possible, this can be used to determine the action to perf...
void disableLooping() override
Disables all looping.
std::shared_ptr< LaunchHandle > getLaunchHandle() override
Some clip types can be launched, if that's possible, this returns a handle to trigger starting/stoppi...
bool canBeAddedTo(ClipOwner &) override
Tests whether this clip can go on the given parent.
Clip::Array unpackTakes(bool toNewTracks) override
Attempts to unpack the takes to new clips.
void setLoopRangeBeats(BeatRange) override
Sets the loop range the clip should use in beats.
void legatoNote(MidiNote &note, const juce::Array< MidiNote * > &notesToUse, BeatPosition maxEndBeat, juce::UndoManager &)
Lengthens or shortens a note to match the start of the next note in the given array.
juce::StringArray getTakeDescriptions() const override
Returns the descriptions of any takes.
void setCurrentTake(int takeIndex) override
Sets a given take index to be the current take.
bool isCurrentTakeComp() override
Returns true if the current take is a comp.
void setNumberOfLoops(int) override
Sets the clip looping a number of times.
bool hasAnyTakes() const override
Returns true if this clip has any takes.
void copyFrom(const MidiList &, juce::UndoManager *)
Clears the current list and copies the others contents and properties.
MidiChannel getMidiChannel() const
Gets the list's midi channel number.
@ none
No automation, add the sequence as plain MIDI with the channel of the clip.
@ expression
Add the automation as EXP assuming the source sequence is MPE MIDI.
virtual void changed()
This should be called to send a change notification to any SelectableListeners that are registered wi...
BeatPosition getStartBeat() const
Returns the start beat in the Edit of this item.
BeatDuration getLengthInBeats() const
Returns the duration in beats the of this item.
TimePosition getTimeOfRelativeBeat(BeatDuration) const
Returns an Edit time point for a given number of beats from the start of this item.
BeatDuration getOffsetInBeats() const
Returns an the offset of this item in beats.
TimeRange getEditTimeRange() const
Returns the time range of this item.
T get(T... args)
T is_pointer_v
#define TRANS(stringLiteral)
#define jassert(expression)
#define jassertfalse
T max(T... args)
T min(T... args)
Type jlimit(Type lowerLimit, Type upperLimit, Type valueToConstrain) noexcept
bool isPositiveAndBelow(Type1 valueToTest, Type2 upperLimit) noexcept
int roundToInt(const FloatType value) noexcept
juce::String getName(LaunchQType t)
Retuns the name of a LaunchQType for display purposes.
bool canContainMIDI(const ClipOwner &co)
Returns true if this Track can contain MidiClip[s].
juce::Array< Track * > getAllTracks(const Edit &edit)
Returns all the tracks in an Edit.
Represents a duration in beats.
Represents a position in beats.
Represents a duration in real-life time.
Represents a position in real-life time.
TimePosition getStartOfSource() const
Returns what would be the the start of the source material in the timeline.
Provides a thread-safe way to share a clip's levels with an audio engine without worrying about the C...
Defines the place to insert Track[s].
#define CRASH_TRACER
This macro adds the current location to a stack which gets logged if a crash happens.