AudioProcessorValueTreeState with child processors' parameters

#1

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;
};
0 Likes

#2

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?

0 Likes

#3

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() };
}
0 Likes

#4

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;
};
0 Likes

#5

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.

0 Likes