ValueTree Listener callback

Hi all,

I am trying to create an attachment that keeps a property in a juce::ValueTree and a juce::RangedAudioParameter in sync. But i’m hitting the following assertion:

   #if JUCE_DEBUG && ! JUCE_DISABLE_AUDIOPROCESSOR_BEGIN_END_GESTURE_CHECKING
    // This means you've called beginChangeGesture twice in succession without
    // a matching call to endChangeGesture. That might be fine in most hosts,
    // but it would be better to avoid doing it.
    jassert (! isPerformingGesture);
    isPerformingGesture = true;
   #endif

Below is the PropertyAttachment class i made.

#pragma once

#include <JuceHeader.h>

class PropertyAttachment : private juce::ValueTree::Listener {
public:

    PropertyAttachment(juce::RangedAudioParameter& param, juce::CachedValue<float>& p)
    : attachment(param, [this](float v) { setValue(v); })
    , property(p)
    {
        attachment.sendInitialUpdate();
        property.getValueTree().addListener(this);
    }

    ~PropertyAttachment()
    {
        property.getValueTree().removeListener(this);
    }

private:

    void valueTreePropertyChanged(juce::ValueTree& tree, const juce::Identifier& ID) override
    {
        if (property.getPropertyID() == ID)
            propertyChanged();
    }

    void propertyChanged()
    {
        if (! ignoreCallbacks)
            attachment.setValueAsCompleteGesture(property.get());
    }

    void setValue(float value)
    {
        juce::ScopedValueSetter<bool> svs(ignoreCallbacks, true);
        property.setValue(value, nullptr);
    }

    juce::ParameterAttachment attachment;
    juce::CachedValue<float>& property;
    bool ignoreCallbacks { false };
};

Here is what is triggering the assertion:

When setValue() is called (due to dragging a slider which is attached to the parameter), I set ignoreCallbacks = true using ScopedValueSetter before calling property.setValue(...). This should suppress the call to attachment.setValueAsCompleteGesture() that comes from the ValueTree notifying its listeners.

But during debugging, I noticed that propertyChanged() still gets called with ignoreCallbacks == false, which, i think, shouldn’t be possible. This leads to the assertion when attachment.setValueAsCompleteGesture() tries to call beginChangeGesture() a second time (the first time is called when the slider started dragging).

Below is the call stack. It looks like ignoreCallbacks is already false when the listener is being invoked, even though ScopedValueSetter should still be in scope.

My question: Is it possible that the listener callback is being triggered after ScopedValueSetter goes out of scope, even though everything appears synchronous?

Or is there something else I’m missing about how ValueTree notifies its listeners?

juce::AudioProcessorParameter::beginChangeGesture() juce_AudioProcessor.cpp:1549
juce::ParameterAttachment::beginGesture() juce_ParameterAttachments.cpp:74
`juce::ParameterAttachment::setValueAsCompleteGesture'::`2'::<lambda_1>::operator()(float) juce_ParameterAttachments.cpp:63
juce::ParameterAttachment::callIfParameterValueChanged<`juce::ParameterAttachment::setValueAsCompleteGesture'::`2'::<lambda_1> >(float,<lambda_1> &&) juce_ParameterAttachments.cpp:97
juce::ParameterAttachment::setValueAsCompleteGesture(float) juce_ParameterAttachments.cpp:61
PropertyAttachment::propertyChanged() PropertyAttachment.h:32
PropertyAttachment::valueTreePropertyChanged(juce::ValueTree &, const juce::Identifier &) PropertyAttachment.h:26
`juce::ValueTree::SharedObject::sendPropertyChangeMessage'::`2'::<lambda_1>::operator()(juce::ValueTree::Listener &) juce_ValueTree.cpp:107
juce::ListenerList<juce::ValueTree::Listener,juce::Array<juce::ValueTree::Listener *,juce::DummyCriticalSection,0> >::callCheckedExcluding<`juce::ValueTree::SharedObject::sendPropertyChangeMessage'::`2'::<lambda_1> &,juce::ListenerList<juce::ValueTree::Listener,juce::Array<juce::ValueTree::Listener *,juce::DummyCriticalSection,0> >::DummyBailOutChecker>(juce::ValueTree::Listener *,const juce::ListenerList<juce::ValueTree::Listener,juce::Array<juce::ValueTree::Listener *,juce::DummyCriticalSection,0> >::DummyBailOutChecker &,<lambda_1> &) juce_ListenerList.h:271
juce::ListenerList<juce::ValueTree::Listener,juce::Array<juce::ValueTree::Listener *,juce::DummyCriticalSection,0> >::callExcluding<`juce::ValueTree::SharedObject::sendPropertyChangeMessage'::`2'::<lambda_1> &>(juce::ValueTree::Listener *,<lambda_1> &) juce_ListenerList.h:203
juce::ValueTree::SharedObject::callListeners<`juce::ValueTree::SharedObject::sendPropertyChangeMessage'::`2'::<lambda_1> >(juce::ValueTree::Listener *,<lambda_1>) juce_ValueTree.cpp:93
juce::ValueTree::SharedObject::callListenersForAllParents<`juce::ValueTree::SharedObject::sendPropertyChangeMessage'::`2'::<lambda_1> >(juce::ValueTree::Listener *,<lambda_1>) juce_ValueTree.cpp:101
juce::ValueTree::SharedObject::sendPropertyChangeMessage(const juce::Identifier &, juce::ValueTree::Listener *) juce_ValueTree.cpp:107
juce::ValueTree::SharedObject::setProperty(const juce::Identifier &, const juce::var &, juce::UndoManager *, juce::ValueTree::Listener *) juce_ValueTree.cpp:145
juce::ValueTree::setPropertyExcludingListener(juce::ValueTree::Listener *, const juce::Identifier &, const juce::var &, juce::UndoManager *) juce_ValueTree.cpp:780
juce::ValueTree::setProperty(const juce::Identifier &, const juce::var &, juce::UndoManager *) juce_ValueTree.cpp:770
juce::CachedValue::setValue(const float &, juce::UndoManager *) juce_CachedValue.h:263
PropertyAttachment::setValue(float) PropertyAttachment.h:38

Looking at that call stack I dont’ think the scoped value setter will have gone out of scope. setValue is still at the bottom of your stack trace there.

I would put a breakpoint in setValue. When you hit the breakpoint add a watchpoint on the ignoreCallbacks so that the debugger will break when the value changes, hit play and see what happens.

I tried your suggestion but unfortunately the debugger is unable to watch the variable due to “compiler optimizations”. I am in debug mode though, so that is kind of strange. Maybe i need some additional compiler flags.

But I actually already fixed the problem since it was coming from somewhere else (which was not really visible from the debugger).

I am syincing parameters between plugin instances and the syncing triggered the valueTreePropertyChanged callback while i was dragging the slider connected to that parameter. That caused beginChangeGesture to get called twice without a prior call to endChangeGesture.

I fixed it by checking if a gesture has started for the parameter, if it has started then it won’t call setValueAsCompleteGesture.

Thanks for your help anyways :slight_smile:

2 Likes