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

« « « Anklang Documentation
Loading...
Searching...
No Matches
tracktion_EditClip.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
14EditClip::EditClip (const juce::ValueTree& v, EditItemID clipID, ClipOwner& targetParent, ProjectItemID sourceEditID)
15 : AudioClipBase (v, clipID, Type::edit, targetParent),
16 waveInfo (getAudioFile().getInfo())
17{
18 renderOptions = RenderOptions::forEditClip (*this);
19
20 sourceIdUpdater.setFunction ([this] { sourceMediaChanged(); });
21 sourceFileReference.setToProjectFileReference (sourceEditID);
22
23 auto um = getUndoManager();
24 copyColourFromMarker.referTo (state, IDs::copyColour, um, false);
25 trimToMarker.referTo (state, IDs::trimToMarker, um, false);
26 renderEnabled.referTo (state, IDs::renderEnabled, um, true);
27}
28
29EditClip::~EditClip()
30{
31 for (auto e : referencedEdits)
32 e->removeListener (this);
33
34 notifyListenersOfDeletion();
35}
36
37AudioFile EditClip::getAudioFile() const
38{
39 if (isTracktionEditFile (getCurrentSourceFile()))
40 return AudioFile (edit.engine);
41
42 return AudioClipBase::getAudioFile();
43}
44
45//==============================================================================
46void EditClip::initialise()
47{
48 AudioClipBase::initialise();
49
50 if (waveInfo.sampleRate <= 0 || waveInfo.lengthInSamples <= 0)
51 updateWaveInfo();
52
53 if (! renderEnabled)
54 setUsesProxy (false);
55
56 sourceIdUpdater.triggerAsyncUpdate();
57}
58
59void EditClip::cloneFrom (Clip* c)
60{
61 if (auto other = dynamic_cast<EditClip*> (c))
62 {
63 AudioClipBase::cloneFrom (other);
64
65 copyColourFromMarker .setValue (other->copyColourFromMarker, nullptr);
66 trimToMarker .setValue (other->trimToMarker, nullptr);
67 renderEnabled .setValue (other->renderEnabled, nullptr);
68 }
69}
70
71//==============================================================================
72juce::String EditClip::getSelectableDescription()
73{
74 return TRANS("Edit Clip") + " - \"" + getName() + "\"";
75}
76
77juce::File EditClip::getOriginalFile() const
78{
79 jassert (editSnapshot == nullptr || editSnapshot->getFile() == sourceFileReference.getFile());
80 return editSnapshot != nullptr ? editSnapshot->getFile() : juce::File();
81}
82
83void EditClip::setLoopDefaults()
84{
85 // first check to see if these have been set
86 if (loopInfo.getNumerator() == 0
87 && loopInfo.getDenominator() == 0
88 && loopInfo.getNumBeats() == 0)
89 updateLoopInfoBasedOnSource (true);
90}
91
92//==============================================================================
93bool EditClip::needsRender() const
94{
95 if (! renderEnabled || editSnapshot == nullptr)
96 return false;
97
98 return editSnapshot->getLength() > 0.0s;
99}
100
101RenderManager::Job::Ptr EditClip::getRenderJob (const AudioFile& destFile)
102{
103 TRACKTION_ASSERT_MESSAGE_THREAD
104
105 // do this here so we don't end up creating a new instance of our Edit
106 if (auto existing = edit.engine.getRenderManager().getRenderJobWithoutCreating (destFile))
107 return existing;
108
109 auto j = EditRenderJob::getOrCreateRenderJob (edit.engine,
110 destFile, *renderOptions,
111 sourceFileReference.getSourceProjectItemID(),
112 true, getIsReversed());
113 j->setName (TRANS("Creating Edit Clip") + ": " + getName());
114
115 return j;
116}
117
118//==============================================================================
119void EditClip::renderComplete()
120{
121 // update wave info so the render file is seen as valid
122 updateWaveInfo();
123 AudioClipBase::renderComplete();
124}
125
126juce::String EditClip::getRenderMessage()
127{
128 TRACKTION_ASSERT_MESSAGE_THREAD
129
130 if (renderJob == nullptr || getCurrentSourceFile().existsAsFile())
131 return {};
132
133 auto numRenderingJobs = edit.engine.getRenderManager().getNumJobs();
134 juce::String remainderMessage;
135
136 if (numRenderingJobs > 0)
137 remainderMessage << " (" << juce::String (numRenderingJobs) << " " << TRANS("remaining") << ")";
138
139 if (renderJob == nullptr)
140 return TRANS("Rendering referenced Edits") + "..." + remainderMessage;
141
142 const float progress = renderJob->getCurrentTaskProgress();
143
144 if (progress <= 0.0f)
145 return TRANS("Rendering referenced Edits") + "..." + remainderMessage;
146
147 return TRANS("Rendering Edit: ") + juce::String (juce::roundToInt (progress * 100.0f)) + "%";
148}
149
150juce::String EditClip::getClipMessage()
151{
152 if (! sourceFileReference.getSourceProjectItemID().isValid())
153 return TRANS("No source set");
154
155 if (renderOptions->getNumTracks() == 0)
156 return TRANS("No tracks selected to render");
157
158 if (! renderEnabled)
159 return TRANS("Rendering disabled");
160
161 return AudioClipBase::getClipMessage();
162}
163
164//==============================================================================
165void EditClip::sourceMediaChanged()
166{
167 if (sourceMediaReEntrancyCheck)
168 return;
169
170 const juce::ScopedValueSetter<bool> svs (sourceMediaReEntrancyCheck, true);
171
172 auto newID = sourceFileReference.getSourceProjectItemID();
173
174 // Check this here because when any ProjectItem in this project gets changed the Edit will call
175 // sendSourceFileUpdate bypassing the usual check in setSourceProjectItemID
176 if (isInitialised && lastSourceId == newID)
177 return;
178
179 const bool resetTracksToDefault = (! edit.isLoading() && ! lastSourceId.isValid());
180
181 lastSourceId = newID;
182 editSnapshot = EditSnapshot::getEditSnapshot (edit.engine, newID);
183 const bool invalidSource = editSnapshot == nullptr || ! editSnapshot->isValid();
184
185 if (invalidSource)
186 {
187 if (renderJob != nullptr)
188 renderJob->removeListener (this);
189
190 setCurrentSourceFile ({});
191 renderJob = nullptr;
192 }
193
194 if (resetTracksToDefault)
195 {
196 if (editSnapshot != nullptr)
197 renderOptions->setTrackIDs (editSnapshot->getTracks());
198 else
199 renderOptions->setTrackIDs ({});
200 }
201
202 updateReferencedEdits();
203 updateWaveInfo();
204 generateHash();
205
206 if (! invalidSource)
207 updateSourceFile();
208
209 changed();
210
211 if (isInitialised)
212 updateLoopInfoBasedOnSource (true);
213}
214
215void EditClip::changed()
216{
217 // update the hash in case the source has changed and we need to re-generate the render inside AudioClipBase
218 generateHash();
219 AudioClipBase::changed();
220}
221
222bool EditClip::isUsingFile (const AudioFile& af)
223{
224 if (AudioClipBase::isUsingFile (af))
225 return true;
226
227 auto audioFile = RenderManager::getAudioFileForHash (edit.engine, edit.getTempDirectory (false), getHash());
228
229 if (audioFile == af)
230 return true;
231
232 return false;
233}
234
235void EditClip::valueTreePropertyChanged (juce::ValueTree& v, const juce::Identifier& i)
236{
237 if (v == state && i == IDs::renderEnabled)
238 {
239 TRACKTION_ASSERT_MESSAGE_THREAD
240 renderEnabled.forceUpdateOfCachedValue();
241
242 setUsesProxy (renderEnabled);
243 changed();
244
245 if (renderEnabled)
246 updateSourceFile();
247 else
248 cancelCurrentRender();
249 }
250 else
251 {
252 AudioClipBase::valueTreePropertyChanged (v, i);
253 }
254}
255
256ProjectItem::Ptr EditClip::createUniqueCopy()
257{
258 if (auto item = sourceFileReference.getSourceProjectItem())
259 return item->createCopy();
260
261 return {};
262}
263
264void EditClip::updateWaveInfo()
265{
266 // If the edit is empty this will cause the AudioSegmentList structure to have undefined content.
267 // need to find a way around this, maybe just use a default length of 5s so silence is generated
268 jassert ((! needsRender()) || getSourceLength() > 0s);
269 const auto sourceLength = getSourceLength() == 0s ? 5.0 : getSourceLength().inSeconds();
270
271 waveInfo.bitsPerSample = renderOptions->getBitDepth();
272 waveInfo.sampleRate = renderOptions->getSampleRate();
273 waveInfo.numChannels = renderOptions->getStereo() ? 2 : 1;
274 waveInfo.lengthInSamples = static_cast<SampleCount> (sourceLength * waveInfo.sampleRate);
275
276 updateLoopInfoBasedOnSource (false);
277}
278
279HashCode EditClip::generateHash()
280{
282
283 // Because edit clips can contain edit clips recursively we can't just rely
284 // on the source edit time as a hash, we need to drill down retrieving any
285 // nested EditClips and xor their source hash's.
286 HashCode editClipHash = 0;
287
288 for (auto snapshot : referencedEdits)
289 editClipHash ^= snapshot->getHash();
290
291 HashCode newHash = editClipHash
292 ^ renderOptions->getHash()
293 ^ static_cast<HashCode> (getIsReversed() * 768);
294
295 if (hash != newHash)
296 {
297 markAsDirty();
298 hash = newHash;
299 }
300
301 return newHash;
302}
303
304void EditClip::setTracksToRender (const juce::Array<EditItemID>& trackIDs)
305{
306 if (renderOptions != nullptr)
307 {
308 renderOptions->setTrackIDs (trackIDs);
309 generateHash();
310 }
311}
312
313void EditClip::updateReferencedEdits()
314{
316
317 juce::ReferenceCountedArray<EditSnapshot> current, added, removed;
318
319 if (editSnapshot != nullptr)
320 current = editSnapshot->getNestedEditObjects();
321
322 // find any new
323 for (int i = current.size(); --i >= 0;)
324 {
325 auto snapshot = current.getUnchecked (i);
326
327 if (referencedEdits.contains (snapshot))
328 continue;
329
330 referencedEdits.add (snapshot);
331 added.add (snapshot);
332 }
333
334 // then any that have been removed
335 for (int i = referencedEdits.size(); --i >= 0;)
336 {
337 auto snapshot = referencedEdits.getUnchecked (i);
338
339 if (current.contains (snapshot))
340 continue;
341
342 removed.add (snapshot);
343 referencedEdits.remove (i);
344 }
345
346 // then we need to de-register any removed and register any added
347 for (auto snapshot : removed)
348 snapshot->removeListener (this);
349
350 for (auto snapshot : added)
351 snapshot->addListener (this);
352}
353
354void EditClip::updateLoopInfoBasedOnSource (bool updateLength)
355{
356 if (editSnapshot == nullptr || ! editSnapshot->isValid())
357 return;
358
359 // first try and get info from the source edit
360 auto tempo = editSnapshot->getTempo();
361 auto timeSigNum = editSnapshot->getTimeSigNumerator();
362 auto timeSigDenom = editSnapshot->getTimeSigDenominator();
363 auto pitch = editSnapshot->getPitch();
364 double clipNumBeats = 1.0;
365
366 if (tempo > 0.0)
367 {
368 clipNumBeats = (tempo * getSourceLength().inSeconds()) / 60.0;
369 loopInfo.setNumBeats (clipNumBeats);
370 loopInfo.setBpm (tempo, waveInfo);
371 // need to set num beats or the tempo will get messed up
372 }
373
374 if (timeSigNum > 0 && timeSigDenom > 0)
375 {
376 loopInfo.setNumerator (timeSigNum);
377 loopInfo.setDenominator (timeSigDenom);
378 }
379
380 // if these haven't been set then match them to the new edit
381 auto& ts = edit.tempoSequence;
382 auto clipPos = getPosition();
383
384 if (loopInfo.getNumerator() == 0 || loopInfo.getDenominator() == 0)
385 {
386 loopInfo.setNumerator (ts.getTimeSigAt (clipPos.getStart()).numerator);
387 loopInfo.setDenominator (ts.getTimeSigAt (clipPos.getStart()).denominator);
388 }
389
390 if (loopInfo.getNumBeats() == 0)
391 loopInfo.setNumBeats (length.get().inSeconds() * (ts.getTempoAt (clipPos.getStart()).getBpm() / 60.0));
392
393 // also need to adjust clip length
394 if (updateLength)
395 {
396 if (! (loopInfo.getNumerator() == 0
397 && loopInfo.getDenominator() == 0
398 && loopInfo.getNumBeats() == 0))
399 {
400 auto editBpm = ts.getTempoAt (clipPos.getStart()).getBpm();
401
402 if (tempo == 0.0)
403 tempo = loopInfo.getBpm (getWaveInfo());
404
405 if (editBpm > 0.0 && tempo > 0.0 && tempo < 400)
406 {
407 auto bpmRatio = tempo / editBpm;
408 jassert (bpmRatio > 0.1 && bpmRatio < 10.0); // sensible?
409 auto newLength = getSourceLength() * bpmRatio;
410 setLength (newLength, true);
411 }
412
413 loopInfo.setNumBeats (clipNumBeats);
414 setAutoTempo (true);
415 }
416 }
417
418 if (updateLength && pitch > 0)
419 loopInfo.setRootNote (pitch);
420}
421
422double EditClip::getCurrentStretchRatio() const
423{
424 if (audioSegmentList != nullptr && ! audioSegmentList->getSegments().isEmpty())
425 return audioSegmentList->getSegments().getReference (0).getStretchRatio();
426
427 return getSpeedRatio();
428}
429
430//==============================================================================
431void EditClip::editChanged (EditSnapshot&)
432{
433 // If any of the Edit's we are referencing have changed we need to re-check them all
434 updateReferencedEdits();
435 generateHash();
436}
437
438}} // namespace tracktion { inline namespace engine
A clip in an edit.
This is the main source of an Edit clip and is responsible for managing its properties.
#define TRANS(stringLiteral)
#define jassert(expression)
int roundToInt(const FloatType value) noexcept
juce::String getName(LaunchQType t)
Retuns the name of a LaunchQType for display purposes.
size_t hash(size_t seed, const T &v)
Hashes a type with a given seed and returns the new hash value.
#define CRASH_TRACER
This macro adds the current location to a stack which gets logged if a crash happens.