APVTS: Value returned by getParameterAsValue() breaks connection to the tree

Edit: I think this is a bug, see the answer below

I have a weird issue that I can’t quite track down. My hope is that someone around here can provide me with some advice on how to move on.

I have an AudioProcessor that is registered as a listener on its AudioProcessorValueTreeState. I want to modify some parameter value from the GUI and get a callback in the AudioProcessor.
In the GUI, I have a collection of buttons that modify a Value and set it to specific values when pressed. I connect this value to the AudioProcessorValueTreeState like this:

auto& myValue = treeState.getParameterAsValue("myParameterIDString");
myButtonCollection.getSelectionAsValue().referTo(myValue);

When I press one of the buttons, I can track that the underlying Value changes. In fact, as it now refers to data in the AudioProcessorValueTreeState, I can actually see ValueTreePropertyValueSource::setValue() being called. What is weird is that my plugin processor doesn’t receive a call to parameterChanged() when it runs in certain hosts (other hosts or the standalone version work fine). I stepped through the code with the debugger in various ways, but there is so many callbacks happening in result to the call to ValueTreePropertyValueSource::setValue() that it is almost impossible to find out where the call to my AudioProcessor gets lost.

What I do know is that, when the callback arrives, it arrives as a consequence to a call to AudioProcessorValueTreeState::valueTreePropertyChanged. This call doesn’t happen when I test it in the problematic host, so my callback gets lost somewhere before that.

Now I’m wondering what strategy I can use to narrow this down, especially since it only happens in a certain host. I would be grateful for any hints.

/////
Additional Info: JUCE 5.4.3

Here’s the conclusion: A Value retrieved from the AudioProcessorValueTreeState gets detached from the tree if the content is replaced by a call to AudioProcessorValueTreeState::replaceState(). As this is the recommended way of loading a preset, I think that this must be a bug.

The long story

The ValueTree seems to consist of several subtrees, one for each VST parameter. I temporarily modified ValueTreePropertyValueSource::setValue() from this

void setValue (const var& newValue) override { 
    tree.setProperty (property, newValue, undoManager); 
}

to this

void setValue (const var& newValue) override  { 
    tree.setProperty (property, newValue, undoManager); 
    auto root = tree.getRoot();
    auto xmlStr = root.toXmlString();
}

When I click my button and observe xmlStr in a host where the plugin works like it should, I get the full tree, as expected:

<?xml version="1.0" encoding="UTF-8"?>
<PARAMETERS>  
  <PARAM id="bypass" value="0"/>
  <PARAM id="time" value="5e2"/>
  <PARAM id="feedback" value="2.5e1"/>
  <PARAM id="freeze" value="0"/>
  <PARAM id="lfoshape" value="1"/>
... more here ...

When I observe it in the problematic host, I get

<?xml version="1.0" encoding="UTF-8"?>
<PARAM id="lfoshape" value="1"/>

It seems as if the part of the tree that is responsible for this specific parameter got detached from the ValueTree but I’m not doing anything specific with that in my plugin code.
After some more debugging I noticed that the fault only occurs somewhere after my constructors are done.
I added auto xmlStr = tree.getRoot().toXmlString(); to all other callbacks in the ValueTreePropertyValueSource and I noticed that when I recall a preset with

treeState.replaceStare(ValueTree::fromXml(*myPresetData))`

I get a call to ValueTreePropertyValueSource::valueTreeParentChanged(). When I observe the xmlStr in this callback, it shows the detached tree. This means that the connection between the tree and the Value breaks when the tree state is replaced.

2 Likes

Here’s a temporary fix that seems to work for our product. Instead of
treeState.getParameterAsValue(ID)
you would write
APVTSValueAdapter::getParameterAsValue(treeState, ID).
I would still prefer a true solution to this.

APVTSValueAdapter.h

#pragma once
#include "../JuceLibraryCode/JuceHeader.h"

class APVTSValueAdapter : public Value::ValueSource,
                          private RangedAudioParameter::Listener
{
public:
    APVTSValueAdapter(AudioProcessorValueTreeState& treeState, String parameterID);
    virtual ~APVTSValueAdapter();

    virtual var getValue() const override { return m_currentValue; }
    virtual void setValue(const var& newValue) override;

    static Value getParameterAsValue(AudioProcessorValueTreeState& treeState, String parameterID)
    {
        return juce::Value(static_cast<Value::ValueSource*>(new APVTSValueAdapter(treeState, parameterID)));
    }
private:
    virtual void parameterValueChanged(int parameterIndex, float newValue) override;
    virtual void parameterGestureChanged(int parameterIndex, bool gestureIsStarting) override {}

    RangedAudioParameter* m_parameter;
    float m_currentValue; // cached for speed. Required? Maybe not.
};

APVTSValueAdapter.cpp

#include "APVTSValueAdapter.h"

APVTSValueAdapter::APVTSValueAdapter(AudioProcessorValueTreeState& treeState, String parameterID)
{
    m_parameter = treeState.getParameter(parameterID);
    jassert(m_parameter);
    m_parameter->addListener(this);
    m_currentValue = m_parameter->convertFrom0to1(m_parameter->getValue());
}

APVTSValueAdapter::~APVTSValueAdapter()
{
    m_parameter->removeListener(this);
}

void APVTSValueAdapter::setValue(const var& newValue)
{
    m_parameter->setValueNotifyingHost(m_parameter->convertTo0to1(newValue));
}

void APVTSValueAdapter::parameterValueChanged(int parameterIndex, float newValue)
{
    m_currentValue = m_parameter->convertFrom0to1(newValue);
    sendChangeMessage(false);
}

Here’s an update. The Problem could potentially be fixed with a change in the APVTS itself:

void AudioProcessorValueTreeState::replaceState (const ValueTree& newState)
{
    ScopedLock lock (valueTreeChanging);

    // This was the original line:
    // state = newState;
    state.copyPropertiesAndChildrenFrom(newState);

    if (undoManager != nullptr)
        undoManager->clearUndoHistory();
}

See here: Bug: ValueTree and connected Value objects loose connection

Does this fix the issue of AVTPS not notifying listeners upon replacing of state?

If it does, why? Also, how does one create a persistent modification to the JUCE library code in one’s JUCE project to include this change?

It depends on how the listeners are attached to the APVTS. We had issues when we attached as a Value::Listener to the actual Value objects inside the value tree. When you call replaceState you swap out the actual value tree objects. You listeners will then be refering to Value objects that are no longer a part of the value tree and the value tree will consist of new Value objects that your listeners know nothing about. The suggested change fixes this issue, because copyPropertiesAndChildrenFrom leaves the existing objects in place and copies their actual values. Thus, the Listeners still refer to the Value objects in the tree and continue to receive updates when the values in the tree change.

If you attach your listeners as a RangedAudioParameter::Listener (or similar) this will probably not solve the issue, but you could still try it just in case. In any case, some additional digging and debugging should help you narrow down why things don’t work as expected. We never had problems with RangedAudioParameter::Listener but we also moved to a custom parameter system a while back… My info may not be completely up to date.