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

« « « Anklang Documentation
Loading...
Searching...
No Matches
tracktion_Clipboard.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
14Clipboard::Clipboard() {}
15Clipboard::~Clipboard() { clearSingletonInstance(); }
16
18
19void Clipboard::clear()
20{
21 if (! isEmpty())
22 {
23 content.reset();
24 broadcaster.sendChangeMessage();
25 }
26}
27
28void Clipboard::setContent (std::unique_ptr<ContentType> newContent)
29{
30 if (content != newContent)
31 {
32 content = std::move (newContent);
33 broadcaster.sendChangeMessage();
34 }
35}
36
37const Clipboard::ContentType* Clipboard::getContent() const { return content.get(); }
38bool Clipboard::isEmpty() const { return content == nullptr; }
39
40void Clipboard::addListener (juce::ChangeListener* l) { broadcaster.addChangeListener (l); }
41void Clipboard::removeListener (juce::ChangeListener* l) { broadcaster.removeChangeListener (l); }
42
43Clipboard::ContentType::EditPastingOptions::EditPastingOptions (Edit& e, EditInsertPoint& ip, SelectionManager* sm)
44 : edit (e), insertPoint (ip), selectionManager (sm)
45{}
46
47Clipboard::ContentType::EditPastingOptions::EditPastingOptions (Edit& e, EditInsertPoint& ip)
48 : edit (e), insertPoint (ip)
49{}
50
51//==============================================================================
52Clipboard::ContentType::~ContentType() {}
53
54bool Clipboard::ContentType::pasteIntoEdit (Edit& edit, EditInsertPoint& insertPoint, SelectionManager* sm) const
55{
57 Track::Ptr startTrack;
58 TimePosition startPos;
59 insertPoint.chooseInsertPoint (startTrack, startPos, false, sm);
60
61 if (startTrack == nullptr)
62 {
64 return false;
65 }
66
67 Clipboard::ContentType::EditPastingOptions options (edit, insertPoint, sm);
68 options.startTrack = startTrack.get();
69 options.startTime = startPos;
70
71 return pasteIntoEdit (options);
72}
73
74bool Clipboard::ContentType::pasteIntoEdit (const EditPastingOptions&) const { return false; }
75
76
77//==============================================================================
78//==============================================================================
79Clipboard::ProjectItems::ProjectItems() {}
80Clipboard::ProjectItems::~ProjectItems() {}
81
82AudioTrack* getOrInsertAudioTrackNearestIndex (Edit& edit, int trackIndex)
83{
84 int i = 0;
85
86 // find the next audio track on or after the given index..
87 for (auto t : getAllTracks (edit))
88 {
89 if (i >= trackIndex)
90 if (auto at = dynamic_cast<AudioTrack*> (t))
91 return at;
92
93 ++i;
94 }
95
96 return edit.insertNewAudioTrack (TrackInsertPoint (nullptr, getAllTracks (edit).getLast()), nullptr).get();
97}
98
99static TimePosition pasteMIDIFileIntoEdit (Edit& edit, const juce::File& midiFile,
100 int& targetTrackIndex, int& targetSlotIndex,
101 TimePosition startTime, bool importTempoChanges)
102{
105 juce::Array<BeatPosition> tempoChangeBeatNumbers;
107 juce::Array<int> numerators, denominators;
108
109 auto newClipEndTime = startTime;
110 BeatDuration len;
111 bool importAsNoteExpression = false;
112
113 if (MidiList::looksLikeMPEData (midiFile))
114 importAsNoteExpression = edit.engine.getUIBehaviour()
115 .showOkCancelAlertBox (TRANS("Import as Note Expression?"),
116 TRANS("This MIDI file looks like it contains multi-channel MPE data. Do you want to convert this to note expression or import as multiple clips?"),
117 TRANS("Convert to Expression"),
118 TRANS("Separate Clips"));
119
120 if (MidiList::readSeparateTracksFromFile (midiFile, lists,
121 tempoChangeBeatNumbers, bpms,
122 numerators, denominators, len,
123 importAsNoteExpression))
124 {
125 auto& tempoSequence = edit.tempoSequence;
126
127 auto startBeat = tempoSequence.toBeats (startTime);
128 auto endBeat = startBeat + len;
129
130 for (auto& list : lists)
131 endBeat = std::max (endBeat, startBeat + BeatDuration::fromBeats (list->getLastBeatNumber().inBeats()));
132
133 endBeat = startBeat + BeatDuration::fromBeats (std::ceil ((endBeat - startBeat).inBeats()));
134
135 if (importTempoChanges)
136 {
137 if (tempoChangeBeatNumbers.size() > 0)
138 tempoSequence.removeTemposBetween (TimeRange (startTime, tempoSequence.toTime (endBeat))
139 .expanded (0.001_td), true);
140
141 for (int i = 0; i < tempoChangeBeatNumbers.size(); ++i)
142 {
143 auto insertTime = tempoSequence.toTime (startBeat + toDuration (tempoChangeBeatNumbers.getUnchecked (i)));
144 auto& origTempo = tempoSequence.getTempoAt (insertTime);
145
146 if (std::abs (origTempo.getBpm() - bpms.getUnchecked (i)) > 0.001)
147 if (auto tempo = tempoSequence.insertTempo (insertTime))
148 tempo->setBpm (bpms.getUnchecked (i));
149
150 auto& origTimeSig = tempoSequence.getTimeSigAt (insertTime);
151
152 if (origTimeSig.denominator != denominators.getUnchecked (i)
153 || origTimeSig.numerator != numerators.getUnchecked (i))
154 {
155 if (auto timeSig = tempoSequence.insertTimeSig (insertTime))
156 timeSig->setStringTimeSig (juce::String (numerators.getUnchecked (i))
157 + "/" + juce::String (denominators.getUnchecked (i)));
158 }
159 }
160 }
161
162 auto lastTrackEndTime = BeatPosition::fromBeats (Edit::getMaximumEditEnd().inSeconds()); // Assumes 60bpm
163 --targetTrackIndex;
164
165 for (auto list : lists)
166 {
167 auto listBeatStart = list->getFirstBeatNumber();
168 auto listBeatEnd = std::max (listBeatStart + 1_bd,
169 std::max (list->getLastBeatNumber(),
170 toPosition (endBeat - startBeat)));
171
172 if (lastTrackEndTime > listBeatStart)
173 ++targetTrackIndex;
174
175 lastTrackEndTime = listBeatEnd;
176
177 juce::ValueTree clipState (IDs::MIDICLIP);
178 clipState.setProperty (IDs::channel, list->getMidiChannel(), nullptr);
179
180 if (list->state.isValid())
181 clipState.addChild (list->state, -1, nullptr);
182
183 if (auto at = getOrInsertAudioTrackNearestIndex (edit, targetTrackIndex))
184 {
185 const auto timeRange = tempoSequence.toTime ({ startBeat, endBeat });
186
187 auto clipName = list->getImportedMidiTrackName();
188 if (auto fn = list->getImportedFileName(); fn.isNotEmpty())
189 clipName += " - " + fn;
190
191 auto targetOwner = [&]() -> ClipOwner*
192 {
193 if (targetSlotIndex < 0)
194 return at;
195
196 auto& slotList = at->getClipSlotList();
197 auto slots = slotList.getClipSlots();
198
199 if (targetSlotIndex < slots.size())
200 return slots[targetSlotIndex];
201
202 if (! slots.isEmpty())
203 return slots.getFirst();
204
205 return at;
206 }();
207
208 if (auto newClip = insertClipWithState (*targetOwner,
209 clipState, clipName, TrackItem::Type::midi,
210 { timeRange, 0_td }, DeleteExistingClips::no, false))
211 {
212 if (auto mc = dynamic_cast<MidiClip*> (newClip))
213 {
214 if (mc->getClipSlot() != nullptr)
215 {
216 mc->setUsesProxy (false);
217 mc->setStart (0_tp, false, true);
218 mc->setLoopRangeBeats (mc->getEditBeatRange());
219 }
220
221 if (importAsNoteExpression)
222 mc->setMPEMode(true);
223 }
224
225 newClipEndTime = std::max (newClipEndTime, newClip->getPosition().getEnd());
226 }
227 }
228 else
229 {
230 break;
231 }
232 }
233 }
234
235 return newClipEndTime;
236}
237
239{
240 bool shouldImportTempoChangesFromMIDI = false;
241 bool separateTracks = false;
242 bool snapBWavsToOriginalTime = false;
243};
244
245static void askUserAboutProjectItemPastingOptions (Engine& engine,
246 const Clipboard::ProjectItems& items,
248{
249 auto& pm = engine.getProjectManager();
250 auto& ui = engine.getUIBehaviour();
251
252 bool importedMIDIFilesHaveTempoChanges = false;
253 int numAudioClips = 0;
254 int numAudioClipsWithBWAV = 0;
255
256 for (auto& item : items.itemIDs)
257 {
258 if (auto source = pm.getProjectItem (item.itemID))
259 {
260 auto file = source->getSourceFile();
261
262 if (file.exists())
263 {
264 if (source->isMidi())
265 {
266 if (! importedMIDIFilesHaveTempoChanges)
267 importedMIDIFilesHaveTempoChanges = MidiList::fileHasTempoChanges (file);
268 }
269 else if (source->isWave())
270 {
271 ++numAudioClips;
272
273 if (AudioFile (engine, file).getMetadata()[juce::WavAudioFormat::bwavTimeReference].isNotEmpty())
274 ++numAudioClipsWithBWAV;
275 }
276 }
277 }
278 }
279
280 if (importedMIDIFilesHaveTempoChanges)
281 options.shouldImportTempoChangesFromMIDI = ui.showOkCancelAlertBox (TRANS("MIDI Clip"),
282 TRANS("Do you want to import tempo and time signature changes from the MIDI clip?"),
283 TRANS("Import"),
284 TRANS("Ignore"));
285
286 if (numAudioClips > 1)
287 {
288 if (numAudioClipsWithBWAV > 0 && ! engine.getEngineBehaviour().ignoreBWavTimestamps())
289 {
290 #if JUCE_MODAL_LOOPS_PERMITTED
291 juce::ToggleButton toggle (TRANS("Snap to BWAV"));
292 toggle.setSize (200, 20);
293
295 .createAlertWindow (TRANS("Add multiple files"),
296 TRANS("Do you want to add multiple files to one track or to separate tracks?"),
297 {}, {}, {}, juce::AlertWindow::QuestionIcon,
298 0, nullptr));
299
300 aw->addCustomComponent (&toggle);
301 aw->addButton (TRANS("One track"), 0);
302 aw->addButton (TRANS("Separate tracks"), 1);
303
304 options.separateTracks = aw->runModalLoop() == 1;
305 options.snapBWavsToOriginalTime = toggle.getToggleState();
306 #else
307 options.separateTracks = false;
308 options.snapBWavsToOriginalTime = false;
309 #endif
310 }
311 else
312 {
313 options.separateTracks = ! ui.showOkCancelAlertBox (TRANS("Add multiple files"),
314 TRANS("Do you want to add multiple files to one track or to separate tracks?"),
315 TRANS("One track"),
316 TRANS("Separate tracks"));
317 options.snapBWavsToOriginalTime = false;
318 }
319 }
320 else if (numAudioClips == 1 && numAudioClipsWithBWAV == 1)
321 {
322 if (engine.getEngineBehaviour().ignoreBWavTimestamps())
323 options.snapBWavsToOriginalTime = false;
324 else
325 options.snapBWavsToOriginalTime = ui.showOkCancelAlertBox (TRANS("BWAV Clip"),
326 TRANS("Do you want clip placed at BWAV timestamp or cursor position?"),
327 TRANS("BWAV timestamp"),
328 TRANS("Cursor position"));
329 }
330}
331
332inline bool isRecursiveEditClipPaste (const Clipboard::ProjectItems& items, Edit& edit)
333{
334 auto& pm = edit.engine.getProjectManager();
335
336 for (auto& item : items.itemIDs)
337 if (auto source = pm.getProjectItem (item.itemID))
338 if (source->isEdit() && source->getID() == edit.getProjectItemID())
339 return true;
340
341 return false;
342}
343
344bool Clipboard::ProjectItems::pasteIntoEdit (const EditPastingOptions& options) const
345{
346 auto& e = options.edit.engine;
347 auto& pm = e.getProjectManager();
348 auto& ui = options.edit.engine.getUIBehaviour();
349 bool anythingPasted = false;
350
351 ProjectItemPastingOptions pastingOptions;
352
353 pastingOptions.separateTracks = options.preferredLayout == FileDragList::consecutiveTracks;
354
355 if (! options.silent)
356 askUserAboutProjectItemPastingOptions (e, *this, pastingOptions);
357
358 if (isRecursiveEditClipPaste (*this, options.edit))
359 {
360 if (! options.silent)
361 ui.showWarningAlert (TRANS("Can't Import Edit"),
362 TRANS("You can't paste an edit clip into itself!"));
363
364 return false;
365 }
366
367 auto [insertPointTrack, clipOwner, time] = options.insertPoint.chooseInsertPoint (false, options.selectionManager,
368 [] (auto& t) { return t.isAudioTrack() || t.isFolderTrack(); });
369 auto startTime = time.value_or (0_tp);
370
371 if (insertPointTrack == nullptr || clipOwner == nullptr)
372 {
374 return false;
375 }
376
377 const bool pastingInToClipLauncher = dynamic_cast<ClipSlot*> (clipOwner) != nullptr;
378
379 int targetTrackIndex = insertPointTrack->getIndexInEditTrackList();
380 SelectableList itemsAdded;
381
382 for (auto [index, item] : juce::enumerate (itemIDs))
383 {
384 if (auto sourceItem = pm.getProjectItem (item.itemID))
385 {
386 auto file = sourceItem->getSourceFile();
387 auto newClipEndTime = startTime;
388
389 if (file.exists())
390 {
391 if (sourceItem->isMidi())
392 {
393 int targetSlotIndex = -1;
394
395 if (auto targetSlot = dynamic_cast<ClipSlot*> (clipOwner))
396 targetSlotIndex = findClipSlotIndex (*targetSlot);
397
398 newClipEndTime = pasteMIDIFileIntoEdit (options.edit, file, targetTrackIndex, targetSlotIndex,
399 startTime, pastingOptions.shouldImportTempoChangesFromMIDI);
400 }
401 else if (sourceItem->isWave())
402 {
403 sourceItem->verifyLength();
404 jassert (sourceItem->getLength() > 0);
405
406 if (auto clipSlot = dynamic_cast<ClipSlot*> (clipOwner->getClipOwnerSelectable()))
407 if (auto existingClip = clipSlot->getClip())
408 existingClip->removeFromParent();
409
410 if (auto newClip = insertWaveClip (*clipOwner,
411 sourceItem->getName(), sourceItem->getID(),
412 { { startTime, TimePosition::fromSeconds (startTime.inSeconds() + sourceItem->getLength()) }, 0_td },
413 DeleteExistingClips::no))
414 {
415 newClipEndTime = newClip->getPosition().getEnd();
416 itemsAdded.add (newClip.get());
417
418 if (pastingOptions.snapBWavsToOriginalTime)
419 newClip->snapToOriginalBWavTime();
420
421 // Set sensible defaults for new launcher clips
422 if (newClip->getClipSlot())
423 {
424 if (newClip->effectsEnabled())
425 newClip->enableEffects (false, false);
426
427 newClip->setUsesProxy (false);
428 newClip->setAutoTempo (true);
429 newClip->setStart (0_tp, false, true);
430 newClip->setLoopRangeBeats ({ 0_bp, newClip->getLengthInBeats() });
431 }
432 }
433
434 }
435 else if (sourceItem->isEdit())
436 {
437 sourceItem->verifyLength();
438 jassert (sourceItem->getLength() > 0);
439
440 if (auto newClip = insertEditClip (*clipOwner,
441 { startTime, startTime + TimeDuration::fromSeconds (sourceItem->getLength()) },
442 sourceItem->getID()))
443 {
444 newClipEndTime = newClip->getPosition().getEnd();
445 itemsAdded.add (newClip.get());
446
447 // Set sensible defaults for new launcher clips
448 if (newClip->getClipSlot())
449 {
450 if (newClip->effectsEnabled())
451 newClip->enableEffects (false, false);
452
453 newClip->setUsesProxy (false);
454 newClip->setAutoTempo (true);
455 newClip->setLoopRangeBeats ({ 0_bp, newClip->getLengthInBeats() });
456 }
457 }
458 }
459
460 anythingPasted = true;
461
462 if (int (index) < int (itemIDs.size() - 1))
463 {
464 if (pastingInToClipLauncher)
465 {
466 if (juce::ModifierKeys::currentModifiers.isCommandDown() || options.preferredLayout == FileDragList::consecutiveTracks)
467 {
468 ++targetTrackIndex;
469 auto newTrack = getOrInsertAudioTrackNearestIndex (options.edit, targetTrackIndex);
470
471 auto slot = dynamic_cast<ClipSlot*> (clipOwner);
472 auto& at = *dynamic_cast<AudioTrack*> (&slot->track);
473 auto& list = at.getClipSlotList();
474
475 auto idx = list.getClipSlots().indexOf (slot);
476
477 options.edit.getSceneList().ensureNumberOfScenes (list.getClipSlots().size());
478
479 clipOwner = newTrack->getClipSlotList().getClipSlots()[idx];
480 }
481 else
482 {
483 auto slot = dynamic_cast<ClipSlot*> (clipOwner);
484 auto& at = *dynamic_cast<AudioTrack*> (&slot->track);
485 auto& list = at.getClipSlotList();
486
487 auto idx = list.getClipSlots().indexOf (slot) + 1;
488
489 options.edit.getSceneList().ensureNumberOfScenes (idx + 1);
490
491 clipOwner = list.getClipSlots()[idx];
492 }
493 }
494 else
495 {
496 if (pastingOptions.separateTracks)
497 ++targetTrackIndex;
498 else
499 startTime = newClipEndTime;
500
501 clipOwner = getOrInsertAudioTrackNearestIndex (options.edit, targetTrackIndex);
502 }
503 }
504 }
505 }
506 }
507
508 if (itemsAdded.isNotEmpty())
509 if (auto sm = options.selectionManager)
510 sm->select (itemsAdded);
511
512 return anythingPasted;
513}
514
515bool Clipboard::ProjectItems::pasteIntoProject (Project& project) const
516{
517 for (auto& item : itemIDs)
518 if (auto source = project.engine.getProjectManager().getProjectItem (item.itemID))
519 if (auto newItem = project.createNewItem (source->getSourceFile(),
520 source->getType(),
521 source->getName(),
522 source->getDescription(),
523 source->getCategory(),
524 true))
525 newItem->copyAllPropertiesFrom (*source);
526
527 return ! itemIDs.empty();
528}
529
530//==============================================================================
531//==============================================================================
532Clipboard::Clips::Clips() {}
533Clipboard::Clips::~Clips() {}
534
535void Clipboard::Clips::addClip (int trackOffset, const juce::ValueTree& state)
536{
537 ClipInfo ci;
538 ci.trackOffset = trackOffset;
539 ci.state = state.createCopy();
540
541 clips.push_back (ci);
542}
543
544void Clipboard::Clips::addSelectedClips (const SelectableList& selectedObjects,
545 TimeRange range,
546 AutomationLocked automationLocked)
547{
548 if (range.isEmpty())
549 range = Edit::getMaximumEditTimeRange();
550
551 auto selectedClipContents = getClipSelectionWithCollectionClipContents (selectedObjects);
552
553 Clip::Array clipsToPaste;
554
555 for (auto& c : selectedClipContents.getItemsOfType<Clip>())
556 if (c->getEditTimeRange().overlaps (range))
557 clipsToPaste.add (c);
558
559 if (clipsToPaste.isEmpty())
560 return;
561
562 auto& ed = clipsToPaste.getFirst()->edit;
563
564 auto allTracks = getAllTracks (ed);
565
566 auto firstSlotIndex = ed.engine.getEngineBehaviour().getEditLimits().maxClipsInTrack;
567 auto firstTrackIndex = ed.engine.getEngineBehaviour().getEditLimits().maxNumTracks;
568 auto overallStartTime = TimePosition::fromSeconds (Edit::maximumLength);
569
570 for (auto clip : clipsToPaste)
571 {
572 overallStartTime = std::min (overallStartTime, std::max (clip->getPosition().getStart(), range.getStart()));
573 firstTrackIndex = std::min (firstTrackIndex, std::max (0, allTracks.indexOf (clip->getTrack())));
574
575 if (auto slot = clip->getClipSlot())
576 firstSlotIndex = std::min (firstSlotIndex, slot->getIndex());
577 }
578
579 for (auto clip : clipsToPaste)
580 {
581 if (clip->getEditTimeRange().overlaps (range))
582 {
583 auto clipPos = clip->getPosition();
584 auto clippedStart = std::max (clipPos.getStart(), range.getStart());
585 auto clippedOffset = clipPos.getOffset() + (clippedStart - clipPos.getStart());
586 auto clippedEnd = std::min (clipPos.getEnd(), range.getEnd());
587
588 ClipInfo info;
589
590 info.grouped = clip->isGrouped();
591
592 clip->flushStateToValueTree();
593 info.state = clip->state.createCopy();
594
595 addValueTreeProperties (info.state,
596 IDs::start, (clippedStart - overallStartTime).inSeconds(),
597 IDs::length, (clippedEnd - clippedStart).inSeconds(),
598 IDs::offset, clippedOffset.inSeconds());
599
600 auto acb = dynamic_cast<AudioClipBase*> (clip);
601
602 if (acb != nullptr)
603 {
604 // If we're just pasting in to the Edit without trying to fit it in to a range,
605 // we need to flush the fade in/out so pasted clips don't get default edge fades
606 if (range == Edit::getMaximumEditTimeRange())
607 {
608 addValueTreeProperties (info.state,
609 IDs::fadeIn, acb->getFadeIn().inSeconds(),
610 IDs::fadeOut, acb->getFadeOut().inSeconds());
611 }
612 else
613 {
614 auto inOutPoints = clip->getEditTimeRange().getIntersectionWith (range);
615 TimeRange fadeIn (clipPos.getStart(), clipPos.getStart() + acb->getFadeIn());
616 TimeRange fadeOut (clipPos.getEnd() - acb->getFadeOut(), clipPos.getEnd());
617
618 addValueTreeProperties (info.state,
619 IDs::fadeIn, fadeIn.overlaps (inOutPoints) ? fadeIn.getIntersectionWith (inOutPoints).getLength().inSeconds() : 0.0,
620 IDs::fadeOut, fadeOut.overlaps (inOutPoints) ? fadeOut.getIntersectionWith (inOutPoints).getLength().inSeconds() : 0.0);
621 }
622
623 // Also flush these properties so the defaults aren't picked up
624 addValueTreeProperties (info.state,
625 IDs::proxyAllowed, acb->canUseProxy(),
626 IDs::resamplingQuality, juce::VariantConverter<ResamplingQuality>::toVar (acb->getResamplingQuality()));
627 }
628
629 info.trackOffset = allTracks.indexOf (clip->getTrack()) - firstTrackIndex;
630
631 if (auto slot = clip->getClipSlot())
632 info.slotOffset = slot->getIndex() - firstSlotIndex;
633
634 if (acb == nullptr || acb->getAutoTempo())
635 {
636 info.hasBeatTimes = true;
637
638 auto& ts = ed.tempoSequence;
639 info.startBeats = BeatPosition::fromBeats ((ts.toBeats (clippedStart) - ts.toBeats (overallStartTime)).inBeats());
640 info.lengthBeats = ts.toBeats (clippedEnd) - ts.toBeats (clippedStart);
641 info.offsetBeats = BeatPosition::fromBeats (ts.getBeatsPerSecondAt (clippedStart) * clippedOffset.inSeconds());
642 }
643
644 clips.push_back (info);
645 }
646 }
647
648 if (automationLocked == AutomationLocked::yes)
649 addAutomation (TrackSection::findSections (clipsToPaste), range);
650}
651
652void Clipboard::Clips::addAutomation (const juce::Array<TrackSection>& trackSections, TimeRange range)
653{
654 if (range.isEmpty() || trackSections.isEmpty())
655 return;
656
657 auto& edit = trackSections.getFirst().track->edit;
658 auto allTracks = getAllTracks (edit);
659 auto firstTrackIndex = edit.engine.getEngineBehaviour().getEditLimits().maxNumTracks;
660 auto overallStartTime = TimePosition::fromSeconds (Edit::maximumLength);
661
662 for (const auto& trackSection : trackSections)
663 {
664 overallStartTime = std::min (overallStartTime, std::max (trackSection.range.getStart(), range.getStart()));
665 firstTrackIndex = std::min (firstTrackIndex, std::max (0, allTracks.indexOf (trackSection.track)));
666 }
667
668 for (const auto& trackSection : trackSections)
669 {
670 for (auto plugin : trackSection.track->pluginList)
671 {
672 for (int k = 0; k < plugin->getNumAutomatableParameters(); k++)
673 {
674 auto param = plugin->getAutomatableParameter (k);
675
676 if (param->getCurve().getNumPoints() > 0)
677 {
678 AutomationCurveSection section;
679 section.pluginName = plugin->getName();
680 section.paramID = param->paramID;
681 section.trackOffset = std::max (0, allTracks.indexOf (trackSection.track) - firstTrackIndex);
682 section.valueRange = param->getCurve().getValueLimits();
683
684 const auto endTolerence = TimeDuration::fromSeconds (0.0001);
685 auto intersection = trackSection.range.getIntersectionWith (range);
686 auto reducedIntersection = intersection.reduced (endTolerence);
687 auto clippedStart = intersection.getStart();
688 auto clippedEnd = intersection.getEnd();
689
690 for (int l = 0; l < param->getCurve().getNumPoints(); ++l)
691 {
692 auto pt = param->getCurve().getPoint (l);
693
694 if (reducedIntersection.containsInclusive (pt.time))
695 section.points.push_back ({ pt.time, pt.value, pt.curve });
696 }
697
698 if (section.points.empty())
699 {
700 section.points.push_back ({ clippedStart, param->getCurve().getValueAt (clippedStart), 1.0f });
701 section.points.push_back ({ clippedEnd, param->getCurve().getValueAt (clippedEnd), 0.0f });
702 }
703 else
704 {
705 if (section.points[0].time > clippedStart + endTolerence)
706 section.points.insert (section.points.begin(), { clippedStart + endTolerence, param->getCurve().getValueAt (clippedStart + endTolerence), 0.0f });
707
708 if (section.points[section.points.size() - 1].time < clippedEnd - endTolerence)
709 section.points.push_back ({ clippedEnd - endTolerence, param->getCurve().getValueAt (clippedEnd - endTolerence), 0.0f });
710 }
711
712 for (auto& p : section.points)
713 p.time = p.time - TimeDuration::fromSeconds (overallStartTime.inSeconds());
714
715 std::sort (section.points.begin(), section.points.end());
716 automationCurves.push_back (std::move (section));
717 }
718 }
719 }
720 }
721}
722
723static void fixClipTimes (juce::ValueTree& state, const Clipboard::Clips::ClipInfo& clip,
725 TempoSequence& tempoSequence, TimePosition startOffset)
726{
727 TimePosition start, offset;
728 TimeDuration length;
729
730 if (clip.hasBeatTimes)
731 {
732 BeatDuration slotOffset;
733
734 if (clip.slotOffset.has_value())
735 for (const auto& info : otherClips)
736 if (info.trackOffset == clip.trackOffset && info.slotOffset.has_value() && *info.slotOffset < *clip.slotOffset)
737 slotOffset = slotOffset + info.lengthBeats;
738
739 auto offsetInBeats = BeatDuration::fromBeats (tempoSequence.toBeats (startOffset).inBeats());
740 auto range = tempoSequence.toTime ({ clip.startBeats + offsetInBeats + slotOffset, clip.startBeats + offsetInBeats + slotOffset + clip.lengthBeats });
741 start = range.getStart();
742 length = range.getLength();
743 offset = TimePosition::fromSeconds (clip.offsetBeats.inBeats() / tempoSequence.getBeatsPerSecondAt (start));
744 }
745 else
746 {
747 start = TimePosition::fromSeconds (static_cast<double> (state.getProperty (IDs::start)))
748 + TimeDuration::fromSeconds (startOffset.inSeconds());
749 length = TimeDuration::fromSeconds (static_cast<double> (state.getProperty (IDs::length)));
750 offset = TimePosition::fromSeconds (static_cast<double> (state.getProperty (IDs::offset)));
751 }
752
753 // if clip is coming from preset, it'll have this
754 // property, so resize it to match tempo
755 if (const double srcBpm = state[IDs::bpm]; srcBpm > 0)
756 {
757 auto& destTempo = tempoSequence.getTempoAt (start);
758 length = TimeDuration::fromSeconds (length.inSeconds() * srcBpm / destTempo.getBpm());
759 }
760
761 state.setProperty (IDs::start, start.inSeconds(), nullptr);
762 state.setProperty (IDs::length, length.inSeconds(), nullptr);
763 state.setProperty (IDs::offset, offset.inSeconds(), nullptr);
764
765 state.removeProperty (IDs::bpm, nullptr);
766 state.removeProperty (IDs::key, nullptr);
767}
768
769static bool pastePointsToCurve (const std::vector<AutomationCurve::AutomationPoint>& points, juce::Range<float> valueRange, AutomationCurve& targetCurve, TimeRange targetRange)
770{
771 AutomationCurve newCurve;
772 newCurve.setOwnerParameter (targetCurve.getOwnerParameter());
773 auto dstRange = targetCurve.getValueLimits();
774 jassert (! dstRange.isEmpty());
775
776 for (auto p : points)
777 {
778 if (dstRange != valueRange)
779 {
780 auto normalised = (p.value - valueRange.getStart()) / valueRange.getLength();
781 p.value = dstRange.getStart() + dstRange.getLength() * normalised;
782 }
783
784 newCurve.addPoint (p.time, p.value, p.curve);
785 }
786
787 if (newCurve.getLength() > 0_td)
788 {
789 if (targetRange.isEmpty())
790 targetRange = targetRange.withLength (newCurve.getLength());
791 else
792 newCurve.rescaleAllTimes (targetRange.getLength() / newCurve.getLength());
793
794 targetCurve.mergeOtherCurve (newCurve, targetRange, 0_tp, 0_td, false, false);
795 return true;
796 }
797
798 return false;
799}
800
801bool Clipboard::Clips::pasteIntoEdit (const EditPastingOptions& options) const
802{
803 if (clips.empty())
804 return false;
805
806 auto targetTrack = options.startTrack;
807 auto targetClipOwnerID = options.targetClipOwnerID;
808
809 if (targetTrack == nullptr)
810 {
811 auto placement = options.insertPoint.chooseInsertPoint (false, options.selectionManager,
812 [] (auto& t) { return t.isAudioTrack() || t.isFolderTrack(); });
813 targetTrack = placement.track;
814 targetClipOwnerID = placement.clipOwner != nullptr ? placement.clipOwner->getClipOwnerID() : EditItemID();
815 jassert (targetTrack != nullptr);
816 }
817
818 // We can't paste into a folder or submix track, so find the next clip track
819 while (targetTrack != nullptr && targetTrack->isFolderTrack())
820 targetTrack = targetTrack->getSiblingTrack (1, false);
821
822 if (targetTrack == nullptr)
823 return false;
824
826 SelectableList itemsAdded;
827
828 for (auto& clip : clips)
829 {
830 auto newClipState = clip.state.createCopy();
831 EditItemID::remapIDs (newClipState, nullptr, options.edit, &remappedIDs);
832 fixClipTimes (newClipState, clip, clips, options.edit.tempoSequence, options.startTime);
833
834 if (newClipState.hasType (IDs::MARKERCLIP))
835 {
836 if (auto markerTrack = options.edit.getMarkerTrack())
837 {
838 if (auto newClip = markerTrack->insertClipWithState (newClipState))
839 {
840 itemsAdded.add (newClip);
841
842 if (auto mc = dynamic_cast<MarkerClip*> (newClip))
843 options.edit.getMarkerManager().checkForDuplicates (*mc, false);
844 }
845 }
846 else
847 {
849 }
850 }
851 else if (newClipState.hasType (IDs::CHORDCLIP))
852 {
853 if (auto chordTrack = options.edit.getChordTrack())
854 {
855 if (auto newClip = chordTrack->insertClipWithState (newClipState))
856 itemsAdded.add (newClip);
857 }
858 }
859 else if (newClipState.hasType (IDs::ARRANGERCLIP))
860 {
861 if (auto arrangerTrack = options.edit.getArrangerTrack())
862 {
863 if (auto newClip = arrangerTrack->insertClipWithState (newClipState))
864 itemsAdded.add (newClip);
865 }
866 }
867 else
868 {
869 if (auto clipSlot = findClipSlotForID (options.edit, targetClipOwnerID))
870 {
871 if (clip.grouped)
872 {
873 options.edit.engine.getUIBehaviour().showWarningMessage (TRANS ("Group clips can not be added to the clip launcher"));
874 }
875 else
876 {
877 auto calcSlotOffset = [&]
878 {
879 auto offset = 0;
880
881 for (const auto& c : clips)
882 if (c.trackOffset == clip.trackOffset && c.startBeats < clip.startBeats)
883 offset++;
884
885 return offset;
886 };
887
888 auto slotOffset = clip.slotOffset.has_value() ? *clip.slotOffset : calcSlotOffset();
889
890 auto tracks = getAudioTracks (options.edit);
891 auto trackIndex = tracks.indexOf (dynamic_cast<AudioTrack*> (&clipSlot->track));
892 auto slotIndex = clipSlot->getIndex();
893
894 trackIndex += clip.trackOffset;
895 slotIndex += slotOffset;
896
897 options.edit.getSceneList().ensureNumberOfScenes (slotIndex + 1);
898 options.edit.ensureNumberOfAudioTracks (trackIndex + 1);
899
900 if (auto at = getAudioTracks (options.edit)[trackIndex])
901 clipSlot = at->getClipSlotList().getClipSlots()[slotIndex];
902
903 if (auto existingClip = clipSlot->getClip())
904 existingClip->removeFromParent();
905
906 if (auto newClip = insertClipWithState (*clipSlot, newClipState))
907 itemsAdded.add (newClip);
908 }
909 }
910 else if (auto clipTrack = dynamic_cast<ClipTrack*> (targetTrack->getSiblingTrack (clip.trackOffset, false)))
911 {
912 if (auto newClip = clipTrack->insertClipWithState (newClipState))
913 itemsAdded.add (newClip);
914 }
915 else
916 {
918 }
919 }
920 }
921
923 for (auto c : itemsAdded.getItemsOfType<Clip>())
924 {
925 if (c->isGrouped())
926 {
927 auto originalGroup = c->getGroupID();
928
929 if (groupMap.find (originalGroup) == groupMap.end())
930 groupMap[originalGroup] = c->edit.createNewItemID();
931
932 c->setGroup (groupMap[originalGroup]);
933 }
934 }
935
936 for (auto& curve : automationCurves)
937 {
938 if (! curve.points.empty())
939 {
940 const TimeRange destCurveTimeRange (options.startTime, TimeDuration());
941
942 if (auto clipTrack = dynamic_cast<ClipTrack*> (targetTrack->getSiblingTrack (curve.trackOffset, false)))
943 {
944 for (auto plugin : clipTrack->pluginList)
945 {
946 if (plugin->getName() == curve.pluginName)
947 {
948 if (auto targetParam = plugin->getAutomatableParameterByID (curve.paramID))
949 {
950 pastePointsToCurve (curve.points, curve.valueRange, targetParam->getCurve(), destCurveTimeRange);
951 break;
952 }
953 }
954 }
955 }
956 else
957 {
959 }
960 }
961 }
962
963 if (itemsAdded.isEmpty())
964 return false;
965
966 if (auto sm = options.selectionManager)
967 {
968 bool first = true;
969 for (auto i : itemsAdded)
970 {
971 if (auto c = dynamic_cast<Clip*> (i); c->isGrouped())
972 sm->select (c->getGroupClip(), ! first);
973 else
974 sm->select (i, ! first);
975 first = false;
976 }
977 }
978
979 if (options.setTransportToEnd && ! options.edit.getTransport().isPlaying())
980 options.edit.getTransport().setPosition (getTimeRangeForSelectedItems (itemsAdded).getEnd());
981
982 return true;
983}
984
985bool Clipboard::Clips::pasteIntoEdit (Edit& edit, EditInsertPoint& insertPoint, SelectionManager* sm) const
986{
987 Clipboard::ContentType::EditPastingOptions options (edit, insertPoint, sm);
988 auto placement = insertPoint.chooseInsertPoint (false, sm,
989 [] (auto& t) { return t.isAudioTrack() || t.isFolderTrack(); });
990
991 options.startTrack = placement.track;
992 options.startTime = placement.time.value_or (0_tp);
993 options.targetClipOwnerID = placement.clipOwner != nullptr ? placement.clipOwner->getClipOwnerID()
994 : EditItemID();
995
996 return pasteIntoEdit (options);
997}
998
999bool Clipboard::Clips::pasteAfterSelected (Edit& edit, EditInsertPoint& insertPoint, SelectionManager& sm) const
1000{
1001 EditPastingOptions options (edit, insertPoint, &sm);
1002 auto placement = insertPoint.chooseInsertPoint (true, &sm,
1003 [] (auto& t) { return t.isAudioTrack() || t.isFolderTrack(); });
1004
1005 options.startTrack = placement.track;
1006 options.startTime = placement.time.value_or (0_tp);
1007 options.targetClipOwnerID = placement.clipOwner != nullptr ? placement.clipOwner->getClipOwnerID()
1008 : EditItemID();
1009
1010 return pasteIntoEdit (options);
1011}
1012
1013static juce::Array<ClipTrack*> findTracksToInsertInto (Edit& edit, EditInsertPoint& insertPoint, SelectionManager& sm)
1014{
1015 auto tracks = sm.getItemsOfType<ClipTrack>();
1016 bool noFolders = true;
1017
1018 for (auto ft : sm.getItemsOfType<FolderTrack>())
1019 {
1020 for (auto t : ft->getAllAudioSubTracks (true))
1021 tracks.addIfNotAlreadyThere (t);
1022
1023 noFolders = false;
1024 }
1025
1026 for (auto c : sm.getItemsOfType<Clip>())
1027 {
1028 tracks.addIfNotAlreadyThere (c->getClipTrack());
1029 insertPoint.setNextInsertPoint (edit.getTransport().position.get(), c->getTrack());
1030 }
1031
1032 if (tracks.isEmpty() && noFolders)
1033 tracks.addArray (getClipTracks (edit));
1034
1035 return tracks;
1036}
1037
1038static TimeDuration getNewClipsTotalLength (const Clipboard::Clips& clips, Edit& edit)
1039{
1040 TimePosition total;
1041
1042 for (auto& i : clips.clips)
1043 {
1044 auto end = i.hasBeatTimes ? edit.tempoSequence.toTime (i.startBeats + i.lengthBeats)
1045 : TimePosition::fromSeconds (static_cast<double> (i.state.getProperty (IDs::start))
1046 + static_cast<double> (i.state.getProperty (IDs::length)));
1047
1048 total = std::max (total, end);
1049 }
1050
1051 return toDuration (total);
1052}
1053
1054bool Clipboard::Clips::pasteInsertingAtCursorPos (Edit& edit, EditInsertPoint& insertPoint, SelectionManager& sm) const
1055{
1056 if (clips.empty())
1057 return false;
1058
1059 auto tracks = findTracksToInsertInto (edit, insertPoint, sm);
1060 auto insertLength = getNewClipsTotalLength (*this, edit);
1061
1062 if (tracks.isEmpty() || insertLength <= TimeDuration())
1063 return false;
1064
1065 auto cursorPos = edit.getTransport().getPosition();
1066 auto firstTrackIndex = tracks.getFirst()->getIndexInEditTrackList();
1067
1068 for (auto t : tracks)
1069 {
1070 t->splitAt (cursorPos);
1071 t->insertSpaceIntoTrack (cursorPos, insertLength);
1072 firstTrackIndex = std::min (firstTrackIndex, t->getIndexInEditTrackList());
1073 }
1074
1075 EditPastingOptions options (edit, insertPoint, &sm);
1076 options.startTime = std::max (0_tp, cursorPos);
1077 return pasteIntoEdit (options);
1078}
1079
1080//==============================================================================
1081//==============================================================================
1082Clipboard::Scenes::Scenes() {}
1083Clipboard::Scenes::~Scenes() {}
1084
1085bool Clipboard::Scenes::pasteIntoEdit (const EditPastingOptions& options) const
1086{
1087 auto& sceneList = options.edit.getSceneList();
1088
1089 auto insertIndex = sceneList.getNumScenes();
1090 if (auto sm = options.selectionManager)
1091 {
1092 auto items = sm->getSelectedObjects().getItemsOfType<Scene>();
1093 if (items.size() > 0)
1094 {
1095 insertIndex = 0;
1096 for (auto s : items)
1097 insertIndex = std::max (insertIndex, s->getIndex() + 1);
1098 }
1099 sm->deselectAll();
1100 }
1101
1103 SelectableList itemsAdded;
1104
1105 for (auto info : scenes)
1106 {
1107 if (auto newScene = sceneList.insertScene (insertIndex))
1108 {
1109 newScene->state.copyPropertiesAndChildrenFrom (info.state, &options.edit.getUndoManager());
1110
1111 options.edit.ensureNumberOfAudioTracks (int (info.clips.size()));
1112 auto tracks = getAudioTracks (options.edit);
1113 for (auto [idx, clip] : juce::enumerate (info.clips))
1114 {
1115 auto track = tracks[int (idx)];
1116 auto slot = track->getClipSlotList().getClipSlots()[insertIndex];
1117
1118 if (clip.isValid())
1119 {
1120 auto newClipState = clip.createCopy();
1121 EditItemID::remapIDs (newClipState, nullptr, options.edit, &remappedIDs);
1122
1123 insertClipWithState (*slot, newClipState);
1124 }
1125 }
1126 itemsAdded.add (newScene);
1127 }
1128
1129 insertIndex++;
1130 }
1131
1132 if (itemsAdded.isEmpty())
1133 return false;
1134
1135 if (auto sm = options.selectionManager)
1136 sm->select (itemsAdded);
1137
1138 return true;
1139}
1140
1141//==============================================================================
1142//==============================================================================
1143Clipboard::Tracks::Tracks() {}
1144Clipboard::Tracks::~Tracks() {}
1145
1146bool Clipboard::Tracks::pasteIntoEdit (const EditPastingOptions& options) const
1147{
1149
1150 juce::Array<Track::Ptr> newTracks;
1152
1153 auto targetTrack = options.startTrack;
1154
1155 // When pasting tracks, always paste after the selected group of tracks if the target is
1156 // within the selection
1157 auto allTracks = getAllTracks (options.edit);
1158
1159 if (options.selectionManager != nullptr && options.selectionManager->isSelected (targetTrack.get()))
1160 for (auto t : options.selectionManager->getItemsOfType<Track>())
1161 if (allTracks.indexOf (t) > allTracks.indexOf (targetTrack.get()))
1162 targetTrack = t;
1163
1164 if (options.selectionManager != nullptr)
1165 options.selectionManager->deselectAll();
1166
1167 for (auto& trackState : tracks)
1168 {
1169 auto newTrackTree = trackState.createCopy();
1170 EditItemID::remapIDs (newTrackTree, nullptr, options.edit, &remappedIDs);
1171
1172 Track::Ptr parentTrack, precedingTrack;
1173
1174 if (targetTrack != nullptr)
1175 parentTrack = targetTrack->getParentTrack();
1176
1177 precedingTrack = targetTrack;
1178
1179 if (auto newTrack = options.edit.insertTrack (TrackInsertPoint (parentTrack.get(),
1180 precedingTrack.get()),
1181 newTrackTree,
1182 options.selectionManager))
1183 {
1184 newTracks.add (newTrack);
1185
1186 targetTrack = newTrack;
1187 }
1188 else
1189 {
1190 break;
1191 }
1192 }
1193
1194 // Find any parameters on the Track that have modifier assignments
1195 // Check to see if they're assigned to the old modifier IDs
1196 // If they are, find the new modifier ID equivalents and update them
1197 // If they can't be found leave them if they're global or a parent of the new track.
1198 for (auto track : newTracks)
1199 {
1200 for (auto param : track->getAllAutomatableParams())
1201 {
1202 auto assignments = param->getAssignments();
1203
1204 for (int i = assignments.size(); --i >= 0;)
1205 {
1206 auto ass = assignments.getUnchecked (i);
1207
1208 // Macro reassignment is done during Plugin::giveNewIDsToPlugins so
1209 // we need to make sure we don't remove these
1210 if (dynamic_cast<MacroParameter::Assignment*> (ass.get()) != nullptr)
1211 continue;
1212
1213 const auto oldID = EditItemID::fromProperty (ass->state, IDs::source);
1214 const auto newID = remappedIDs[oldID];
1215
1216 if (newID.isValid())
1217 {
1218 ass->state.setProperty (IDs::source, newID, nullptr);
1219 }
1220 else
1221 {
1222 // If the modifier is on this track, keep it
1223 // If oldID is found in a global track, keep it
1224 // If oldID is found in a parent track, keep it
1225 if (auto t = getTrackContainingModifier (options.edit, findModifierForID (options.edit, oldID)))
1226 if (t == track.get() || TrackList::isFixedTrack (t->state) || track->isAChildOf (*t))
1227 continue;
1228
1229 // Otherwise remove the assignment
1230 param->removeModifier (*ass);
1231 }
1232 }
1233 }
1234 }
1235
1236 return true;
1237}
1238
1239//==============================================================================
1240//==============================================================================
1241Clipboard::TempoChanges::TempoChanges (const TempoSequence& ts, TimeRange range)
1242{
1243 auto beats = ts.toBeats (range);
1244
1245 BeatPosition startBeat = BeatPosition::fromBeats (std::floor (beats.getStart().inBeats() + 0.5));
1246 BeatPosition endBeat = BeatPosition::fromBeats (std::floor (beats.getEnd().inBeats() + 0.5));
1247
1248 bool pointAtStart = false;
1249 bool pointAtEnd = false;
1250
1251 for (auto t : ts.getTempos())
1252 {
1253 if (t->startBeatNumber == startBeat) pointAtStart = true;
1254 if (t->startBeatNumber == endBeat) pointAtEnd = true;
1255
1256 if (range.containsInclusive (t->getStartTime()))
1257 changes.push_back ({ t->startBeatNumber - toDuration (startBeat),
1258 t->getBpm(),
1259 t->getCurve() });
1260 }
1261
1262 if (! pointAtStart)
1263 changes.insert (changes.begin(),
1264 { BeatPosition(),
1265 ts.getBpmAt (ts.toTime (startBeat)),
1266 ts.getTempoAt (BeatPosition::fromBeats (juce::roundToInt (startBeat.inBeats()))).getCurve() });
1267
1268 if (! pointAtEnd)
1269 changes.push_back ({ toPosition (endBeat - startBeat),
1270 ts.getBpmAt (ts.toTime (endBeat)),
1271 ts.getTempoAt (BeatPosition::fromBeats (juce::roundToInt (endBeat.inBeats()))).getCurve() });
1272}
1273
1274Clipboard::TempoChanges::~TempoChanges() {}
1275
1276bool Clipboard::TempoChanges::pasteIntoEdit (const EditPastingOptions& options) const
1277{
1278 return pasteTempoSequence (options.edit.tempoSequence, TimeRange::emptyRange (options.startTime));
1279}
1280
1281bool Clipboard::TempoChanges::pasteTempoSequence (TempoSequence& ts, TimeRange targetRange) const
1282{
1283 if (changes.empty())
1284 return false;
1285
1286 EditTimecodeRemapperSnapshot snap;
1287 snap.savePreChangeState (ts.edit);
1288
1289 auto lengthInBeats = toDuration (changes.back().beat);
1290
1291 if (targetRange.isEmpty())
1292 targetRange = targetRange.withEnd (ts.toTime (ts.toBeats (targetRange.getStart()) + lengthInBeats));
1293
1294 auto startBeat = BeatPosition::fromBeats (std::floor (ts.toBeats (targetRange.getStart()).inBeats() + 0.5));
1295 auto endBeat = BeatPosition::fromBeats (std::floor (ts.toBeats (targetRange.getEnd()).inBeats() + 0.5));
1296
1297 double finalBPM = ts.getBpmAt (ts.toTime (endBeat));
1298 ts.removeTemposBetween (ts.toTime ({ startBeat, endBeat }), false);
1299 ts.insertTempo (ts.toTime (startBeat));
1300
1301 for (auto& tc : changes)
1302 ts.insertTempo (BeatPosition::fromBeats (juce::roundToInt ((tc.beat.inBeats() / lengthInBeats.inBeats()) * (endBeat - startBeat).inBeats() + startBeat.inBeats())),
1303 tc.bpm, tc.curve);
1304
1305 ts.insertTempo (ts.toTime (endBeat));
1306 ts.insertTempo (endBeat, finalBPM, 1.0f);
1307
1308 for (int i = ts.getNumTempos(); --i >= 1;)
1309 {
1310 auto tcurr = ts.getTempo (i);
1311 auto tprev = ts.getTempo (i - 1);
1312
1313 if (tcurr->startBeatNumber >= startBeat && tcurr->startBeatNumber <= endBeat
1314 && tcurr->startBeatNumber == tprev->startBeatNumber
1315 && tcurr->getBpm() == tprev->getBpm())
1316 ts.removeTempo (i, false);
1317 }
1318
1319 snap.remapEdit (ts.edit);
1320 return true;
1321}
1322
1323#if TRACKTION_UNIT_TESTS && ENGINE_UNIT_TESTS_CLIPBOARD
1324
1325//==============================================================================
1326//==============================================================================
1327class ClipboardTempoTests : public juce::UnitTest
1328{
1329public:
1330 ClipboardTempoTests() : juce::UnitTest ("ClipboardTempoTests", "Tracktion") {}
1331
1332 //==============================================================================
1333 void runTest() override
1334 {
1335 runCopyTests();
1336 runCopyTestsUsingBeatInsertion();
1337 runTrackCopyPasteTests();
1338 }
1339
1340private:
1341 template<typename TimeType>
1342 void expectEquals (TimeType t, double t2)
1343 {
1344 juce::UnitTest::expectEquals (t.inSeconds(), t2);
1345 }
1346
1347 void expectEquals (BeatPosition t, double t2)
1348 {
1349 juce::UnitTest::expectEquals (t.inBeats(), t2);
1350 }
1351
1352 void expectEquals (BeatDuration t, double t2)
1353 {
1354 juce::UnitTest::expectEquals (t.inBeats(), t2);
1355 }
1356
1357 void expectTempoSetting (TempoSetting& tempo, double bpm, float curve)
1358 {
1359 expectWithinAbsoluteError (tempo.getBpm(), bpm, 0.001);
1360 expectWithinAbsoluteError (tempo.getCurve(), curve, 0.001f);
1361 }
1362
1363 void runCopyTests()
1364 {
1365 auto edit = Edit::createSingleTrackEdit (*Engine::getEngines()[0]);
1366 auto& ts = edit->tempoSequence;
1367
1368 beginTest ("Simple copy/paste");
1369 {
1370 ts.getTempo (0)->setBpm (120.0);
1371
1372 // N.B. bars start at 0!
1373 expectEquals (ts.toBeats ({ 0, {} }), 0.0);
1374 expectEquals (ts.toTime ({ 0, {} }), 0.0);
1375 expectEquals (ts.toBeats ({ 8, {} }), 32.0);
1376 expectEquals (ts.toTime ({ 8, {} }), 16.0);
1377
1378 ts.insertTempo (ts.toBeats ({ 5, {} }), 60.0, 1.0f);
1379 ts.insertTempo (ts.toBeats ({ 9, {} }), 120.0, 1.0f);
1380
1381 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 4, {} })), 120.0, 1.0f);
1382 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 6, {} })), 60.0, 1.0f);
1383 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 8, {} })), 60.0, 1.0f);
1384 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 10, {} })), 120.0, 1.0f);
1385
1386 const BeatRange beatRangeToCopy (ts.toBeats ({ 6, {} }), ts.toBeats ({ 8, {} }));
1387 const auto timeRangeToCopy = ts.toTime (beatRangeToCopy);
1388 const BeatDuration numBeatsToInsert = beatRangeToCopy.getLength();
1389
1390 // Copy tempo changes
1391 Clipboard::TempoChanges tempoChanges (ts, timeRangeToCopy);
1392
1393 // Insert empty space
1394 const auto timeToInsertAt = ts.toTime ({ 2, {} });
1395 auto& tempoAtInsertionPoint = ts.getTempoAt (timeToInsertAt);
1396
1397 const auto beatRangeToInsert = BeatRange (ts.toBeats (timeToInsertAt), numBeatsToInsert);
1398 const auto lengthInTimeToInsert = ts.toTime (toPosition (beatRangeToInsert.getLength()));
1399 insertSpaceIntoEdit (*edit, TimeRange (timeToInsertAt, toDuration (lengthInTimeToInsert)));
1400
1401 const auto numBeatsInserted = beatRangeToInsert.getLength();
1402 const int numBarsInserted = juce::roundToInt (numBeatsInserted.inBeats() / tempoAtInsertionPoint.getMatchingTimeSig().denominator);
1403 expectWithinAbsoluteError (numBeatsInserted.inBeats(), 8.0, 0.0001);
1404 expect (numBarsInserted == 2);
1405
1406 // Ensure tempos are correct at original region
1407 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 4 + numBarsInserted, {} })), 120.0, 1.0f);
1408 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 6 + numBarsInserted, {} })), 60.0, 1.0f);
1409 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 8 + numBarsInserted, {} })), 60.0, 1.0f);
1410 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 10 + numBarsInserted, {} })), 120.0, 1.0f);
1411
1412 // Paste tempo changes
1413 tempoChanges.pasteTempoSequence (ts, TimeRange (timeToInsertAt, lengthInTimeToInsert));
1414
1415 // Ensure tempos are correct at inserted region
1416 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 0, {} })), 120.0, 1.0f);
1417 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 1, BeatDuration::fromBeats (3) })), 120.0, 1.0f);
1418 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 2, {} })), 60.0, 1.0f);
1419 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 3, BeatDuration::fromBeats (3) })), 60.0, 1.0f);
1420 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 4, {} })), 120.0, 1.0f);
1421 }
1422 }
1423
1424 void runCopyTestsUsingBeatInsertion()
1425 {
1426 auto edit = Edit::createSingleTrackEdit (*Engine::getEngines()[0]);
1427 auto& ts = edit->tempoSequence;
1428
1429 beginTest ("Simple copy/paste");
1430 {
1431 ts.getTempo (0)->setBpm (120.0);
1432
1433 // N.B. bars start at 0!
1434 expectEquals (ts.toBeats ({ 0, {} }), 0.0);
1435 expectEquals (ts.toTime ({ 0, {} }), 0.0);
1436 expectEquals (ts.toBeats ({ 8, {} }), 32.0);
1437 expectEquals (ts.toTime ({ 8, {} }), 16.0);
1438
1439 ts.insertTempo (ts.toBeats ({ 5, {} }), 60.0, 1.0f);
1440 ts.insertTempo (ts.toBeats ({ 9, {} }), 120.0, 1.0f);
1441
1442 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 4, {} })), 120.0, 1.0f);
1443 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 6, {} })), 60.0, 1.0f);
1444 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 8, {} })), 60.0, 1.0f);
1445 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 10, {} })), 120.0, 1.0f);
1446
1447 const BeatRange beatRangeToCopy (ts.toBeats ({ 6, {} }), ts.toBeats ({ 8, {} }));
1448 const auto timeRangeToCopy = ts.toTime (beatRangeToCopy);
1449
1450 // Copy tempo changes
1451 Clipboard::TempoChanges tempoChanges (ts, timeRangeToCopy);
1452
1453 // Insert empty space
1454 const auto timeToInsertAt = ts.toTime ({ 2, {} });
1455 auto& tempoAtInsertionPoint = ts.getTempoAt (timeToInsertAt);
1456 const auto beatRangeToInsert = beatRangeToCopy.movedToStartAt (ts.toBeats (timeToInsertAt));
1457 insertSpaceIntoEditFromBeatRange (*edit, beatRangeToInsert);
1458
1459 const auto numBeatsInserted = beatRangeToInsert.getLength();
1460 const int numBarsInserted = juce::roundToInt (numBeatsInserted.inBeats() / tempoAtInsertionPoint.getMatchingTimeSig().denominator);
1461 expectWithinAbsoluteError (numBeatsInserted.inBeats(), 8.0, 0.0001);
1462 expect (numBarsInserted == 2);
1463
1464 // Ensure tempos are correct at original region
1465 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 4 + numBarsInserted, {} })), 120.0, 1.0f);
1466 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 6 + numBarsInserted, {} })), 60.0, 1.0f);
1467 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 8 + numBarsInserted, {} })), 60.0, 1.0f);
1468 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 10 + numBarsInserted, {} })), 120.0, 1.0f);
1469
1470 // Paste tempo changes
1471 tempoChanges.pasteTempoSequence (ts, TimeRange (timeToInsertAt, ts.toTime (beatRangeToInsert.getEnd())));
1472
1473 // Ensure tempos are correct at inserted region
1474 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 0, {} })), 120.0, 1.0f);
1475 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 1, BeatDuration::fromBeats (3) })), 120.0, 1.0f);
1476 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 2, {} })), 60.0, 1.0f);
1477 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 3, BeatDuration::fromBeats (3) })), 60.0, 1.0f);
1478 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 4, {} })), 120.0, 1.0f);
1479 }
1480 }
1481
1482 void runTrackCopyPasteTests()
1483 {
1484 beginTest ("Root tracks copy/paste");
1485 {
1486 auto edit = Edit::createSingleTrackEdit (*Engine::getEngines()[0]);
1487 edit->ensureNumberOfAudioTracks (3);
1488
1489 {
1490 auto tracks = getAudioTracks(*edit);
1491 juce::UnitTest::expectEquals (tracks.size(), 3);
1492 tracks[0]->setName ("First");
1493 tracks[1]->setName ("Second");
1494 tracks[2]->setName ("Third");
1495
1496 Clipboard::Tracks clipboardTracks;
1497
1498 for (auto at : tracks)
1499 clipboardTracks.tracks.push_back (at->state.createCopy());
1500
1501 // Rename existing tracks for testing later
1502 tracks[0]->setName ("First Original");
1503 tracks[1]->setName ("Second Original");
1504 tracks[2]->setName ("Third Original");
1505
1506 EditInsertPoint insertPoint (*edit);
1507 Clipboard::ContentType::EditPastingOptions opts (*edit, insertPoint);
1508 opts.startTrack = tracks[2];
1509 expect (clipboardTracks.pasteIntoEdit (opts));
1510 }
1511
1512 {
1513 auto tracks = getAudioTracks(*edit);
1514 juce::UnitTest::expectEquals (tracks.size(), 6);
1515 juce::UnitTest::expectEquals<juce::String> (tracks[0]->getName(), "First Original");
1516 juce::UnitTest::expectEquals<juce::String> (tracks[1]->getName(), "Second Original");
1517 juce::UnitTest::expectEquals<juce::String> (tracks[2]->getName(), "Third Original");
1518 juce::UnitTest::expectEquals<juce::String> (tracks[3]->getName(), "First");
1519 juce::UnitTest::expectEquals<juce::String> (tracks[4]->getName(), "Second");
1520 juce::UnitTest::expectEquals<juce::String> (tracks[5]->getName(), "Third");
1521 }
1522 }
1523
1524 beginTest ("Root tracks paste in to folder");
1525 {
1526 auto edit = Edit::createSingleTrackEdit (*Engine::getEngines()[0]);
1527
1528 {
1529 edit->ensureNumberOfAudioTracks (3);
1530 auto tracks = getAudioTracks (*edit);
1531 juce::UnitTest::expectEquals (tracks.size(), 3);
1532 tracks[0]->setName ("First");
1533 tracks[1]->setName ("Second");
1534 tracks[2]->setName ("Third");
1535 }
1536
1537 auto ft = edit->insertNewFolderTrack (TrackInsertPoint ({ *getAudioTracks (*edit).getLast(), false }), nullptr, false);
1538
1539 {
1540 Clipboard::Tracks clipboardTracks;
1541 auto tracks = getAudioTracks (*edit);
1542
1543 for (auto at : tracks)
1544 clipboardTracks.tracks.push_back (at->state.createCopy());
1545
1546 // Rename existing tracks for testing later
1547 tracks[0]->setName ("First Original");
1548 tracks[1]->setName ("Second Original");
1549 tracks[2]->setName ("Third Original");
1550
1551 EditInsertPoint insertPoint (*edit);
1552 Clipboard::ContentType::EditPastingOptions opts (*edit, insertPoint);
1553 opts.startTrack = ft;
1554 expect (clipboardTracks.pasteIntoEdit (opts));
1555 }
1556
1557 {
1558 auto tracks = getAudioTracks (*edit);
1559 juce::UnitTest::expectEquals (tracks.size(), 6);
1560 juce::UnitTest::expectEquals<juce::String> (tracks[0]->getName(), "First Original");
1561 juce::UnitTest::expectEquals<juce::String> (tracks[1]->getName(), "Second Original");
1562 juce::UnitTest::expectEquals<juce::String> (tracks[2]->getName(), "Third Original");
1563 juce::UnitTest::expectEquals<juce::String> (tracks[3]->getName(), "First");
1564 juce::UnitTest::expectEquals<juce::String> (tracks[4]->getName(), "Second");
1565 juce::UnitTest::expectEquals<juce::String> (tracks[5]->getName(), "Third");
1566 }
1567 }
1568
1569 beginTest ("Tracks inside folder copy/paste");
1570 {
1571 auto edit = Edit::createSingleTrackEdit (*Engine::getEngines()[0]);
1572
1573 {
1574 edit->ensureNumberOfAudioTracks (3);
1575 auto tracks = getAudioTracks(*edit);
1576 juce::UnitTest::expectEquals (tracks.size(), 3);
1577 tracks[0]->setName ("First");
1578 tracks[1]->setName ("Second");
1579 tracks[2]->setName ("Third");
1580 }
1581
1582 auto ft = edit->insertNewFolderTrack (TrackInsertPoint ({}), nullptr, false);
1583
1584 {
1585 auto tracks = getAudioTracks(*edit);
1586 edit->moveTrack (tracks[2], TrackInsertPoint (ft.get(), nullptr));
1587 edit->moveTrack (tracks[1], TrackInsertPoint (ft.get(), nullptr));
1588 edit->moveTrack (tracks[0], TrackInsertPoint (ft.get(), nullptr));
1589 }
1590
1591 {
1592 auto subTracks = ft->getInputTracks();
1593 juce::UnitTest::expectEquals (subTracks.size(), 3);
1594 juce::UnitTest::expectEquals<juce::String> (subTracks[0]->getName(), "First");
1595 juce::UnitTest::expectEquals<juce::String> (subTracks[1]->getName(), "Second");
1596 juce::UnitTest::expectEquals<juce::String> (subTracks[2]->getName(), "Third");
1597 }
1598
1599 {
1600 Clipboard::Tracks clipboardTracks;
1601
1602 for (auto at : ft->getInputTracks())
1603 clipboardTracks.tracks.push_back (at->state.createCopy());
1604
1605 // Rename existing tracks for testing later
1606 auto subTracks = ft->getInputTracks();
1607 subTracks[0]->setName ("First Original");
1608 subTracks[1]->setName ("Second Original");
1609 subTracks[2]->setName ("Third Original");
1610
1611 EditInsertPoint insertPoint (*edit);
1612 Clipboard::ContentType::EditPastingOptions opts (*edit, insertPoint);
1613 opts.startTrack = subTracks[2];
1614 expect (clipboardTracks.pasteIntoEdit (opts));
1615 }
1616
1617 {
1618 auto subTracks = ft->getInputTracks();
1619 juce::UnitTest::expectEquals (subTracks.size(), 6);
1620 juce::UnitTest::expectEquals<juce::String> (subTracks[0]->getName(), "First Original");
1621 juce::UnitTest::expectEquals<juce::String> (subTracks[1]->getName(), "Second Original");
1622 juce::UnitTest::expectEquals<juce::String> (subTracks[2]->getName(), "Third Original");
1623 juce::UnitTest::expectEquals<juce::String> (subTracks[3]->getName(), "First");
1624 juce::UnitTest::expectEquals<juce::String> (subTracks[4]->getName(), "Second");
1625 juce::UnitTest::expectEquals<juce::String> (subTracks[5]->getName(), "Third");
1626 }
1627 }
1628 }
1629};
1630
1631static ClipboardTempoTests clipboardTempoTests;
1632
1633#endif
1634
1635//==============================================================================
1636//==============================================================================
1637Clipboard::AutomationPoints::AutomationPoints (const AutomationCurve& curve, TimeRange range)
1638{
1639 valueRange = curve.getValueLimits();
1640
1641 bool pointAtStart = false;
1642 bool pointAtEnd = false;
1643
1644 for (int i = 0; i < curve.getNumPoints(); ++i)
1645 {
1646 auto p = curve.getPoint (i);
1647
1648 if (p.time == range.getStart()) pointAtStart = true;
1649 if (p.time == range.getEnd()) pointAtEnd = true;
1650
1651 if (range.containsInclusive (p.time))
1652 {
1653 p.time = p.time - TimeDuration::fromSeconds (range.getStart().inSeconds());
1654 points.push_back (p);
1655 }
1656 }
1657
1658 if (! pointAtStart)
1659 points.insert (points.begin(), AutomationCurve::AutomationPoint (TimePosition(), curve.getValueAt (range.getStart()), 0));
1660
1661 if (! pointAtEnd)
1662 points.push_back (AutomationCurve::AutomationPoint (TimePosition::fromSeconds (range.getLength().inSeconds()), curve.getValueAt (range.getEnd()), 0));
1663}
1664
1665Clipboard::AutomationPoints::~AutomationPoints() {}
1666
1667bool Clipboard::AutomationPoints::pasteIntoEdit (const EditPastingOptions&) const
1668{
1669 jassertfalse; // TODO: what to do here?
1670 return false;
1671}
1672
1673bool Clipboard::AutomationPoints::pasteAutomationCurve (AutomationCurve& targetCurve, TimeRange targetRange) const
1674{
1675 return pastePointsToCurve (points, valueRange, targetCurve, targetRange);
1676}
1677
1678//==============================================================================
1679//==============================================================================
1680Clipboard::MIDIEvents::MIDIEvents() {}
1681Clipboard::MIDIEvents::~MIDIEvents() {}
1682
1683std::pair<juce::Array<MidiNote*>, juce::Array<MidiControllerEvent*>> Clipboard::MIDIEvents::pasteIntoClip (MidiClip& clip,
1684 const juce::Array<MidiNote*>& selectedNotes,
1685 const juce::Array<MidiControllerEvent*>& selectedEvents,
1686 TimePosition cursorPosition, const std::function<BeatPosition (BeatPosition)>& snapBeat,
1687 int destController) const
1688{
1689 auto notesAdded = pasteNotesIntoClip (clip, selectedNotes, cursorPosition, snapBeat);
1690 auto controllersAdded = pasteControllersIntoClip (clip, selectedNotes, selectedEvents, cursorPosition, snapBeat, destController);
1691
1692 return { notesAdded, controllersAdded };
1693}
1694
1695juce::Array<MidiNote*> Clipboard::MIDIEvents::pasteNotesIntoClip (MidiClip& clip, const juce::Array<MidiNote*>& selectedNotes,
1696 TimePosition cursorPosition, const std::function<BeatPosition (BeatPosition)>& snapBeat) const
1697{
1698 if (notes.empty())
1699 return {};
1700
1701 juce::Array<MidiNote> midiNotes;
1702
1703 for (auto& n : notes)
1704 midiNotes.add (MidiNote (n));
1705
1706 auto beatRange = midiNotes.getReference (0).getRangeBeats();
1707
1708 for (auto& n : midiNotes)
1709 beatRange = beatRange.getUnionWith (n.getRangeBeats());
1710
1711 BeatPosition insertPos;
1712
1713 if (clip.isLooping())
1714 insertPos = clip.getContentBeatAtTime (cursorPosition) + toDuration (clip.getLoopStartBeats());
1715 else
1716 insertPos = clip.getContentBeatAtTime (cursorPosition);
1717
1718 if (! selectedNotes.isEmpty())
1719 {
1720 BeatPosition endOfSelection;
1721
1722 for (auto n : selectedNotes)
1723 endOfSelection = std::max (endOfSelection, n->getEndBeat());
1724
1725 insertPos = endOfSelection;
1726 }
1727
1728 if (clip.isLooping())
1729 {
1730 const auto offsetBeats = clip.getOffsetInBeats() + toDuration (clip.getLoopStartBeats());
1731
1732 if ((insertPos - offsetBeats) < BeatPosition() || insertPos - offsetBeats >= toPosition (clip.getLoopLengthBeats() - 0.001_bd))
1733 return {};
1734 }
1735 else
1736 {
1737 const auto offsetBeats = clip.getOffsetInBeats();
1738
1739 if ((insertPos - offsetBeats) < BeatPosition() || insertPos - offsetBeats >= toPosition (clip.getLengthInBeats() - 0.001_bd))
1740 return {};
1741 }
1742
1743 auto deltaBeats = insertPos - beatRange.getStart();
1744
1745 if (snapBeat != nullptr)
1746 deltaBeats = toDuration (snapBeat (toPosition (deltaBeats)));
1747
1748 auto& sequence = clip.getSequence();
1749 auto um = &clip.edit.getUndoManager();
1750 juce::Array<MidiNote*> notesAdded;
1751
1752 for (auto& n : midiNotes)
1753 {
1754 n.setStartAndLength (n.getStartBeat() + deltaBeats, n.getLengthBeats(), nullptr);
1755
1756 if (auto note = sequence.addNote (n, um))
1757 notesAdded.add (note);
1758 }
1759
1760 return notesAdded;
1761}
1762
1763juce::Array<MidiControllerEvent*> Clipboard::MIDIEvents::pasteControllersIntoClip (MidiClip& clip,
1764 const juce::Array<MidiNote*>& selectedNotes,
1765 const juce::Array<MidiControllerEvent*>& selectedEvents,
1766 TimePosition cursorPosition, const std::function<BeatPosition (BeatPosition)>& snapBeat,
1767 int destController) const
1768{
1769 if (controllers.empty())
1770 return {};
1771
1773
1774 for (auto& e : controllers)
1775 midiEvents.add (MidiControllerEvent (e));
1776
1777 if (notes.size() > 0)
1778 destController = -1;
1779
1780 juce::Array<int> controllerTypes;
1781 for (auto& e : midiEvents)
1782 controllerTypes.addIfNotAlreadyThere (e.getType());
1783
1784 if (controllerTypes.size() > 1)
1785 destController = -1;
1786
1787 if (destController != -1)
1788 {
1789 for (auto& e : midiEvents)
1790 e.setType (destController, nullptr);
1791
1792 controllerTypes.clear();
1793 controllerTypes.add (destController);
1794 }
1795
1796 auto beatRange = BeatRange (midiEvents.getReference (0).getBeatPosition(), BeatDuration());
1797
1798 for (auto& e : midiEvents)
1799 beatRange = beatRange.getUnionWith (BeatRange (e.getBeatPosition(), BeatDuration()));
1800
1801 BeatPosition insertPos;
1802
1803 if (clip.isLooping())
1804 insertPos = clip.getContentBeatAtTime (cursorPosition) + toDuration (clip.getLoopStartBeats());
1805 else
1806 insertPos = clip.getContentBeatAtTime (cursorPosition);
1807
1808 if (! selectedNotes.isEmpty())
1809 {
1810 BeatPosition endOfSelection;
1811
1812 for (auto n : selectedNotes)
1813 endOfSelection = std::max (endOfSelection, n->getEndBeat());
1814
1815 insertPos = endOfSelection;
1816 }
1817 else if (! selectedEvents.isEmpty())
1818 {
1819 BeatPosition endOfSelection;
1820
1821 for (auto e : selectedEvents)
1822 if (controllerTypes.contains (e->getType()))
1823 endOfSelection = std::max (endOfSelection, e->getBeatPosition());
1824
1825 insertPos = endOfSelection + BeatDuration::fromBeats (1.0);
1826 }
1827
1828 if (clip.isLooping())
1829 {
1830 auto offsetBeats = toDuration (clip.getLoopStartBeats() + clip.getOffsetInBeats());
1831
1832 if (insertPos - offsetBeats < 0_bp || insertPos - offsetBeats >= toPosition (clip.getLoopLengthBeats() - 0.001_bd))
1833 return {};
1834 }
1835 else
1836 {
1837 auto offsetBeats = clip.getOffsetInBeats();
1838
1839 if (insertPos - offsetBeats < 0_bp || insertPos - offsetBeats >= toPosition (clip.getLengthInBeats() - 0.001_bd))
1840 return {};
1841 }
1842
1843 auto deltaBeats = insertPos - beatRange.getStart();
1844
1845 if (snapBeat != nullptr)
1846 deltaBeats = toDuration (snapBeat (toPosition (deltaBeats)));
1847
1848 auto& sequence = clip.getSequence();
1849 auto um = &clip.edit.getUndoManager();
1851
1852 std::vector<juce::ValueTree> itemsToRemove;
1853
1854 for (auto evt : sequence.getControllerEvents())
1855 if (controllerTypes.contains (evt->getType()) && evt->getBeatPosition() >= beatRange.getStart() + deltaBeats && evt->getBeatPosition() <= beatRange.getEnd() + deltaBeats)
1856 itemsToRemove.push_back (evt->state);
1857
1858 for (auto& v : itemsToRemove)
1859 sequence.state.removeChild (v, um);
1860
1861 for (auto& e : midiEvents)
1862 {
1863 e.setBeatPosition (e.getBeatPosition() + deltaBeats, um);
1864
1865 if (auto evt = sequence.addControllerEvent (e, um))
1866 eventsAdded.add (evt);
1867 }
1868
1869 return eventsAdded;
1870}
1871
1872bool Clipboard::MIDIEvents::pasteIntoEdit (const EditPastingOptions&) const
1873{
1874 return false;
1875}
1876
1877//==============================================================================
1878namespace
1879{
1880 static TimePosition snapTimeToNearestBeat (Edit& e, TimePosition t)
1881 {
1882 return TimecodeSnapType::get1BeatSnapType().roundTimeNearest (t, e.tempoSequence);
1883 }
1884}
1885
1886//==============================================================================
1887//==============================================================================
1888Clipboard::Pitches::Pitches() {}
1889Clipboard::Pitches::~Pitches() {}
1890
1891bool Clipboard::Pitches::pasteIntoEdit (const EditPastingOptions& options) const
1892{
1893 if (pitches.empty())
1894 return false;
1895
1896 if (options.selectionManager != nullptr)
1897 options.selectionManager->deselectAll();
1898
1899 auto startBeat = options.edit.tempoSequence.toBeats (snapTimeToNearestBeat (options.edit, options.startTime));
1900 auto firstPitchBeat = BeatPosition::fromBeats (static_cast<double> (pitches.front().getProperty (IDs::startBeat)));
1901 auto offset = startBeat - firstPitchBeat;
1902 auto um = &options.edit.getUndoManager();
1903
1904 for (auto& state : pitches)
1905 {
1906 auto time = options.edit.tempoSequence.toTime (BeatPosition::fromBeats (static_cast<double> (state.getProperty (IDs::startBeat))) + offset);
1907
1908 if (auto pitch = options.edit.pitchSequence.insertPitch (time))
1909 {
1910 jassert (pitch->state.getNumChildren() == 0); // this would need handling
1911
1912 copyValueTreeProperties (pitch->state, state, um,
1913 [] (const juce::Identifier& name) { return name != IDs::startBeat; });
1914
1915 if (options.selectionManager != nullptr)
1916 options.selectionManager->addToSelection (*pitch);
1917 }
1918 }
1919
1920 return true;
1921}
1922
1923//==============================================================================
1924//==============================================================================
1925Clipboard::TimeSigs::TimeSigs() {}
1926Clipboard::TimeSigs::~TimeSigs() {}
1927
1928bool Clipboard::TimeSigs::pasteIntoEdit (const EditPastingOptions& options) const
1929{
1930 if (timeSigs.empty())
1931 return false;
1932
1933 if (options.selectionManager != nullptr)
1934 options.selectionManager->deselectAll();
1935
1936 auto startBeat = options.edit.tempoSequence.toBeats (snapTimeToNearestBeat (options.edit, options.startTime));
1937 auto firstTimeSigBeat = BeatPosition::fromBeats (static_cast<double> (timeSigs.front().getProperty (IDs::startBeat)));
1938 auto offset = startBeat - firstTimeSigBeat;
1939 auto um = &options.edit.getUndoManager();
1940
1941 for (auto& state : timeSigs)
1942 {
1943 auto time = options.edit.tempoSequence.toTime (BeatPosition::fromBeats (static_cast<double> (state.getProperty (IDs::startBeat))) + offset);
1944
1945 if (auto timeSig = options.edit.tempoSequence.insertTimeSig (time))
1946 {
1947 jassert (timeSig->state.getNumChildren() == 0); // this would need handling
1948 copyValueTreeProperties (timeSig->state, state, um,
1949 [] (const juce::Identifier& name) { return name != IDs::startBeat; });
1950
1951 if (options.selectionManager != nullptr)
1952 options.selectionManager->addToSelection (*timeSig);
1953 }
1954 }
1955
1956 return true;
1957}
1958
1959//==============================================================================
1960//==============================================================================
1961Clipboard::Plugins::Plugins (const Plugin::Array& items)
1962{
1963 for (auto& item : items)
1964 {
1965 item->edit.flushPluginStateIfNeeded (*item);
1966 plugins.push_back (item->state.createCopy());
1967
1968 if (auto rackInstance = dynamic_cast<RackInstance*> (item))
1969 {
1970 if (auto type = rackInstance->type)
1971 {
1972 auto newEntry = std::make_pair (makeSafeRef (type->edit), type->state);
1973
1974 if (std::find (rackTypes.begin(), rackTypes.end(), newEntry) == rackTypes.end())
1975 rackTypes.push_back (newEntry);
1976 }
1977 }
1978 }
1979}
1980
1981Clipboard::Plugins::~Plugins() {}
1982
1983static bool pastePluginBasedOnSelection (Edit& edit, const Plugin::Ptr& newPlugin,
1984 SelectionManager* selectionManager)
1985{
1986 if (selectionManager == nullptr)
1987 return false;
1988
1989 if (RackType::Ptr selectedRack = selectionManager->getFirstItemOfType<RackType>())
1990 {
1991 selectedRack->addPlugin (newPlugin, { 0.5f, 0.5f }, false);
1992 return true;
1993 }
1994
1995 if (Plugin::Ptr selectedPlugin = selectionManager->getFirstItemOfType<Plugin>())
1996 {
1997 if (auto list = selectedPlugin->getOwnerList())
1998 {
1999 auto index = list->indexOf (selectedPlugin.get());
2000
2001 if (index >= 0)
2002 {
2003 list->insertPlugin (newPlugin, index, selectionManager);
2004 return true;
2005 }
2006 }
2007
2008 if (auto selectedRack = edit.getRackList().findRackContaining (*selectedPlugin))
2009 {
2010 selectedRack->addPlugin (newPlugin, { 0.5f, 0.5f }, false);
2011 return true;
2012 }
2013 }
2014
2015 if (auto selectedClip = selectionManager->getFirstItemOfType<Clip>())
2016 if (selectedClip->addClipPlugin (newPlugin, *selectionManager))
2017 return true;
2018
2019 return false;
2020}
2021
2022static bool pastePluginIntoTrack (const Plugin::Ptr& newPlugin, EditInsertPoint& insertPoint, SelectionManager* sm)
2023{
2024 TimePosition startPos;
2025 Track::Ptr track;
2026 insertPoint.chooseInsertPoint (track, startPos, false, sm,
2027 [] (auto& t) { return t.isAudioTrack() || t.isFolderTrack() || t.isMasterTrack(); });
2028 jassert (track != nullptr);
2029
2030 if (track != nullptr && track->canContainPlugin (newPlugin.get()))
2031 {
2032 track->pluginList.insertPlugin (newPlugin, 0, sm);
2033 return true;
2034 }
2035
2036 return false;
2037}
2038
2039static EditItemID::IDMap pasteRackTypesInToEdit (Edit& edit, const std::vector<std::pair<SafeSelectable<Edit>, juce::ValueTree>>& editAndTypeStates)
2040{
2041 EditItemID::IDMap reassignedIDs;
2042
2043 for (const auto& editAndTypeState : editAndTypeStates)
2044 {
2045 if (editAndTypeState.first == &edit)
2046 continue;
2047
2048 auto typeState = editAndTypeState.second;
2049 auto reassignedRackType = typeState.createCopy();
2050 EditItemID::remapIDs (reassignedRackType, nullptr, edit, &reassignedIDs);
2051 edit.getRackList().addRackTypeFrom (reassignedRackType);
2052 }
2053
2054 return reassignedIDs;
2055}
2056
2057bool Clipboard::Plugins::pasteIntoEdit (const EditPastingOptions& options) const
2058{
2060 bool anyPasted = false;
2061
2062 auto rackIDMap = pasteRackTypesInToEdit (options.edit, rackTypes);
2063
2064 auto pluginsToPaste = plugins;
2065 std::reverse (pluginsToPaste.begin(), pluginsToPaste.end()); // Reverse the array so they get pasted in the correct order
2066
2067 for (auto& item : pluginsToPaste)
2068 {
2069 auto stateCopy = item.createCopy();
2070 EditItemID::remapIDs (stateCopy, nullptr, options.edit);
2071
2072 // Remap RackTypes after the otehr IDs or it will get overwritten
2073 if (stateCopy[IDs::type].toString() == IDs::rack.toString())
2074 {
2075 auto oldRackID = EditItemID::fromProperty (stateCopy, IDs::rackType);
2076 auto remappedRackID = rackIDMap[oldRackID];
2077
2078 if (remappedRackID.isValid())
2079 stateCopy.setProperty (IDs::rackType, remappedRackID, nullptr);
2080 }
2081
2082 if (auto newPlugin = options.edit.getPluginCache().getOrCreatePluginFor (stateCopy))
2083 {
2084 if (pastePluginBasedOnSelection (options.edit, newPlugin, options.selectionManager)
2085 || pastePluginIntoTrack (newPlugin, options.insertPoint, options.selectionManager))
2086 {
2087 anyPasted = true;
2088
2089 // If we've pasted a plugin into a different track, see if it should still be under modifier control
2090 if (auto track = newPlugin->getOwnerTrack())
2091 {
2092 for (auto param : newPlugin->getAutomatableParameters())
2093 {
2094 auto assignments = param->getAssignments();
2095
2096 for (int i = assignments.size(); --i >= 0;)
2097 {
2098 auto ass = assignments.getUnchecked (i);
2099
2100 if (auto mpa = dynamic_cast<MacroParameter::Assignment*> (ass.get()))
2101 {
2102 if (auto mp = getMacroParameterForID (options.edit, mpa->macroParamID))
2103 if (auto t = mp->getTrack())
2104 if (! (t == track || track->isAChildOf (*t)))
2105 param->removeModifier (*ass);
2106 }
2107 else
2108 {
2109 if (auto t = getTrackContainingModifier (options.edit,
2110 findModifierForID (options.edit, EditItemID::fromProperty (ass->state, IDs::source))))
2111 if (! (t == track || TrackList::isFixedTrack (t->state) || track->isAChildOf (*t)))
2112 param->removeModifier (*ass);
2113 }
2114 }
2115 }
2116 }
2117 }
2118 }
2119 }
2120
2121 return anyPasted;
2122}
2123
2124//==============================================================================
2125//==============================================================================
2126Clipboard::Takes::Takes (const WaveCompManager& waveCompManager)
2127{
2128 items = waveCompManager.getActiveTakeTree().createCopy();
2129}
2130
2131Clipboard::Takes::~Takes() {}
2132
2133bool Clipboard::Takes::pasteIntoClip (WaveAudioClip& c) const
2134{
2135 if (items.isValid())
2136 return c.getCompManager().pasteComp (items).isValid();
2137
2138 return false;
2139}
2140
2141//==============================================================================
2142//==============================================================================
2143Clipboard::Modifiers::Modifiers() {}
2144Clipboard::Modifiers::~Modifiers() {}
2145
2146bool Clipboard::Modifiers::pasteIntoEdit (const EditPastingOptions& options) const
2147{
2148 if (modifiers.empty())
2149 return false;
2150
2151 if (options.selectionManager != nullptr)
2152 {
2153 if (auto firstSelectedMod = options.selectionManager->getFirstItemOfType<Modifier>())
2154 {
2155 if (auto t = getTrackContainingModifier (options.edit, firstSelectedMod))
2156 {
2157 if (auto modifierList = t->getModifierList())
2158 {
2159 auto modList = getModifiersOfType<Modifier> (*modifierList);
2160
2161 for (int i = modList.size(); --i >= 0;)
2162 {
2163 if (modList.getObjectPointer (i) == firstSelectedMod)
2164 {
2165 for (auto m : modifiers)
2166 {
2167 EditItemID::remapIDs (m, nullptr, options.edit);
2168 modifierList->insertModifier (m, i + 1, options.selectionManager);
2169 }
2170
2171 return true;
2172 }
2173 }
2174 }
2175 }
2176 }
2177 }
2178
2179 if (options.startTrack != nullptr)
2180 {
2181 if (auto modifierList = options.startTrack->getModifierList())
2182 {
2183 for (auto m : modifiers)
2184 {
2185 EditItemID::remapIDs (m, nullptr, options.edit);
2186 modifierList->insertModifier (m, -1, options.selectionManager);
2187 }
2188
2189 return true;
2190 }
2191 }
2192
2193 return false;
2194}
2195
2196}} // namespace tracktion { inline namespace engine
T begin(T... args)
T ceil(T... args)
ElementType getUnchecked(int index) const
bool isEmpty() const noexcept
int size() const noexcept
ElementType getFirst() const noexcept
void add(const ElementType &newElement)
void clear()
ElementType & getReference(int index) noexcept
void addChangeListener(ChangeListener *listener)
void removeChangeListener(ChangeListener *listener)
bool isValid() const noexcept
static LookAndFeel & getDefaultLookAndFeel() noexcept
static ModifierKeys currentModifiers
constexpr ValueType getStart() const noexcept
constexpr ValueType getLength() const noexcept
void expectEquals(ValueType actual, ValueType expected, String failureMessage=String())
void beginTest(const String &testName)
void expectWithinAbsoluteError(ValueType actual, ValueType expected, ValueType maxAbsoluteError, String failureMessage=String())
virtual void runTest()=0
static const char *const bwavTimeReference
The Engine is the central class for all tracktion sessions.
UIBehaviour & getUIBehaviour() const
Returns the UIBehaviour class.
EngineBehaviour & getEngineBehaviour() const
Returns the EngineBehaviour instance.
ProjectManager & getProjectManager() const
Returns the ProjectManager instance.
T end(T... args)
T find(T... args)
T floor(T... args)
T insert(T... args)
#define TRANS(stringLiteral)
#define jassert(expression)
#define jassertfalse
#define JUCE_IMPLEMENT_SINGLETON(Classname)
typedef int
T make_pair(T... args)
T max(T... args)
T min(T... args)
constexpr auto enumerate(Range &&range, Index startingValue={})
int roundToInt(const FloatType value) noexcept
void insertSpaceIntoEdit(Edit &edit, TimeRange timeRange)
Inserts blank space in to an Edit, splitting clips if necessary.
juce::String getName(LaunchQType t)
Retuns the name of a LaunchQType for display purposes.
SelectableList getClipSelectionWithCollectionClipContents(const SelectableList &in)
Returns a list of clips.
void insertSpaceIntoEditFromBeatRange(Edit &edit, BeatRange beatRange)
Inserts a number of blank beats in to the Edit.
juce::Array< Track * > getAllTracks(const Edit &edit)
Returns all the tracks in an Edit.
int findClipSlotIndex(ClipSlot &slot)
Returns the index of the ClipSlot in the list it is owned by.
juce::Array< AudioTrack * > getAudioTracks(const Edit &edit)
Returns all the AudioTracks in an Edit.
Modifier::Ptr findModifierForID(ModifierList &ml, EditItemID modifierID)
Returns a Modifier if it can be found in the list.
Clip * insertClipWithState(ClipOwner &clipOwner, juce::ValueTree clipState)
Inserts a clip with the given state in to the ClipOwner's clip list.
RangeType< BeatPosition > BeatRange
A RangeType based on beats.
constexpr TimePosition toPosition(TimeDuration)
Converts a TimeDuration to a TimePosition.
RangeType< TimePosition > TimeRange
A RangeType based on real time (i.e.
constexpr TimeDuration toDuration(TimePosition)
Converts a TimePosition to a TimeDuration.
T push_back(T... args)
T reverse(T... args)
T sort(T... args)
time
#define CRASH_TRACER
This macro adds the current location to a stack which gets logged if a crash happens.