Studio One nonRealTime render hangs on Windows

Anyone else have Studio One hang (only on Windows) when rendering their VI track?

It works fine on macOS in Studio One and all other hosts on both platforms.

Cheers,

Rail

With some logging in AudioProcessorGraph… we can see that isPrepared never changes state in Studio One on Windows…

template <typename FloatType, typename SequenceType>
static void processBlockForBuffer (AudioBuffer<FloatType>& buffer, MidiBuffer& midiMessages,
                                   AudioProcessorGraph& graph,
                                   std::unique_ptr<SequenceType>& renderSequence,
                                   Atomic<int>& isPrepared)
{
    if (graph.isNonRealtime())
    {
        while (isPrepared.get() == 0)
            Thread::sleep (1);

        const ScopedLock sl (graph.getCallbackLock());  // <<<<<-----  Never gets here in Studio One on Windows

        if (renderSequence != nullptr)
            renderSequence->perform (buffer, midiMessages, graph.getPlayHead());
    }
    else
    {
        const ScopedLock sl (graph.getCallbackLock());

        if (isPrepared.get() == 1)
        {
            if (renderSequence != nullptr)
                renderSequence->perform (buffer, midiMessages, graph.getPlayHead());
        }
        else
        {
            buffer.clear();
            midiMessages.clear();
        }
    }
}

Rail

More logging…

void AudioProcessorGraph::handleAsyncUpdate()
{
    Logger::writeToLog ("AudioProcessorGraph::handleAsyncUpdate");
    
    buildRenderingSequence();
    isPrepared = 1;
}

//==============================================================================
void AudioProcessorGraph::prepareToPlay (double sampleRate, int estimatedSamplesPerBlock)
{
    Logger::writeToLog ("AudioProcessorGraph::prepareToPlay");
    
    setRateAndBufferSizeDetails (sampleRate, estimatedSamplesPerBlock);
    clearRenderingSequence();
    triggerAsyncUpdate();
    
    Logger::writeToLog ("AudioProcessorGraph::prepareToPlay done");
}

In my AudioProcessor::prepareToPlay() I have:

    m_pMainGraph->setNonRealtime (m_bNonRealtime);
    
    if (m_bNonRealtime)
        {
        Logger::writeToLog ("Release the graph resources...");
        m_pMainGraph->releaseResources();
        }
    
    m_pMainGraph->prepareToPlay (m_dMainSampleRate, m_iMainBufferSize);

and the log shows…

mainproc::prepareToPlay - SampleRate: 48000, Input Channels = 0, Output Channels = 2, BufferSize = 256
Is Non-Realtime: 1
Release the graph resources…
AudioProcessorGraph::prepareToPlay
AudioProcessorGraph::prepareToPlay done

and ends there while the progress timer in Studio One just sits there counting up while it spins in the while loop.

So the AsyncUpdater isn’t calling the AudioProcessorGraph::handleAsyncUpdate() triggered in AudioProcessorGraph::prepareToPlay()

Rail

In Reaper the logging shows:

7:43:43pm mainproc::prepareToPlay - SampleRate: 48000, Input Channels = 0, Output Channels = 32, BufferSize = 1024
Is Non-Realtime: 0
AudioProcessorGraph::prepareToPlay
AudioProcessorGraph::prepareToPlay done
AudioProcessorGraph::handleAsyncUpdate
Saving State Chunk

7:44:28pm mainproc::prepareToPlay - SampleRate: 48000, Input Channels = 0, Output Channels = 32, BufferSize = 1024
Is Non-Realtime: 1
Release the graph resources…
AudioProcessorGraph::prepareToPlay
AudioProcessorGraph::prepareToPlay done

7:44:28pm mainproc::prepareToPlay - SampleRate: 48000, Input Channels = 0, Output Channels = 32, BufferSize = 1024
Is Non-Realtime: 1
Release the graph resources…
AudioProcessorGraph::prepareToPlay
AudioProcessorGraph::prepareToPlay done

7:44:28pm mainproc::prepareToPlay - SampleRate: 48000, Input Channels = 0, Output Channels = 32, BufferSize = 1024
Is Non-Realtime: 1
Release the graph resources…
AudioProcessorGraph::prepareToPlay
AudioProcessorGraph::prepareToPlay done
AudioProcessorGraph::handleAsyncUpdate

7:45:13pm mainproc::prepareToPlay - SampleRate: 48000, Input Channels = 0, Output Channels = 32, BufferSize = 1024
Is Non-Realtime: 1
Release the graph resources…
AudioProcessorGraph::prepareToPlay
AudioProcessorGraph::prepareToPlay done

7:45:13pm mainproc::prepareToPlay - SampleRate: 48000, Input Channels = 0, Output Channels = 32, BufferSize = 1024
Is Non-Realtime: 0
AudioProcessorGraph::prepareToPlay
AudioProcessorGraph::prepareToPlay done

7:45:13pm mainproc::prepareToPlay - SampleRate: 48000, Input Channels = 0, Output Channels = 32, BufferSize = 1024
Is Non-Realtime: 0
AudioProcessorGraph::prepareToPlay
AudioProcessorGraph::prepareToPlay done

Saving State Chunk

So this “fixes” the problem… but doesn’t answer why…

void AudioProcessorGraph::prepareToPlay (double sampleRate, int estimatedSamplesPerBlock)
{
    setRateAndBufferSizeDetails (sampleRate, estimatedSamplesPerBlock);
    clearRenderingSequence();
    
    if (isNonRealtime())
        handleAsyncUpdate();
    else
        triggerAsyncUpdate();
}

prepareToPlay is being called on the Message Thread.

I’m guessing that S1 on Windows is blocking the Message Thread with it’s progress bar (or something).

Rail

Thank you for the investigation!

1 Like

The mm-lock in prepare to play is a absolute nogo. Will cause a deadlock in any host which block the message-thread while calling preparetoplay or SetStateInformation… Also async initalisation inside prepare to play is unreliable.

(The use of mmlock is always a bad sign in plugin ins)

I don’t have an MM-Lock in my local copy of AudioProcessorGraph.

I believe there’s a mod in the forum here which replaces the MM-Lock in buildRenderingSequence() with

  const ScopedLock sl (buildLock); // MessageManagerLock mml;

Rail

Hi Tom,

I see you changed the commit to:

I believe prepareToPlay is (almost) always called on the Message Thread. Have you checked if the RealTime call to prepareToPlay is ever on another Thread?

Perhaps make AudioProcessor::nonRealtime atomic?

Rail

I believe prepareToPlay could happen on the audio thread in older versions of Pro Tools.

I’ll test with Pro Tools 10.

My qualm is if this change could negatively affect real-time performance since 99% of the time this will now be a synchronous call.

Cheers,

Rail

@Rail_Jon_Rogut okay
@t0m

Sorry for interfering, I don’t want to confuse anybody, I have nothing specific to say to the current problem, but I remember a lot of trouble with AudioProcessorGraphs inside plugins.

The road to success is to remove any message-thread dependency from that class.
No AsyncUpdates, no MessageManager Locks etc…

If you call prepareToPlay the dsp must be ready after that call, from wherever the call comes from, so the triggerAsyncUpdate irritates me.

I guess someone have chosen this design, because rendering helper creates the plugin instances?!, which is pretty much standard to do this on the message thread, if this is not necessary, I don’t understand the MessageManagerLock inside buildRenderingSequence, maybe just use an extra CriticalSection for these kind of tasks and don’t messing up with the message-thread.

1 Like

@ chkn Looking at the source if the graph isn’t prepared in processBlock() then in real-time it’ll drop the buffer until the graph is prepared… so the AsyncUpdate worked in that regard.

Rail

Urgh. Yes you’re right about things slowing down - I’ll put things back as they were.

Sorry. My original thought was to limit the “fix” to JUCE_WIN and if the Host were Studio One… but my tests have been positive with the earlier change and I’ve seeded the build to our beta testers yesterday.

Cheers,

Rail

But anyway, restricting the fix to only the isNonRealtime() case makes sense since the previous code was not problematic in realtime cases, so in the end combining the two could do the trick:

if (isNonRealtime() && MessageManager::getInstance()->isThisTheMessageThread())
1 Like

and with the next update the trouble will begin again, the only chance to fix this problems forever is to remove the message-thread dependancy.

as host I expect the dsp to be ready after prepare to play, and not in the near future.

@t0m
Why is there a MessageManagerLock in buildRenderingSequence?

1 Like

I think Jules has admitted that if he could he’d probably rewrite the AudioProcessorGraph class (check out the recent ADC JUCE Q&A) :slight_smile:

Cheers,

Rail

A quick test with Pro Tools 10.3.10 on macOS shows prepareToPlay() is called on the Message Thread.

Cheers.

Rail

I’ve tracked down one instance where it was possible - before PT 12 we could receive a larger than expected buffer size and JUCE would call prepareToPlay with the new buffer size from the audio callback.

Something I’d like to add to JUCE would be some automatic chunking of audio buffers so that prepareToPlay would give you the real maximum size you could possibly get in your plug-in’s process block, rather than just a “hint” of what it might be. That would get rid of any related issues and make users’ code simpler.

4 Likes