AudioProcessorValueTreeState with child processors' parameters

I’m trying to wrap my head around using AudioProcessorValueTreeState in a plugin using child processors. Ideally, I’d like to get a parent processor with an AudioProcessorValueTreeState reflecting parameters present in any child processors, with the child processors providing the parent constructor their parameters themselves.

I can have child processors with their own AudioProcessorValueTreeStates, but those aren’t seen by the plugin host as they don’t show up in the parent’s parameters.

I can have the child processors provide parameters to the parent’s APVTS in the parent’s constructor, as in the example below, but there’s some cumbersome acrobatics involved in getting the parameter linked to the appropriate child variables – the parameter listener will only provide the [0 1] range, so I appear to need to keep each parameter’s NormalisableRange somewhere in the child…

It would be great to just pass a pointer to the parent APVTS to the child, but the child needs to be initialized in order to provide its parameters to the parent constructor, so the APVTS doesn’t exist when the child is being initialized.

Can an APVTS add a child’s entire APVTS to it? Or extract and/or move the child APVTS as a ParameterLayout in the parent APVTS constructor?

If I’m missing something simple or obvious, what is it? Anybody else facing the problem of surfacing child parameters to the parent processor? It seems like this isn’t an esoteric use case – providing host-visible parameters linked to child processors in a Graph or ProcessorChain or whatever. Any thoughts greatly appreciated.

class BaseProcessor : public AudioProcessor 
{
// ... default shared AudioProcessor setup methods ...
};

class GainProcessor : public BaseProcessor, public AudioProcessorParameter::Listener
{
public:
    GainProcessor() {}
    std::vector<std::unique_ptr<RangedAudioParameter>> createParameters() {
        std::vector<std::unique_ptr<RangedAudioParameter>> params;
        auto gainParam = std::make_unique<AudioParameterFloat> ("gain", "Gain", 0.0f, 2.0f, 1.0f);
        gainParam->addListener(this);
        gainRange = gainParam.get()->getNormalisableRange();
        params.push_back(std::move(gainParam));
        return params;
    }
    
    void parameterValueChanged (int parameterIndex, float newValue) override {
        gainParamValue = gainRange.convertFrom0to1(newValue);
    }
    void parameterGestureChanged (int, bool) override {}
    
    void processBlock (AudioSampleBuffer& buffer, MidiBuffer& midiMessages) override
    {
        buffer.applyGain(gainParamValue);
    }

private:
    float gainParamValue = 1.0f;
    NormalisableRange<float> gainRange;
};

class ParentProcessor : public BaseProcessor {
public:
    //==============================================================================
    ParentProcessor()
        : gainProcessor()
        , avts (*this, nullptr, "PARAMETERS", createParameters() )
    {}
    
    AudioProcessorValueTreeState::ParameterLayout createParameters()
    {
        std::vector<std::unique_ptr<RangedAudioParameter>> params;
        auto gainParams = gainProcessor.createParameters();
        for (auto& p : gainParams)
            params.push_back(std::move(p));
        return { params.begin(), params.end() };
    }
    
    virtual void prepareToPlay (double sampleRate, int samplesPerBlock) override
    {
        gainProcessor.prepareToPlay(sampleRate, samplesPerBlock);
    }

    virtual void processBlock (AudioSampleBuffer& buffer, MidiBuffer& midiMessages) override
    {
        for (int i = getTotalNumInputChannels(); i < getTotalNumOutputChannels(); ++i)
            buffer.clear (i, 0, buffer.getNumSamples());
        gainProcessor.processBlock(buffer, midiMessages);
    }

    GainProcessor gainProcessor;
    AudioProcessorValueTreeState avts;
};
1 Like

One thing that occurs to me is to attach multiple apvts as children of one parent ValueTree, and pass that around. I’m a newbie myself and don’t know how to get at the parameters of each child apvts from the mother node. I have verified that a child apvts does continue to function normally after being added to a parent ValueTree. Can someone expand on this?

Assuming, you wrote those AudioProcessors to be hosted in your plugin…

I solved this with a static method in the main processor, that collects the parameters of the sub processors into a vector.

Easy variant is to give a reference to the vector to each sub processor like

static void SubProcessor::createParameterLayout (AudioProcessorValueTreeState::ParameterLayout& layout)
{
    auto drive = std::make_unique<AudioParameterFloat>(DistortionProcessor::paramDrive, 
                                                       NEEDS_TRANS ("Overdrive"),
                                                       NormalisableRange<float> (1.0, 11.0), 1.0);
    layout.add (std::move (drive));
}

static AudioProcessorValueTreeState::ParameterLayout MainProcessor::createParameterLayout()
{
    AudioProcessorValueTreeState::ParameterLayout layout;
    SubProcessor::createParameterLayout (layout);
    return layout;
}

More elegant is to use AudioProcessorParameterGroups for each sub processor:

static std::unique_ptr<AudioProcessorParameterGroup> SubProcessor::createProcessorParameters ()
{
    auto drive = std::make_unique<AudioParameterFloat>(DistortionProcessor::paramDrive, 
                                                       NEEDS_TRANS ("Overdrive"),
                                                       NormalisableRange<float> (1.0, 11.0), 1.0);
    layout.push_back (std::move (drive));
}

static AudioProcessorValueTreeState::ParameterLayout MainProcessor::createParameterLayout()
{
    std::vector<std::unique_ptr<AudioProcessorParameterGroup>> params;

    params.push_back (SubProcessor::createProcessorParameters());
    return { params.begin(), params.end() };
}
3 Likes

daniel, many thanks – making the child processor’s createProcessorParameters() method static is the key. Now I can create the APVTS with necessary child processor parameters, and initialize the child processor afterwards, passing a pointer to the generated APVTS to the child processor constructor.

To bring it back to my original example, the code now looks like this:

class BaseProcessor : public AudioProcessor 
{
// ... default shared AudioProcessor setup methods ...
};

class GainProcessor : public BaseProcessor
{
public:
    GainProcessor(AudioProcessorValueTreeState& s) : avts(s) {}
    
    static std::unique_ptr<AudioProcessorParameterGroup> createProcessorParameters ()
    {
        std::unique_ptr<AudioProcessorParameterGroup> layout (std::make_unique<AudioProcessorParameterGroup>("groupGain", "Gain", "|"));
        auto gain = std::make_unique<AudioParameterFloat> ("gain", "Gain", 0.0f, 2.0f, 1.0f);
        layout.get()->addChild(std::move(gain));
        return layout;
    }
    
    void prepareToPlay (double sampleRate, int samplesPerBlock) override {
        gainParamValue = avts.getRawParameterValue("gain");
    }
    
    void processBlock (AudioSampleBuffer& buffer, MidiBuffer& midiMessages) override
    {
        buffer.applyGain(*gainParamValue);
    }

private:
    AudioProcessorValueTreeState& avts;
    float* gainParamValue;
};

class ParentProcessor : public BaseProcessor {
public:
    //==============================================================================
    ParentProcessor()
        : avts (*this, nullptr, "PARAMETERS", createParameters() )
        , gainProcessor(avts)
    {}
    
    AudioProcessorValueTreeState::ParameterLayout createParameters()
    {
        std::vector<std::unique_ptr<AudioProcessorParameterGroup>> params;
        params.push_back (GainProcessor::createProcessorParameters());
        return { params.begin(), params.end() };
    }
  
    void prepareToPlay (double sampleRate, int samplesPerBlock) override
    {
        gainProcessor.prepareToPlay(sampleRate, samplesPerBlock);
    }

    void processBlock (AudioSampleBuffer& buffer, MidiBuffer& midiMessages) override
    {
        for (int i = getTotalNumInputChannels(); i < getTotalNumOutputChannels(); ++i)
            buffer.clear (i, 0, buffer.getNumSamples());
        gainProcessor.processBlock(buffer, midiMessages);
    }

    AudioProcessorValueTreeState avts;
    GainProcessor gainProcessor;
};

N.B. since they share the necessary information via the AudioProcessorValueTreeState, there is actually no need for a common base class for your sub processors. IMHO this ties it unnecessarily to the one setup, but if it was me, I would want to recycle those sub processors.

I like this solution, and am using a similar technique myself. However, one problem with this is that it makes instantiating GainProcessor on its own (for unit testing) almost impossible.

After all, GainProcessor requires an initialized AudioProcessorValueTreeState to be instantiated, but to create a new AudioProcessorValueTreeState, it needs to attach to an AudioProcessor. A chicken-and-the-egg scenario.

I haven’t been able to solve this yet. If anyone has any suggestions, I would love to hear it!

Yes, this chicken and egg situation has also kept me scratching my head. I’ve now moved on to the following, with the above example updated below:

class BaseProcessor : public AudioProcessor
{
// ... generic AudioProcessor setup methods
};

class GainProcessor : public BaseProcessor
{
public:
    GainProcessor(bool c = false)
        : isChild(c)
        , localAvts(*this, nullptr, "PARAMETERS", createProcessorParameters())
    {
        if (!isChild) 
            initializeAvts(localAvts);
    }

    void initializeAvts(AudioProcessorValueTreeState& s)
    {
        avts = &s;
    }
    
    std::unique_ptr<AudioProcessorParameterGroup> createProcessorParameters ()
    {
        std::unique_ptr<AudioProcessorParameterGroup> layout (std::make_unique<AudioProcessorParameterGroup>("groupGain", "Gain", "|"));
        auto gain = std::make_unique<AudioParameterFloat> ("gain", "Gain", 0.0f, 2.0f, 1.0f);
        layout.get()->addChild(std::move(gain));
        return layout;
    }
    
    void prepareToPlay (double sampleRate, int samplesPerBlock) override
    {
        gainParamValue = avts->getRawParameterValue("gain");
    }
    
    void processBlock (AudioSampleBuffer& buffer, MidiBuffer& midiMessages) override
    {
        buffer.applyGain(*gainParamValue);
    }

private:
    bool isChild;
    AudioProcessorValueTreeState* avts;
    AudioProcessorValueTreeState localAvts;
    float* gainParamValue;
};

class ParentProcessor : public BaseProcessor {
public:
    //==============================================================================
    ParentProcessor()
        : gainProcessor(true)
        , parentAvts (*this, nullptr, "PARAMETERS", createParameters())
    {
        gainProcessor.initializeAvts(parentAvts);
    }
    
    AudioProcessorValueTreeState::ParameterLayout createParameters()
    {
        std::vector<std::unique_ptr<AudioProcessorParameterGroup>> params;
        params.push_back (gainProcessor.createProcessorParameters());
        return { params.begin(), params.end() };
    }
  
    void prepareToPlay (double sampleRate, int samplesPerBlock) override
    {
        gainProcessor.prepareToPlay(sampleRate, samplesPerBlock);
    }

    void processBlock (AudioSampleBuffer& buffer, MidiBuffer& midiMessages) override
    {
        for (int i = getTotalNumInputChannels(); i < getTotalNumOutputChannels(); ++i)
            buffer.clear (i, 0, buffer.getNumSamples());
        gainProcessor.processBlock(buffer, midiMessages);
    }

    GainProcessor gainProcessor;
    AudioProcessorValueTreeState parentAvts;
};

GainProcessor can be initialized by itself or via the parent processor, and works with its AVPTS* variable avts, which gets assigned with the initializeAvts() method.

In the standalone case, GainProcessor’s constructor initializes localAvts, then calls initializeAvts().

In the parent/child case, GainProcessor gets initialized first, then ParentProcessor initializes its own parentAvts with the gainProcessor instance’s createProcessorParameters() (not a static method anymore) then passes parentAvts to GainProcessor.

This is all pretty convoluted and clumsy, but at least it works, and can be extended to multiple child processors. If anyone has a more simple or elegant solution, I’d love to know.

3 Likes