ParameterAttachment and ValueTree

Hi all,

I’ve created a class ValueTreeParameterAttachment using ParameterAttachment to synchronize ValueTree properties with plug-in parameters.
In my test project it is working. But this seems to be too good to be true and way too easy. Any suggestions if that’s a good idea to sync those things that way?

ValueTreeParameterAttachment.h

#pragma once
#include <JuceHeader.h>

class ValueTreeParameterAttachment : public ValueTree::Listener
{
  
public:
    
    ValueTreeParameterAttachment(RangedAudioParameter& p, ValueTree& vt, const Identifier& i, UndoManager* um = nullptr);
    ~ValueTreeParameterAttachment();
    
    void valueTreePropertyChanged (ValueTree& treeWhosePropertyHasChanged, const Identifier& property) override;
    
    
private:
    
    RangedAudioParameter& parameter;
    ValueTree tree;
    Identifier propertyId;
    UndoManager* undoManager;
    std::unique_ptr<ParameterAttachment> attachment;
    
    
    std::function<void (float)> updateValueTree = [this] (float newValue) {
        if (tree.isValid()) {
            tree.setProperty(propertyId, newValue, undoManager);
        }
    };
    
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ValueTreeParameterAttachment)
};

ValueTreeParameterAttachment.cpp

#include "ValueTreeParameterAttachment.h"


ValueTreeParameterAttachment::ValueTreeParameterAttachment(juce::RangedAudioParameter &p, juce::ValueTree &vt, const juce::Identifier &i, juce::UndoManager *um) : parameter(p), tree(vt), propertyId(i), undoManager(um) {
    
    
    attachment = std::make_unique<ParameterAttachment>(p, updateValueTree, um);
    tree.addListener(this);
}

ValueTreeParameterAttachment::~ValueTreeParameterAttachment() {
    tree.removeListener(this);
}

void ValueTreeParameterAttachment::valueTreePropertyChanged(juce::ValueTree &treeWhosePropertyHasChanged, const juce::Identifier &property) {
    
   
    if (treeWhosePropertyHasChanged.getType() == tree.getType() && property == propertyId) {
        
        float newValue = treeWhosePropertyHasChanged[property];

            attachment->setValueAsCompleteGesture(newValue);

            DBG("VT Property: \n" <<tree.getProperty(property).toString());
            DBG("Parameter: \n" << parameter.getCurrentValueAsText() << "\n\n");
    }
}

PluginProcessor.h

#pragma once

#include <JuceHeader.h>

#include "ValueTreeParameterAttachment.h"

//==============================================================================

/**

*/

**class** ApvtsandValueTreeSyncTestAudioProcessor : **public** AudioProcessor

{

**public** :

//==============================================================================

ApvtsandValueTreeSyncTestAudioProcessor();

~ApvtsandValueTreeSyncTestAudioProcessor();

//==============================================================================

**void** prepareToPlay ( **double** sampleRate, **int** samplesPerBlock) **override** ;

**void** releaseResources() **override** ;

#ifndef JucePlugin_PreferredChannelConfigurations

**bool** isBusesLayoutSupported ( **const** BusesLayout& layouts) **const** **override** ;

#endif

**void** processBlock (AudioBuffer< **float** >&, MidiBuffer&) **override** ;

//==============================================================================

AudioProcessorEditor* createEditor() **override** ;

**bool** hasEditor() **const** **override** ;

//==============================================================================

**const** String getName() **const** **override** ;

**bool** acceptsMidi() **const** **override** ;

**bool** producesMidi() **const** **override** ;

**bool** isMidiEffect() **const** **override** ;

**double** getTailLengthSeconds() **const** **override** ;

//==============================================================================

**int** getNumPrograms() **override** ;

**int** getCurrentProgram() **override** ;

**void** setCurrentProgram ( **int** index) **override** ;

**const** String getProgramName ( **int** index) **override** ;

**void** changeProgramName ( **int** index, **const** String& newName) **override** ;

//==============================================================================

**void** getStateInformation (MemoryBlock& destData) **override** ;

**void** setStateInformation ( **const** **void** * data, **int** sizeInBytes) **override** ;

**const** Identifier apvtsId {"APVTS"};

**const** Identifier valueTreeName {"TestValueTree"};

**const** Identifier propertyName {"TestProperty"};

**private** :

//==============================================================================

AudioProcessorValueTreeState parameters {* **this** , **nullptr** , apvtsId, {

std::make_unique<AudioParameterBool>("TestParameter", "Parameter On/Off", **true** ),

}};

ValueTree valueTree{valueTreeName};

ValueTreeParameterAttachment valueTreeAttachment {*parameters.getParameter("TestParameter"), valueTree, propertyName};

JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ApvtsandValueTreeSyncTestAudioProcessor)

};

PluginProcessor.cpp

#include "PluginProcessor.h"
#include "PluginEditor.h"

//==============================================================================
ApvtsandValueTreeSyncTestAudioProcessor::ApvtsandValueTreeSyncTestAudioProcessor()
#ifndef JucePlugin_PreferredChannelConfigurations
     : AudioProcessor (BusesProperties()
                     #if ! JucePlugin_IsMidiEffect
                      #if ! JucePlugin_IsSynth
                       .withInput  ("Input",  AudioChannelSet::stereo(), true)
                      #endif
                       .withOutput ("Output", AudioChannelSet::stereo(), true)
                     #endif
                       )
#endif
{
    valueTree.getOrCreateChildWithName(propertyName, nullptr);
    valueTree.setProperty(propertyName, true, nullptr);
   
}

ApvtsandValueTreeSyncTestAudioProcessor::~ApvtsandValueTreeSyncTestAudioProcessor()
{
}

//==============================================================================
const String ApvtsandValueTreeSyncTestAudioProcessor::getName() const
{
    return JucePlugin_Name;
}

bool ApvtsandValueTreeSyncTestAudioProcessor::acceptsMidi() const
{
   #if JucePlugin_WantsMidiInput
    return true;
   #else
    return false;
   #endif
}

bool ApvtsandValueTreeSyncTestAudioProcessor::producesMidi() const
{
   #if JucePlugin_ProducesMidiOutput
    return true;
   #else
    return false;
   #endif
}

bool ApvtsandValueTreeSyncTestAudioProcessor::isMidiEffect() const
{
   #if JucePlugin_IsMidiEffect
    return true;
   #else
    return false;
   #endif
}

double ApvtsandValueTreeSyncTestAudioProcessor::getTailLengthSeconds() const
{
    return 0.0;
}

int ApvtsandValueTreeSyncTestAudioProcessor::getNumPrograms()
{
    return 1;   // NB: some hosts don't cope very well if you tell them there are 0 programs,
                // so this should be at least 1, even if you're not really implementing programs.
}

int ApvtsandValueTreeSyncTestAudioProcessor::getCurrentProgram()
{
    return 0;
}

void ApvtsandValueTreeSyncTestAudioProcessor::setCurrentProgram (int index)
{
}

const String ApvtsandValueTreeSyncTestAudioProcessor::getProgramName (int index)
{
    return {};
}

void ApvtsandValueTreeSyncTestAudioProcessor::changeProgramName (int index, const String& newName)
{
}

//==============================================================================
void ApvtsandValueTreeSyncTestAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
{
    // Use this method as the place to do any pre-playback
    // initialisation that you need..
}

void ApvtsandValueTreeSyncTestAudioProcessor::releaseResources()
{
    // When playback stops, you can use this as an opportunity to free up any
    // spare memory, etc.
}

#ifndef JucePlugin_PreferredChannelConfigurations
bool ApvtsandValueTreeSyncTestAudioProcessor::isBusesLayoutSupported (const BusesLayout& layouts) const
{
  #if JucePlugin_IsMidiEffect
    ignoreUnused (layouts);
    return true;
  #else
    // This is the place where you check if the layout is supported.
    // In this template code we only support mono or stereo.
    if (layouts.getMainOutputChannelSet() != AudioChannelSet::mono()
     && layouts.getMainOutputChannelSet() != AudioChannelSet::stereo())
        return false;

    // This checks if the input layout matches the output layout
   #if ! JucePlugin_IsSynth
    if (layouts.getMainOutputChannelSet() != layouts.getMainInputChannelSet())
        return false;
   #endif

    return true;
  #endif
}
#endif

void ApvtsandValueTreeSyncTestAudioProcessor::processBlock (AudioBuffer<float>& buffer, MidiBuffer& midiMessages)
{
    ScopedNoDenormals noDenormals;
    auto totalNumInputChannels  = getTotalNumInputChannels();
    auto totalNumOutputChannels = getTotalNumOutputChannels();

    // In case we have more outputs than inputs, this code clears any output
    // channels that didn't contain input data, (because these aren't
    // guaranteed to be empty - they may contain garbage).
    // This is here to avoid people getting screaming feedback
    // when they first compile a plugin, but obviously you don't need to keep
    // this code if your algorithm always overwrites all the output channels.
    for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i)
        buffer.clear (i, 0, buffer.getNumSamples());

    // This is the place where you'd normally do the guts of your plugin's
    // audio processing...
    // Make sure to reset the state if your inner loop is processing
    // the samples and the outer loop is handling the channels.
    // Alternatively, you can process the samples with the channels
    // interleaved by keeping the same state.
    for (int channel = 0; channel < totalNumInputChannels; ++channel)
    {
        auto* channelData = buffer.getWritePointer (channel);

        // ..do something to the data...
    }
}

//==============================================================================
bool ApvtsandValueTreeSyncTestAudioProcessor::hasEditor() const
{
    return true; // (change this to false if you choose to not supply an editor)
}

AudioProcessorEditor* ApvtsandValueTreeSyncTestAudioProcessor::createEditor()
{
    return new ApvtsandValueTreeSyncTestAudioProcessorEditor (*this, valueTree);
}

//==============================================================================
void ApvtsandValueTreeSyncTestAudioProcessor::getStateInformation (MemoryBlock& destData)
{
    // You should use this method to store your parameters in the memory block.
    // You could do that either as raw data, or use the XML or ValueTree classes
    // as intermediaries to make it easy to save and load complex data.
    
    MemoryOutputStream mos(destData, false);
    parameters.state.writeToStream(mos);
    
    
}

void ApvtsandValueTreeSyncTestAudioProcessor::setStateInformation (const void* data, int sizeInBytes)
{
    // You should use this method to restore your parameters from this memory block,
    // whose contents will have been created by the getStateInformation() call.
    
    parameters.replaceState(ValueTree::readFromData(data, sizeInBytes));
    
}



//==============================================================================
// This creates new instances of the plugin..
AudioProcessor* JUCE_CALLTYPE createPluginFilter()
{
    return new ApvtsandValueTreeSyncTestAudioProcessor();
}

PluginEditor.h

#pragma once

#include <JuceHeader.h>
#include "PluginProcessor.h"

//==============================================================================
/**
*/
class ApvtsandValueTreeSyncTestAudioProcessorEditor  : public AudioProcessorEditor, public ValueTree::Listener
{
public:
    ApvtsandValueTreeSyncTestAudioProcessorEditor (ApvtsandValueTreeSyncTestAudioProcessor& p, ValueTree t);
    ~ApvtsandValueTreeSyncTestAudioProcessorEditor();

    //==============================================================================
    void paint (Graphics&) override;
    void resized() override;
    
    void valueTreePropertyChanged (ValueTree& treeWhosePropertyHasChanged, const Identifier& property) override;

private:
    // This reference is provided as a quick way for your editor to
    // access the processor object that created it.
    ApvtsandValueTreeSyncTestAudioProcessor& processor;
    ValueTree tree;
    
    
    ToggleButton button;
    
    
    

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ApvtsandValueTreeSyncTestAudioProcessorEditor)
};

PluginEditor.cpp

#include "PluginProcessor.h"
#include "PluginEditor.h"

//==============================================================================
ApvtsandValueTreeSyncTestAudioProcessorEditor::ApvtsandValueTreeSyncTestAudioProcessorEditor (ApvtsandValueTreeSyncTestAudioProcessor& p, ValueTree t)
    : AudioProcessorEditor (&p), processor (p), tree(t)
{
    // Make sure that before the constructor has finished, you've set the
    // editor's size to whatever you need it to be.
    
    
    tree.addListener(this);
   
    addAndMakeVisible(button);
    button.setButtonText("buttonTest");
    button.setToggleState(true, dontSendNotification);
    button.onClick = [this] {
        tree.setProperty(processor.propertyName, button.getToggleState(), nullptr);
    };
    setSize (400, 300);
}

ApvtsandValueTreeSyncTestAudioProcessorEditor::~ApvtsandValueTreeSyncTestAudioProcessorEditor()
{
}

//==============================================================================
void ApvtsandValueTreeSyncTestAudioProcessorEditor::paint (Graphics& g)
{
    // (Our component is opaque, so we must completely fill the background with a solid colour)
    g.fillAll (getLookAndFeel().findColour (ResizableWindow::backgroundColourId));

    
}

void ApvtsandValueTreeSyncTestAudioProcessorEditor::resized()
{
    // This is generally where you'll want to lay out the positions of any
    // subcomponents in your editor..
    
    button.centreWithSize(200, 30);
}

void ApvtsandValueTreeSyncTestAudioProcessorEditor::valueTreePropertyChanged(juce::ValueTree &treeWhosePropertyHasChanged, const juce::Identifier &property) {
    
    if (treeWhosePropertyHasChanged.getType() == processor.valueTreeName) {
//        if (button.getToggleState() != (bool)treeWhosePropertyHasChanged[processor.propertyName]) {
            button.setToggleState(treeWhosePropertyHasChanged[processor.propertyName], dontSendNotification);
//        }
    }
}

You’re only syncing ValueTree values back to the RangedAudioParameters, not the other way around, right?
Because RangedAudioParameters are updated from the audio thread (not always but usually) and you don’t want your audio callback to execute Value.:Listener callbacks. Those should be called asynchronously from the GUI thread.

Also, I’m not sure why you do this in the first place.
The APVTS is a ValueTree representation of the parameters already.

No, I do sync in both directions.

That’s what I was afraid of… But when I compare to the other helper classes like ButtonParameterAttachment, the executing lambda when a RangedAudioPrameter changes is

    void ButtonParameterAttachment::setValue (float newValue)
    {
    const ScopedValueSetter<bool> svs (ignoreCallbacks, true);
    button.setToggleState (newValue >= 0.5f, sendNotificationSync);
    }

and a potential Button::Listener callback could also be triggered, right? Or is that ScopedValueSetter
preventing this?

Well, yes, but in my particular case I use a Component class with all settings stored in a ValueTree and I need that stored as RangedAudioParamter in my plugin to work properly with the host. The ValueTree doesn’t match to the PARAM id= value= structure used in the APVTS, so I thought I could sync both ValueTrees together…

update: the docs of ParameterAttachment says that the parameterChangedCallback lambda is called on the message thread. So, do I really have a potential issue calling something wrong on the audio thread?

Hey @StefanS ,

Did you end up deciding this was safe? I’m facing a similar problem with a synth module I’ve written that stores settings in a ValueTree which I want to update using APVTS parameters. This seems like the best way forward at the moment.

Hi Imagiro,

I’ll have to look up if I’m using the exact code like the above but yes, I’m using the ValueTreeParameterAttachment quite a lot and never had any issues with it. That said, I cannot guarantee that it’s working everywhere. I’m only on macOS and Pro Tools AAX at the moment.

Cheers,
Stefan

1 Like