Bug: VST3 Program parameter does not record automation

Setup:

  • using the VST3 Program parameter, as provided by the JUCE VST3 wrapper
  • when loading a preset from the plugin GUI, we call updateHostDisplay() to trigger an update to the VST3 Program parameter. (Side note: This worked fine in the past but is currently broken - see pull request here).

Problem:

When the VST3 Program parameter is updated in juce_VST3_Wrapper.cpp:910 - audioProcessorChanged() there is no beginParameterChanged() or endParameterChanged() message sent to the host. This leads to hosts not recording this change in their automation data.

Is there a solution to work around this?

Bump

Wrapping the program parameter update in beginEdit() and endEdit() calls should ensure that automation data gets recorded correctly in hosts (I noticed that some hosts were recording it correctly without this, but not all). We’ve added this to develop here:

My experience in the past was that “beginEdit() - change Value - endEdit()” would not record anything in some DAWs because the value gets set to the new value, then reset back to the old value immediately on the same sample position - so DAWs sometimes seem to just ignore the change. This may only apply in some automation record modes (e.g. Touch mode) though.

Our fix for this was to call beginEdit() - then change the value and call endEdit() from a Timer a little later on. So the user at least sees that there is a change in the automation lane for the duration of the Timer period. This gets complicated tough when multiple successive changes are made. In that case you’ll have to “reference count” your beginEdit() / endEdit() calls to make sure that you don’t trigger too many / multiple calls. We added this “reference counting” feature (which also makes multi-touch GUIs much simpler because there you run into nested “change gestures” as well).

Which DAWs were you seeing this in? I was testing with Reaper which didn’t record program parameter automation before this change and now does, but if there are DAWs which are still not recording automation after this change then we can take a look.

I was testing this in Reaper earlier. I’ll try it again and report back.

I quickly tested one of our plugins with the already mentioned commit:

  • Reaper: It works just fine in latch mode, but the touch mode instantly resets the parameter back to the old value. I guess it is to be expected and something we can live with.
  • Bitwig 3.1.2: It writes automation just fine in Latch mode and behaves like Reaper in the Touch mode. However, when reading the automation data, the first change to the plugin program kicks Bitwig out of the Automation read mode (which normally happens when the user touches a control that is currently automated).

Here’s my findings for the Bitwig problem:

  1. Setup a plugin, manually add automation for the “Program” parameter: a step function from the initial value of 0 to the value of 1.
  2. Start playback
  3. When the parameter value changes from 0 to 1, AudioProcessor::setCurrentProgram() is called. Our code loads the program and asynchronously triggers a callback that eventually calls updateHostDisplay() from the message thread.
  4. updateHostDisplay() eventually ends up in juce_VST3_Wrapper.cpp:946 where it correctly calculates the normalized value for the Program parameter to be 1.0.
  5. In line 949 when testing if the value has changed, EditController::getParamNormalized(JuceAudioProcessor::paramPreset)) incorrectly returns 0.0. It should already be 1.0, especially since this is executed from the message thread long after the parameter was changed from the host.
  6. As the Program parameter appears to have changed, lines 951-955 are executed, where the beginEdit() and endEdit() make it seem as if the user changed the value so Bitwig correctly stops reading automation and Bitwigs “Restore Automation Control” button next to the time display in the top bar turns green.

Edit: It seems to be a timing issue.

While debugging, I saw that the JuceVST3EditController::ProgramChangeParameter gets its value set directly from Bitwig host code, where it eventually ends up in juce_VST3_Wrapper.cpp:595: ProgramChangeParameter::setNormalized(). This generally seems to happen after setCurrentProgram() is called from the audio thread. Depending on the timing, this may happen before or after our code asynchronically executes updateHostDisplay() so the parameter value typically ends up being incorrect.
I think updateHostDisplay() should be able to be called synchronously from inside setCurrentProgram(). So it seems to me that EditController::getParamNormalized (JuceAudioProcessor::paramPreset) in juce_VST3_Wrapper.cpp:949 is not the right way to check if the parameter has changed - it should better reference some local variable that is updated BEFORE the call to setCurrentProgram()

Here’s a quick fix that solves the issue for me. I added another local variable that tracks the currently loaded program and is updated immediately before the call to setCurrentProgram(). This allows the code in JuceVST3EditController::audioProcessorChanged() to compare to this local variable instead of the incorrect EditController parameter.

It solves the problem for me but I’m not sure if this really covers all situations. The new variable is a redundant representation of data that is actually stored elsewhere and I don’t know the codebase enough to see if there are other situations where the variable value should be updated.

Edit: Rounding errors ruin the show. I’ll change some things and report back
Edit2: Fixed it.

Thanks for investigating. As you say, it looks like the parameter value is out of date and EditController::getParamNormalized() is returning the old value. I think this is because we are processing the parameter updates from the host in processParameterChanges() and calling AudioProcessor::setCurrentProgram() before setting the actual parameter value. The solution should be to go via the edit controller here and move the setCurrentProgram() logic to the actual parameter object.

Can you apply this diff and see if it fixes the issue for you? I’ve tested it in Reaper and it seems to be working:

0001-VST3-Set-program-parameter-via-edit-controller-using.patch (4.0 KB)

I was thinking about updating the parameter value myself from processParameterChanges() but I decided against that as I was seing the host directly updating the parameter itself.

That’s what I’m also seing with your patch. Shortly after ProgramChangeParameter::setNormalized() is called from processParameterChanged() I see a second call to ProgramChangeParameter::setNormalized() that originates directly from Bitwig. This is the stacktrace:

> 	myPlugin.vst3!juce::JuceVST3EditController::ProgramChangeParameter::setNormalized(double v) Line 597
> 	myPlugin.vst3!Steinberg::Vst::EditController::setParamNormalized(unsigned long tag, double value) Line 186
> 	BitwigPluginHost64.exe!000000014003eed7()	Unknown
> 	BitwigPluginHost64.exe!0000000140048cdb()	Unknown

It seems to me like it’s coming from the GUI thread.

In your patch, at line 599 you catch that the ProgramChangeParameter already is at the correct value so setCurrentProgram() is not called a second time. So in the end it works but admittedly is seems a little unsafe with multiple threads potentially accessing the same stuff concurrently.

Edit: You can install and use the demo version of Bitwig to investigate this for yourself. Maybe this is a special behaviour of Bitwig, idk.

@ed95 I tested your fix a little more with Ableton Live Lite 10.1.2 and it shows the same problem as I had in Bitwig before your fix: As soon as automation is read, Ableton Live stops reading the automation as if the user manually touched the parameter. So Ableton Live is also affected.

I verified that your comparison in juce_VST3_Wrapper.cpp:953 works - it does. No spurious beginEdit() or endEdit() is sent from this place. However processParameterChanges() in line 2596 calls paramChanged() which in line 922 calls performEdit(). This in turn calls componentHandler->performEdit() which is host code - so I guess this is what ultimately makes Live think the user touched the parameter.

I think it ultimately boils down to the same thing: From all I know, the EditController is supposed to be updated from the Host via the message thread. Using the state of the EditControllers parameters (as it was before your patch) is wrong due to timing issues. Updating the Edit Controllers parameters ourselves (as done in your patch) is also wrong because that is supposed to happen via the host from the message thread.

My PullRequest works in Ableton Live because it doesn’t touch the EditControllers parameters from the audio thread and instead uses its own variables .

Thanks for the further info and apologies for the delay in getting back to you. You’re correct that going via the EditController is wrong as its methods should only be called on the message thread. We’ve pushed a fix to develop which uses the existing JUCE AudioParameter infrastructure to propagate the program parameter changes which should avoid these issues, and we’ve made the logic for determining whether the program has changed a bit more robust. I’ve tested this in Live, Reaper and BitWig and no longer see the spurious updates or automation being cancelled. Please let us know if this fixes things for you:

1 Like

Thanks for adding this to develop!
I’m sorry to report that Ableton doesn’t seem to record Automation anymore. In Bitwig it appears to work. Can you reproduce this behaviour?

Edit: I can see that execution runs through lines 963-968 so beginEdit() and endEdit() is called. I can also see that Ableton Live switches the “currently visible automation lane” in the arranger view so that it shows the program parameter. So Ableton recognizes that there is a change, but apparently doesn’t record that change. Not sure why, tbh.

I’m not able to reproduce this - I’ve tested in both Live 10 and 11 and both are recording automation fine for the program parameter.

How are you setting this up? The steps I’ve taken are to add the plug-in, unfold the device parameters, right-click on the program parameter and select “Show automation”, then hit record and move the parameter control.

Here’s what I did:

  1. Open new project
  2. Add plugin to a track
  3. switch the arranger view to automation mode (the small button above the track list on the right side of the screen)
  4. hit record and touch the preset-selector component in our plugin.

I can see that Ableton colours the entire track slightly red as soon as I load a new preset and the track content switches to the automation lane of the program parameter, however, it doesn’t actually record the changes.

One thing we may be doing differently is how we notify about the changed program. I do it exclusively via updateHostDisplay() - that is: Loading a preset via my plugin GUI eventually calls updateHostDisplay(). I never actually touch the program parameter itself. I suspect you may be editing the program parameter and that’s why we see a difference.

I noticed that in line 958 the value of paramValue never actually changes over the course of my tests (the value I see while debugging stays the same over the entire lifecycle of the plugin instance). It seems to me like line 964 paramChanged() never fully updates the value of the parameter. Maybe that’s why Ableton recognizes the beginning of the change gesture but doesn’t actually record the change - it just never sees a change in the parameter value.

I debugged into paramChanged() and noticed that the if (...) in line 617 makes ProgramChangeParameter::setNormalized() abort in line 619 when it is called from updateHostDisplay(). That’s why valueNormalized is never actually updated which is exactly what I observed.

Can you try this for ProgramChangeParameter::setNormalized()? It seems to fix things for me.

        bool setNormalized (Vst::ParamValue v) override
        {
            auto programValue = roundToInt (toPlain (v));
                        
            if (isPositiveAndBelow(programValue, owner.getNumPrograms())
                && programValue != owner.getCurrentProgram())
                owner.setCurrentProgram(programValue);

            if (valueNormalized == v)
                return false;

            valueNormalized = v;
            changed();

            return true;
        }

I can reproduce this now, thanks. We’ve pushed a fix to develop:

1 Like