AudioProcessorValueTreeState Improvements

Hello JUCErs

We’ve made some changes to the AudioProcessorValueTreeState class:

The highlights are:

  • It’s now possible to have an APVTS manage a wide range of different parameter types, including JUCE’s built-in AudioParameterBool, AudioParameterChoice, AudioParameterFloat and AudioParameterInt classes.
  • You can avoid the cumbersome two-stage initialisation process, where you first add parameters and then assign the ValueTree state, with a new constructor that does both operations.

To make this happen we’ve introduced a new generic AudioProcessorParameter type, RangedAudioParameter (derived from AudioProcessorParameterWithID), that defines most of its behaviour via a NormalisableRange. The AudioProcessorValueTreeState can manage any type derived from RangedAudioParameter, which now includes JUCE’s built-in parameter types, and opens it up to user-defined parameter types too.

Here’s an example of the new best practice:

YourAudioProcessor()
    : apvts (*this, &undoManager, "PARAMETERS",
             { std::make_unique<AudioParameterFloat> ("a", "Parameter A", NormalisableRange<float> (-100.0f, 100.0f), 0.0f),
               std::make_unique<AudioParameterInt> ("b", "Parameter B", 0, 5, 2) })

You can also use this approach to manage AudioProcessorParameterGroups; the ParameterLayout parameter of the AudioProcessorValueTreeState constructor is variadic and able to take an arbitrary sequence of RangedAudioParameters and AudioProcessorParameterGroups of RangedAudioParameters. There’s also an iterator-based constructor that will allow you to create an AudioProcessorValueTreeState from a previously-populated container.

We’re also taking the opportunity to remove some of the bloat from the AudioProcessorValueTreeState interface. The AudioProcessorValueTreeState::createAndAddParameter function has been deprecated. This function was slowly increasing in complexity as we added features to the AudioProcessorParameter classes, with no obvious way to avoid repeating ourselves in code. The replacement function takes a std::unique_ptr<RangedAudioParameter> instead, bypassing the large number of function parameters required. Since this method has been the only way to actually add plug-in parameters to an AudioProcessorValueTreeState, this deprecation will affect everyone. However, there is a low-effort, drop-in replacement available. Code that previously looked like

createAndAddParameter (paramID1, paramName1, ...);

can be replaced with

using Parameter = AudioProcessorValueTreeState::Parameter;
createAndAddParameter (std::make_unique<Parameter> (paramID1, paramName1, ...));

where AudioProcessorValueTreeState::Parameter is a RangedAudioParameter subclass that reproduces the behaviour of the old, internal APVTS parameter type.

Please give the new functionality a try and report back if you run into any difficulties!

13 Likes

Yay, great addition! I actually stopped using the VTS because I was hitting lots of issues around these same problems.

I haven’t checked the code but hopefully it is possible to change the normalizable range object internal to this class during runtime! The inability to change the range object inside of the vts parameter was a big pain point during my development : )

Best,

J

1 Like

Really? I’m guessing that I’m not the only one with production code using this. How will this affect compatibility with existing DAW projects (automation, etc)?

Yes. That function was only increasing in complexity, with extra parameters needing to be bolted on whenever we added a feature to the parameter types. The new interface is much cleaner and easier to maintain.

If you use the replacement code I described in my post then there will be no change in behaviour.

This looks good, but what is now the best way to retrieve parameter data in our audio callback?

The previous method (as explained here) stores pointers to actual RangedAudioParameter derivatives, letting you call operator float() on AudioParameterFloat for example. The function createAndAddParameter() only gives back a RangedAudioParameter base class. What is the intended workflow using this?

You can dynamic_cast it to the more specialised parameter type.

Something like this would do the job:

Another option is to store pointers to the derived types before adding them to the APVTS, thereby avoiding dynamic_cast:

auto myParam = std::make_unique<DerivedParamType> (/*...*/);
derivedParamTypePtr = myParam.get(); // derivedParamTypePtr is a DerivedParamType* data member
apvts.createAndAddParameter (std::move (myParam));

I’m sorry, but that sounds like a terrible anti-pattern. Imho dynamic_cast() is overly used as it is, and this change trades in one nuisance for another.

Why not (add) something along these lines?

template <typename Derivative, typename... Args>
Derivative* createAndAddParameter(Args&&... args)
{
    auto param = std::make_unique<Derivative>(std::forward<Args>(args)...);
    auto ptr = param.get();
    createAndAddParameter(std::move(param));
    return ptr;
}

param = createAndAddParameter<AudioParameterFloat>(...);

At least that’ll return the proper type.

1 Like

You could always add that as a non-member helper function if this is something you find yourself doing a lot.

Sure, but since JUCE is first and foremost an audio-plugin framework I can’t imagine I’d be the only one doing this a lot. :stuck_out_tongue:

Sure, I was just suggesting something that was easy to fit into the initialization list.

AudioProcessorValueTreeState::ParameterLayout createParameterLayout()
{
    std::vector<std::unique_ptr<RangedAudioParameter>> params;

    // Add your other parameters programmatically

    auto param = std::make_unique<AudioParameterInt> ("intParam", "Int Param", 2, 7, 5);
    yourIntParam = param.get();
    params.push_back (std::move (param));

    return { params.begin(), params.end() };
}

YourProcessor()
{
    // Do something with yourIntParam
    DBG (*yourIntParam);
}

AudioParameterInt* yourIntParam = nullptr;
AudioProcessorValueTreeState parameters { *this, nullptr, "PARAMS", createParameterLayout() };

The ParameterLayout also has a variadic constructor, so you could avoid the std::vector if you don’t need to add parameters programatically.

2 Likes

Alright, thanks, that makes more sense! So the basic pattern is: create your unique_ptr, store the derivative ptr in your processor, pass the unique_ptr to the tree.

1 Like

The recommended way is now to construct the AudioProcessorValueTreeState directly with a ParameterLayout containing all the parameters.

I got some code where I pass around the valueTreeState to different classes at construction, and create a couple of parameters within my mainProcessor, and some other ones in those other classes. something along those lines :

struct MainProcessor : public AudioProcessor
{
    MainProcessor()
    : state (*this, nullptr)
    , anotherProcessorClass (state)
    {
        state.createAndAddParameter (...);
    }

    AudioProcessorValueTreeState state;
    AnotherProcessorClass anotherProcessorClass;
};

struct AnotherProcessorClass
{
    AnotherProcessorClass (AudioProcessorValueTreeState& state)
    {
        state.addListener (this);
        state.createAndAddParameter (...);
    }
};

Was it bad practice?
It seems to me I can’t really do that anymore with the new recommended AudioProcessorValueTreeState constructor no? (I only had a quick look, so perhaps I’m missing something simple)

Is there a straightforward way of attaching Projucer-generated GUI elements (e.g. Sliders, Buttons etc) to AudioProcessorValueTreeState?

===
I am using Projucer [v5.4.1] to add a Slider as a GUI subcomponent.
I would like to be able to use SliderAttachment to attach that Projucer-made Slider to a parameter within the APVTS, but with Projucer, I end up with following code:

in AudioPluginEditor.h

private:
//[UserVariables] – You can add your own custom variables in this section.

AudioProcessorValueTreeState &valueTreeState;
typedef AudioProcessorValueTreeState::SliderAttachment SliderAttachment;
std::unique_ptr<SliderAttachment> testAttachment;

//[/UserVariables]

//==================================================== 
std::unique_ptr<Slider> test_slider;

in AudioPluginEditor.ccp

// constructor...
testAttachment.reset(new SliderAttachment(valueTreeState, "test_int", *test_slider.get()));

// destructor ...
///[/Destructor_pre]
test_slider = nullptr;
//[Destructor]. You can add your own custom destruction code here...

The problem is that “test_slider = nullptr” throws an exception:

“Exception thrown: read access violation.”

… , but, when I add my own - not Projucer-generated - code for a Slider& as a class member:

Slider &member_slider;

and

testAttachment.reset(new SliderAttachment(valueTreeState, "test_int", member_slider));

… it works fine.

The above suggests some rather inelegant, makeshift workarounds involving copy&paste, but I am wondering if there exists a more systemic approach.

If you have a set number of elements I would just make them members, in the case of that generated code they’re destroying test_slider before the slider attachment which isn’t allowed (you’ll have to add testAttachment = nullptr before the line where test_slider is set to nullptr)

If your slider is a member and the slider attachment is heap allocated to a std::unique_ptr then the destruction order will be fine

Thanks.
The point is that I am already spoiled by Projucer’s interactive GUI layout features and don’t want to regress to adding GUI class members by hand.
In the “days of yore”, Projucer used to make “plain” members.
Perhaps it would be a good enhancement to add an option to Projucer that would allow a choice between heap and stack allocation. Just an idea that may turn out to be naive in the context of other forces at play.

AFAIK the GUI editor is basically there for legacy reasons, in this post it’s mentioned they don’t plan to add any features to it.

1 Like

I’m looking at one of my first plugins to bring it up to date with what I’ve learned in the last year and to use 5.4.1 but I can’t seem to get an AudioParameterChoice to initialise using the example given.
This is the sort of thing I’m doing :

auto choice = std::make_unique<AudioParameterChoice> ("choice", "Choice", {"Choice 1", "Choice 2", "Choice 3", "Choice 4"}, 1);

but the compiler complains No matching function for call to 'make_unique'

However my original code doing something like :

auto choice = new AudioParameterChoice ("choice", "Choice", {"Choice 1", "Choice 2", "Choice 3", "Choice 4"}, 1);

or even doing :

std::unique_ptr<AudioParameterChoice> choice;
choice.reset( new AudioParameterChoice ("choice", "Choice", {"Choice 1", "Choice 2", "Choice 3", "Choice 4"}, 1) );

doesn’t complain at all.

What am I missing?

The previous version called the Constructor, so it had the proper type information available to implicitly cast the Strings initialiser list to a StringArray. The make_unique however doesn’t have the argument types of the constructor available, it just forwards the arguments.

The solution is, to supply the type like:

auto choice = std::make_unique<AudioParameterChoice> ("choice", 
                                                      "Choice", 
                                                      StringArray ({"Choice 1", "Choice 2", "Choice 3", "Choice 4"}), 
                                                      1);

Haven’t tested it, but something like that should work…

3 Likes

Thanks, exactly that! I even looked at that and thought, "no must be fine if it works with new". Useful to know that restriction about make_unique.