Remove AsyncUpdater from ParameterAttachment

AsyncUpdater::triggerAsyncUpdate() locks when the message posts to the message queue:

bool MessageManager::postMessageToSystemQueue (MessageBase* message)
{
    jassert (appDelegate != nil);
    appDelegate->messageQueue.post (message);
    return true;
}
//from juce_osx_MessageQueue.h
    void post (MessageManager::MessageBase* const message) 
    {
        messages.add (message);
        wakeUp();
    }

private:
    ReferenceCountedArray<MessageManager::MessageBase, CriticalSection> messages;

ReferenceCountedArray::add() locks here and uses the CriticalSection above:

    ObjectClass* add (ObjectClass* newObject)
    {
        const ScopedLockType lock (getLock());
        values.add (newObject);

        if (newObject != nullptr)
            newObject->incReferenceCount();

        return newObject;
    }

ParameterAttachment::parameterValueChanged() calls triggerAsyncUpdate() (potentially on the audio thread) when the parameter has changed:

void ParameterAttachment::parameterValueChanged (int, float newValue)
{
    lastValue = newValue;

    if (MessageManager::getInstance()->isThisTheMessageThread())
    {
        cancelPendingUpdate();
        handleAsyncUpdate();
    }
    else
    {
        triggerAsyncUpdate();
    }
}

A possible solution is to use an atomic and a timerCallback():

class ParameterAttachment : private AudioProcessorParameter::Listener, 
                            private Timer
{
public:
    ParameterAttachment(...) 
    {
         //same as existing ctor implementation
        startTimerHz(60);
    }
private:
    Atomic<bool> parameterChanged { false };
    void parameterValueChanged(int, float newValue) override
    {
        if( newValue != lastValue )
        {
            lastValue = newValue;
            parameterChanged.set(true);
        }
    }

    void timerCallback() override
    {
        if( parameterChanged.compareAndSetBool(false, true) )
            if( setValue ) //the std::function<void (float)> parameterChangedCallback
                setValue( lastValue );
    }
}

This will solve the issue of ParameterAttachment locking the audio thread whenever a parameter change happens and the parameterValueChanged callback is called from the audio thread.

@matkatmusic I hit the same issue and tried your implementation. I found on that line with messages.add (message) that the messages array could also quite easily need to grow in size needing allocation.

I got it working with these modifiactions:

class ParameterAttachment : private AudioProcessorParameter::Listener,
                            private Timer
{
public:
    ParameterAttachment(...)
    {
        //same as existing ctor implementation then finally
        startTimerHz (60);
    }
    ~ParameterAttachment()
    {
        //same as existing dtor implementation then finally
        valueIsUpToDate.test_and_set (std::memory_order_acquire);
    }
    
private:
    std::atomic_flag valueIsUpToDate = ATOMIC_FLAG_INIT;

    void parameterValueChanged(int, float newValue) override
    {
        if (MessageManager::getInstance()->isThisTheMessageThread())
        {
            valueIsUpToDate.test_and_set (std::memory_order_acquire);
            NullCheckedInvocation::invoke (setValue, parameter.convertFrom0to1 (newValue));
        }
        else
        {
            lastValue.store (newValue, std::memory_order_release);
            valueIsUpToDate.clear (std::memory_order_release);
        }
    }

    void timerCallback() override
    {
        if (! valueIsUpToDate.test_and_set (std::memory_order_acquire))
            NullCheckedInvocation::invoke (setValue, parameter.convertFrom0to1 (lastValue.load (std::memory_order_acquire)));
    }
}
1 Like