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

« « « Anklang Documentation
Loading...
Searching...
No Matches
tracktion_SelectedMidiEvents.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
14SelectedMidiEvents::SelectedMidiEvents (MidiClip& m) : clips ({ &m })
15{
16}
17
18SelectedMidiEvents::SelectedMidiEvents (juce::Array<MidiClip*> m) : clips (m)
19{
20 // SelectedMidiEvents must always have one clip
21 jassert (m.size() > 0);
22
23 #if JUCE_DEBUG
24 // All the clips must be frmom the same edit
25 for (int i = 1; i < m.size(); i++)
26 jassert (&m[0]->edit == &m[i]->edit);
27 #endif
28}
29
30SelectedMidiEvents::~SelectedMidiEvents()
31{
32 notifyListenersOfDeletion();
33}
34
35Edit& SelectedMidiEvents::getEdit()
36{
37 return clips[0]->edit;
38}
39
40//==============================================================================
41MidiClip* SelectedMidiEvents::clipForEvent (MidiNote* note) const
42{
43 for (auto* c : clips)
44 if (c->getSequence().getNotes().contains (note))
45 return c;
46
47 // SelectedMidiEvents must never have events without the parent clip
49 return {};
50}
51
52MidiClip* SelectedMidiEvents::clipForEvent (MidiSysexEvent* sysex) const
53{
54 for (auto* c : clips)
55 if (c->getSequence().getSysexEvents().contains (sysex))
56 return c;
57
58 // SelectedMidiEvents must never have events without the parent clip
60 return {};
61}
62
63MidiClip* SelectedMidiEvents::clipForEvent (MidiControllerEvent* controller) const
64{
65 for (auto* c : clips)
66 if (c->getSequence().getControllerEvents().contains (controller))
67 return c;
68
69 // SelectedMidiEvents must never have events without the parent clip
71 return {};
72}
73
74//==============================================================================
75void SelectedMidiEvents::addSelectedEvent (MidiNote* note, bool addToCurrent)
76{
77 if (! addToCurrent)
78 selectedNotes.clearQuick();
79
80 // don't allow notes that don't belong to this set of clips to be selected
81 if (note != nullptr && clipForEvent (note) == nullptr)
82 return;
83
84 if (note != nullptr && ! contains (*note))
85 selectedNotes.add (note);
86
87 sendSelectionChangedMessage (nullptr);
88
89 if (selectedNotes.isEmpty())
90 deselect();
91 else
92 SelectionManager::refreshAllPropertyPanelsShowing (*this);
93
94 if (selectedSysexes.size() > 0)
95 {
96 selectedSysexes.clearQuick();
97 SelectionManager::refreshAllPropertyPanelsShowing (*this);
98 }
99
100 if (selectedControllers.size() > 0)
101 {
102 selectedControllers.clearQuick();
103 SelectionManager::refreshAllPropertyPanelsShowing (*this);
104 }
105}
106
107void SelectedMidiEvents::addSelectedEvent (MidiSysexEvent* sysex, bool addToCurrent)
108{
109 if (! addToCurrent)
110 selectedSysexes.clearQuick();
111
112 // don't allow sysexes that don't belong to this set of clips to be selected
113 if (sysex != nullptr && clipForEvent (sysex) == nullptr)
114 return;
115
116 if (sysex != nullptr && ! contains (*sysex))
117 selectedSysexes.add (sysex);
118
119 if (selectedSysexes.isEmpty())
120 deselect();
121
122 sendSelectionChangedMessage (nullptr);
123
124 if (selectedNotes.size() > 0)
125 {
126 selectedNotes.clearQuick();
127 SelectionManager::refreshAllPropertyPanelsShowing (*this);
128 }
129 if (selectedControllers.size() > 0)
130 {
131 selectedControllers.clearQuick();
132 SelectionManager::refreshAllPropertyPanelsShowing (*this);
133 }
134}
135
136void SelectedMidiEvents::addSelectedEvent (MidiControllerEvent* controller, bool addToCurrent)
137{
138 if (! addToCurrent)
139 selectedControllers.clearQuick();
140
141 // don't allow controllers that don't belong to this set of clips to be selected
142 if (controller != nullptr && clipForEvent (controller) == nullptr)
143 return;
144
145 if (controller != nullptr && ! contains (*controller))
146 selectedControllers.add (controller);
147
148 if (selectedControllers.isEmpty())
149 deselect();
150
151 sendSelectionChangedMessage (nullptr);
152
153 if (selectedNotes.size() > 0)
154 {
155 selectedNotes.clearQuick();
156 SelectionManager::refreshAllPropertyPanelsShowing (*this);
157 }
158 if (selectedSysexes.size() > 0)
159 {
160 selectedSysexes.clearQuick();
161 SelectionManager::refreshAllPropertyPanelsShowing (*this);
162 }
163}
164
165void SelectedMidiEvents::removeSelectedEvent (MidiNote* note)
166{
167 if (note != nullptr)
168 {
169 selectedNotes.removeAllInstancesOf (note);
170 sendSelectionChangedMessage (nullptr);
171 }
172
173 if (! anythingSelected())
174 deselect();
175}
176
177void SelectedMidiEvents::removeSelectedEvent (MidiSysexEvent* sysex)
178{
179 if (sysex != nullptr)
180 {
181 selectedSysexes.removeAllInstancesOf (sysex);
182 sendSelectionChangedMessage (nullptr);
183 }
184
185 if (! anythingSelected())
186 deselect();
187}
188
189void SelectedMidiEvents::removeSelectedEvent (MidiControllerEvent* controller)
190{
191 if (controller != nullptr)
192 {
193 selectedControllers.removeAllInstancesOf (controller);
194 sendSelectionChangedMessage (nullptr);
195 }
196
197 if (! anythingSelected())
198 deselect();
199}
200
201void SelectedMidiEvents::setSelected (SelectionManager& sm, const juce::Array<MidiNote*>& notes, bool addToSelection, bool allowMixedSelection)
202{
203 if (! addToSelection)
204 selectedNotes.clearQuick();
205
206 for (auto n : notes)
207 {
208 if (n != nullptr && clipForEvent (n) == nullptr)
209 continue;
210
211 if (n != nullptr && ! contains (*n))
212 selectedNotes.add (n);
213 }
214
215 sendSelectionChangedMessage (&sm);
216
217 if (selectedSysexes.size() > 0 && ! allowMixedSelection)
218 selectedSysexes.clearQuick();
219
220 if (selectedControllers.size() > 0 && ! allowMixedSelection)
221 selectedControllers.clearQuick();
222
223 if (getNumSelected() > 0)
224 sm.selectOnly (this);
225 else
226 deselect();
227}
228
229void SelectedMidiEvents::setSelected (SelectionManager& sm, const juce::Array<MidiSysexEvent*>& events, bool addToSelection, bool allowMixedSelection)
230{
231 if (! addToSelection)
232 selectedSysexes.clearQuick();
233
234 for (auto e : events)
235 {
236 if (e != nullptr && clipForEvent (e) == nullptr)
237 continue;
238
239 if (e != nullptr && ! contains (*e))
240 selectedSysexes.add (e);
241 }
242
243 sendSelectionChangedMessage (&sm);
244
245 if (selectedNotes.size() > 0 && ! allowMixedSelection)
246 selectedNotes.clearQuick();
247
248 if (selectedControllers.size() > 0 && ! allowMixedSelection)
249 selectedControllers.clearQuick();
250
251 if (getNumSelected() > 0)
252 sm.selectOnly (this);
253 else
254 deselect();
255}
256
257void SelectedMidiEvents::setSelected (SelectionManager& sm, const juce::Array<MidiControllerEvent*>& events, bool addToSelection, bool allowMixedSelection)
258{
259 if (! addToSelection)
260 selectedControllers.clearQuick();
261
262 for (auto e : events)
263 {
264 if (e != nullptr && clipForEvent (e) == nullptr)
265 continue;
266
267 if (e != nullptr && ! contains (*e))
268 selectedControllers.add (e);
269 }
270
271 sendSelectionChangedMessage (&sm);
272
273 if (selectedNotes.size() > 0 && ! allowMixedSelection)
274 selectedNotes.clearQuick();
275
276 if (selectedSysexes.size() > 0 && ! allowMixedSelection)
277 selectedSysexes.clearQuick();
278
279 if (getNumSelected() > 0)
280 sm.selectOnly (this);
281 else
282 deselect();
283}
284
285bool SelectedMidiEvents::contains (const MidiNote& note) const noexcept
286{
287 for (auto n : selectedNotes)
288 if (n->state == note.state)
289 return true;
290
291 return false;
292}
293
294bool SelectedMidiEvents::contains (const MidiSysexEvent& event) const noexcept
295{
296 for (auto s : selectedSysexes)
297 if (s->state == event.state)
298 return true;
299
300 return false;
301}
302
303bool SelectedMidiEvents::contains (const MidiControllerEvent& event) const noexcept
304{
305 for (auto s : selectedControllers)
306 if (s->state == event.state)
307 return true;
308
309 return false;
310}
311
312bool SelectedMidiEvents::isSelected (const MidiNote* event) const
313{
314 return event != nullptr
315 && SelectionManager::findSelectionManagerContaining (this) != nullptr
316 && contains (*event);
317}
318
319bool SelectedMidiEvents::isSelected (const MidiSysexEvent* event) const
320{
321 return event != nullptr
322 && SelectionManager::findSelectionManagerContaining (this) != nullptr
323 && contains (*event);
324}
325
326bool SelectedMidiEvents::isSelected (const MidiControllerEvent* event) const
327{
328 return event != nullptr
329 && SelectionManager::findSelectionManagerContaining (this) != nullptr
330 && contains (*event);
331}
332
333//==============================================================================
334juce::String SelectedMidiEvents::getSelectableDescription()
335{
336 return TRANS("MIDI Events");
337}
338
339void SelectedMidiEvents::selectionStatusChanged (bool isNowSelected)
340{
341 if (! anythingSelected())
342 deselect();
343
344 if (! isNowSelected)
345 {
346 selectedNotes.clearQuick();
347 selectedSysexes.clearQuick();
348 selectedControllers.clearQuick();
349 sendSelectionChangedMessage (nullptr);
350 }
351}
352
353void SelectedMidiEvents::moveEvents (TimeDuration deltaStart, TimeDuration deltaLength, int deltaNote)
354{
355 auto* undoManager = &getEdit().getUndoManager();
356 auto notes = selectedNotes; // Use a copy in case any of them get deleted while moving
357
358 juce::Array<MidiClip*> uniqueClips;
359
360 auto startTime = TimePosition::fromSeconds (std::numeric_limits<double>::max());
361 auto endTime = TimePosition::fromSeconds (std::numeric_limits<double>::lowest());
362
364
365 for (auto note : notes)
366 {
367 if (auto clip = clipForEvent (note))
368 {
369 note->setNoteNumber (note->getNoteNumber() + deltaNote, undoManager);
370
371 if (shouldLockControllerToNotes && shouldLockControllerToNotes())
372 {
373 uniqueClips.addIfNotAlreadyThere (clip);
374
375 startTime = std::min (startTime, note->getEditStartTime (*clip));
376 endTime = std::max (endTime, note->getEditEndTime (*clip));
377 }
378
379 auto pos = note->getEditTimeRange (*clip);
380 auto newStartBeat = clip->getContentBeatAtTime (pos.getStart() + deltaStart) + toDuration (clip->getLoopStartBeats());
381 auto newEndBeat = clip->getContentBeatAtTime (pos.getEnd() + deltaStart + deltaLength) + toDuration (clip->getLoopStartBeats());
382
383 if (! deltaBeat.has_value())
384 deltaBeat = newStartBeat - note->getBeatPosition();
385
386 note->setStartAndLength (newStartBeat, newEndBeat - newStartBeat, undoManager);
387 }
388 }
389
390 for (auto sysexEvent : selectedSysexes)
391 {
392 if (auto clip = clipForEvent (sysexEvent))
393 {
394 auto deltaTime = sysexEvent->getEditTime (*clip) + deltaStart;
395 sysexEvent->setBeatPosition (clip->getContentBeatAtTime (deltaTime) + toDuration (clip->getLoopStartBeats()), undoManager);
396 }
397 }
398
399 if (shouldLockControllerToNotes && shouldLockControllerToNotes() && notes.size() > 0)
400 {
401 moveControllerData (uniqueClips, nullptr, *deltaBeat, startTime, endTime, false);
402 }
403 else
404 {
405 for (auto controllerEvent : selectedControllers)
406 {
407 auto& clip = *clipForEvent (controllerEvent);
408
409 uniqueClips.addIfNotAlreadyThere (&clip);
410
411 startTime = std::min (startTime, controllerEvent->getEditTime (clip));
412 endTime = std::max (endTime, controllerEvent->getEditTime (clip));
413
414 auto start = controllerEvent->getEditTime (clip);
415 auto newStartBeat = clip.getContentBeatAtTime (start + deltaStart) + toDuration (clip.getLoopStartBeats());
416
417 if (! deltaBeat.has_value())
418 deltaBeat = newStartBeat - controllerEvent->getBeatPosition();
419 }
420
421 moveControllerData (uniqueClips, &selectedControllers, *deltaBeat,
422 startTime - TimeDuration::fromSeconds (0.001), endTime + TimeDuration::fromSeconds (0.001),
423 false);
424 }
425}
426
427void SelectedMidiEvents::setNoteLengths (BeatDuration newLength)
428{
429 auto um = &getEdit().getUndoManager();
430
431 for (auto note : selectedNotes)
432 note->setStartAndLength (note->getStartBeat(), newLength, um);
433}
434
435void SelectedMidiEvents::setVelocities (int newVelocity)
436{
437 auto undoManager = &getEdit().getUndoManager();
438
439 for (auto note : selectedNotes)
440 note->setVelocity (newVelocity, undoManager);
441}
442
443void SelectedMidiEvents::changeColour (uint8_t newColour)
444{
445 auto undoManager = &getEdit().getUndoManager();
446
447 for (auto note : selectedNotes)
448 note->setColour (newColour, undoManager);
449}
450
451void SelectedMidiEvents::nudge (TimecodeSnapType snapType, int leftRight, int upDown)
452{
453 auto& ed = getEdit();
454 auto undoManager = &ed.getUndoManager();
455
456 if (upDown != 0)
457 for (auto note : selectedNotes)
458 note->setNoteNumber (note->getNoteNumber() + upDown, undoManager);
459
460 if (leftRight != 0)
461 {
462 if (auto firstSelected = selectedNotes.getFirst())
463 {
464 if (auto clip = clips[0])
465 {
466 auto start = firstSelected->getEditStartTime (*clip);
467
468 auto snapped = leftRight < 0
469 ? snapType.roundTimeDown (start - TimeDuration::fromSeconds (0.01), ed.tempoSequence)
470 : snapType.roundTimeUp (start + TimeDuration::fromSeconds (0.01), ed.tempoSequence);
471
472 auto delta = ed.tempoSequence.toBeats (snapped)
473 - ed.tempoSequence.toBeats (start);
474
475 juce::Array<MidiClip*> uniqueClips;
476 auto startTime = TimePosition::fromSeconds (std::numeric_limits<double>::max());
477 auto endTime = TimePosition::fromSeconds (std::numeric_limits<double>::lowest());
478
479 for (auto note : selectedNotes)
480 {
481 auto noteClip = clipForEvent (note);
482 uniqueClips.addIfNotAlreadyThere (noteClip);
483
484 startTime = std::min (startTime, note->getEditStartTime (*noteClip));
485 endTime = std::max (endTime, note->getEditEndTime (*noteClip));
486
487 note->setStartAndLength (note->getStartBeat() + delta,
488 note->getLengthBeats(),
489 undoManager);
490 }
491
492 if (shouldLockControllerToNotes && shouldLockControllerToNotes())
493 moveControllerData (uniqueClips, nullptr, delta, startTime, endTime, false);
494 }
495 }
496 }
497}
498
499TimeRange SelectedMidiEvents::getSelectedRange() const
500{
501 bool doneFirst = false;
503
504 for (auto n : selectedNotes)
505 {
506 if (auto clip = clipForEvent (n))
507 {
508 auto noteRange = n->getEditTimeRange (*clip);
509
510 if (! doneFirst)
511 {
512 time = noteRange;
513 doneFirst = true;
514 continue;
515 }
516
517 time = time.getUnionWith (noteRange);
518 }
519 }
520
521 return time;
522}
523
524void SelectedMidiEvents::sendSelectionChangedMessage (SelectionManager* sm)
525{
526 if (sm != nullptr)
527 {
528 ++(sm->selectionChangeCount);
529 sm->sendChangeMessage();
530 }
531
532 changed();
533 sendChangeMessage();
534}
535
536void SelectedMidiEvents::setClips (juce::Array<MidiClip*> clips_)
537{
538 // You should try and avoid changing the list of clips, instead
539 // create a new SelectedMidiEvents for the new clips. If you do
540 // change the clips, then this fuction will deselect and events
541 // that no longer belong to a clip in the clip list.
542
543 clips = clips_;
544
545 for (int i = selectedNotes.size(); --i >= 0;)
546 if (clipForEvent (selectedNotes[i]) == nullptr)
547 selectedNotes.remove (i);
548
549 for (int i = selectedSysexes.size(); --i >= 0;)
550 if (clipForEvent (selectedSysexes[i]) == nullptr)
551 selectedSysexes.remove (i);
552
553 for (int i = selectedControllers.size(); --i >= 0;)
554 if (clipForEvent (selectedControllers[i]) == nullptr)
555 selectedControllers.remove (i);
556}
557
558void SelectedMidiEvents::moveControllerData (const juce::Array<MidiClip*>& clips, const juce::Array<MidiControllerEvent*>* onlyTheseEvents,
559 BeatDuration deltaBeats, TimePosition startTime, TimePosition endTime, bool makeCopy)
560{
561 for (auto c : clips)
562 {
563 juce::Array<juce::ValueTree> itemsToRemove;
564
565 auto& seq = c->getSequence();
566
568
569 for (auto evt : seq.getControllerEvents())
570 {
571 if (evt->getEditTime (*c) >= startTime && evt->getEditTime (*c) < endTime)
572 {
573 if (makeCopy)
574 seq.addControllerEvent (MidiControllerEvent (evt->state.createCopy()), c->getUndoManager());
575
576 auto beat = evt->getBeatPosition() + deltaBeats;
577 evt->setBeatPosition (beat, c->getUndoManager());
578 movedEvents.add (evt);
579 }
580 }
581
582 auto& ts = c->edit.tempoSequence;
583
584 const auto startTimeAfter = ts.toTime (ts.toBeats (startTime) + deltaBeats);
585 const auto endTimeAfter = ts.toTime (ts.toBeats (endTime) + deltaBeats);
586
587 for (auto evt : seq.getControllerEvents())
588 if (onlyTheseEvents == nullptr || onlyTheseEvents->contains (evt))
589 if (! movedEvents.contains (evt) && evt->getEditTime (*c) >= startTimeAfter && evt->getEditTime (*c) <= endTimeAfter)
590 itemsToRemove.add (evt->state);
591
592 for (auto& v : itemsToRemove)
593 seq.state.removeChild (v, c->getUndoManager());
594 }
595}
596
597}} // namespace tracktion { inline namespace engine
int size() const noexcept
void remove(int indexToRemove)
void add(const ElementType &newElement)
bool contains(ParameterType elementToLookFor) const
bool addIfNotAlreadyThere(ParameterType newElement)
#define TRANS(stringLiteral)
#define jassert(expression)
#define jassertfalse
T max(T... args)
T min(T... args)
RangeType< TimePosition > TimeRange
A RangeType based on real time (i.e.
constexpr TimeDuration toDuration(TimePosition)
Converts a TimePosition to a TimeDuration.
T has_value(T... args)
Represents a duration in beats.
Represents a duration in real-life time.
Represents a position in real-life time.
time