During troubleshooting, we noticed that the JUCE VST3 wrapper processes changes to the wrapper-generated “program change parameter” from the audio thread. This leaves us confused - why does it do that? Here is what happens:
The host changes the “program change parameter”
The JUCE VST3 wrapper flags the parameter as “changed” but doesn’t do anything else
during the next call to juce_VST3_Wrapper.cpp:process() (line 2470) the wrapper calls processParameterChanges() (line 2401) which will finally load the requested program in line 2427.
We find this pretty weird, given that loading a program usually is a time-consuming thing that shouldn’t happen from the audio thread - especially, since that parameter can (and will) be automated by users.
As a solution, we check if the current thread is the GUI thread - and if not, post a message to asynchronously load the program. However, that doesn’t fix all problems.
E.g. MAGIX Sequoia has a special way of copying plugins, where it will
store the plugin state and parameter values of the source plugin
create a new plugin instance on the destination track
reload all parameter values from the previously saved values
finally pass the previously saved plugin state to the plugin.
This creates problems. Now after step 4, the plugin has the correct state. But due to step 3, with the next processBlock() the program change parameter will still be flagged as “changed”, causing the plugin to reload the program according to the value of the “program change parameter” - possibly overwriting the actual state of the plugin.
So our request is to process the “program change parameter” synchronously. This will avoid such problems.
VST3 changes parameters on the audio thread, this is called ‘sample-accurate’ automation and is a very good thing.
The ‘program change parameter’ is a quick and dirty hack to assist while you transition away from legacy code. It was possibly added as an afterthought which is why it’s implemented the same as regular parameters…
If changing preset is too time-consuming for the audio thread, pass a message to the controller to perform the action on a non-realtime thread. As you noted, this will cause race-conditions. Steinberg’s own VST2 wrapper had the same issue. My solution to fix that is:
On program load discard any pending parameter updates (the content of ‘mGuiTransfer’ and '‘mInputTransfer’).
Use a timer such that any program change parameter updates that occur soon after loading a preset are ignored. After some experimentation I chose a 1000ms wait.
I understand that changing parameters from the audio thread is a good idea. The ‘program change parameter’ however shouldn’t be handled in the same way, imo. It’s also not documented as the hacky transition help that it appears to be so consequently we didn’t expect this behaviour.
This works fine for some hosts, and it doesn’t work for others. e.g. Some hosts suspend audio processing if there is no material on a track and only start processing once there is an item. This can lead to the first “processBlock()” happening many minutes after loading a program.
Yes, this is what I meant with my second post. Sure I can add those changes to the wrapper code myself. However, we already have a long list of changes where we deviate from the “stock” JUCE codebase with our own fixes and additions (mostly for unit testing). I would rather follow a more “official” solution, that’s why I asked here in the first place.
Bottom line is: If there is a chance to see this change included in a future JUCE version, I’ll happily contribute a pull request. If not, I’m open to other solutions that don’t require changes to JUCE code.
okay, fair enough, I see that it makes sense like that. We can asynchronously load the preset from the GUI thread if we need to.
How can we avoid the race condition that happens when parameter changes from before a program change are executed by processBlock() after the program change? From the plugin side there is no way to tell where a changed value actually came from. IMO the wrapper should discard any unprocessed changes when a program is loaded / a plugin state is restored.
You’re getting a race because the parameter change triggered by #3 is only served on the message thread after #4 happened, right?
Would it be feasible for you to also serve #4 in a similar fashion? instead of loading the plug-in state synchronously when #4 happens, you queue that for being served on the message thread (now with lambdas it’s even easier than before), so that the sequence will go like this and the order of operations remains consistent:
#3: reload all parameter values (triggers a later program change) #4: pass the previously saved state to the plug-in (gets queued on the message thread)
then on the message thread they are handled retaining the order:
deferred handling of #3: loads whatever program was connected with automation
deferred handling of #4: finally, the intended state stored in the session for the plug-in is loaded into it
Unfortunately, we can only post a message when we actually know that something has changed. That is the problem. The VST3 wrapper delays the update of the program change parameter until the first processBlock() which happens long after the plugin state was restored. If we post messages on the GUI thread, they would also be in the wrong order.
Sure, we could ignore the first update to the program change parameter after a program load. But this creates new problems - what if that update was triggered by the user who actually wanted to change the program? A timeout doesn’t work. As stated before, some hosts suspend audio processing so the first processBlock() after a program load could be minutes later. Really, when processBlock() updates the program change parameter, we have no way of knowing if the new value came from before or after the program load.
IMO the cleanest solution would be to actually discard any queued parameter changes when a new program is loaded or the plugin state is restored. IMO this must be handled from the VST3 wrapper.