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

« « « Anklang Documentation
Loading...
Searching...
No Matches
tracktion_EditClipRenderJob.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 bool deleteEdit, bool silenceOnBackup, bool reverse)
16{
17 AudioFile targetFile (e, params.destFile);
18
19 if (auto ptr = e.getRenderManager().getRenderJobWithoutCreating (targetFile))
20 {
21 // we'll need to delete the edit if we're already generating for it
22 if (deleteEdit)
23 {
24 jassertfalse; // we shouldn't hit this any more, let me know if you do
25 std::unique_ptr<Edit> scopedEdit (params.edit);
26 }
27
28 return ptr;
29 }
30
31 return new EditRenderJob (e, params, deleteEdit, silenceOnBackup, reverse);
32}
33
35 ProjectItemID itemID, bool silenceOnBackup, bool reverse)
36{
37 if (auto ptr = e.getRenderManager().getRenderJobWithoutCreating (destFile))
38 return ptr;
39
40 return new EditRenderJob (e, destFile, ro, itemID, silenceOnBackup, reverse);
41}
42
43//==============================================================================
44EditRenderJob::EditRenderJob (Engine& e, const Renderer::Parameters& p, bool deleteEdit,
45 bool silenceOnBackup_, bool reverse_)
46 : Job (e, AudioFile (e, p.destFile)),
47 renderOptions (e),
48 params (p),
49 editDeleter (p.edit, deleteEdit),
50 silenceOnBackup (silenceOnBackup_),
51 reverse (reverse_),
52 thumbnailToUpdate (256, e.getAudioFileFormatManager().readFormatManager,
53 e.getAudioFileManager().getAudioThumbnailCache())
54{
55}
56
57EditRenderJob::EditRenderJob (Engine& e, const AudioFile& destFile_, const RenderOptions& ro,
58 ProjectItemID edID, bool silenceOnBackup_, bool reverse_)
59 : Job (e, AudioFile (destFile_)),
60 renderOptions (ro, nullptr),
61 itemID (edID),
62 params (e),
63 silenceOnBackup (silenceOnBackup_), reverse (reverse_),
64 thumbnailToUpdate (256, e.getAudioFileFormatManager().readFormatManager,
65 e.getAudioFileManager().getAudioThumbnailCache())
66{
67}
68
70{
71 renderPasses.clear();
72
73 if (! editDeleter.willDeleteObject())
74 if (editDeleter != nullptr)
75 editDeleter->getTransport().editHasChanged();
76}
77
78juce::String EditRenderJob::getLastError() const
79{
80 const juce::ScopedLock sl (errorLock);
81 return lastError;
82}
83
84void EditRenderJob::setLastError (const juce::String& e)
85{
86 const juce::ScopedLock sl (errorLock);
87 lastError = e;
88}
89
90//==============================================================================
92{
93 if (params.edit == nullptr || itemID.isValid())
94 {
96
97 Edit::LoadContext context;
98 auto contextUpdater = std::async (std::launch::async, [this, &context]
99 {
100 while (! context.completed)
101 {
102 context.shouldExit = shouldExit();
103
104 if (context.shouldExit)
105 break;
106
107 juce::Thread::sleep (100);
108 }
109 });
110 juce::ignoreUnused (contextUpdater);
111
112 auto edit = loadEditForExamining (params.engine->getProjectManager(), itemID, Edit::EditRole::forRendering);
113
114 // it's difficult to determine the marked region or selections at this point, so we'll ignore it,
115 // assuming that this code will only be used for rendering entire EditClips, and not sections of edits.
116 jassert (! renderOptions.markedRegion);
117 jassert (! renderOptions.selectedClips);
118 jassert (! renderOptions.selectedTracks);
119
120 params = renderOptions.getRenderParameters (*edit);
121 params.edit = edit.get();
122 params.destFile = proxy.getFile();
123 params.tracksToDo = renderOptions.getTrackIndexes (*edit);
124 params.category = ProjectItem::Category::none;
125
126 editDeleter.setOwned (edit.release());
127 }
128
130 callBlocking ([this] { renderStatus = std::make_unique<Edit::ScopedRenderStatus> (*params.edit, false); });
131
132 if (params.separateTracks)
133 renderSeparateTracks();
134 else
135 renderPasses.add (new RenderPass (*this, params, "Edit Render"));
136
137 return true;
138}
139
141{
143
144 // do these in order so we don't jump in and out of the Edit
145 if (auto pass = renderPasses.getFirst())
146 {
147 if (pass->task == nullptr)
148 pass->initialise();
149
150 if (pass->task == nullptr || pass->task->runJob() == ThreadPoolJob::jobHasFinished)
151 renderPasses.removeObject (pass);
152 }
153
154 return renderPasses.isEmpty();
155}
156
158{
160
161 if (result.items.size() > 0
162 || (params.category == ProjectItem::Category::none && proxy.getFile().existsAsFile()))
163 result.result = juce::Result::ok();
164
165 return result.result.wasOk();
166}
167
168//==============================================================================
169EditRenderJob::RenderPass::RenderPass (EditRenderJob& j,
170 Renderer::Parameters& renderParams,
171 const juce::String& description)
172 : owner (j), r (renderParams), desc (description), originalCategory (r.category),
173 tempFile (r.destFile, juce::TemporaryFile::useHiddenFile)
174{
175 r.category = ProjectItem::Category::none;
176 r.destFile = tempFile.getFile();
177}
178
179EditRenderJob::RenderPass::~RenderPass()
180{
181 auto errorMessage (task != nullptr ? task->errorMessage : juce::String());
182 owner.setLastError (errorMessage);
183 const bool completedOk = task != nullptr ? task->getCurrentTaskProgress() == 1.0f : false;
184 task = nullptr;
185
186 if (owner.editDeleter.willDeleteObject())
187 callBlocking ([this] { Renderer::turnOffAllPlugins (*r.edit); });
188
189 // overwite with temp file
190 if (! errorMessage.isEmpty() && owner.silenceOnBackup)
191 owner.generateSilence (tempFile.getFile());
192
193 if (tempFile.getFile().existsAsFile() && (completedOk || owner.silenceOnBackup))
194 tempFile.overwriteTargetFileWithTemporary();
195 else
196 tempFile.getTargetFile().deleteFile();
197
198 // swap this back to the original
199 r.destFile = tempFile.getTargetFile();
200 r.category = originalCategory;
201
202 // reverse if needed
203 if (owner.reverse)
204 {
205 juce::TemporaryFile tempReverseFile (r.destFile);
206
207 if (r.destFile.existsAsFile())
208 if (AudioFileUtils::reverse (owner.engine, r.destFile, tempReverseFile.getFile(), owner.progress, nullptr))
209 if (tempReverseFile.getFile().existsAsFile())
210 tempReverseFile.overwriteTargetFileWithTemporary();
211 }
212
213 if (! r.destFile.existsAsFile())
214 return;
215
216 if (r.category != ProjectItem::Category::none && r.destFile.existsAsFile())
217 {
219
220 auto proj = getProjectForEdit (*r.edit);
221
222 if (proj == nullptr)
223 {
225 return;
226 }
227
228 if (proj->isReadOnly())
229 {
230 r.edit->engine.getUIBehaviour().showWarningMessage (TRANS("Couldn't add the new file to the project (because this project is read-only)"));
231 }
232 else
233 {
234 bool ok = true;
235
236 if (! r.createMidiFile && errorMessage.isNotEmpty())
237 {
238 ok = false;
239 r.destFile.deleteFile();
240 }
241
242 if (ok)
243 {
244 juce::String newItemDesc;
245 newItemDesc << TRANS("Rendered from edit") << r.edit->getName().quoted() << " " << TRANS("On") << " "
246 << juce::Time::getCurrentTime().toString (true, true);
247
248 if (auto item = proj->createNewItem (r.destFile,
249 r.createMidiFile ? ProjectItem::midiItemType()
250 : ProjectItem::waveItemType(),
251 r.destFile.getFileNameWithoutExtension().trim(),
252 newItemDesc,
253 r.category,
254 true))
255 {
256 jassert (item->getID().isValid());
257 owner.result.items.add (item);
258 }
259 else
260 {
262 }
263 }
264 }
265 }
266
267 // validates the AudioFile by giving it a sample rate etc.
268 owner.engine.getAudioFileManager().checkFileForChangesAsync (AudioFile (owner.engine, r.destFile));
269}
270
271bool EditRenderJob::RenderPass::initialise()
272{
273 jassert (task == nullptr);
274 jassert (r.sampleRateForAudio > 7000);
275
276 callBlocking ([this]
277 {
279 r.edit->initialiseAllPlugins();
280 r.edit->getTransport().stop (false, true);
281 });
282
283 if (r.tracksToDo.countNumberOfSetBits() > 0
284 && r.destFile.hasWriteAccess()
285 && ! r.destFile.isDirectory())
286 {
287 auto tracksToDo = toTrackArray (*r.edit, r.tracksToDo);
288
289 // Initialise playhead and continuity
291 auto playHeadState = std::make_unique<tracktion::graph::PlayHeadState> (*playHead);
292 auto processState = std::make_unique<ProcessState> (*playHeadState, r.edit->tempoSequence);
293
294 CreateNodeParams cnp { *processState };
295 cnp.sampleRate = r.sampleRateForAudio;
296 cnp.blockSize = r.blockSizeForAudio;
297 cnp.allowedClips = r.allowedClips.isEmpty() ? nullptr : &r.allowedClips;
298 cnp.allowedTracks = r.tracksToDo.isZero() ? nullptr : &tracksToDo;
299 cnp.forRendering = true;
300 cnp.includePlugins = r.usePlugins;
301 cnp.includeMasterPlugins = r.useMasterPlugins;
302 cnp.addAntiDenormalisationNoise = r.addAntiDenormalisationNoise;
303 cnp.includeBypassedPlugins = false;
304 cnp.allowClipSlots = r.edit->engine.getEngineBehaviour().areClipSlotsEnabled();
305
307 callBlocking ([this, &node, &cnp] { node = createNodeForEdit (*r.edit, cnp); });
308
309 if (node)
310 {
312 std::move (node), std::move (playHead), std::move (playHeadState), std::move (processState),
313 &owner.progress, &owner.thumbnailToUpdate);
314 return task->errorMessage.isEmpty();
315 }
316 }
317
318 return false;
319}
320
321//==============================================================================
322void EditRenderJob::renderSeparateTracks()
323{
324 // The logic here is fairly complicated but esseintially we want the following resulting tracks:
325 // 1. Any top-level audio tracks
326 // 2. Any top-level sub-mix folder tracks
327 // 3. Any audio or sub-mix folder tracks contained in Folder tracks
328 // 4. Only tracks that are contained in the tracksToDo mask
329
330 jassert (params.separateTracks);
331 auto originalTracksToDo = params.tracksToDo;
332 juce::Array<juce::File> createdFiles;
333
334 for (int i = 0; i <= originalTracksToDo.getHighestBit(); ++i)
335 {
336 if (originalTracksToDo[i])
337 {
338 if (auto track = dynamic_cast<Track*> (getAllTracks (*params.edit)[i]))
339 {
340 if (track->isPartOfSubmix())
341 continue;
342
343 auto ft = dynamic_cast<FolderTrack*> (track);
344 auto at = dynamic_cast<AudioTrack*> (track);
345
346 if (ft == nullptr && at == nullptr)
347 continue;
348
349 juce::BigInteger tracksToDo;
350 tracksToDo.setBit (i);
351
352 if (ft != nullptr)
353 {
354 if (! ft->isSubmixFolder())
355 continue;
356
357 for (auto* subTrack : ft->getAllSubTracks (true))
358 {
359 const int subTrackIndex = subTrack->getIndexInEditTrackList();
360
361 if (originalTracksToDo[subTrackIndex])
362 tracksToDo.setBit (subTrackIndex);
363 }
364 }
365
366 auto getDescription = [at, ft]
367 {
368 return ft != nullptr ? TRANS("Rendering Submix Track") + " " + juce::String (ft->getFolderTrackNumber()) + "..."
369 : TRANS("Rendering Track") + " " + juce::String (at->getAudioTrackNumber()) + "...";
370 };
371
372 auto file = proxy.getFile();
373 auto trackFile = file.getSiblingFile (file.getFileNameWithoutExtension()
374 + " " + track->getName()
375 + " " + TRANS("Render") + " 0"
376 + file.getFileExtension());
377
378 params.destFile = juce::File (juce::File::createLegalPathName (getNonExistentSiblingWithIncrementedNumberSuffix (trackFile, false)
379 .getFullPathName()));
380 params.tracksToDo = tracksToDo;
381
382 if (Renderer::checkTargetFile (track->edit.engine, params.destFile))
383 renderPasses.add (new RenderPass (*this, params, getDescription()));
384
385 // Temporarily create the output file so that it affects the next call to
386 // getNonExistentSiblingWithIncrementedNumberSuffix
387 createdFiles.add (params.destFile);
388 params.destFile.replaceWithText ("");
389 }
390 }
391 }
392
393 for (auto f : createdFiles)
394 f.deleteFile();
395
396 params.tracksToDo = originalTracksToDo;
397}
398
399bool EditRenderJob::generateSilence (const juce::File& fileToWriteTo)
400{
402
404
405 if (os == nullptr || params.audioFormat == nullptr)
406 return false;
407
408 const int numChans = params.mustRenderInMono ? 1 : 2;
409 std::unique_ptr<juce::AudioFormatWriter> writer (params.audioFormat->createWriterFor (os.get(), params.sampleRateForAudio,
410 (unsigned int) numChans,
411 params.bitDepth, {}, 0));
412
413 if (writer == nullptr)
414 return false;
415
416 os.release();
417 auto numToDo = (SampleCount) tracktion::toSamples (params.time.getLength(), params.sampleRateForAudio);
418 auto blockSize = std::min (4096, (int) numToDo);
419 SampleCount numDone = 0;
420
421 // should probably use an AudioScratchBuffer here
422 juce::AudioBuffer<float> buffer (numChans, blockSize);
423 buffer.clear();
424
425 while (numDone < numToDo)
426 {
427 if (shouldExit())
428 return false;
429
430 auto numThisTime = std::min ((int) numToDo, blockSize);
431 writer->writeFromAudioSampleBuffer (buffer, 0, numThisTime);
432
433 progress = (float) (numDone / (float) numToDo);
434 thumbnailToUpdate.addBlock (numDone, buffer, 0, numThisTime);
435
436 numDone += numThisTime;
437 }
438
439 return true;
440}
441
442}} // namespace tracktion { inline namespace engine
T async(T... args)
void add(const ElementType &newElement)
virtual AudioFormatWriter * createWriterFor(OutputStream *streamToWriteTo, double sampleRateToUse, unsigned int numberOfChannels, int bitsPerSample, const StringPairArray &metadataValues, int qualityOptionIndex)=0
void addBlock(int64 sampleNumberInSource, const AudioBuffer< float > &newData, int startOffsetInBuffer, int numSamples) override
BigInteger & setBit(int bitNumber)
std::unique_ptr< FileOutputStream > createOutputStream(size_t bufferSize=0x8000) const
bool replaceWithText(const String &textToWrite, bool asUnicode=false, bool writeUnicodeHeaderBytes=false, const char *lineEndings="\r\n") const
static String createLegalPathName(const String &pathNameToFix)
bool existsAsFile() const
bool isEmpty() const noexcept
ObjectClass * getFirst() const noexcept
void clear(bool deleteObjects=true)
ObjectClass * add(ObjectClass *newObject)
void removeObject(const ObjectClass *objectToRemove, bool deleteObject=true)
bool wasOk() const noexcept
static Result ok() noexcept
bool shouldExit() const noexcept
static Time JUCE_CALLTYPE getCurrentTime() noexcept
String toString(bool includeDate, bool includeTime, bool includeSeconds=true, bool use24HourClock=false) const
bool renderNextBlock() override
During a render process this will be repeatedly called.
bool setUpRender() override
Subclasses should override this to set-up their render process.
bool completeRender() override
This is called once after all the render blocks have completed.
static Ptr getOrCreateRenderJob(Engine &, Renderer::Parameters &, bool deleteEdit, bool silenceOnBackup, bool reverse)
Returns a job that will have been started to generate the Render described by the params.
@ forRendering
Creates an Edit for rendering, not output device playback.
A context passed to the Options struct which will get updated about load process and can be signaled ...
The Engine is the central class for all tracktion sessions.
RenderManager & getRenderManager() const
Returns the RenderManager instance.
ProjectManager & getProjectManager() const
Returns the ProjectManager instance.
An ID representing one of the items in a Project.
Job::Ptr getRenderJobWithoutCreating(const AudioFile &audioFile)
This will return a Ptr to an existing render job for an audio file or nullptr if no job is in progres...
Represents a set of user properties used to control a render operation, using a ValueTree to hold the...
Renderer::Parameters getRenderParameters(Edit &, SelectionManager *, TimeRange markedRegion)
Returns a set of renderer parameters which can be used to describe a render operation.
static void turnOffAllPlugins(Edit &)
Deinitialises all the plugins for the Edit.
static bool checkTargetFile(Engine &, const juce::File &)
Cheks a file for write access etc.
T is_pointer_v
#define TRANS(stringLiteral)
#define jassert(expression)
#define jassertfalse
typedef float
T min(T... args)
void ignoreUnused(Types &&...) noexcept
std::unique_ptr< Edit > loadEditForExamining(ProjectManager &pm, ProjectItemID itemID, Edit::EditRole role)
Uses the ProjectManager to find an Edit file and open it.
juce::Array< Track * > toTrackArray(Edit &edit, const juce::BigInteger &tracksToAdd)
Returns an Array of Track[s] corresponding to the set bits of all tracks in an Edit.
juce::Array< Track * > getAllTracks(const Edit &edit)
Returns all the tracks in an Edit.
std::unique_ptr< tracktion::graph::Node > createNodeForEdit(EditPlaybackContext &epc, std::atomic< double > &audibleTimeToUpdate, const CreateNodeParams &params)
Creates a Node to play back an Edit with live inputs and outputs.
Project::Ptr getProjectForEdit(const Edit &e)
Tries to find the project that contains this edit (but may return nullptr!)
static bool reverse(Engine &, const juce::File &source, const juce::File &destination, std::atomic< float > &progress, juce::ThreadPoolJob *job=nullptr, bool canCreateWavIntermediate=true)
Reverses a file updating a progress value and checking the exit status of a given job.
#define CRASH_TRACER
This macro adds the current location to a stack which gets logged if a crash happens.