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

« « « Anklang Documentation
Loading...
Searching...
No Matches
tracktion_CompManager.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
11#include "../../playback/audionodes/tracktion_AudioNode.h"
12#include "../../playback/audionodes/tracktion_WaveAudioNode.h"
13#include "../../playback/audionodes/tracktion_FadeInOutAudioNode.h"
14#include "../../playback/audionodes/tracktion_CombiningAudioNode.h"
15
16
17namespace tracktion { inline namespace engine
18{
19
21 private juce::Timer
22{
23 RenderTrigger (CompManager& o) : owner (o) { owner.takesTree.addListener (this); }
24 ~RenderTrigger() override { owner.takesTree.removeListener (this); }
25
26 void trigger() { startTimer (10); }
27 void valueTreeChanged() override { trigger(); }
28
29 void timerCallback() override
30 {
31 if (juce::Component::isMouseButtonDownAnywhere())
32 return;
33
34 stopTimer();
35 owner.triggerCompRender();
36 }
37
38 CompManager& owner;
40};
41
42//==============================================================================
44 : takesTree (v), clip (c)
45{
46 clip.edit.engine.getCompFactory().addComp (*this);
47
48 renderTrigger = std::make_unique<RenderTrigger> (*this);
49
50 lastOffset = clip.getPosition().getOffset().inSeconds();
51 lastTimeRatio = getSourceTimeMultiplier();
52}
53
55{
56 takesTree.removeListener (this);
57 clip.state.removeListener (this);
58 clip.edit.engine.getCompFactory().removeComp (*this);
59}
60
61void CompManager::initialise()
62{
63 jassert (takesTree.getParent() == clip.state);
64
65 refreshCachedTakeLengths();
66 lastRenderedTake = clip.getCurrentTake();
67 setActiveTakeIndex (lastRenderedTake);
68 lastHash = getTakeHash (lastRenderedTake);
69
70 lastOffset = getOffset();
71 lastTimeRatio = getSourceTimeMultiplier();
72
73 takesTree.addListener (this);
74 addOrRemoveListenerIfNeeded();
75}
76
77//==============================================================================
78juce::ValueTree CompManager::getSection (int takeIndex, int sectionIndex) const
79{
80 return takesTree.getChild (takeIndex).getChild (sectionIndex);
81}
82
84{
85 auto activeTake = getActiveTakeTree();
86
87 if (! isTakeComp (activeTake))
88 return activeTake;
89
90 // Could use a binary chop search here for speed
91 auto numSections = activeTake.getNumChildren();
92
93 for (int i = 0; i < numSections; ++i)
94 {
95 auto section = activeTake.getChild (i);
96
97 if (time < double (section.getProperty (IDs::endTime)))
98 return section;
99 }
100
101 return {};
102}
103
104int CompManager::findSectionWithEndTime (juce::Range<double> timeRange, int takeIndex, bool& timeFoundAtStartOfSection) const
105{
106 auto activeTree = getActiveTakeTree();
107 auto numSections = activeTree.getNumChildren();
108
109 if (numSections == 0)
110 return -1;
111
112 auto section = activeTree.getChild (0);
113 int index = int (section.getProperty (IDs::takeIndex));
114
115 for (int i = 0; i < numSections; ++i)
116 {
117 double endTime = section.getProperty (IDs::endTime);
118 auto nextSection = activeTree.getChild (i + 1);
119 int nextSectionIndex = nextSection.getProperty (IDs::takeIndex);
120
121 if (timeRange.contains (endTime))
122 {
123 if (takeIndex == -1 || takeIndex == index || takeIndex == nextSectionIndex)
124 {
125 timeFoundAtStartOfSection = (takeIndex == nextSectionIndex);
126 return i;
127 }
128 }
129
130 section = nextSection;
131 index = nextSectionIndex;
132 }
133
134 return -1;
135}
136
138{
139 jassert (section.hasType (IDs::COMPSECTION));
140
141 return { section.getSibling (-1).getProperty (IDs::endTime, 0.0),
142 section.getProperty (IDs::endTime) };
143}
144
145//==============================================================================
147{
148 jassert (index < takesTree.getNumChildren());
149
150 if (! juce::isPositiveAndBelow (index, takesTree.getNumChildren()))
151 return;
152
153 if (getActiveTakeIndex() != index)
154 clip.setCurrentTake (index);
155
156 if (isTakeComp (index))
158}
159
161{
162 for (int i = 0; i < takesTree.getNumChildren(); ++i)
163 if (isTakeComp (takesTree.getChild (i)))
164 return i;
165
166 return takesTree.getNumChildren();
167}
168
169//==============================================================================
171{
172 return clip.getTakeDescriptions()[index].fromFirstOccurrenceOf (". ", false, false);
173}
174
175//==============================================================================
177{
178 auto endTime = clip.isLooping() ? getLoopLength()
180 return juce::Range<double> (0.0, endTime) - getOffset();
181}
182
184{
185 if (effectiveTimeMultiplier == 0.0)
186 return 1.0;
187
188 return 1.0 / effectiveTimeMultiplier;
189}
190
191HashCode CompManager::getTakeHash (int takeIndex) const
192{
193 auto takeTree = takesTree.getChild (takeIndex);
194
195 if (! takeTree.isValid())
196 return -1;
197
198 auto hash = getBaseTakeHash (takeIndex);
199 auto lastTime = -getOffset();
200
201 for (int i = takeTree.getNumChildren(); --i >= 0;)
202 {
203 auto segment = takeTree.getChild (i);
204 auto end = static_cast<double> (segment.getProperty (IDs::endTime));
205 auto take = static_cast<int> (segment.getProperty (IDs::takeIndex));
206
207 hash = hash
208 ^ static_cast<HashCode> (take)
209 ^ static_cast<HashCode> ((end - lastTime) * 1000.0);
210
211 lastTime = end;
212 }
213
214 return hash;
215}
216
217//==============================================================================
218void CompManager::changeSectionIndexAtTime (double time, int takeIndex)
219{
220 auto section = findSectionAtTime (time);
221
222 if (! section.isValid())
223 return;
224
225 if (section.hasType (IDs::TAKE))
226 setActiveTakeIndex (takeIndex);
227 else if (section.hasType (IDs::COMPSECTION))
228 section.setProperty (IDs::takeIndex, takeIndex, getUndoManager());
229
230 clip.changed();
231}
232
233void CompManager::removeSectionIndexAtTime (double time, int takeIndex)
234{
235 auto section = findSectionAtTime (time);
236 const int sectionIndex = section.getProperty (IDs::takeIndex);
237
238 if (! section.hasType (IDs::COMPSECTION)
239 || (takeIndex != -1 && sectionIndex != takeIndex))
240 return;
241
242 auto um = getUndoManager();
243 auto previous = section.getSibling (-1);
244 auto next = section.getSibling (1);
245 bool remove = false;
246
247 // First check if we need to remove two sections
248 if (next.isValid() && previous.isValid())
249 {
250 if (int (previous.getProperty (IDs::takeIndex)) == sectionIndex
251 && int (next.getProperty (IDs::takeIndex)) == sectionIndex)
252 {
253 section.getParent().removeChild (section, um);
254 previous.getParent().removeChild (previous, um);
255 clip.changed();
256 return;
257 }
258 }
259
260 // Otherwise we have to modify adjacent sections
261 if (next.isValid())
262 {
263 if (int (next.getProperty (IDs::takeIndex)) != -1)
264 section.setProperty (IDs::takeIndex, -1, um);
265 else
266 remove = true;
267 }
268 else if (previous.isValid())
269 {
270 remove = true;
271 }
272
273 if (remove)
274 section.getParent().removeChild (section, um);
275
276 clip.changed();
277}
278
279//==============================================================================
281{
282 jassert (section.hasType (IDs::COMPSECTION));
283
284 newTime = getCompRange().clipValue (newTime);
285 const double currentEndTime = section.getProperty (IDs::endTime);
286
287 if (currentEndTime != newTime)
288 {
289 removeSectionsWithinRange (juce::Range<double>::between (currentEndTime, newTime), section);
290 section.setProperty (IDs::endTime, newTime, getUndoManager());
291 }
292}
293
294void CompManager::moveSection (juce::ValueTree& section, double timeDelta)
295{
296 const double currentEndTime = section.getProperty (IDs::endTime);
297 moveSectionToEndAt (section, currentEndTime + timeDelta);
298}
299
300void CompManager::moveSectionToEndAt (juce::ValueTree& section, double newEndTime)
301{
302 jassert (section.hasType (IDs::COMPSECTION));
303 newEndTime = getCompRange().clipValue (newEndTime);
304
305 const double currentEndTime = section.getProperty (IDs::endTime);
306 auto previous = section.getSibling (-1);
307 const double previousEndTime = previous.getProperty (IDs::endTime);
308 const double timeDelta = newEndTime - currentEndTime;
309
310 if (newEndTime > currentEndTime)
311 {
312 moveSectionEndTime (section, newEndTime);
313
314 if (previous.isValid())
315 moveSectionEndTime (previous, previousEndTime + timeDelta);
316 }
317 else if (newEndTime < currentEndTime)
318 {
319 if (previous.isValid())
320 moveSectionEndTime (previous, previousEndTime + timeDelta);
321
322 moveSectionEndTime (section, newEndTime);
323 }
324}
325
326juce::ValueTree CompManager::addSection (int takeIndex, double endTime)
327{
329 addNewComp();
330
332 {
334 return {};
335 }
336
337 auto activeTake = getActiveTakeTree();
338 jassert (activeTake.isValid());
339 int insertIndex = -1;
340
341 for (const auto& section : activeTake)
342 {
343 ++insertIndex;
344 const double sectionEnd = section.getProperty (IDs::endTime);
345
346 if (sectionEnd > endTime)
347 break;
348 }
349
350 auto* um = getUndoManager();
351 juce::ValueTree newSection (IDs::COMPSECTION);
352 newSection.setProperty (IDs::takeIndex, takeIndex, um);
353 newSection.setProperty (IDs::endTime, endTime, um);
354 activeTake.addChild (newSection, insertIndex, um);
355
356 return newSection;
357}
358
360{
361 jassert (section.hasType (IDs::COMPSECTION));
362 section.getParent().removeChild (section, getUndoManager());
363}
364
366{
367 auto takeTree = getActiveTakeTree();
368 jassert (takeTree.hasType (IDs::TAKE) && isTakeComp (takeTree));
369
370 for (int i = takeTree.getNumChildren(); --i >= 0;)
371 {
372 auto section = takeTree.getChild (i);
373 const double endTime = section.getProperty (IDs::endTime);
374
375 if (timeRange.getStart() <= endTime && endTime <= timeRange.getEnd())
376 {
377 if (section != sectionToKeep)
378 {
379 // Set the take index of the next section or it will jump as we remove the section
380 if (sectionToKeep.isValid() && double (sectionToKeep[IDs::endTime]) > endTime)
381 {
382 auto next = section.getSibling (1);
383
384 if (next.isValid())
385 next.setProperty (IDs::takeIndex, section[IDs::takeIndex], getUndoManager());
386 }
387
388 removeSection (section);
389 }
390 }
391
392 if (endTime < timeRange.getStart())
393 break;
394 }
395}
396
398{
399 auto existingSection = findSectionAtTime (time);
400
401 if (! existingSection.isValid())
402 return {};
403
404 auto currentSectionIndex = existingSection.getParent().indexOf (existingSection);
405
406 auto* um = getUndoManager();
407 auto newSection = existingSection.createCopy();
408 newSection.setProperty (IDs::endTime, time, um);
409 existingSection.getParent().addChild (newSection, currentSectionIndex, um);
410
411 return newSection;
412}
413
414//==============================================================================
415juce::UndoManager* CompManager::getUndoManager() const
416{
417 return &clip.edit.getUndoManager();
418}
419
420void CompManager::keepSectionsSortedAndInRange()
421{
422 auto activeTakeIndex = getActiveTakeIndex();
423 auto takeTree = takesTree.getChild (activeTakeIndex);
424
425 if (! takeTree.isValid())
426 return;
427
428 auto compRange = getCompRange();
429
430 if (compRange.getLength() > 0.0)
431 {
432 // first remove all out of bounds
433 for (int i = takeTree.getNumChildren(); --i >= 0;)
434 {
435 auto sectionTimes = getSectionTimes (takeTree.getChild (i));
436
437 if (! (sectionTimes.getStart() < compRange.getEnd() && compRange.getStart() < sectionTimes.getEnd())
438 || sectionTimes.getEnd() < compRange.getStart())
439 {
440 takeTree.removeChild (i, getUndoManager());
441 }
442 }
443
444 // then truncate the last
445 const int lastSectionIndex = takeTree.getNumChildren() - 1;
446 auto lastSection = takeTree.getChild (lastSectionIndex);
447
448 if (lastSection.isValid())
449 {
450 auto sectionTimes = getSectionTimes (lastSection);
451
452 if (sectionTimes.getEnd() > compRange.getEnd())
453 lastSection.setProperty (IDs::endTime, compRange.getEnd(), getUndoManager());
454 }
455 }
456
457 struct EndTimeSorter
458 {
459 static int compareElements (const juce::ValueTree& first, const juce::ValueTree& second)
460 {
461 const double firstTime = first[IDs::endTime];
462 const double secondTime = second[IDs::endTime];
463
464 return secondTime < firstTime ? 1 : (secondTime > firstTime ? -1 : 0);
465 }
466 };
467
468 EndTimeSorter sorter;
469 takeTree.sort (sorter, getUndoManager(), false);
470}
471
472juce::ValueTree CompManager::getNewCompTree() const
473{
474 juce::ValueTree newTake (IDs::TAKE);
475
476 // Need to give the comp a blank section to draw onto
477 auto newSection = createValueTree (IDs::COMPSECTION,
478 IDs::takeIndex, -1,
479 IDs::endTime, getMaxCompLength());
480
481 newTake.addChild (newSection, -1, nullptr);
482
483 return newTake;
484}
485
486void CompManager::reCacheInfo()
487{
488 refreshCachedTakeLengths();
489 updateOffsetAndRatioFromSource();
490 addOrRemoveListenerIfNeeded();
491}
492
493void CompManager::refreshCachedTakeLengths()
494{
495 displayWarning = false;
496 const bool autoTempo = getAutoTempo();
497
498 double speedRatio = 1.0;
499 double maxSourceLength = 0.0;
500
501 for (int i = getNumTakes(); --i >= 0;)
502 maxSourceLength = std::max (maxSourceLength, getTakeLength (i));
503
504 if (autoTempo)
505 {
506 // need to find the tempo changes between the source start and how long the result will be
507 const double sourceTempo = getSourceTempo();
508 const double sourceStart = clip.getPosition().getStart().inSeconds();
509 const double takeEnd = sourceStart + maxSourceLength - getOffset();
510
511 auto& ts = clip.edit.tempoSequence;
512 auto& tempoSetting = ts.getTempoAt (TimePosition::fromSeconds (sourceStart));
513
514 displayWarning = &tempoSetting != &ts.getTempoAt (TimePosition::fromSeconds (takeEnd));
515 speedRatio = tempoSetting.getBpm() / sourceTempo;
516 }
517 else
518 {
519 speedRatio = std::max (0.001, clip.getSpeedRatio());
520 }
521
522 effectiveTimeMultiplier = 1.0 / speedRatio;
523 maxCompLength = maxSourceLength;
524}
525
526void CompManager::updateOffsetAndRatioFromSource()
527{
528 const double newOffset = getOffset();
529 const double newRatio = getSourceTimeMultiplier();
530
531 if (newOffset == lastOffset && newRatio == lastTimeRatio)
532 return;
533
534 const double ratioDiff = newRatio / lastTimeRatio;
535 const double offsetDiff = lastOffset - newOffset;
536
537 const int numTakes = getNumTakes();
538 const juce::Range<int> compRange (numTakes, numTakes + getNumComps());
539
540 for (int i = compRange.getStart(); i < compRange.getEnd(); ++i)
541 {
542 auto takeTree = takesTree.getChild (i);
543
544 for (int s = takeTree.getNumChildren(); --s >= 0;)
545 {
546 auto segment = takeTree.getChild (s);
547 double oldEnd = segment.getProperty (IDs::endTime);
548
549 double newEnd = (ratioDiff != 1.0) ? ((oldEnd - offsetDiff) * ratioDiff) + offsetDiff
550 : oldEnd + offsetDiff;
551
552 segment.setProperty (IDs::endTime, newEnd, nullptr);
553 }
554 }
555
556 lastOffset = newOffset;
557 lastTimeRatio = newRatio;
558
560}
561
562void CompManager::addOrRemoveListenerIfNeeded()
563{
564 clip.state.removeListener (this);
565
566 if (clip.hasAnyTakes())
567 clip.state.addListener (this);
568}
569
570void CompManager::valueTreePropertyChanged (juce::ValueTree& tree, const juce::Identifier& id)
571{
572 if (tree != clip.state)
573 return;
574
575 if (! clip.hasAnyTakes())
576 return;
577
578 if (id == IDs::start || id == IDs::length || id == IDs::offset
579 || id == IDs::loopStart || id == IDs::loopLength
580 || id == IDs::loopStartBeats || id == IDs::loopLengthBeats
581 || id == IDs::transpose || id == IDs::pitchChange
582 || id == IDs::elastiqueMode || id == IDs::elastiqueOptions
583 || id == IDs::autoPitch || id == IDs::autoTempo
584 || id == IDs::warpTime || id == IDs::speed)
585 {
586 discardCachedData();
587 refreshCachedTakeLengths();
588 updateOffsetAndRatioFromSource();
590 }
591}
592
593//==============================================================================
594CompManager::Ptr CompFactory::getCompManager (Clip& clip)
595{
596 {
597 const juce::ScopedLock sl (compLock);
598
599 for (auto c : comps)
600 if (&c->getClip() == &clip && c->getTakesTree().getParent() == clip.state)
601 return c;
602 }
603
604 if (auto wac = dynamic_cast<WaveAudioClip*> (&clip))
605 return new WaveCompManager (*wac);
606
607 if (auto mc = dynamic_cast<MidiClip*> (&clip))
608 return new MidiCompManager (*mc);
609
611 return {};
612}
613
614void CompFactory::addComp (CompManager& cm)
615{
616 const juce::ScopedLock sl (compLock);
617 jassert (! comps.contains (&cm));
618 comps.addIfNotAlreadyThere (&cm);
619}
620
621void CompFactory::removeComp (CompManager& cm)
622{
623 const juce::ScopedLock sl (compLock);
624 jassert (comps.contains (&cm));
625 comps.removeAllInstancesOf (&cm);
626}
627
628//==============================================================================
630{
631 CompRenderContext (Engine& e, const juce::Array<ProjectItemID> takesIDs_, const juce::ValueTree& takeTree_, int activeTakeIndex_,
632 double sourceTimeMultiplier_, double offset_, double maxLength_, double xFade)
633 : engine (e), takesIDs (takesIDs_), takeTree (takeTree_.createCopy()),
634 activeTakeIndex (activeTakeIndex_),
635 sourceTimeMultiplier (sourceTimeMultiplier_), offset (offset_), maxLength (maxLength_), crossfadeLength (xFade)
636 {
637 }
638
639 Engine& engine;
641 const juce::ValueTree takeTree;
642 const int activeTakeIndex;
643 const double sourceTimeMultiplier, offset, maxLength, crossfadeLength;
644
646};
647
650{
651 FlattenRetrier (CompManager& o, int t, bool delFiles)
652 : owner (o), takeIndex (t), deleteSourceFiles (delFiles)
653 {
654 startTimer (250);
655 }
656
657 void setIndex (int newTakeIndex) noexcept
658 {
659 takeIndex = newTakeIndex;
660 }
661
662private:
663 CompManager& owner;
664 int takeIndex;
665 bool deleteSourceFiles;
666
667 void timerCallback() override
668 {
669 owner.flattenTake (takeIndex, deleteSourceFiles);
670 }
671
673};
674
677{
678 CompUpdater (Clip& c)
679 : clip (c), compFile (c.edit.engine) {}
680
681 void setStrip (juce::Component* s)
682 {
683 if (strip == s)
684 return;
685
686 strip = s;
687 startTimer (updateInterval);
688 shouldStop = false;
689 }
690
691 void setCompFile (const AudioFile& newCompFile)
692 {
693 compFile = newCompFile;
694 startTimer (updateInterval);
695 shouldStop = false;
696 }
697
698private:
699 enum { updateInterval = 40 };
700
701 Clip& clip;
703 AudioFile compFile;
704 bool shouldStop = false;
705
706 void timerCallback() override
707 {
708 if (strip != nullptr)
709 strip->repaint();
710
711 if (shouldStop)
712 {
713 clip.changed();
714 stopTimer();
715 return;
716 }
717
718 if (compFile.getFile() == juce::File()
719 || ! clip.edit.engine.getAudioFileManager().proxyGenerator.isProxyBeingGenerated (compFile))
720 {
721 shouldStop = true;
722 }
723 }
724
726};
727
728//==============================================================================
729WaveCompManager::WaveCompManager (WaveAudioClip& owner)
730 : CompManager (owner, owner.state.getOrCreateChildWithName (IDs::TAKES, &owner.edit.getUndoManager())),
731 clip (owner), lastCompFile (clip.edit.engine), compUpdater (new CompUpdater (clip))
732{
733 for (auto take : takesTree)
734 if (isTakeComp (take))
735 if (! ProjectItemID::fromProperty (take, IDs::source).isValid())
736 take.setProperty (IDs::source, ProjectItemID::createNewID (clip.edit.getProjectItemID().getProjectID()).toString(), &owner.edit.getUndoManager());
737}
738
739WaveCompManager::~WaveCompManager() {}
740
742 juce::OwnedArray<SmartThumbnail>& thumbnails) const
743{
744 const int numTakes = getNumTakes();
745
746 for (int i = 0; i < numTakes; ++i)
747 {
748 const AudioFile takeFile (getSourceFileForTake (i));
749
750 if (takeFile.isNull())
751 {
752 if (thumbnails[i] != nullptr)
753 thumbnails.set (i, nullptr);
754 else
755 thumbnails.add (nullptr);
756 }
757 else
758 {
759 if (auto existing = thumbnails[i])
760 existing->setNewFile (takeFile);
761 else
762 thumbnails.add (new SmartThumbnail (clip.edit.engine, takeFile, comp, &clip.edit));
763 }
764 }
765}
766
768{
769 if (lastHash != 0)
770 return TemporaryFileManager::getFileForCachedCompRender (clip, lastHash).getFile();
771
772 return {};
773}
774
775//==============================================================================
776double WaveCompManager::getTakeLength (int takeIndex) const
777{
778 return getSourceFileForTake (takeIndex).getInfo().getLengthInSeconds();
779}
780
781double WaveCompManager::getOffset() const { return clip.getPosition().getOffset().inSeconds(); }
782double WaveCompManager::getLoopLength() const { return clip.getLoopLength().inSeconds(); }
783bool WaveCompManager::getAutoTempo() { return clip.getAutoTempo(); }
784
785double WaveCompManager::getSourceTempo()
786{
787 const AudioFileInfo info (getSourceFileForTake (0).getInfo());
788 return clip.getLoopInfo().getBpm (info);
789}
790
791juce::String WaveCompManager::getWarning()
792{
793 juce::String message;
794 const bool warnAboutReverse = clip.getIsReversed();
795
796 if (shouldDisplayWarning() || warnAboutReverse)
797 {
799 message << TRANS("When using multiple tempo changes comp editing will not be aligned with playback.")
800 << (warnAboutReverse ? "\n\n" : "");
801
802 if (warnAboutReverse)
803 message << TRANS("Only the rendered comp will be reversed. It is best to edit your comp forwards and then reverse the clip.");
804 }
805
806 return message;
807}
808
810{
811 compUpdater->setStrip (strip);
812}
813
814float WaveCompManager::getRenderProgress() const
815{
816 return clip.edit.engine.getAudioFileManager().proxyGenerator.getProportionComplete (lastCompFile);
817}
818
820{
822 {
823 keepSectionsSortedAndInRange();
824 startTimer (compGeneratorDelay);
825 }
826}
827
828void WaveCompManager::flattenTake (int takeIndex, bool deleteSourceFiles)
829{
830 if (getRenderProgress() < 1.0f || ! lastCompFile.isValid())
831 {
832 if (flattenRetrier == nullptr)
833 flattenRetrier = std::make_unique<FlattenRetrier> (*this, takeIndex, deleteSourceFiles);
834 else
835 flattenRetrier->setIndex (takeIndex);
836
837 return;
838 }
839
840 flattenRetrier = nullptr;
841
842 auto takeTree = takesTree.getChild (takeIndex);
843
844 if (auto item = getOrCreateProjectItemForTake (takeTree))
845 {
846 auto destCompFile = getDefaultTakeFile (takeIndex);
847
848 if (! destCompFile.existsAsFile() || item->getSourceFile() != destCompFile)
849 {
850 jassert (destCompFile.getParentDirectory().hasWriteAccess());
851 clip.edit.engine.getAudioFileManager().releaseFile (lastCompFile);
852
853 if (lastCompFile.getFile().moveFileTo (destCompFile))
854 {
855 item->setSourceFile (destCompFile);
856 }
857 else if (lastCompFile.getFile().copyFileTo (destCompFile))
858 {
859 lastCompFile.deleteFile();
860 item->setSourceFile (destCompFile);
861 }
862 else
863 {
865 item->setSourceFile ({});
866
867 clip.edit.engine.getUIBehaviour().showWarningAlert (TRANS("Problem flattening comp"),
868 TRANS("There was a problem creating the comp file at XYYX, "
869 "please ensure you have write access to this directory and try again.")
870 .replace ("XYYX", "\"" + destCompFile.getFullPathName() + "\""));
871 return;
872 }
873 }
874
875 // Keep a local reference here to avoid re-creating it during the clear process
876 CompManager::Ptr ptr = &clip.getCompManager();
877 clip.getTakes()[takeIndex] = item->getID();
878 clip.setCurrentTake (takeIndex);
879 clip.deleteAllUnusedTakes (deleteSourceFiles);
880 clip.getSourceFileReference().setToProjectFileReference (item->getID());
881 clip.setShowingTakes (false);
882 }
883 else
884 {
886 }
887}
888
891{
892 if (! (compTree.hasType (IDs::TAKE) || compTree.hasProperty (IDs::isComp)))
893 {
895 return {};
896 }
897
898 auto comp = compTree.createCopy();
899 auto dest = isCurrentTakeComp() ? getActiveTakeTree() : addNewComp();
900 comp.setProperty (IDs::source, dest[IDs::source], nullptr);
901 copyValueTree (dest, comp, getUndoManager());
902
903 return dest;
904}
905
906//==============================================================================
907void WaveCompManager::setProjectItemIDForTake (int takeIndex, ProjectItemID newID) const
908{
909 if (takeIndex < clip.getTakes().size())
910 clip.getTakes()[takeIndex] = newID;
911
912 auto takeTree = takesTree.getChild (takeIndex);
913 jassert (takeTree.isValid());
914
915 if (takeTree.isValid())
916 takeTree.setProperty (IDs::source, newID.toString(), getUndoManager());
917}
918
919ProjectItemID WaveCompManager::getProjectItemIDForTake (int takeIndex) const
920{
921 return clip.getTakes()[takeIndex];
922}
923
924AudioFile WaveCompManager::getSourceFileForTake (int takeIndex) const
925{
926 auto& e = clip.edit.engine;
927 return AudioFile (e, e.getProjectManager().findSourceFile (getProjectItemIDForTake (takeIndex)));
928}
929
930juce::File WaveCompManager::getDefaultTakeFile (int takeIndex) const
931{
932 if (auto project = getProjectForEdit (clip.edit))
933 {
934 auto firstTakeItem = project->getProjectItemForID (clip.getTakes()[0]);
935
936 if (firstTakeItem == nullptr)
937 return {};
938
939 int clipNum = 0;
940
941 if (auto ct = clip.getClipTrack())
942 clipNum = ct->getClips().indexOf (&clip) + 1;
943
944 juce::String compName;
945 compName << "_clip_" << clipNum << "_comp_" << (takeIndex - getNumTakes() + 2);
946
947 auto firstTakeFile = firstTakeItem->getSourceFile();
948 auto baseFileName = firstTakeFile.getFileNameWithoutExtension().upToFirstOccurrenceOf ("_take_", false, false);
949 auto compFileName = baseFileName + compName;
950
951 return firstTakeFile.existsAsFile()
952 ? firstTakeFile.getSiblingFile (compFileName)
953 .getNonexistentSibling().withFileExtension (firstTakeFile.getFileExtension())
954 : project->getDirectoryForMedia (ProjectItem::Category::recorded)
955 .getChildFile (compFileName).withFileExtension ("wav");
956 }
957
958 return {};
959}
960
961ProjectItem::Ptr WaveCompManager::getOrCreateProjectItemForTake (juce::ValueTree& takeTree)
962{
963 if (auto project = getProjectForEdit (clip.edit))
964 {
965 auto takeIndex = takeTree.getParent().indexOf (takeTree);
966
967 if (auto item = project->getProjectItemForID (getProjectItemIDForTake (takeIndex)))
968 return item;
969
970 auto destCompFile = getDefaultTakeFile (takeIndex);
971
972 if (auto item = project->createNewItem (destCompFile, ProjectItem::waveItemType(),
973 destCompFile.getFileNameWithoutExtension(),
974 {}, ProjectItem::Category::recorded, false))
975 {
976 // If we've had to create a new ProjectItem we need to update the take ProjectItemID to reflect it
977 setProjectItemIDForTake (takeIndex, item->getID());
978 return item;
979 }
980 }
981
982 return {};
983}
984
986{
987 auto newTake = getNewCompTree();
988 auto newID = ProjectItemID::createNewID (clip.edit.getProjectItemID().getProjectID());
989
990 newTake.setProperty (IDs::source, newID.toString(), nullptr);
991 newTake.setProperty (IDs::isComp, true, nullptr);
992
993 // Add last so all the properties are set
994 takesTree.addChild (newTake, -1, getUndoManager());
995 clip.setCurrentTake (takesTree.getNumChildren() - 1);
996
997 return newTake;
998}
999
1000//==============================================================================
1002{
1003 const double xFadeMs = clip.edit.engine.getPropertyStorage().getProperty (SettingID::compCrossfadeMs, 20.0);
1004
1005 return new CompRenderContext (clip.edit.engine, clip.getTakes(), getActiveTakeTree(), getActiveTakeIndex(),
1006 getSourceTimeMultiplier(), getOffset(), getMaxCompLength(), xFadeMs / 1000.0);
1007}
1008
1011{
1013
1014 // first build the audio graph of the comp
1015 CombiningAudioNode compNode;
1016 const int blockSize = 32768;
1017 juce::Range<double> takeRange (0.0, context.maxLength);
1018 auto crossfadeLength = context.crossfadeLength;
1019 auto halfCrossfade = crossfadeLength / 2.0;
1020
1021 auto numSegments = context.takeTree.getNumChildren();
1022 auto timeRatio = context.sourceTimeMultiplier;
1023 auto offset = context.offset / timeRatio;
1024 double startTime = 0.0;
1025
1026 for (int i = 0; i < numSegments; ++i)
1027 {
1028 if (job.shouldExit())
1029 return false;
1030
1031 auto compSegment = context.takeTree.getChild (i);
1032 auto takeIndex = (int) compSegment.getProperty (IDs::takeIndex);
1033 auto endTime = double (compSegment.getProperty (IDs::endTime)) / timeRatio;
1034
1035 if (juce::isPositiveAndBelow (takeIndex, context.takesIDs.size()))
1036 {
1037 const ProjectItemID takeID (context.takesIDs[takeIndex]);
1038 jassert (takeID.isValid());
1039
1040 const AudioFile takeFile (context.engine, context.engine.getProjectManager().findSourceFile (takeID));
1041 AudioNode* node = new WaveAudioNode (takeFile, takeRange, 0.0, {}, {},
1043
1044 auto segmentTimes = juce::Range<double> (startTime, endTime).expanded (halfCrossfade) + offset;
1045 juce::Range<double> fadeIn, fadeOut;
1046
1047 if (i != 0)
1048 fadeIn = { segmentTimes.getStart(), segmentTimes.getStart() + crossfadeLength };
1049
1050 if (i != (numSegments - 1))
1051 fadeOut = { segmentTimes.getEnd() - crossfadeLength, segmentTimes.getEnd() };
1052
1053 if (! (fadeIn.isEmpty() && fadeOut.isEmpty()))
1054 node = new FadeInOutAudioNode (node, fadeIn, fadeOut, AudioFadeCurve::convex, AudioFadeCurve::convex);
1055
1056 compNode.addInput ({ std::max (0.0, segmentTimes.getStart()),
1057 std::min (segmentTimes.getEnd(), context.maxLength) },
1058 node);
1059 }
1060
1061 startTime = endTime;
1062 }
1063
1064 if (job.shouldExit())
1065 return false;
1066
1067 {
1068 AudioNodeProperties props;
1069 compNode.getAudioNodeProperties (props);
1070 }
1071
1072 PlayHead localPlayhead;
1073
1074 {
1075 juce::Array<AudioNode*> allNodes;
1076 allNodes.add (&compNode);
1077
1079 {
1080 takeRange.getStart(),
1081 writer.getSampleRate(),
1082 blockSize,
1083 &allNodes,
1084 localPlayhead
1085 };
1086
1087 compNode.prepareAudioNodeToPlay (info);
1088 }
1089
1090 // now prepare the render context
1091 juce::AudioBuffer<float> renderingBuffer (writer.getNumChannels(), blockSize + 256);
1092 auto renderingBufferChannels = juce::AudioChannelSet::canonicalChannelSet (renderingBuffer.getNumChannels());
1093
1094 AudioRenderContext rc (localPlayhead, takeRange,
1095 &renderingBuffer, renderingBufferChannels, 0, blockSize,
1096 nullptr, 0.0,
1097 AudioRenderContext::playheadJumped, true);
1098
1099 localPlayhead.setPosition (takeRange.getStart());
1100 localPlayhead.playLockedToEngine ({ takeRange.getStart(), Edit::maximumLength });
1101
1102 if (job.shouldExit())
1103 return false;
1104
1105 // now perform the render
1106 auto streamTime = takeRange.getStart();
1107 auto blockLength = blockSize / writer.getSampleRate();
1108 SampleCount samplesToWrite = juce::roundToInt (takeRange.getLength() * writer.getSampleRate());
1109
1110 for (;;)
1111 {
1112 auto blockEnd = std::min (streamTime + blockLength, takeRange.getEnd());
1113 rc.streamTime = { streamTime, blockEnd };
1114
1115 auto numSamplesDone = (int) std::min (samplesToWrite, (SampleCount) blockSize);
1116 samplesToWrite -= numSamplesDone;
1117
1118 rc.bufferNumSamples = numSamplesDone;
1119
1120 if (numSamplesDone > 0)
1121 {
1122 compNode.prepareForNextBlock (rc);
1123 compNode.renderOver (rc);
1124 }
1125
1126 rc.continuity = AudioRenderContext::contiguous;
1127 streamTime = blockEnd;
1128
1129 auto prog = (float) ((streamTime - takeRange.getStart()) / takeRange.getLength()) * 0.9f;
1130 progress = juce::jlimit (0.0f, 0.9f, prog);
1131
1132 if (job.shouldExit())
1133 return false;
1134
1135 // NB buffer gets trashed by this call
1136 if (numSamplesDone <= 0 || ! writer.isOpen()
1137 || ! writer.appendBuffer (renderingBuffer, numSamplesDone))
1138 break;
1139 }
1140
1141 // complete render
1142 localPlayhead.stop();
1143 writer.closeForWriting();
1144 progress = 1.0f;
1145
1146 return true;
1147}
1148
1149//==============================================================================
1150class CompGeneratorJob : public AudioProxyGenerator::GeneratorJob
1151{
1152public:
1154
1155 CompGeneratorJob (WaveAudioClip& wc, const AudioFile& comp)
1156 : GeneratorJob (comp), engine (wc.edit.engine), clipID (wc.itemID),
1157 context (wc.getCompManager().createRenderContext())
1158 {
1159 setName (TRANS("Creating Comp") + ": " + wc.getName());
1160 }
1161
1162private:
1163 Engine& engine;
1164 EditItemID clipID;
1166
1167 bool render() override
1168 {
1170 AudioFile tempFile (*proxy.engine,
1171 proxy.getFile().getSiblingFile ("temp_comp_" + juce::String::toHexString (juce::Random::getSystemRandom().nextInt64()))
1172 .withFileExtension (proxy.getFile().getFileExtension()));
1173
1174 bool ok = render (tempFile);
1175
1176 if (ok)
1177 {
1178 ok = proxy.deleteFile();
1179 (void) ok;
1180 jassert (ok);
1181 ok = tempFile.getFile().moveFileTo (proxy.getFile());
1182 jassert (ok);
1183 }
1184
1185 tempFile.deleteFile();
1186 engine.getAudioFileManager().releaseFile (proxy);
1187
1188 return ok;
1189 }
1190
1191 bool render (const AudioFile& tempFile)
1192 {
1194
1195 // just use the first existing take as the basis for the comp render
1196 juce::File takeFile;
1197
1198 for (auto& takeID : context->takesIDs)
1199 {
1200 takeFile = engine.getProjectManager().findSourceFile (takeID);
1201
1202 if (takeFile.existsAsFile())
1203 break;
1204 }
1205
1206 if (! takeFile.existsAsFile())
1207 return false;
1208
1209 const AudioFile firstTakeAudioFile (engine, takeFile);
1210 AudioFileInfo sourceInfo (firstTakeAudioFile.getInfo());
1211
1212 // need to strip AIFF metadata to write to wav files
1213 if (sourceInfo.metadata.getValue ("MetaDataSource", "None") == "AIFF")
1214 sourceInfo.metadata.clear();
1215
1216 AudioFileWriter writer (tempFile, engine.getAudioFileFormatManager().getWavFormat(),
1217 sourceInfo.numChannels, sourceInfo.sampleRate,
1218 std::max (16, sourceInfo.bitsPerSample),
1219 sourceInfo.metadata, 0);
1220
1221 return writer.isOpen()
1222 && context != nullptr
1223 && WaveCompManager::renderTake (*context, writer, *this, progress);
1224 }
1225
1227};
1228
1229static void beginCompGeneration (WaveAudioClip& clip, int takeIndex)
1230{
1232 auto& cm = clip.getCompManager();
1233
1234 clip.edit.engine.getAudioFileManager()
1235 .proxyGenerator.beginJob (new CompGeneratorJob (clip, TemporaryFileManager::getFileForCachedCompRender (clip, cm.getTakeHash (takeIndex))));
1236}
1237
1238void WaveCompManager::timerCallback()
1239{
1240 stopTimer();
1241
1242 if (! clip.hasAnyTakes())
1243 {
1244 lastHash = 0;
1245 return;
1246 }
1247
1248 auto takeIndex = getActiveTakeIndex();
1249 auto hash = getTakeHash (takeIndex);
1250
1251 if (isTakeComp (lastRenderedTake) && hash != lastHash)
1252 {
1253 // stops the last render job and deletes the source
1254 clip.edit.engine.getAudioFileManager().proxyGenerator
1255 .deleteProxy (TemporaryFileManager::getFileForCachedCompRender (clip, lastHash));
1256 }
1257
1258 lastRenderedTake = takeIndex;
1259 lastHash = hash;
1260
1261 lastCompFile = TemporaryFileManager::getFileForCachedCompRender (clip, lastHash);
1262 const bool isComp = isTakeComp (lastRenderedTake);
1263
1264 if (isComp && (! lastCompFile.isValid()))
1265 {
1266 auto takeTree = takesTree.getChild (lastRenderedTake);
1267 beginCompGeneration (clip, lastRenderedTake);
1268 compUpdater->setCompFile (lastCompFile);
1269 }
1270 else if (! isComp)
1271 {
1272 lastHash = 0;
1273 }
1274
1275 if (clip.getCurrentSourceFile() != lastCompFile.getFile())
1276 {
1277 clip.setCurrentSourceFile (lastCompFile.getFile());
1278 SelectionManager::refreshAllPropertyPanels();
1279 }
1280}
1281
1282
1283//==============================================================================
1284MidiCompManager::MidiCompManager (MidiClip& owner)
1285 : CompManager (owner, owner.state.getOrCreateChildWithName (IDs::COMPS, &owner.edit.getUndoManager())),
1286 clip (owner), midiTakes (owner.state.getChildWithName (IDs::TAKES))
1287{
1289
1290 // For MidiClips this needs to have a "COMPS" type or it will conflict with the actual takes
1291 // For MIDI comps the offset is taken care of by the tempo sequence calculations
1292 lastOffset = 0.0;
1293}
1294
1295MidiCompManager::~MidiCompManager() {}
1296
1297void MidiCompManager::initialise()
1298{
1300 int takeIndex = 0;
1301 const int numTakes (clip.getNumTakes (true));
1302 auto um = getUndoManager();
1303
1304 // Check current takes are up to date and add new ones
1305 for (int i = 0; i < numTakes; ++i)
1306 {
1307 auto takeTree = takesTree.getChild (takeIndex);
1308
1309 if (! takeTree.isValid())
1310 {
1311 if (auto ml = clip.getTakeSequence (i))
1312 {
1313 juce::ValueTree newTake (IDs::TAKE);
1314 newTake.setProperty (IDs::isComp, ml->isCompList(), um);
1315 takesTree.addChild (newTake, takeIndex, um);
1316 }
1317 }
1318
1319 ++takeIndex;
1320 }
1321
1322 // Then remove any left over
1323 for (int i = takeIndex; i < takesTree.getNumChildren(); ++i)
1324 takesTree.removeChild (i, getUndoManager());
1325
1326 lastHash = ! lastHash;
1327 CompManager::initialise();
1328}
1329
1330MidiList* MidiCompManager::getSequenceLooped (int index)
1331{
1332 auto sourceSequence = clip.getTakeSequence (index);
1333
1334 if (! clip.isLooping())
1335 return sourceSequence;
1336
1337 if (auto ml = cachedLoopSequences[index])
1338 return ml;
1339
1340 if (sourceSequence == nullptr)
1341 return {};
1342
1343 return cachedLoopSequences.set (index, clip.createSequenceLooped (*sourceSequence).release());
1344}
1345
1346//==============================================================================
1347HashCode MidiCompManager::getBaseTakeHash (int takeIndex) const
1348{
1349 return takeIndex
1350 ^ static_cast<HashCode> (clip.getLoopLengthBeats().inBeats() * 153.0)
1351 ^ static_cast<HashCode> (clip.getLoopStartBeats().inBeats() * 264.0);
1352}
1353
1354double MidiCompManager::getTakeLength (int takeIndex) const
1355{
1356 if (clip.isLooping())
1357 return clip.getLoopLengthBeats().inBeats();
1358
1359 if (auto ml = clip.getTakeSequence (takeIndex))
1360 return ml->getLastBeatNumber().inBeats();
1361
1362 return 0.0;
1363}
1364
1365void MidiCompManager::discardCachedData()
1366{
1367 cachedLoopSequences.clear();
1368}
1369
1371{
1373
1375 return;
1376
1377 keepSectionsSortedAndInRange();
1378
1379 if (! clip.hasAnyTakes())
1380 {
1381 lastHash = 0;
1382 return;
1383 }
1384
1385 auto takeIndex = getActiveTakeIndex();
1386 auto hash = getTakeHash (takeIndex);
1387
1388 if (hash == lastHash)
1389 return;
1390
1391 lastRenderedTake = takeIndex;
1392 lastHash = hash;
1393
1394 if (isTakeComp (lastRenderedTake))
1395 createComp (getActiveTakeTree());
1396 else
1397 lastHash = 0;
1398
1399 if (clip.getCurrentTake() != takeIndex)
1400 clip.setCurrentTake (takeIndex);
1401
1402 clip.changed();
1403 clip.edit.restartPlayback();
1404}
1405
1406void MidiCompManager::flattenTake (int takeIndex, bool /*deleteSourceFiles*/)
1407{
1408 if (clip.getCurrentTake() != takeIndex)
1409 clip.setCurrentTake (takeIndex);
1410
1411 clip.deleteAllUnusedTakesConfirmingWithUser();
1412}
1413
1415{
1417 auto newTake = getNewCompTree();
1418 newTake.setProperty (IDs::isComp, true, nullptr);
1419
1420 // Now add the take to to the clip list
1422 clip.addTake (blankSeq, MidiList::NoteAutomationType::none);
1423
1424 if (auto ml = clip.getTakeSequence (clip.getNumTakes (true) - 1))
1425 ml->setCompList (true);
1426 else
1428
1429 takesTree.addChild (newTake, -1, getUndoManager());
1430 clip.setCurrentTake (clip.getNumTakes (true) - 1);
1431
1432 return newTake;
1433}
1434
1435void MidiCompManager::createComp (const juce::ValueTree& takeTree)
1436{
1438
1439 if (! takeTree.isValid())
1440 {
1441 clip.edit.engine.getUIBehaviour().showWarningMessage (TRANS("There was a problem creating the MIDI comp"));
1442 return;
1443 }
1444
1445 if (auto dest = clip.getTakeSequence (lastRenderedTake))
1446 {
1447 jassert (dest->isCompList());
1448 auto um = getUndoManager();
1449 dest->clear (um);
1450
1451 const auto numSegments = takeTree.getNumChildren();
1452 const auto numTakes = getNumTakes();
1453 const auto loopStart = toDuration (clip.getLoopStartBeats());
1454 BeatPosition startBeat;
1455
1456 for (int i = 0; i < numSegments; ++i)
1457 {
1458 auto compSegment = takeTree.getChild (i);
1459 const auto takeIndex = static_cast<int> (compSegment.getProperty (IDs::takeIndex));
1460 const auto endBeat = BeatPosition::fromBeats (static_cast<double> (compSegment.getProperty (IDs::endTime)));
1461
1462 if (juce::isPositiveAndBelow (takeIndex, numTakes))
1463 {
1464 if (auto src = getSequenceLooped (takeIndex))
1465 {
1466 const BeatRange beats (startBeat, endBeat);
1467
1468 for (auto n : src->getNotes())
1469 {
1470 auto nBeats = n->getRangeBeats();
1471
1472 if (nBeats.getStart() > endBeat)
1473 break;
1474
1475 auto newRange = beats.getIntersectionWith (nBeats);
1476
1477 if (! newRange.isEmpty())
1478 {
1479 MidiNote newNote (n->state.createCopy());
1480 newRange = newRange + loopStart;
1481 newNote.setStartAndLength (newRange.getStart(), newRange.getLength(), nullptr);
1482 dest->addNote (newNote, um);
1483 }
1484 }
1485
1486 for (auto e : src->getControllerEvents())
1487 {
1488 auto b = e->getBeatPosition();
1489
1490 if (b > endBeat)
1491 break;
1492
1493 if (b > startBeat)
1494 dest->addControllerEvent (b + loopStart, e->getType(), e->getControllerValue(), e->getMetadata(), um);
1495 }
1496
1497 for (auto e : src->getSysexEvents())
1498 {
1499 auto b = e->getBeatPosition();
1500
1501 if (b > endBeat)
1502 break;
1503
1504 if (b > startBeat)
1505 dest->addSysExEvent (e->getMessage(), b + loopStart, um);
1506 }
1507 }
1508 else
1509 {
1511 }
1512 }
1513
1514 startBeat = endBeat;
1515 }
1516
1517 dest->setCompList (true);
1518 }
1519}
1520
1521}} // namespace tracktion { inline namespace engine
void add(const ElementType &newElement)
int getNumChannels() const noexcept
static AudioChannelSet JUCE_CALLTYPE stereo()
static AudioChannelSet JUCE_CALLTYPE canonicalChannelSet(int numChannels)
bool moveFileTo(const File &targetLocation) const
bool copyFileTo(const File &targetLocation) const
ObjectClass * set(int indexToChange, ObjectClass *newObject, bool deleteOldElement=true)
ObjectClass * add(ObjectClass *newObject)
int64 nextInt64() noexcept
static Random & getSystemRandom() noexcept
constexpr Range expanded(ValueType amount) const noexcept
constexpr ValueType getStart() const noexcept
constexpr ValueType getEnd() const noexcept
ValueType clipValue(const ValueType value) const noexcept
constexpr ValueType getLength() const noexcept
constexpr bool contains(const ValueType position) const noexcept
String getValue(StringRef, const String &defaultReturnValue) const
String upToFirstOccurrenceOf(StringRef substringToEndWith, bool includeSubStringInResult, bool ignoreCase) const
static String toHexString(IntegerType number)
bool shouldExit() const noexcept
void stopTimer() noexcept
void startTimer(int intervalInMilliseconds) noexcept
bool hasType(const Identifier &typeName) const noexcept
void removeChild(const ValueTree &child, UndoManager *undoManager)
ValueTree getChild(int index) const
int getNumChildren() const noexcept
bool isValid() const noexcept
ValueTree & setProperty(const Identifier &name, const var &newValue, UndoManager *undoManager)
void addListener(Listener *listener)
int indexOf(const ValueTree &child) const noexcept
void addChild(const ValueTree &child, int index, UndoManager *undoManager)
ValueTree getParent() const noexcept
const var & getProperty(const Identifier &name) const noexcept
ValueTree createCopy() const
void removeListener(Listener *listener)
ValueTree getSibling(int delta) const noexcept
void sort(ElementComparator &comparator, UndoManager *undoManager, bool retainOrderOfEquivalentItems)
bool hasProperty(const Identifier &name) const noexcept
Smart wrapper for writing to an audio file.
double getSampleRate() const noexcept
Returns the sample rate of the writer, should only be called on an open writer.
bool appendBuffer(juce::AudioBuffer< float > &buffer, int numSamples)
Appends an AudioBuffer to the file.
int getNumChannels() const noexcept
Returns the num channels of the writer, should only be called on an open writer.
bool isOpen() const noexcept
Returns true if the file is open and ready to write to.
void closeForWriting()
Deletes the writer and releases the file handle.
Base class for nodes in an audio playback graph.
A clip in an edit.
virtual juce::String getName() const override
Returns the name of the clip.
void changed() override
This should be called to send a change notification to any SelectableListeners that are registered wi...
virtual bool hasAnyTakes() const
Returns true if this clip has any takes.
virtual bool isLooping() const
Returns true if this clip is currently looping.
juce::ValueTree state
The ValueTree of the Clip state.
double getSpeedRatio() const noexcept
Returns the speed ratio i.e.
virtual void setCurrentTake(int)
Sets a given take index to be the current take.
virtual juce::StringArray getTakeDescriptions() const
Returns the descriptions of any takes.
ClipPosition getPosition() const override
Returns the ClipPosition on the parent Track.
virtual int getCurrentTake() const
Returns the current take index.
An AudioNode that mixes a sequence of clips of other nodes.
int findSectionWithEndTime(juce::Range< double > range, int takeIndex, bool &timeFoundAtStartOfSection) const
Returns the index of the section whose end lies within the given time range.
void moveSectionToEndAt(juce::ValueTree &section, double newEndTime)
Moves a section to an absolute end time also moving the previous section's end time by the same ammou...
virtual void triggerCompRender()=0
Triggers the render of the comp.
bool shouldDisplayWarning() const noexcept
Returns true if the source should display a warning about using multi-tempo changes.
juce::ValueTree getActiveTakeTree() const
Returns the active take tree.
void setActiveTakeIndex(int index)
Sets the active take index.
juce::String getTakeName(int index) const
Returns the name of a take.
double getSpeedRatio() const
Returns the effective speed ratio used for displaying waveforms.
HashCode getTakeHash(int takeIndex) const
Returns a hash code representing a take.
int getActiveTakeIndex() const
Returns the active take index.
juce::ValueTree splitSectionAtTime(double time)
Find the current section at the given time and splits it in two ready for a new comp section.
void changeSectionIndexAtTime(double time, int takeIndex)
Changes the index of the active comp's section at a given time.
void removeSectionsWithinRange(juce::Range< double > timeRange, const juce::ValueTree &sectionToKeep)
Removes all sections which lie within the given time range.
virtual juce::ValueTree addNewComp()=0
Adds a new comp to the end of the takes list optionally making it active.
double getSourceTimeMultiplier() const
Returns the current time multiplier in use by the source, either the speed ratio or auto tempo ratio.
juce::Range< double > getCompRange() const
Returns the time range available for comping i.e.
double getMaxCompLength() const
Returns the maximum length that a comp can be.
int getNumComps() const
Returns the number of comps that are comps.
virtual void flattenTake(int takeIndex, bool deleteSourceFiles)=0
Should flatten the comp and remove all other takes.
void removeSectionIndexAtTime(double time, int takeIndex)
Removes a section from the comp at the given time if the section is at the given take index.
bool isCurrentTakeComp() const
Returns true if the current take is a comp.
juce::ValueTree addSection(int takeIndex, double endTime)
Adds a new section at a given time and returns the index of it.
void moveSection(juce::ValueTree &section, double timeDelta)
Moves a section by the specified time delta also moving the previous section's end time by the same a...
juce::Range< double > getSectionTimes(const juce::ValueTree &) const
Returns the time range a given section occupies for a given take.
CompManager(Clip &, const juce::ValueTree &)
Creates a CompManager for a given clip.
juce::ValueTree getSection(int takeIndex, int sectionIndex) const
Returns the section at the given index of a given take.
void moveSectionEndTime(juce::ValueTree &section, double newTime)
Moves a section's end time to the new time specified.
int getNumTakes() const
Returns the number of takes that are not comps.
juce::ValueTree findSectionAtTime(double time)
Returns either the section for the current comp at a given time or if a whole take is being used the ...
void removeSection(const juce::ValueTree &sectionToRemove)
Removes a section from the active comp if it is within range.
bool isTakeComp(int takeIndex) const
Returns true if the given take at an index is a comp.
const EditItemID itemID
Every EditItem has an ID which is unique within the edit.
static constexpr double maximumLength
The maximum length an Edit can be.
TempoSequence tempoSequence
The global TempoSequence of this Edit.
juce::UndoManager & getUndoManager() noexcept
Returns the juce::UndoManager used for this Edit.
Engine & engine
A reference to the Engine.
The Engine is the central class for all tracktion sessions.
CompFactory & getCompFactory() const
Returns the CompFactory instance.
AudioFileFormatManager & getAudioFileFormatManager() const
Returns the AudioFileFormatManager that maintains a list of available audio file formats.
AudioFileManager & getAudioFileManager() const
Returns the AudioFileManager instance.
ProjectManager & getProjectManager() const
Returns the ProjectManager instance.
An AudioNode that fades its input node in/out at given times.
@ none
No automation, add the sequence as plain MIDI with the channel of the clip.
An ID representing one of the items in a Project.
static ProjectItemID createNewID(int projectID) noexcept
Generates a new ID for a given project.
juce::File findSourceFile(ProjectItemID)
tries to find the media file used by a particular object.
SmartThumnail automatically tracks changes to an AudioFile and will update its cache if the file chan...
TempoSetting & getTempoAt(TimePosition) const
Returns the TempoSetting at the given position.
An audio clip that uses an audio file as its source.
WaveCompManager & getCompManager()
Returns the WaveCompManager for this clip.
An AudioNode that plays back a wave file.
juce::ValueTree pasteComp(const juce::ValueTree &compTree) override
Pastes an existing comp to this manager and returns the newly added tree.
CompRenderContext * createRenderContext() const
Returns a context to render the current taste of this comp.
void flattenTake(int takeIndex, bool deleteSourceFiles) override
Should flatten the comp and remove all other takes.
void triggerCompRender() override
Triggers the render of the comp.
void setStripToUpdate(juce::Component *strip) override
Sets a component to be updated during render processes.
juce::ValueTree addNewComp() override
Adds a new comp to the end of the takes list optionally making it active.
static bool renderTake(CompRenderContext &, AudioFileWriter &, juce::ThreadPoolJob &, std::atomic< float > &progress)
Renders the comp using the given writer and ThreadPoolJob.
void updateThumbnails(juce::Component &, juce::OwnedArray< SmartThumbnail > &thumbnails) const
Updates an array of thumbnails so they represent the takes and are in the correct order etc.
juce::File getCurrentCompFile() const
Returns the current comp file.
T is_pointer_v
#define TRANS(stringLiteral)
#define jassert(expression)
#define JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(className)
#define jassertfalse
typedef int
typedef double
T max(T... args)
T min(T... args)
Type jlimit(Type lowerLimit, Type upperLimit, Type valueToConstrain) noexcept
bool isPositiveAndBelow(Type1 valueToTest, Type2 upperLimit) noexcept
int roundToInt(const FloatType value) noexcept
Project::Ptr getProjectForEdit(const Edit &e)
Tries to find the project that contains this edit (but may return nullptr!)
Holds some really basic properties of a node.
Passed into AudioNodes when they are being initialised, to give them useful contextual information th...
size_t hash(size_t seed, const T &v)
Hashes a type with a given seed and returns the new hash value.
remove
Represents a position in beats.
constexpr double inSeconds() const
Returns the TimeDuration as a number of seconds.
constexpr double inSeconds() const
Returns the TimePosition as a number of seconds.
int bufferNumSamples
The number of samples to write into the audio buffer.
int continuity
A set of flags to indicate what the relationship is between this block and the previous one.
legacy::EditTimeRange streamTime
The time window which needs to be rendered into the current block.
TimePosition getStart() const
Returns the start time.
TimeDuration getOffset() const
Returns the offset.
ID for objects of type EditElement - e.g.
Updates a strip during a comp render and notifies the Clip when it finishes.
Re-calls flatten take to allow the comp time to finish rendering if needed.
time
#define CRASH_TRACER
This macro adds the current location to a stack which gets logged if a crash happens.