Can't pass params to renderToFile

I’m new to using Tracktion Engine, but have been really pleased with how much I’ve got done so quickly. However I’m struggling with passing params to renderToFile.

This code works fine, it renders the entire timeline:

auto tf = std::make_unique<juce::TemporaryFile> (".wav");
bool renderSuccess = tracktion::Renderer::renderToFile(*edit, tf->getFile(), false);

However when I try and pass parameters I get an error:

auto tf = std::make_unique<juce::TemporaryFile> (".wav");
tracktion::Renderer::Parameters params (*edit);
params.destFile = tf->getFile();
params.time = params.time.withLength (tracktion::TimeDuration::fromSeconds(4));
params.audioFormat = engine.getAudioFileFormatManager().getWavFormat();
juce::File renderedFile = tracktion::Renderer::renderToFile("Exporting audio", params);
JUCE Assertion failure in tracktion_Plugin.cpp:664
jassert (initialiseCount > 0);

Seems to be something to do with plugins not being initialised properly, but I can’t see why it should behave differently. Am I doing something wrong? Many thanks!

Tracktion Engine v3.0.0
Juce v8.0.3

That is strange. Are you using the develop branch of TE?

I tried develop and master both do the same.

I’ve been doing a lot of digging and debugging, and ended up creating a new simple as possible project to experiment with renderToFile, I’m getting a different error now. Unfortunately I can’t find an example in the demos or tutorials of using renderToFile.

I’ve created a public repo for the basic Juce app and will update it once I have this working in case it’s helpful to anyone else. GitHub - simonadcock/tracktion-engine-render-test

I’m just trying to load an audio file, then renderToFile with params. (Without parms works).

Below is a summary of the code:

edit->ensureNumberOfAudioTracks(1);
auto track = tracktion_engine::getAudioTracks(*edit)[0];
tracktion_engine::AudioFile audioFile(edit->engine, file);
auto newClip = track->insertWaveClip(file.getFileNameWithoutExtension(), file, { { {}, tracktion::TimeDuration::fromSeconds(audioFile.getLength()) }, {} }, false);

juce::File outputFile = juce::File::getSpecialLocation(juce::File::userDesktopDirectory).getChildFile("output.wav");
auto tracks = tracktion_engine::getAllTracks(*edit);
auto bitset = tracktion_engine::toBitSet(tracks);
tracktion::Renderer::Parameters params (*edit);
params.destFile = outputFile;
params.time = tracktion::TimeRange(tracktion::TimePosition::fromSeconds(0), tracktion::TimePosition::fromSeconds(4));
params.audioFormat = engine.getAudioFileFormatManager().getWavFormat();
params.tracksToDo = bitset;
juce::File renderedFile = tracktion::Renderer::renderToFile("Exporting audio", params);

It now crashes in TracktionNodePlayer, looks like processStateToUse is null.

However if I run the following it does work as expected:

tracktion::Renderer::renderToFile({}, outputFile, *edit, timeRange, bitset, true, true, {}, false);

Any pointers would be really appreciated!

I can’t replicate this. I’ve added this test to our TestRunner app which is basically the same as yours:

    TEST_CASE ("renderToFile")
    {
        auto& engine = *Engine::getEngines()[0];
        auto edit = test_utilities::createTestEdit (engine);
        edit->ensureNumberOfAudioTracks (1);

        auto fileLength = 5_td;
        auto sinFile = graph::test_utilities::getSinFile<juce::WavAudioFormat> (44100.0, fileLength.inSeconds());

        auto track = getAudioTracks (*edit)[0];
        insertWaveClip (*track, {}, sinFile->getFile(), { .time = { 0_tp, fileLength } },
                        DeleteExistingClips::no);

        juce::TemporaryFile destFile (".wav");
        auto tracks = tracktion_engine::getAllTracks(*edit);
        auto bitset = tracktion_engine::toBitSet(tracks);

        Renderer::Parameters params (*edit);
        params.time = params.time.withLength (fileLength);
        params.audioFormat = engine.getAudioFileFormatManager().getWavFormat();
        params.destFile = destFile.getFile();
        params.time = tracktion::TimeRange(tracktion::TimePosition::fromSeconds(0), tracktion::TimePosition::fromSeconds(4));
        params.audioFormat = engine.getAudioFileFormatManager().getWavFormat();
        params.tracksToDo = bitset;
        [[maybe_unused]] juce::File renderedFile = tracktion::Renderer::renderToFile("Exporting audio", params);
    }

If I step through the last line I get this:

  • Renderer::renderToFile
  • render_utils::createRenderTask
  • processState etc. are created
  • Edit Node is created then Renderer::RenderTask is created

Can you step through the above in your example and see if any of the above steps aren’t happening?

Hi Dave,
Great to meet you at ADC last week! It seems that because I’m basically running the engine headless (the UI is a WebView) I want to run the render task without using runTaskWithProgressBar. The other render functions allows you to pass in a useThread bool. I’ve amended the function that accepts parameters to the following and it now works as expected. I don’t know if it would be preferable to pass useThread in as one of the parameters.
Does this seem a sensible approach? Or have I missed something?

juce::File Renderer::renderToFile (const juce::String& taskDescription, const Parameters& r, bool useThread)
    {
    CRASH_TRACER
    
    jassert (r.sampleRateForAudio > 7000);
    jassert (r.edit != nullptr);
    jassert (r.engine != nullptr);
    
    TransportControl::stopAllTransports (*r.engine, false, true);
    
    turnOffAllPlugins (*r.edit);
    
    if (r.tracksToDo.countNumberOfSetBits() > 0
        && r.destFile.hasWriteAccess()
        && ! r.destFile.isDirectory())
    {
        auto task = render_utils::createRenderTask (r, taskDescription, nullptr, nullptr);
        
        if (useThread)
        {
            auto& ui = r.edit->engine.getUIBehaviour();
            if (task)
            {
                ui.runTaskWithProgressBar (*task);
                turnOffAllPlugins (*r.edit);
                
                if (r.destFile.existsAsFile())
                {
                    if (task->errorMessage.isNotEmpty())
                    {
                        r.destFile.deleteFile();
                        ui.showWarningMessage (task->errorMessage);
                        return {};
                    }
                    
                    return r.destFile;
                }
                
                if (task->getCurrentTaskProgress() >= 0.9f && task->errorMessage.isNotEmpty())
                    ui.showWarningMessage (task->errorMessage);
            }
            else
            {
                ui.showWarningMessage (TRANS("Couldn't render, as the selected region was empty"));
            }
        }
        else if (task)
        {
            while (task->runJob() == juce::ThreadPoolJob::jobNeedsRunningAgain)
            {}
            return r.destFile;
        }
    }
    return {};
}

You might want to look at the new EditRenderer class (at the top of tracktion_Renderer.h) as that provides a way to render asynchronously with a stop/progress handle and completion callback.

If you have a web UI, this is probably the preferable way to do it so you don’t just block the UI whilst rendering.