Best practices for AudioProcessorValueTreeState and child components

Hi there,

I’m starting to flesh my first plugin out more seriously, and have a few questions around 2020 best practices for APVTS that are still a bit fuzzy after walking through the demos, tutorials, forums.

  1. Is it best practice to pass an APVTS reference into your editor like in the tutorial and then continue to pass that reference along to any child component that for example has GUI controls that would want to be bound to it?

  2. On the Processor side, is it best practice to inherit from public AudioProcessorValueTreeState::Listener and again to pass along a reference to the APVTS wherever you need it?

  3. Is it discouraged / not possible to have a dynamic number of parameters in my APVTS? I just ran into t0m’s reply here.

Example: Let’s say I’m building a synth that can have a dynamic number of oscillators, each with attack and delay, something really basic looking like:

parameters (*this, nullptr, Identifier ("parameters"),
    { std::make_unique<AudioProcessorParameterGroup> ("osc1", "Oscillator 1", " | ",
        std::make_unique<AudioParameterFloat> ("attack", "Attack",  0.0f, 32.0f, 0.2f),
        std::make_unique<AudioParameterFloat> ("release", "Release", 0.0f, 32.0f, 1.0f)),

Can I programmatically add these parameters as I add my oscillators? Or is the best practice to go ahead and manually map out each potential oscillator in the constructor, ensuring the host has a stable parameter interface? If the latter, is there a way they can be hidden from the host unless in use?

I noticed that createAndAddParameter (std::make_unique<Parameter> is declared as subpar in the docs/tutorial, but i’m not sure if it’s for Historical Baggage Reasons or because dynamic parameter creation is generally a nightmare.

  1. C++ n00b question: Since AVPTS is essentially used to describe a data model complete with types and defaults, and most real projects are going to declare many parameters, is there a reason why the best practice is to build out the parameter tree in the processor’s constructor vs. having a more “first class” location for it? Especially when there are dozens of params, they seem oddly stuffed in the constructor. I wondered if people were storing their data model on its own, possibly in some kind of data format that is programmatically loaded by the processor?..

  2. Looking again at my “multi oscillator” example — is there a way or would it be normal to try to send a AudioProcessorParameterGroup's tree into a child component instead of the whole AVPTS tree? I’m wondering if subsetting somehow can be used to help separate concerns (each child oscillator only needs access the params relevant to it). Or do groups exist purely for organizing presentation of the parameters to the host and I should feel fine indiscriminately giving every child component a reference to AVPTS. You get a reference, and you get a reference! Everybody gets a reference!!!

3 Likes

A chunk of time has passed but I’d still love to know how people are organizing parameters in their plugins.

Are people passing references to the apvts through child components and dsp classes, passing subsets/containers of parameters, or [insert some other strategy]? Anyone storing their parameters as data instead of code? :smiling_imp:

I just have one big createLayout() function that returns an AudioProcessorValueTreeState::ParameterLayout of all the parameters I need and which is just passed as the ParameterLayout argument in the APVTS constructor, basically just this:
tree(*this, nullptr, "synthParams", createLayout())
in the AudioProcessor’s initializer list. All the child components have attachment members of the relevant type (e.g. std::unique_ptr<juce::AudioProcessorValueTreeState::ButtonAttachment>)
for each parameter they contain, each child also gets a function called something like ‘attachButtons’ which takes a pointer to an APVTS as an argument and attaches the attachment object to the tree with the appropriate ID string. Then all those ‘attach’ functions just get called in the constructor of the PluginEditor.

Thanks for sharing!

Interesting. Any reason you chose a separate attach function to pass the APVTS through vs. doing it on component construction? I’m trying to move away from constructing classes/components with an APVTS for testing purposes, so it’s really interesting to me how people are organizing their code!

Honestly the only reason to create a separate function rather than having each child component attach in its own constructor is to avoid having to pass the APVTS pointer as an argument through like 3-4 layers of child components. I just find it simpler to have all the attachments for the whole plugin done at once in the PluginEditor constructor, having it split up across all the files for the child components feels messier and more cumbersome. Just personal preference, in terms of the actual functionality of the code I can’t imagine there’s any difference.

1 Like

I disagree with the advise given here.

In a good software architecture you limit the knowledge about other structures to a minimum.
And information that correspondents to each other should be as close to each other, ideally so it is on the same screen, only as a last resort in different files. If they are in different files, you should add a comment, that changes here require also changes there.

In the proposed solution the PluginEditor needs knowledge of the members of the sub panel in order to create the connection. Now you cannot change the panel without changing the PluginEditor.
The Panel needs to expose its component, which violates the practice of encapsulation, one of the three pilars of OO design.

A nice paradigm that was posted here on the forum a while back was to add the component with the attachment together in a little struct:

struct AttachedSlider
{
    AttachedSlider (juce::AudioProcessorValueTreeState& state, juce::String paramID)
    : attachment (state, paramID, slider) {}

    juce::Slider slider;
    juce::AudioProcessorValueTreeState::SliderAttachment attachment;
};

Which you can use in your panel like:

private:
    juce::AudioProcessorValueTreeState& state;
    AttachedSlider gain { state, "gain" };

What shows that this solution of forwarding the APVTS makes sens is, that you don’t need to include anything to make it work. No extra knowledge of any details is needed.

Hope that helps

6 Likes
  1. I usually pass a reference to either the plug-in’s APVTS or the main AudioProcessor to each component’s constructor that needs it.

  2. If any class needs to be notified about changes to parameters I make that class inherit from APVTS::Listener. In the processor I usually hold references to parameters and update any DSP objects (filters etc.) at the start of each processBlock call. Sometimes I’ll use a flag to indicate if a certain object needs to be updated or not.

  3. AFAIK dynamic number of parameters isn’t possible. Best thing to do is just create the max number of parameters/groups and include an ‘active’ parameter to indicate if each group is being used or not.

  4. There’s no real need to get fancy with it. Instead of creating/adding parameters directly in the constructor/init list you could create a function that returns your parameter layout which, if you really wanted to, could be in a separate file to your Processor.cpp if it’s particularly cumbersome.

  5. AFAIK there’s no way to get groups from an APVTS, the parameters are stored ‘flatterned’ so you’ll have to access parameters from their ID’s (“gain0” , “gain1”, etc.). Groups also have a disadvantage of not exposing the type of each parameter, they only hold RangedAudioParameter’s (IIRC) so you’d have to dynamic_cast anytime you wanted to get a parameter’s ‘true’ value.
    One solution might be to create a custom ParamGroup struct that holds references to the parameters for a given group using their specific types (AudioParameterFloat, etc.) and in that struct’s constructor pass a reference to the APVTS along with the group’s index/ID so it can get the parameters it needs from the APVTS.

No promises that anything I’ve said is ‘best practices’ in anyway, but that’s typically how I do things and it’s worked pretty well for me (so far :grimacing:).

1 Like

This was what motivated the thread, thanks!!

We discussed this in chat, but this is exactly why I felt so fishy passing the APVTS everywhere. I can sort of accept that APVTS is akin to a data store. But I haven’t loved the idea of passing it everywhere I need a parameter, so it’s nice to see how others are handling this!

This is a nice idea, thanks for (re) sharing it!!

Thanks for chiming in! I’ve asked a fair number of people now and it’s sounding like passing an APVTS reference along is a common / intended path.

This is how I ended up doing it, a sanity increaser for me :slight_smile:

Daniel just said this in discord, and the fact that it’s flat blew my mind a little, especially because of the Value Tree naming.

This is a interesting idea! In chat, @eyalamir said after trying different ways to cut out the “string searching” part of APVTS he has been cutting out the APVTS completely, passing around structs of AudioParameterFloats, etc to wherever they need to be.

The idea of bundling the component and attachment together in a struct hadn’t occurred to me, but it is a nice solution. My strategy was to set up the sub panels with their own attach functions to connect each individual slider/button to the provided APVTS, that way I can just call each panel’s attach function once in the editor’s constructor and make any changes to components and parent components without touching anything in the editor (so long as the attach function stays ofc). But I think you’ve convinced me to take a more encapsulated approach going forward :sunglasses:
Also, does anyone have an alternative way of creating the ParameterLayout? So far I’ve just gone with creating one big createLayout() function that includes all the parameters for the plugin, which means that I have to go back and edit this function to reflect every new parameter component I add.

I took advice from @eyalamir and @daniel on the discord chat about this exact issue and I managed to make a clean method to add my parameters. My plugin is very simple but I guess the approach would be similar in a more complex plugin.

I have a Params header file where I separated parameters in different groups and made structs that are APVTS listeners. I used Eyal’s wonderful ParameterList helper to add the parameters to the layout. The structs keep pointers to the parameters so they can be easily used in the processor class with a simple ->get().

Then in my MidiProcessor class, the getParameterLayout creates the layout object using my param structs that are class members. The APVTS is then initialized in PluginProcessor.h by calling the MidiProcessor getParameterLayout.

If there were several processors, it could be easily split in separate classes and mixed together in PluginProcessor to build the ParameterLayout.

I think my English is a bit messy in my explanations but here is the code, it will probably be easier to understand ^^’

3 Likes

Wow, there are so many things that are great about this, thanks for sharing @stfufane! Funny timing, I spent a good amount of time asking Eyal and Daniel about this topic recently — they are without a doubt community heroes. In my case, my focus has been on testability of dsp classes and wanting to pass only the parameters that the classes need, etc.

I really like the parameters being setup in their own “home” and I really like that they can then be passed around in organized groups to processing classes (vs. passing the apvts monolith). It’s also cool how this approach side steps juggling with parameter ID strings all the time. The lambda/parameterChanged approach is interesting. I’ll see how it works for my needs and report back.

2 Likes

I disagree with this disagreement. The Component hierarchy can be quite ad-hoc and layout-driven, and it may not be the best reflection of the program structure. You may want to change your automatable parameters or their names -having them all connected in one place is the easiest way, and it reflects how the ParameterLayout is created. If you decide to attach a slider that wasn’t attached before or viceversa, using AttachedSlider you have to change all references to it -for something that is set once in the program and never used again. I consider connecting controls to parameters a responsibility of the editor as a whole as a view-controller for the processor, and the Component hierarchy as a facade with the least possible knowledge of the internals. Of course, this is just what works for me.

@stfufane Why did you get rid of the apvts?

Bump on @moritzsur question. Why did you get rid of the apvts?

I don’t want to speak for him, but in case Stéphane doesn’t see this, we chatted at the time and I believe a big motivator was not having to use strings to look up parameters. This can feel clumsy/costly in the audio path compared to directly accessing the parameters. Also, it’s a bit of a monolith that you have to pass around everywhere vs. handing off the relevant parameters to the relevant classes. So sometimes it feels like the abstraction is “in the way” vs. helping.

I ended up with a hybrid approach… I still technically have the apvts wrapper, but before I add parameters to the apvts to be owned, I create vectors of them in groups that make sense for my application, create attachments / listeners, and generally create a data structure that makes sense for the app to work with. This also lets me access the parameters directly.

Ah ok I thought I missed a problem thanks.
I dont really understand the first point since you can access the std::atomic ptr for each parameter but I guess the rest is personal preference.

To access the ptr or the param through the apvts, you need “magic” strings scattered throughout your code — the myParam in calls to parameters.getParameter("myParam") or parameters.getRawParameterValue("myParam") — so they are sorta like string constants with no compile time checking that every class just has to know about.

If you are working with batches of parameters ( “osc1”, “osc2”, “osc3”), then you also might be doing string manipulation to derive the ids, which can feel messy…

But to a large extent it’s definitely personal preference!

Since I was quoted above:

I wouldn’t suggest removing the APVTS from an existing project. It is still a useful class.
It helps for saving and restoring the parameters and additional states.

But it is relatively trivial to write your own and make it more versatile at the same time.

But on the other hand one would expect the APVTS to deal with all issues in a central place, which it doesn’t. The ParameterAttachments still have the potentially dangerous AsyncUpdater. It would be better to have a central Timer polling and updating all attached controls, which would warrant such a centralised approach. So the APVTS is a missed chance, but not worse than most self coded solutions.

Avoiding the getRawParameterValue() in the processing loop was adressed before, it should only be used when setting stuff and then keep the pointer to the atomic as member.
Additionaly the APVTS::addParameterListener() also has the problem of string comparisons (once you use the same callback for multiple parameters), which is just useless. Strings are for humans, numbers are for machines.

While there are as always many ways to Rome, I would strongly advise to store the actual parameter as member, since that has the method for normalising/denormalising and for the components the beginChangeGesture() and engChangeGesture() in one place.
The worst you can do is writing a separate denormalisation of the parameter value in the parameter and the code using the value.

4 Likes

You can easily make static constants for these:

static const juce::String myParamID{ "myParam" };
parameters.getParameter(myParamID);

which turns any typos from a run-time error to a compile-time error.

It’s a shame the APVTS doesn’t use juce::Identifiers for this reason - the underlying ValueTree does, but the APVTS itself uses strings and so yeah, the comparisons are unnecessarily costly.