VST3 parameter updates: automation vs. host refresh

tldr: In JUCE-based VST3s with an internal preset browser, preset changes don’t update parameter values in the host unless you notify it manually — but doing so may trigger unwanted automation recording.

There are basically two ways the host can be notified that some parameters have changed:

  • automation recordable : the parameter is explicitly modified by the user, for example by dragging a slider.
  • not recorded by automation: the parameter is modified indirectly, for example the user switches presets in the internal preset browser of the plugin. In that case all parameters are modified, but you do not want the host to auto-create a thousand automation tracks if you have a thousand parameters. However you still want to ping the host in some way so that it updates its cached values of the parameter.

Why does this matter ? Consider Komplete Kontrol, or the JUCE AudioPluginHost. They both display the current value of parameters (for Komplete Kontrol: in its gui and on the Komplete keyboard screen, for AudioPluginHost: in the “show all parameters” window). Open any VST3 which has a preset browser. When you drag a slider inside the VST3, its value in the host changes accordingly. If you change preset in the vst, you expect the values in the host to change accordingly. This is only the case for JUCE plugins if you explicitey call AudioProcessorParameter::setValueNotifyingHost on all your params. Otherwise the values displayed by KK or AudioPluginHost are simply not the correct ones.

But now, if you call setValueNotifyingHost on preset changes, and load your plugin in a VST3 host such as Reason, and change presets while Reason is recording, it will create an automation track for ALL your parameters.

So there are situations where you want to inform the host that a parameter has changed, without telling it that the parameter is actively being edited by the user.

As far as I understand VST3 (which is not much , unfortunately), you have:

beginEdit / performEdit / endEdit for notifying the host of recordable automation events.
setParamNormalized to just inform the host of the param value change

Problem is JuceVST3EditController::paramChanged always calls both.

If I change it this way, so that performEdit is only called when inside a beginGesture / endGesture it is much better:

int gesture_cnt = 0;
void beginGesture (Vst::ParamID vstParamId)
    {
      if (! inSetState && MessageManager::getInstance()->isThisTheMessageThread()) {
            beginEdit (vstParamId);
             ++gesture_cnt;
      }
    }

    void endGesture (Vst::ParamID vstParamId)
    {
      if (! inSetState && MessageManager::getInstance()->isThisTheMessageThread()) {
            endEdit (vstParamId);
            --gesture_cnt;
      }
    }

    void paramChanged (Steinberg::int32 parameterIndex, Vst::ParamID vstParamId, double newValue)
    {
        if (inParameterChangedCallback || inSetState)
            return;

        if (MessageManager::getInstance()->isThisTheMessageThread())
        {
            EditController::setParamNormalized (vstParamId, newValue);
            if (gesture_cnt) { // notify the host of aa recordable param change only if there was a beginGesture before..
              performEdit (vstParamId, newValue);
            }
        }
        else
        {
            audioProcessor->setParameterValue (parameterIndex, (float) newValue);
        }
    }

But that is not a great hack. It would probably be better to have a way to call AudioProcessor::updateHostDisplay with some argument that would trigger a refresh of the param values seen by the host.

that’s what beginChangeGesture is supposed to be about

But some hosts, such as Reason, do record automation when they receive a performEdit call from the vst3, even if it did not receive a beginEdit before. So you have automation recorded without using beginChangeGesture / endChangeGesture

bad host, fix host :slight_smile:

same issue in ableton live

There is no setParamNormalized on the VST3 host API.
You can use beginEdit / performEdit / endEdit from the message thread.

You can also use IComponentHandler2::setDirty on the message thread, to set the entire state ‘dirty’. But that is meant for non-parameter state.

On the real-time thread you can pass parameter changes to the DAW via IParameterChanges, but you usually wouldn’t use that for communicating a preset change.

So AFAIK JUCE is correct to be using beginEdit / performEdit / endEdit to notify the DAW of parameter changes.

This might be a case of the DAW misbehaving.

I think this is the correct behaviour. If the user is recording automation, parameter changes should be recorded and it doesn’t matter if only one parameter changes or all. The user expects that the playback will play back all these parameter changes. I don’t see the failure here.

3 Likes

Thanks everyone. So as I understand it, plugins are expected to notify the host of the value change of each parameter after a preset is loaded, or any other internal operation in the plugin (like undo / redo, copy / paste etc), even if that means sending a thousand setParamNormalized/performEdit messages. In the case of Reason, auto-creating a thousand automation tracks when the plugin does an undo / redo ou changes preset is a defect of Reason and not the plugin mis-using the VST3 api.

Still I’m very frustrated that we do not have a simple way to just tell the host “hey all, or some, parameters values may have changed” without telling it the actual value. Because that means we have to notify the host at the exact moment the param change happens. If we do it later, like when polling for changes in a timerCallback, the call to setValueNotifyingHost from the message thread will trigger an asynchronous parameter change message executed by the juce VST3 wrapper in the next processBlock, which will overwrite any param change that happening between the call to setValueNotifyingHost and processBlock.

This is SO complicated. Looking at the vst3 sdk code, and the juce code I just can’t figure how this is supposed to work , everything is so convoluted and opaque, there are just to many abstraction layers.

yep, JUCE supports a bunch of plugin APIs, but at its heart, JUCE’s parameter model is architected like VST2 (no meaningful separation of concerns between the Processor and the Editor). And this results in some very awkward cludges to enable JUCE to work with VST3 and other more modern APIs.
As JUCE moves toward supporting sample-accurate parameters, they will be forced to somewhat untangle the distinction between setting a parameter on the Editor vs setting it on the Processor.

1 Like