Ableton offline rendering at altered sample rate issue

Ableton Live has a setting when bouncing a project to use a different sample rate to the one specified for the normal project playback.

We’ve noticed that our VST3 plugins no longer render correctly when this is used, with strange dropouts all over the place.

I tracked the problem up the stack into JUCE until I reached juce_audio_plugin_client_VST3.cpp. I inserted some logs into a file at various points there that show that, indeed, many processBlock() calls seem to simply “not happen” under these settings.

I’m logging whenever the MIDI buffer contains events (logging each event in the buffer), but I’m also logging the number of blocks with empty MIDI buffers that occurred between those too (to keep the logs shorter).

Here are some examples. First, an example where we bounce at the project’s native 44.1kHz (this project contains only 4 MIDI notes heading into our VST3 instrument):

prepareToPlay (44100, 128)
All notes off Channel 1
... (All notes off for each channel)
[35 blocks processed] 4480 samples total
Note on C#2 Velocity 100 Channel 1
[344 blocks processed] 44032 samples total
Note off C#2 Velocity 64 Channel 1
Note on F#2 Velocity 100 Channel 1
[345 blocks processed] 44160 samples total
Note off F#2 Velocity 64 Channel 1
Note on G#1 Velocity 100 Channel 1
[344 blocks processed] 44032 samples total
Note off G#1 Velocity 64 Channel 1
Note on D#2 Velocity 100 Channel 1
[345 blocks processed] 44160 samples total
Note off D#2 Velocity 64 Channel 1
[1563 blocks processed] 200064 samples total
prepareToPlay (44100, 128)

Then here is an example where we render the same project at 48kHz, without changing the sample rate of the audio device:

prepareToPlay (44100, 128)
prepareToPlay (48000, 128)
prepareToPlay (48000, 128)
[0 blocks processed] 0 samples total
All notes off Channel 1
... (All notes off for all MIDI channels)
[38 blocks processed] 4864 samples total
Note on C#2 Velocity 100 Channel 1
[375 blocks processed] 48000 samples total
Note off C#2 Velocity 64 Channel 1
Note on F#2 Velocity 100 Channel 1
[103 blocks processed] 13184 samples total
prepareToPlay (48000, 128)
prepareToPlay (44100, 128)
prepareToPlay (44100, 128)
prepareToPlay (44100, 128)

So it looks like a bunch of audio and MIDI blocks are being dropped.

Thanks for reporting this issue. I’d be interested to know whether you see the same behaviour in JUCE’s AudioPluginDemo, and/or in any non-JUCE plugins you have available, so that we can rule out bugs in Live and in user-level code before investigating further.

Hmm, you’re right. I built the AudioPluginDemo and it doesn’t exhibit the same audio problems, and all the MIDI notes I expect are present in the logs.

The logs I posted were generated by statements I inserted into juce_audio_plugin_client_VST3.cpp, so I assumed that our code wouldn’t be at fault, but that seems to be wrong.

Must it be that we’ve configured the plugin incorrectly via calls to juce::AudioProcessor base class function in our plugin, or virtual method overrides? Have you got any thoughts which of these we might be using incorrectly if so? We’re not observing any issues in any other scenarios (that we know of).

I can think of the following reasons that might cause processBlock not to be called:

  • AudioProcessor::suspendProcessing() might be called and accidentally left enabled during render callbacks, in which case the audio buffer will be cleared and no AudioProcessor function will be called.
  • The AudioProcessor doesn’t have a custom bypass parameter, in which case processBlockBypassed will be called instead of processBlock if the host attempts to bypass the plugin.

Nothing else springs to mind - suspendProcessing feels like the most likely culprit.

We don’t use suspendProcessing anywhere in our codebase, so I don’t think it’s that.

I’m currently investigating if it has anything to our implementation of plugin latency, because I noticed (via the same logs) that our plugin is calling setLatencySamples() once in the middle of the render at 48kHz, but not at the native 44.1kHz. This is surprising - I’m not sure why our plugin thinks its latency changed at this point when rendering has already begun, but does that seem like it could be a culprit?

Maybe that’s related. When AudioProcessor::setLatencySamples is called, the latency will only be updated immediately if the call is made on the message/UI thread. This is because the latency change must go via the host’s IComponentHandler::restartComponent function, which may only be called in the UI thread context. Perhaps you could check which thread is calling setLatencySamples.

It’s a bit odd that this function is being called during processing. I think I’d expect the latency to be set during prepareToPlay, which in VST3 should always happen on the UI thread. Does the latency of your plugin change depending on parameter values or MIDI input?

Yes, it can do. We have effects modules that cause latency when enabled, but can be turned off, and then should cause the plugin to be latency free. These are automatable (but in this case I haven’t added any automation to the project).

As a result we call setLatencySamples() in both prepareToPlay() and processBlock(). I would guess these changes are coming from the audio thread (I’ll check that as you suggest).

My next steps are:

  • Find out if avoiding those calls to setLatencySamples() in processBlock() fixes the problem
  • Find out why our plugin thinks the latency changed

The second of those is really our problem, but if the first fails then that means our assumption that it’s ok to change latency while rendering is wrong, and we’ll have to rethink that.

I’ll update here when I find the answers to these questions, for anyone else who discovers the issue in future.

2 Likes

So yes, these latency changes are indeed the problem, and removing them fixes it. I still think it’s surprising that this caused Ableton to essentially choke while rendering, but there you go.

The root cause I discovered is part of our plugin that can’t compute its latency until it’s been processed for one block (long story…). Our very unsophisticated solution to the problem is to process one block in prepareToPlay() so we know what the latency is before playback begins.

Sorry for taking up your time @reuk, there were parts of our plugin written by my colleague that I hadn’t fully understood when I began debugging this issue. Thanks for your help!

1 Like

No worries, glad I could help a bit! Thanks for sharing your findings.