Slider attachments with deferred (async) updates

I’ve been dealing with a use case that I couldn’t solve with SliderAttachment. Basically I have a set of parameters, all automatable, whose effective ranges are constrained by other parameters. They have fixed nominal ranges, but they can’t take any value at any time. So when they’re set either from automation or the UI, the new value has to be checked and corrected if necessary, and this has to be reflected on the UI. Doing it synchronously is not thread-safe. If I do it on the message thread, it may not work in offline renders. If I do it on the audio thread, there’s a lag for UI input that makes the attempted value visible before the corrected one. Also, many hosts don’t like to get parameter changes on any other than the message thread. So I ended up making a modified attachment class. When a change comes, either from automation or the UI, the processor is notified synchronously (through notifyNewValue). If it comes from the UI, the control is reset to the previous value. The processor reports back the corrected value (through updateValue), and both parameter and controls are updated on the message thread. (In my case, notifyNewValue pushes the change to a fifo that gets checked in processBlock, calling updateValue.) I detached this mechanism from the slider attachment itself, in case there’s more than one control for the same parameter, or more than one editor. AsyncAttachedControlGroup connects to the parameter and holds a vector of AsyncAttachedControl*, from which AsyncSliderAttachment derives. I post this here just in case anyone happens to need it, or anyone thinks there’s a simpler solution.

I’m in a similar situation, where I need the value of two APVTS parameters to constrain each another. I’m curious to see what you posted, but there’s nothing at that Github link any more. Did you find a different solution that worked better?

So far, I’ve been using my own attachment class based on the JUCE ParameterAttachment to connect GUI with parameters (because it’s a custom GUI component). I have the Processor be a Listener to the two linked parameters, and then in the parameterChanged callback, when one linked parameter is changed, I calculate the effect on the other parameter, and use setValueNotifyingHost to change the other parameter.

In order to avoid a loop where the two parameter Listeners ping each other back and forth, I use a bool member to flag that there’s an “adjustmentInProgress”, which blocks other parameterChanged calls from having any effect until the first parameterChanged call has completed.

However, it will still assert sometimes (because somehow there are multiple calls to beginChangeGesture), and I haven’t started testing it with automation yet, so it’s not working smoothly at this point!

I had two cases -a set of linked pairs, and a set where each parameter is bounded by its adjacent neighbors. The linked pair case was solved on the message thread at some point, and I’m not sure it worked. This was the last version of that:

struct LinkedParameterPair : juce::AudioProcessorParameter::Listener, juce::AsyncUpdater
{
    LinkedParameterPair (juce::AudioProcessorParameter* p0, juce::AudioProcessorParameter* p1)
        : p_{ p0, p1 } {}
    
    void parameterValueChanged (int index, float value) override
    {
        auto i{ p_[0]->getParameterIndex() == index };
        auto diff{ value - lastValue_[!i].exchange (value) }; // problems here

        if (!shouldLink() || diff == 0.0f) return;

        if (!juce::MessageManager::getInstance()->isThisTheMessageThread())
        {
            cancelPendingUpdate();
            diff_[i].fetch_add (diff);
            triggerAsyncUpdate();
        }
        else if (!linking_)
        {
            juce::ScopedValueSetter svs{ linking_, true };
            p_[i]->setValueNotifyingHost (p_[i]->getValue() + diff);
        }
    }

    void parameterGestureChanged (int index, bool starting) override
    {
        if (linking_ || !(shouldLink() || gestureStarted_)) return;

        auto p{ p_[p_[0]->getParameterIndex() == index] };
        juce::ScopedValueSetter svs{ linking_, true };

        if (starting) p->beginChangeGesture();
        else          p->endChangeGesture();

        gestureStarted_ = starting;
    }

    void handleAsyncUpdate() override
    {
        for (int i{}; i < 2; ++i)
            if (auto diff{ diff_[i].exchange (0.0f) }; diff != 0.0f)
            {
                juce::ScopedValueSetter svs{ linking_, true };
                p_[i]->setValueNotifyingHost (p_[i]->getValue() + diff);
            }
    }

    std::function<bool()> shouldLink;

private:
    juce::AudioProcessorParameter* p_[2]{};
    std::atomic<float> lastValue_[2]{}, diff_[2]{};
    bool linking_{}, gestureStarted_{};
};

To compute the change, you need the old value. I couldn’t find a way to ensure the last value I stored was the actual last, but I’m also not sure if it matters.

This post was about the other set, which is more complicated, because each parameter is conditioned by two. The message thread solution kept failing -that’s when I chose to move it all to the audio thread through a queue. It was necessary anyway to make it work for offline renders. I let the parameters take any value. When a UI change comes, it’s undone and notified. When it’s confirmed (possibly corrected), it’s set to all controls and the parameter. When a parameter change comes, it’s only notified. When it’s confirmed (possibly corrected), it’s only set to all controls. Confirmation comes from the message thread. This is the current implementation:

async_attach.h (2.3 KB)
async_attach.cpp (3.5 KB)