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

#1

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

0 Likes

#2

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

#3

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);
}
0 Likes