VST3 wrapper calls parameterChanged() from audio thread

I’m getting deadlocks when loading a preset in our VST3 (JUCE 6 Develop a few months old).
Preset load from the main thread, but somehow I’m also getting paramterChanged() calls from the audio thread. This two threads conflict and I’m getting deadlocks.

The call has its source in the juce_VST3_Wrapper.cpp (process method):

Stack trace:

DelayEngine::parameterChanged(const juce::String &, float) DelayEngine.h:243
<lambda>::operator()(juce::AudioProcessorValueTreeState::Listener &) const juce_AudioProcessorValueTreeState.cpp:165
juce::ListenerList::call<…>(<lambda> &&) juce_ListenerList.h:124
juce::AudioProcessorValueTreeState::ParameterAdapter::LockedListeners::call<…>(<lambda> &&) juce_AudioProcessorValueTreeState.cpp:195
juce::AudioProcessorValueTreeState::ParameterAdapter::parameterValueChanged(int, float) juce_AudioProcessorValueTreeState.cpp:165
<lambda>::operator()() const juce_AudioProcessorValueTreeState.cpp:93
decltype(std::__1::forward<juce::AudioProcessorValueTreeState::ParameterAdapter::ParameterAdapter(juce::RangedAudioParameter&)::'lambda'()&>(fp)()) std::__1::__invoke<juce::AudioProcessorValueTreeState::ParameterAdapter::ParameterAdapter(juce::RangedAudioParameter&)::'lambda'()&>(juce::AudioProcessorValueTreeState::ParameterAdapter::ParameterAdapter(juce::RangedAudioParameter&)::'lambda'()&) type_traits:3545
std::__invoke_void_return_wrapper::__call<…>(<lambda> &) __functional_base:348
std::__function::__alloc_func::operator()() functional:1546
std::__function::__func::operator()() functional:1720
std::__function::__value_func::operator()() const functional:1873
std::function::operator()() const functional:2548
juce::AudioProcessorValueTreeState::Parameter::valueChanged(float) juce_AudioProcessorValueTreeState.cpp:75
juce::AudioParameterFloat::setValue(float) juce_AudioParameterFloat.cpp:87
juce::AudioProcessorParameter::setValueNotifyingHost(float) juce_AudioProcessor.cpp:1508
juce::setValueAndNotifyIfChanged(juce::AudioProcessorParameter &, float) juce_VST3_Wrapper.cpp:633
juce::JuceVST3Component::processParameterChanges(Steinberg::Vst::IParameterChanges &) juce_VST3_Wrapper.cpp:3097
juce::JuceVST3Component::process(Steinberg::Vst::ProcessData &) juce_VST3_Wrapper.cpp:3150
VST3_ProcessReplacing(AEffect *, float **, float **, int) 0x000000010ae7aac1
VST_HostedPlugin::ProcessSamples(int, double *, int, int, int, double, midi_List *, bool *, double, double, double, bool) 0x000000010ac91b0f
....

How can this happen? I always thought that parameterChanged() only will be called from the UI thread. This also let me think if our other plugins are safe.

The problem happens randomly but it shows up a lot because i’m having linked parameters like this (it’s a bit simplified here, i make sure it does not call the other method when the value is already the same):

        if (parameterID == _params.paramTimeR)
        {
            setDelayTimeR(newValue);

            if (delayLink)
            {
                _params.parameters[_params.paramTimeL]->setValueNotifyingHost(newValue);
            }
        }

        if (parameterID == _params.paramTimeL)
        {
            setDelayTimeL(newValue);

            if (delayLink)
            {
                _params.parameters[_params.paramTimeR]->setValueNotifyingHost(newValue);
            }
        }

Any help is welcome. I never had this problem so far. Maybe i’m, missing something.

The docs for AudioProcessorParameter::Listener say this:

        /** Receives a callback when a parameter has been changed.

            IMPORTANT NOTE: This will be called synchronously when a parameter changes, and
            many audio processors will change their parameter during their audio callback.
            This means that not only has your handler code got to be completely thread-safe,
            but it's also got to be VERY fast, and avoid blocking. If you need to handle
            this event on your message thread, use this callback to trigger an AsyncUpdater
            or ChangeBroadcaster which you can respond to on the message thread.
        */
        virtual void parameterValueChanged (int parameterIndex, float newValue) = 0;

The same rules hold for the parameter-value-change callback on AudioProcessorListener.

Which locks are involved? At the point of the deadlock, there will normally be at least two threads waiting on two different locks. If both of those locks are in JUCE code, then it would be useful to know which ones (and to see the full stack traces of both threads) so that we can evaluate whether this is likely to be a common problem. If one of the locks is in your code, then you might need to change where you take that lock, so that all threads always acquire all locks in the same order.

Thanks for the information. I was using Reaper on macOS for the tests.

Yes, I’m having two threads. The Main Thread, which loads the preset and was triggered by the UI and the audio processor that also sends parameter changes from the Audio-Thread.

It is LockedListeners where the deadlock happens. Two different threads call setValueNotifyingHost() and I’m having a few listeners attached.

We have two threads that are locked:

__psynch_mutexwait 0x00007fff72409062
_pthread_mutex_firstfit_lock_wait 0x00007fff724c7917
_pthread_mutex_firstfit_lock_slow 0x00007fff724c5937
juce::CriticalSection::enter() const juce_posix_SharedCode.h:39
juce::GenericScopedLock::GenericScopedLock(const juce::CriticalSection &) juce_ScopedLock.h:67
juce::GenericScopedLock::GenericScopedLock(const juce::CriticalSection &) juce_ScopedLock.h:67
juce::AudioProcessorValueTreeState::ParameterAdapter::LockedListeners::call<…>(<lambda> &&) juce_AudioProcessorValueTreeState.cpp:194
juce::AudioProcessorValueTreeState::ParameterAdapter::parameterValueChanged(int, float) juce_AudioProcessorValueTreeState.cpp:165
<lambda>::operator()() const juce_AudioProcessorValueTreeState.cpp:93
decltype(std::__1::forward<juce::AudioProcessorValueTreeState::ParameterAdapter::ParameterAdapter(juce::RangedAudioParameter&)::'lambda'()&>(fp)()) std::__1::__invoke<juce::AudioProcessorValueTreeState::ParameterAdapter::ParameterAdapter(juce::RangedAudioParameter&)::'lambda'()&>(juce::AudioProcessorValueTreeState::ParameterAdapter::ParameterAdapter(juce::RangedAudioParameter&)::'lambda'()&) type_traits:3545
std::__invoke_void_return_wrapper::__call<…>(<lambda> &) __functional_base:348
std::__function::__alloc_func::operator()() functional:1546
std::__function::__func::operator()() functional:1720
std::__function::__value_func::operator()() const functional:1873
std::function::operator()() const functional:2548
juce::AudioProcessorValueTreeState::Parameter::valueChanged(float) juce_AudioProcessorValueTreeState.cpp:75
juce::AudioParameterFloat::setValue(float) juce_AudioParameterFloat.cpp:87
juce::AudioProcessorParameter::setValueNotifyingHost(float) juce_AudioProcessor.cpp:1508
DelayEngine::parameterChanged(const juce::String &, float) DelayEngine.h:253
<lambda>::operator()(juce::AudioProcessorValueTreeState::Listener &) const juce_AudioProcessorValueTreeState.cpp:165
juce::ListenerList::call<…>(<lambda> &&) juce_ListenerList.h:124
juce::AudioProcessorValueTreeState::ParameterAdapter::LockedListeners::call<…>(<lambda> &&) juce_AudioProcessorValueTreeState.cpp:195
juce::AudioProcessorValueTreeState::ParameterAdapter::parameterValueChanged(int, float) juce_AudioProcessorValueTreeState.cpp:165
<lambda>::operator()() const juce_AudioProcessorValueTreeState.cpp:93
decltype(std::__1::forward<juce::AudioProcessorValueTreeState::ParameterAdapter::ParameterAdapter(juce::RangedAudioParameter&)::'lambda'()&>(fp)()) std::__1::__invoke<juce::AudioProcessorValueTreeState::ParameterAdapter::ParameterAdapter(juce::RangedAudioParameter&)::'lambda'()&>(juce::AudioProcessorValueTreeState::ParameterAdapter::ParameterAdapter(juce::RangedAudioParameter&)::'lambda'()&) type_traits:3545
std::__invoke_void_return_wrapper::__call<…>(<lambda> &) __functional_base:348
std::__function::__alloc_func::operator()() functional:1546
std::__function::__func::operator()() functional:1720
std::__function::__value_func::operator()() const functional:1873
std::function::operator()() const functional:2548
juce::AudioProcessorValueTreeState::Parameter::valueChanged(float) juce_AudioProcessorValueTreeState.cpp:75
juce::AudioParameterFloat::setValue(float) juce_AudioParameterFloat.cpp:87
juce::AudioProcessorParameter::setValueNotifyingHost(float) juce_AudioProcessor.cpp:1508
juce::setValueAndNotifyIfChanged(juce::AudioProcessorParameter &, float) juce_VST3_Wrapper.cpp:633
juce::JuceVST3Component::processParameterChanges(Steinberg::Vst::IParameterChanges &) juce_VST3_Wrapper.cpp:3097
juce::JuceVST3Component::process(Steinberg::Vst::ProcessData &) juce_VST3_Wrapper.cpp:3150
VST3_ProcessReplacing(AEffect *, float **, float **, int) 0x000000010c0e4ac1
__psynch_mutexwait 0x00007fff72409062
_pthread_mutex_firstfit_lock_wait 0x00007fff724c7917
_pthread_mutex_firstfit_lock_slow 0x00007fff724c5937
juce::CriticalSection::enter() const juce_posix_SharedCode.h:39
juce::GenericScopedLock::GenericScopedLock(const juce::CriticalSection &) juce_ScopedLock.h:67
juce::GenericScopedLock::GenericScopedLock(const juce::CriticalSection &) juce_ScopedLock.h:67
juce::AudioProcessorValueTreeState::ParameterAdapter::LockedListeners::call<…>(<lambda> &&) juce_AudioProcessorValueTreeState.cpp:194
juce::AudioProcessorValueTreeState::ParameterAdapter::parameterValueChanged(int, float) juce_AudioProcessorValueTreeState.cpp:165
<lambda>::operator()() const juce_AudioProcessorValueTreeState.cpp:93
decltype(std::__1::forward<juce::AudioProcessorValueTreeState::ParameterAdapter::ParameterAdapter(juce::RangedAudioParameter&)::'lambda'()&>(fp)()) std::__1::__invoke<juce::AudioProcessorValueTreeState::ParameterAdapter::ParameterAdapter(juce::RangedAudioParameter&)::'lambda'()&>(juce::AudioProcessorValueTreeState::ParameterAdapter::ParameterAdapter(juce::RangedAudioParameter&)::'lambda'()&) type_traits:3545
std::__invoke_void_return_wrapper::__call<…>(<lambda> &) __functional_base:348
std::__function::__alloc_func::operator()() functional:1546
std::__function::__func::operator()() functional:1720
std::__function::__value_func::operator()() const functional:1873
std::function::operator()() const functional:2548
juce::AudioProcessorValueTreeState::Parameter::valueChanged(float) juce_AudioProcessorValueTreeState.cpp:75
juce::AudioParameterFloat::setValue(float) juce_AudioParameterFloat.cpp:87
juce::AudioProcessorParameter::setValueNotifyingHost(float) juce_AudioProcessor.cpp:1508
juce::setValueAndNotifyIfChanged(juce::AudioProcessorParameter &, float) juce_VST3_Wrapper.cpp:633
juce::JuceVST3EditController::Param::setNormalized(double) juce_VST3_Wrapper.cpp:759
Steinberg::Vst::EditController::setParamNormalized(unsigned int, double) vsteditcontroller.cpp:178
juce::JuceVST3EditController::paramChanged(int, unsigned int, double) juce_VST3_Wrapper.cpp:1195
juce::JuceVST3EditController::audioProcessorParameterChanged(juce::AudioProcessor *, int, float) juce_VST3_Wrapper.cpp:1217
juce::AudioProcessorParameter::sendValueChangedMessageToListeners(float) juce_AudioProcessor.cpp:1584
juce::AudioProcessorParameter::setValueNotifyingHost(float) juce_AudioProcessor.cpp:1509
DelayEngine::parameterChanged(const juce::String &, float) DelayEngine.h:263
<lambda>::operator()(juce::AudioProcessorValueTreeState::Listener &) const juce_AudioProcessorValueTreeState.cpp:165
juce::ListenerList::call<…>(<lambda> &&) juce_ListenerList.h:124
juce::AudioProcessorValueTreeState::ParameterAdapter::LockedListeners::call<…>(<lambda> &&) juce_AudioProcessorValueTreeState.cpp:195
juce::AudioProcessorValueTreeState::ParameterAdapter::parameterValueChanged(int, float) juce_AudioProcessorValueTreeState.cpp:165
<lambda>::operator()() const juce_AudioProcessorValueTreeState.cpp:93
decltype(std::__1::forward<juce::AudioProcessorValueTreeState::ParameterAdapter::ParameterAdapter(juce::RangedAudioParameter&)::'lambda'()&>(fp)()) std::__1::__invoke<juce::AudioProcessorValueTreeState::ParameterAdapter::ParameterAdapter(juce::RangedAudioParameter&)::'lambda'()&>(juce::AudioProcessorValueTreeState::ParameterAdapter::ParameterAdapter(juce::RangedAudioParameter&)::'lambda'()&) type_traits:3545
std::__invoke_void_return_wrapper::__call<…>(<lambda> &) __functional_base:348
std::__function::__alloc_func::operator()() functional:1546
std::__function::__func::operator()() functional:1720
std::__function::__value_func::operator()() const functional:1873
std::function::operator()() const functional:2548
juce::AudioProcessorValueTreeState::Parameter::valueChanged(float) juce_AudioProcessorValueTreeState.cpp:75
juce::AudioParameterFloat::setValue(float) juce_AudioParameterFloat.cpp:87
juce::AudioProcessorParameter::setValueNotifyingHost(float) juce_AudioProcessor.cpp:1508
TalAudioProcessor::loadPresetXml(juce::XmlElement *) PluginProcessor.cpp:341
TalAudioProcessor::loadPreset(juce::File) PluginProcessor.cpp:409
TalAudioProcessor::loadPreset(juce::String) PluginProcessor.cpp:459
PresetComponent::nextPreset(int) TalPresetComponent.h:366
PresetComponent::buttonClicked(juce::Button *) TalPresetComponent.h:320
$_33::operator()(juce::Button::Listener &) const juce_Button.cpp:419
juce::ListenerList::callChecked<…>(const juce::Component::BailOutChecker &, $_33 &&) juce_ListenerList.h:153
juce::Button::sendClickMessage(const juce::ModifierKeys &) juce_Button.cpp:419
juce::Button::setToggleState(bool, juce::NotificationType, juce::NotificationType) juce_Button.cpp:195
juce::Button::setToggleState(bool, juce::NotificationType) juce_Button.cpp:160
juce::Button::internalClickCallback(const juce::ModifierKeys &) juce_Button.cpp:363
juce::Button::mouseUp(const juce::MouseEvent &) juce_Button.cpp:490

I wonder why the host also sends parameter change messages to the listeners. It looks like it does it only if I’m changing presets fast.

But I wasn’t able to find anything that triggers this.

Edit: My guess is that this only can happen when we have bidirectionally linked parameters (meta parameters). What is a safe way to implement them?

I have a LINK parameter in my case and delayL and delayR parameters and Knobs. LINK does link them together and i want that always both change in that case. I don’t want to solve this in my UI. This should work also with the host generic UI.

That call sequence seems wrong. Why are you calling setValueNotifyingHost() from inside parameterChanged()? You’re changing a parameter while it’s being changed?

Or are you changing another parameter when this parameter changes? That should be avoided, if they’re both automatable parameters. When required, the controlling one should be marked as a meta-parameter.

This is explicitly disallowed by the VST3 specification:

No automated parameter must influence another automated parameter!

I guess in your situation, one thread is adjusting L, triggering a callback on R, while another thread is updating R, triggering a callback on L. Each parameter has its own listener mutex, so this results in a hierarchical deadlock. I think that the solution is to avoid linking the parameters in this way, and to implement the linking in the UI. Then, moving either slider in the UI can update both parameters individually, and restoring state can also set the parameter values individually, with no danger of deadlocks.

2 Likes

Yes. It looks like it isn’t a good idea to have linked parameters and trigger them with setValueNotifyingHost() inside the parameterChanged method. They are Meta parameters but also automatable.

I have a LINK parameter with delayL and delayR parameters. LINK does link them together and i want that always both change in that case. I want to use setValueNotifyingHost() to make sure the UI Knobs also update via the attachments.
This works pretty well except for that deadlock case. It looks like it is dangerous to use setValueNotifyingHost() in the parameterChanged method because different threads call this method.

I wonder how to implement parameters that can be linked together. I guess i have to go the good old UI Timer road and only set the parameters with setValue() without notifying anyone.

Thank for the clarification.

This means no automatable Meta Parameters! That is a huge limitation. I don’t how I could miss that.

We’ve actually always violated that rule. We have a couple of parameters that are automatable which set other automatable parameters, and can’t do it just from the UI and preset/session reading, because they also need to set the other parameters when automation sets the primary parameter. It’s bad design, but our users have expected this behavior for many years now. Personally, I think our primary parameter shouldn’t be a parameter at all, but should instead trigger two parameter updates when modified, and the automation should only affect those two parameters. But I’ve lost that battle long ago.

Thanks for sharing this. Same here. Unfortunately, we also violated that rule in some of our products. We never experienced any problems.
It looks like deadlocks can only occur when two parameters are bidirectional and can update each other.

What I would do is, if link is active simply switch the attachment of the right control to reflect the left parameter. And in processing simply use the left parameter for both.

The only drawback of that solution is, if link is active and the user alters the right parameter from the host controls, nothing happens. I don’t know if you can live with that, seems the cleanest solution to me.

1 Like

I can confirm I haven’t seen any issues with automated meta parameters in at least 2 commercial products.

Rail

1 Like

I also had a similar solution in the past. In my case, I had two overlapping knobs with the same bounds and switched visibility.
I did a refactoring last night and wanted to simplify this and solve it in the “backend” instead of the UI. I’m running into the same problems all few years, always believing that there is a clean and simple solution for this :slight_smile:

This time i’m going the path where the controls update each other when linked with an onChange function. The parameters are not synchrone also when linked (when the generic UI was used to change the params for example). So I needed some logic to sync the values when the user opens the UI. In my case, the left control is the master when they are linked.

I also like the idea of switching the attachment. Maybe I will try this next time.

As you said, automation and also the generic UI may not behave the right way with solutions like this.

For me, these UI-based solutions are workarounds and hacks that increase the complexity. It would be great to have a solution for this.

I still see some problems when the DAW (in my case Reaper) sends parameter changes through the audio callback without any reason. This can happen at every moment. Looks like it does it after some paramter has changed. Maybe after he gets the plugin state.

I had some cases where preset loading didn’t load all parameters the right way, because the host also sent his values, based on an older state of the plugin.

It’s still not clear to me why the DAW does this. I don’t have any automation enabled.

Looks like it does it after some paramter has changed. Maybe after he gets the plugin state.
[…] It’s still not clear to me why the DAW does this. I don’t have any automation enabled.

could it be that you’re calling updateHostDisplay() ?

That’s also what I thought at first, but I do not call that method. I’m using my own preset management just setting parameters. Hope I have time to look at it again. Something must be different. I didn’t notice that behavior with our other plugins so far.

I believe you can certainly do that if you take care of the threading in the callback. For example, using something like that:

struct MetaParameter
    : juce::RangedAudioParameter
    , juce::Timer
{
    MetaParameter() { startTimerHz(60); }

    void setValue(float newValue) override
    {
        //On the message thread, update the host right away, otherwise just update
        //the value and notify the host later in a lock-free way
        if (juce::MessageManager::getInstance()->isThisTheMessageThread())
        {
            param1->setValueNotifyingHost(newValue);
            param2->setValueNotifyingHost(newValue);
        }
        else
        {
            param1->setValue(newValue);
            param2->setValue(newValue);
            trigger.store(trigger.load() + 1);
        }
    }

    void timerCallback() override
    {
        auto current = trigger.load();

        if (current != lastTrigger)
        {
            lastTrigger = current;
            param1->sendValueChangedMessageToListeners(param1->getValue());
            param2->sendValueChangedMessageToListeners(param2->getValue());
        }
    }
    
    std::atomic<int> trigger {0};
    int lastTrigger = 0;

    juce::AudioProcessorParameter* param1 = nullptr;
    juce::AudioProcessorParameter* param2 = nullptr;
};
1 Like

Ohh, great idea. Thanks for sharing! This is what I will try in my next refactoring of the plugin.

1 Like