FR: Add non-owning parameters and groups

Currently, parameters added to a plugin must be owned either by the AudioProcessor itself, or by AudioProcessorParameterGroup.

This causes both unergonomic coding, like storing or dynamic_casting raw pointers to the parameters after creation, and also prevents users from manually reordering their parameters, since ordering is done by the groups.

An an ideal world, juce user code should look like (pseudo code):

struct MyPlugin: AudioProcessor
{
    MyPlugin()
    {
        //Parameters are added in order, not owned by the processor.
        //Adding new parameters last for V2 is trivial
        addParameters(filterType, filterCutoff, enableDrive);

        //Group is reported to the processor but has no control over
        //ownership or parameter ordering
        addParameterGroups(filterGroup);
    }

    void processBlock(AudioBuffer<float>&, MidiBuffer&) override
    {
        //When accessing the parameters, members can be directly accessed, no need 
        //to go through APVTS or raw pointers to get the values.
        if (enableDrive.get())
            processDrive();
        
        processFilter(filterType.get(), filterCutoff.get());
    }

    //Parameters are declared directly on the stack:
    AudioParameterInt filterType {...};
    AudioParameterFloat filterCutoff {...};
    AudioParameterBool enableDrive; { ... };

    //Non-owning group:
    ParameterGroup filterGroup { "Filter", filterType, filterCutoff };
};

If I get this right the main difference would be that parameters are not allocated on the heap anymore but exist as normal member variables of the processor, right?

I generally agree with your request, but it also makes me wonder why the team decided to make it fully heap-allocated in the first place. Does it have something to do with the fact that plugins must register their parameters at initialization? That would make sense to me, because parameters are currently already added to the processor before its constructor even begins.

How would that even be possible? Can you explain? We create ours in the constructor.

I believe the stack vs heap would lead to a performance improvement, yes, but that’s not the main reason why I’m suggesting this.

I believe it’s a legacy decision, like quite a lot in JUCE that is just there because that’s how it was originally written. There are no advantages to having AudioProcessor own the memory for the parameters.

Currently parameters can be added either in the body of the constructor by using addParameter(new AudioParameterInt(...)) or with the APVTS that does the same thing under the hood.

That flow shouldn’t change, and if you insist on using heap allocated parameters, you should also be able to.

most JUCE code I see creates and adds the parameters in the initializer list already via a createParameterLayout method. The constructor comes after that and I always just assumed this must be because the base class’ constructor reports to the host that this is the parameter layout.

edit: nvm, base class constructor comes before member variables in an initializer list

The constructor for the members of a derived class comes in after the base class constructor (but before the body of the derived class constructor).

Generally speaking a base class can’t be designed around derived classes doing anything before it’s constructor is called, as the base class has to be fully formed for the derived classes to do anything.

I do my parameters like this in a separate parameters file

#pragma once

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

class Parameters
{
    
public:
    Parameters() = delete;
    
    static juce::AudioProcessorValueTreeState::ParameterLayout createParamLayout()
    {
        return juce::AudioProcessorValueTreeState::ParameterLayout
        {
            std::make_unique<juce::AudioParameterFloat> (juce::ParameterID { "inputGain", 1 }, "Gain", /// etc etc etc
/// etc etc etc 
        };
    }
    
private:
    
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(Parameters);
};

AudioProcessor::AudioProcessor()
#ifndef JucePlugin_PreferredChannelConfigurations
  : AudioProcessor (BusesProperties()
#if ! JucePlugin_IsMidiEffect
#if ! JucePlugin_IsSynth
    .withInput("Input", juce::AudioChannelSet::stereo(), true)
#endif
    .withOutput ("Output", juce::AudioChannelSet::stereo(), true)
#endif
    ),
    apvts(*this, &undoManager, "Parameters", Parameters::createParamLayout())

if reporting the parameter layout to the host can’t be the reason for the way things work rn i’m running out of ideas what the reason could be. likely it would indeed be better the way you suggest

I really just want to clarify - this FR is not about how people structure their code. It’s about adding a way for the parameters interface to be non-owning.

You should still be able to use APVTS, addParameter(), separate parameters file, or however you like to set up your code.

Originally there was an addParameter function in the AudioProcessor to be called in the constructor.
With the AudioProcessorValueTreeState it led to a two phase initialisation problem, i.e. you had to tell the APVTS when you are done with adding parameters, which you did back then by setting a ValueTree to the apvts.state member (it listened to the valueTreeRedirected IIRC).

The main concern is or was, that those parameter objects must not be deleted while the AudioProcessor is alive.

The situation has changed with the option to swap parameters at runtime calling setParameterTree().
If you keep (raw) pointers to the parameters, they will be invalidated/dangling once you call this.

If you own the parameters yourself and delete those at runtime, the wrapper will have problems using those to communicate with the host.

It is hard to come up with a fool proof solution. But I agree with the FR, it would be nice to be able to keep the pointers from creation in a type safe way and not doing the dynamic_cast every time.

1 Like

I really don’t think this is use case that’s supported by hosts and plugin formats anyway (deleting parameters at runtime).

Having that if you ever want to do this and it’s somehow becomes a supported feature of JUCE, it should be pretty easy with something like:

addParameters(first, second, third);

//sometimes later:

//A juce function that, once returned from, means deleting the parameters is safe.
clearParameters();
addParameters(newParam1, newParam2);

BTW, JUCE is already implementing this exact system in Component. Component doesn’t own the Components that are added as children, yet methods exist to update the base class on changes.

2 Likes

It is already implemented: calling AudioProcessor::setParameterTree() discards all previous parameters and informs the host with ChangeDetails::withParameterInfoChanged().

I just mention that you need an infrastructure in place to update all your parameters, owning or non owning.

Yes, and read the docs for it:

 /** Sets the group of parameters managed by this AudioProcessor.

        Replacing the tree after your AudioProcessor has been constructed will
        crash many hosts, so don't do it! You may, however, change parameter and
        group names by iterating the tree returned by getParameterTree().
        Afterwards, call updateHostDisplay() to inform the host of the changes.
        Not all hosts support dynamic changes to parameters and group names.
    */
    void setParameterTree (AudioProcessorParameterGroup&& newTree);

As far as I understand it this is just a feature meant to use on construction when you want to change something about the parameters before constructor ends. This would fail after construction even with the current ownership model.

BTW, once a non-owning version of AudioProcessorParameterGroup exists, setParameterTree should also work the exact same way (and probably crash in the exact same situations).

To maintain backwards compatibility with all existing JUCE functions, I would store the parameter internally in an object that looks like:

struct ParameterHolder
{
    AudioProcessorParameter* param = nullptr;
    std::unique_ptr<AudioProcessorParameter> ownedParam;
};

That can either be constructed from a reference, or from a unique_ptr.

Then I would add overloads to the existing parameter functions, APVTS, etc, to handle references along with the raw pointers or unique_ptr.

1 Like

that is not part of this FR. if it was i wouldn’t have voted for it

Well, the proposed AudioProcessor API itself would be working with the parameter base class, but if you see Eyal’s example in the initial post, the ability to directly reference parameter objects does make user code accessing their parameters more type-safe (since you now have a reference to AudioParameterInt or AudioParameterFloat instead of just AudioProcessorParameter*).

Why are you against that?

1 Like

that’s a topic for a new post, believe me