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

« « « Anklang Documentation
Loading...
Searching...
No Matches
tracktion_WaveAudioClip.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
15 : AudioClipBase (v, clipID, Type::wave, co)
16{
17}
18
20{
21 notifyListenersOfDeletion();
22}
23
24//==============================================================================
26{
28
29 if (getTakesTree().isValid())
30 callBlocking ([this]
31 {
34 });
35}
36
38{
39 if (auto other = dynamic_cast<WaveAudioClip*> (c))
40 {
42 auto takes = state.getChildWithName (IDs::TAKES);
43 copyValueTree (takes, other->state.getChildWithName (IDs::TAKES), nullptr);
44
46 }
47}
48
49//==============================================================================
51{
52 return TRANS("Audio Clip") + " - \"" + getName() + "\"";
53}
54
56{
57 if (sourceLength == 0s)
58 {
59 // If we're using clip effects the audio file may currently be invalid
60 // However, we know that the effects will produce an audio file of the same length as the originial so we'll return this
61 // This could however be a problem with standard warp time, Edit clips and reverse etc...
62
63 sourceLength = TimeDuration::fromSeconds (clipEffects != nullptr || isReversed ? AudioFile (edit.engine, getOriginalFile()).getLength()
64 : getAudioFile().getLength());
65 }
66
67 jassert (sourceLength >= 0s);
68 return sourceLength;
69}
70
72{
74
75 if (compManager != nullptr && isCurrentTakeComp())
76 setCurrentSourceFile (compManager->getCurrentCompFile());
77
78 sourceLength = 0s;
80
81 if (needsRender())
83}
84
86{
87 if (hasAnyTakes() && compManager != nullptr && compManager->isCurrentTakeComp())
88 return compManager->getCurrentCompFile();
89
90 return sourceFileReference.getFile();
91}
92
94{
95 return ! isUsingMelodyne()
96 && (isReversed || (warpTime && canUseProxy()) || (clipEffects != nullptr && canHaveEffects()))
97 && AudioFile (edit.engine, getOriginalFile()).isValid();
98}
99
101{
102 return AudioFile (edit.engine, getOriginalFile()).getHash()
103 ^ static_cast<HashCode> (getWarpTime() ? getWarpTimeManager().getHash() : 0)
104 ^ static_cast<HashCode> (getIsReversed() * 768)
105 ^ static_cast<HashCode> ((clipEffects == nullptr || ! canHaveEffects()) ? 0 : clipEffects->getHash());
106}
107
109{
110 sourceLength = 0s;
112}
113
115{
116 auto& ts = edit.tempoSequence;
117 auto pos = getPosition();
118
119 if (loopInfo.getNumerator() == 0)
120 loopInfo.setNumerator (ts.getTimeSigAt (pos.getStart()).numerator);
121
122 if (loopInfo.getDenominator() == 0)
123 loopInfo.setDenominator (ts.getTimeSigAt (pos.getStart()).denominator);
124
125 if (loopInfo.getNumBeats() == 0.0)
126 loopInfo.setNumBeats (getSourceLength().inSeconds() * (ts.getTempoAt (pos.getStart()).getBpm() / 60.0));
127}
128
129void WaveAudioClip::reassignReferencedItem (const ReferencedItem& item,
130 ProjectItemID newItemID, double newStartTimeSeconds)
131{
132 const auto newStartTime = TimeDuration::fromSeconds (newStartTimeSeconds);
133
134 if (hasAnyTakes())
135 {
136 auto indexInList = getReferencedItems().indexOf (item);
137
138 if (indexInList < 0)
139 {
141 return;
142 }
143
144 if (indexInList == getCurrentTake())
145 sourceFileReference.setToProjectFileReference (newItemID);
146
147 auto take = getTakesTree().getChild (indexInList);
148
149 if (take.isValid())
150 take.setProperty (IDs::source, newItemID.toString(), getUndoManager());
151
152 if (indexInList == 0)
153 {
154 if (! isLooping())
155 setOffset (getPosition().getOffset() - (newStartTime / getSpeedRatio()));
156 else
157 loopStart = loopStart - (newStartTime / getSpeedRatio());
158 }
159 }
160 else
161 {
162 AudioClipBase::reassignReferencedItem (item, newItemID, newStartTimeSeconds);
163 }
164}
165
166//==============================================================================
168{
169 auto um = getUndoManager();
170 auto takesTree = state.getOrCreateChildWithName (IDs::TAKES, um);
171
172 juce::ValueTree take (IDs::TAKE);
173 take.setProperty (IDs::source, id.toString(), um);
174 takesTree.addChild (take, -1, um);
175}
176
178{
179 auto um = getUndoManager();
180 auto takesTree = state.getOrCreateChildWithName (IDs::TAKES, um);
181
182 juce::ValueTree take (IDs::TAKE);
183
184 {
185 SourceFileReference sfr (edit, take, IDs::source);
186 sfr.setToDirectFileReference (f, true);
187 }
188
189 takesTree.addChild (take, -1, um);
190}
191
192int WaveAudioClip::getNumTakes (bool includeComps)
193{
194 if (includeComps)
195 return getTakesTree().getNumChildren();
196
197 return hasAnyTakes() ? getCompManager().getNumTakes() : 0;
198}
199
201{
203
204 auto takesTree = state.getChildWithName (IDs::TAKES);
205
206 for (auto t : takesTree)
207 if (t.hasProperty (IDs::source))
208 takes.add (ProjectItemID::fromProperty (t, IDs::source));
209
210 return takes;
211}
212
214{
215 if (getNumTakes (true) != 0)
216 {
217 state.removeChild (getTakesTree(), getUndoManager());
218 compManager = nullptr;
219 changed();
220 }
221}
222
224{
225 if (currentTakeIndex == takeIndexNeedsUpdating)
226 {
227 currentTakeIndex = -1;
228
229 auto takesTree = getTakesTree();
230 auto pid = sourceFileReference.getSourceProjectItemID().toString();
231
232 for (int i = takesTree.getNumChildren(); --i >= 0;)
233 {
234 if (takesTree.getChild (i).getProperty (IDs::source) == pid)
235 {
236 currentTakeIndex = i;
237 break;
238 }
239 }
240 }
241
242 return currentTakeIndex;
243}
244
245void WaveAudioClip::invalidateCurrentTake() noexcept
246{
247 currentTakeIndex = takeIndexNeedsUpdating;
248}
249
250void WaveAudioClip::invalidateCurrentTake (const juce::ValueTree& parentState) noexcept
251{
252 if (parentState.hasType (IDs::TAKES))
253 invalidateCurrentTake();
254}
255
256void WaveAudioClip::valueTreePropertyChanged (juce::ValueTree& treeWhosePropertyHasChanged,
257 const juce::Identifier& property)
258{
259 AudioClipBase::valueTreePropertyChanged (treeWhosePropertyHasChanged, property);
260
261 if (property == IDs::source)
262 invalidateCurrentTake();
263}
264
265void WaveAudioClip::valueTreeChildAdded (juce::ValueTree& p, juce::ValueTree& c)
266{
267 AudioClipBase::valueTreeChildAdded (p, c);
268 invalidateCurrentTake (p);
269}
270
271void WaveAudioClip::valueTreeChildRemoved (juce::ValueTree& p, juce::ValueTree& c, int oldIndex)
272{
273 AudioClipBase::valueTreeChildRemoved (p, c, oldIndex);
274 invalidateCurrentTake (p);
275}
276
277void WaveAudioClip::valueTreeChildOrderChanged (juce::ValueTree& p, int oldIndex, int newIndex)
278{
279 AudioClipBase::valueTreeChildOrderChanged (p, oldIndex, newIndex);
280 invalidateCurrentTake (p);
281}
282
284{
286 auto takesTree = getTakesTree();
287 auto numTakes = takesTree.getNumChildren();
288 jassert (juce::isPositiveAndBelow (takeIndex, numTakes));
289 takeIndex = juce::jlimit (0, numTakes - 1, takeIndex);
290
291 auto take = takesTree.getChild (takeIndex);
292 jassert (take.isValid());
293
294 auto takeSourceID = ProjectItemID::fromProperty (take, IDs::source);
295 auto mo = edit.engine.getProjectManager().getProjectItem (takeSourceID);
296 invalidateCurrentTake();
297
298 if (mo != nullptr || getCompManager().isTakeComp (takeIndex))
299 sourceFileReference.setToProjectFileReference (takeSourceID);
300 else
301 takesTree.removeChild (take, getUndoManager());
302
304}
305
307{
308 if (hasAnyTakes())
310
311 return false;
312}
313
315{
317 auto takes = getTakes();
319 int numTakes = 0;
320
321 for (int i = 0; i < takes.size(); ++i)
322 {
323 if (compManager == nullptr || ! compManager->isTakeComp (i))
324 {
325 if (auto projectItem = edit.engine.getProjectManager().getProjectItem (takes.getReference (i)))
326 s.add (juce::String (i + 1) + ". " + projectItem->getName());
327 else
328 s.add (juce::String (i + 1) + ". <" + TRANS("Deleted") + ">");
329
330 ++numTakes;
331 }
332 else
333 {
334 s.add (juce::String (i + 1) + ". " + TRANS("Comp") + " #" + juce::String (i + 1 - numTakes));
335 }
336 }
337
338 return s;
339}
340
342{
344
345 #if JUCE_MODAL_LOOPS_PERMITTED
346 auto showWarning = [] (const juce::String& title, const juce::String& message, bool& delFiles) -> int
347 {
349 .createAlertWindow (title, message,
350 {}, {}, {},
351 juce::AlertWindow::QuestionIcon, 0, nullptr));
352
353 juce::ToggleButton delFilesButton (TRANS("Delete Source Files?"));
354 delFilesButton.setSize (400, 20);
355 delFilesButton.setName ({});
356 w->addCustomComponent (&delFilesButton);
357 w->addTextBlock (TRANS("(This will also delete these from any other Edits in this project)"));
358 w->addButton (TRANS("OK"), 1, juce::KeyPress (juce::KeyPress::returnKey));
359 w->addButton (TRANS("Cancel"), 2, juce::KeyPress (juce::KeyPress::escapeKey));
360
361 const int res = w->runModalLoop();
362 delFiles = delFilesButton.getToggleState();
363
364 return res;
365 };
366
368 {
369 if (showWarning (TRANS("Flatten Takes"),
370 TRANS("This will permanently remove all takes in this clip, replacing it with"
371 " the current comp. This operation can not be undone.")
372 + "\n\n"
373 + TRANS("Are you sure you want to do this?"),
374 deleteSourceFiles) == 1)
375 getCompManager().flattenTake (getCurrentTake(), deleteSourceFiles);
376
377 return;
378 }
379 #endif
380
381 bool userIsSure = true;
382
383 #if JUCE_MODAL_LOOPS_PERMITTED
384 userIsSure = (showWarning (TRANS("Delete Unused Takes"),
385 TRANS("This will permanently delete all wave files that are listed as takes in this "
386 "clip, apart from the ones currently being used.")
387 + "\n\n"
388 + TRANS("Are you sure you want to do this?"),
389 deleteSourceFiles) == 1);
390 #endif
391
392 if (userIsSure)
393 deleteAllUnusedTakes (deleteSourceFiles);
394}
395
396static bool isTakeInUse (const WaveAudioClip& clip, ProjectItemID takeProjectItemID)
397{
398 for (auto t : getClipTracks (clip.edit))
399 {
400 for (auto& c : t->getClips())
401 {
402 if (c->getSourceFileReference().getSourceProjectItemID() == takeProjectItemID)
403 return true;
404
405 if (auto wac = dynamic_cast<WaveAudioClip*> (c))
406 if (wac != &clip && wac->getTakes().contains (takeProjectItemID))
407 return true;
408 }
409 }
410
411 return false;
412}
413
414void WaveAudioClip::deleteAllUnusedTakes (bool deleteSourceFiles)
415{
417
418 auto takes = getTakes();
419 bool errors = false;
420
421 if (auto proj = getProjectForEdit (edit))
422 {
423 for (int i = takes.size(); --i >= 0;)
424 {
425 auto takeProjectItemID = takes.getReference (i);
426
427 if (! isTakeInUse (*this, takeProjectItemID))
428 {
429 bool removedOk = ! deleteSourceFiles
430 || proj->getProjectItemForID (takeProjectItemID) == nullptr
431 || proj->removeProjectItem (takeProjectItemID, true);
432
433 if (removedOk)
434 {
435 for (auto child : state)
436 {
437 if (ProjectItemID::fromProperty (child, IDs::source) == takeProjectItemID)
438 {
440 break;
441 }
442 }
443 }
444 else
445 {
446 errors = true;
447 }
448 }
449 }
450
451 clearTakes();
452
453 if (errors)
454 edit.engine.getUIBehaviour().showWarningAlert (TRANS("Delete Unused Takes"),
455 TRANS("Some of the wave files couldn't be deleted"));
456 }
457
458 if (! hasAnyTakes())
459 compManager = nullptr;
460}
461
463{
465 Clip::Array newClips;
466
467 if (Track::Ptr t = getTrack())
468 {
469 const bool shouldBeShowingTakes = isShowingTakes();
470
471 if (shouldBeShowingTakes)
473
474 auto clipNode = state.createCopy();
475
476 if (! clipNode.isValid())
477 return newClips;
478
479 clipNode.removeChild (clipNode.getChildWithName (IDs::TAKES), nullptr);
480
481 int trackIndex = t->getIndexInEditTrackList();
482 auto allTracks = getAllTracks (t->edit);
483 auto takes = getTakes();
484
485 for (int i = 0; i < takes.size(); ++i)
486 {
487 if (compManager->isTakeComp (i))
488 continue;
489
490 AudioTrack::Ptr targetTrack (dynamic_cast<AudioTrack*> (allTracks[++trackIndex]));
491
492 if (toNewTracks || targetTrack == nullptr)
493 targetTrack = t->edit.insertNewAudioTrack (TrackInsertPoint (t->getParentTrack(), t.get()), nullptr);
494
495 if (targetTrack != nullptr)
496 newClips.add (targetTrack->insertWaveClip (getTakeDescriptions()[i], takes[i], getPosition(), false).get());
497
498 t = targetTrack;
499 }
500
501 if (shouldBeShowingTakes)
503 }
504
505 return newClips;
506}
507
509{
511
512 if (compManager == nullptr)
513 {
515 auto ptr = edit.engine.getCompFactory().getCompManager (*this);
516 compManager = dynamic_cast<WaveCompManager*> (ptr.get());
517
518 if (compManager != nullptr)
519 compManager->initialise();
520 else
522 }
523
524 return *compManager;
525}
526
527//==============================================================================
529{
530 TRACKTION_ASSERT_MESSAGE_THREAD
531
532 // do this here so we don't end up creating a multiple copies of a File
533 if (auto existing = edit.engine.getRenderManager().getRenderJobWithoutCreating (destFile))
534 return existing;
535
536 if (clipEffects != nullptr && canHaveEffects())
537 {
538 auto j = clipEffects->createRenderJob (AudioFile (*destFile.engine, destFile.getFile()), AudioFile (*destFile.engine, getOriginalFile()));
539 j->setName (TRANS("Rendering Clip Effects") + ": " + getName());
540 return j;
541 }
542
543 if (getIsReversed())
544 {
545 auto j = ReverseRenderJob::getOrCreateRenderJob (edit.engine, getOriginalFile(), destFile.getFile());
546 j->setName (TRANS("Reversing") + ": " + getName());
547 return j;
548 }
549
550 if (getWarpTime())
551 {
552 auto j = WarpTimeRenderJob::getOrCreateRenderJob (*this, getOriginalFile(), destFile.getFile());
553 j->setName (TRANS("Warping") + ": " + getName());
554 return j;
555 }
556
557 return {};
558}
559
561{
562 TRACKTION_ASSERT_MESSAGE_THREAD
563
564 if (renderJob == nullptr || getAudioFile().isValid())
565 return {};
566
567 const float progress = renderJob == nullptr ? 1.0f : renderJob->getCurrentTaskProgress();
568 juce::String m (clipEffects != nullptr ? TRANS("Rendering effects")
569 : (getWarpTime() ? TRANS("Warping") : TRANS("Reversing")));
570
571 if (progress < 0.0f)
572 return m + "...";
573
574 return m + ": " + juce::String (juce::roundToInt (progress * 100.0f)) + "%";
575}
576
578{
580 return true;
581
582 if (hasAnyTakes() && getCompManager().getCurrentCompFile() == af.getFile())
583 return true;
584
585 return false;
586}
587
588}} // namespace tracktion { inline namespace engine
void add(const ElementType &newElement)
bool getToggleState() const noexcept
virtual void setName(const String &newName)
void setSize(int newWidth, int newHeight)
static const int escapeKey
static const int returnKey
static LookAndFeel & getDefaultLookAndFeel() noexcept
void add(String stringToAdd)
void removeChild(const ValueTree &child, UndoManager *undoManager)
ValueTree getChild(int index) const
int getNumChildren() const noexcept
ValueTree & setProperty(const Identifier &name, const var &newValue, UndoManager *undoManager)
void addChild(const ValueTree &child, int index, UndoManager *undoManager)
ValueTree createCopy() const
ValueTree getChildWithName(const Identifier &type) const
ValueTree getOrCreateChildWithName(const Identifier &type, UndoManager *undoManager)
Base class for Clips that produce some kind of audio e.g.
WarpTimeManager & getWarpTimeManager() const
Returns the WarpTimeManager for this clip used to maipluate warp markers.
virtual bool canHaveEffects() const
Returns true if this clip can have ClipEffects added to it.
void updateSourceFile()
Checks the current source file to see if it's up to date and then triggers a source render if needed.
virtual bool isUsingFile(const AudioFile &)
Should return true if the clip is referencing the file in any way.
virtual void renderComplete()
Callback to indicate that the render has completed.
virtual AudioFile getAudioFile() const
Returns the file used to play back the source and will get proxied etc.
bool getWarpTime() const
Returns true if warp time is enabled.
bool isLooping() const override
Returns true if this clip is currently looping.
bool isUsingMelodyne() const
Returns true if this clip is using Melodyne.
void initialise() override
Initialises the Clip.
bool getIsReversed() const noexcept
Returns true if the clip's source material is reversed.
void markAsDirty()
Resets the dirty flag so that a new render will be attempted.
bool canUseProxy() const noexcept
Retuns true if this clip can use a proxy file.
void changed() override
This should be called to send a change notification to any SelectableListeners that are registered wi...
void cloneFrom(Clip *) override
Clones the given clip to this clip.
Base class for items that can contain clips.
A clip in an edit.
virtual juce::String getName() const override
Returns the name of the clip.
void setOffset(TimeDuration newOffset)
Sets the offset of the clip, i.e.
juce::ValueTree state
The ValueTree of the Clip state.
Track * getTrack() const override
Returns the parent Track this clip is on (if any).
virtual void sourceMediaChanged()
Called when the source media file reference (attribute "source") has changed - i.e.
virtual void setShowingTakes(bool shouldShow)
Sets whether the clip should be showing takes.
double getSpeedRatio() const noexcept
Returns the speed ratio i.e.
virtual bool isShowingTakes() const
Returns true if the clip is showing takes.
juce::UndoManager * getUndoManager() const
Returns the UndoManager.
ClipPosition getPosition() const override
Returns the ClipPosition on the parent Track.
void setCurrentSourceFile(const juce::File &)
Sets a new source file for this clip.
void setActiveTakeIndex(int index)
Sets the active take index.
bool isCurrentTakeComp() const
Returns true if the current take is a comp.
int getNumTakes() const
Returns the number of takes that are not comps.
void setNumerator(int newNumerator)
Sets the numerator of the object.
void setDenominator(int newDenominator)
Sets the denominator of the object.
int getDenominator() const
Returns the denominator of the object.
double getNumBeats() const
Returns the number of beats.
int getNumerator() const
Returns the numerator of the object.
void setNumBeats(double newNumBeats)
Sets the number of beats.
An ID representing one of the items in a Project.
static Ptr getOrCreateRenderJob(Engine &e, const juce::File &source, const juce::File &destination)
Returns a job that will have been started to generate the Render described by the params.
virtual void changed()
This should be called to send a change notification to any SelectableListeners that are registered wi...
This class wraps a string that is generally held in a 'source' property, and which is a reference to ...
void setToProjectFileReference(const juce::File &, bool updateProjectItem)
Points this source at a new file via a project item.
Type
Defines the types of item that can live on Track[s].
HashCode getHash() const
Returns a hash representing this warp list.
An audio clip that uses an audio file as its source.
juce::File getOriginalFile() const override
Must return the file that the source ProjectItemID refers to.
int getCurrentTake() const override
Returns the current take index.
void renderComplete() override
Callback to indicate that the render has completed.
juce::Array< ProjectItemID > getTakes() const override
Returns the ProjectItemID of the clip's takes.
int getNumTakes(bool includeComps) override
Returns the total number of takes.
juce::String getRenderMessage() override
Override this to return a custom message to be displayed over waveforms during rendering.
bool hasAnyTakes() const override
Returns true if this clip has any takes.
void deleteAllUnusedTakesConfirmingWithUser(bool deleteSourceFiles)
Deletes all but the current takes but shows a confirmation dialog first.
WaveAudioClip(const juce::ValueTree &, EditItemID, ClipOwner &)
Creates a WaveAudioClip from a given state.
juce::String getSelectableDescription() override
Subclasses must return a description of what they are.
bool needsRender() const override
Subclasses should override this to return true if they need the rest of the render callbacks.
RenderManager::Job::Ptr getRenderJob(const AudioFile &destFile) override
Subclasses should override this to return a RenderJob suitable for rendering its source file.
HashCode getHash() const override
Must return a unique hash for this clip's source.
bool isCurrentTakeComp() override
Returns true if the current take is a comp.
TimeDuration getSourceLength() const override
Must return the length in seconds of the source material e.g.
void deleteAllUnusedTakes(bool deleteSourceFiles)
Deletes all but the current takes.
void sourceMediaChanged() override
Called when the source media file reference (attribute "source") has changed - i.e.
void setCurrentTake(int takeIndex) override
Sets a given take index to be the current take.
void cloneFrom(Clip *) override
Clones the given clip to this clip.
bool isUsingFile(const AudioFile &) override
Should return true if the clip is referencing the file in any way.
void setLoopDefaults() override
Override this to fill in the LoopInfo structure as best fits the source.
WaveCompManager & getCompManager()
Returns the WaveCompManager for this clip.
juce::StringArray getTakeDescriptions() const override
Returns the descriptions of any takes.
void clearTakes() override
Clears any takes this clip has.
void initialise() override
Initialises the Clip.
void addTake(ProjectItemID)
Adds a new take with the ProjectItemID as the source.
Clip::Array unpackTakes(bool toNewTracks) override
Attempts to unpack the takes to new clips.
void flattenTake(int takeIndex, bool deleteSourceFiles) override
Should flatten the comp and remove all other takes.
#define TRANS(stringLiteral)
#define jassert(expression)
#define jassertfalse
Type jlimit(Type lowerLimit, Type upperLimit, Type valueToConstrain) noexcept
bool isPositiveAndBelow(Type1 valueToTest, Type2 upperLimit) noexcept
int roundToInt(const FloatType value) noexcept
juce::Array< Track * > getAllTracks(const Edit &edit)
Returns all the tracks in an Edit.
juce::Array< ClipTrack * > getClipTracks(const Edit &edit)
Returns all the ClipTracks in an Edit.
Project::Ptr getProjectForEdit(const Edit &e)
Tries to find the project that contains this edit (but may return nullptr!)
Represents a duration in real-life time.
ID for objects of type EditElement - e.g.
Defines the place to insert Track[s].
#define CRASH_TRACER
This macro adds the current location to a stack which gets logged if a crash happens.