ProcessorChain with dynamic Processor

Hi, I’m playing aroung with FilterDesign::designIIRHighpassHighOrderButterworthMethod, I want to set the *processorChain.get<>.state with the *coefficient.getObjectPointer(i).

However, i realized that the getObjectPointer(i) with i can be more than 0, which will return a dynamically number of coefficients, depend on the order i put.

But i can only initialize ProcessorChain with defined Processors (eg: dsp::ProcessorChain<Duplicator>). The processor chain also has no method to add processor dynamically, so I’m wondering if I can just define a lot of Duplicator (eg: dsp::ProcessorChain<Duplicator,Duplicator,Duplicator,Duplicator>) so that i can set the state if i want, is that ok? Thanks for any help.

The processor duplicator is meant to transform a single-channel processor into a multichannel one, so that’s not what you need here. We have a helper class like this that can be used for that exact purpose:

/** A chain of a dynamic number of processors of the same type. Use init to set the numbers of processors you want to
    chain and to initialize them with constructor arguments
 */
template <typename ProcessorType>
class DynamicProcessorChain
{
public:
    /** Initializes this chain to create a given number of chained processors. ProcessorType
        needs a default constructor to call this constructor
     */
    void init (size_t numProcessors)
    {
        processors.clear();
        processors.reserve (numProcessors);

        for (size_t i = 0; i < numProcessors; ++i)
            processors.push_back (std::make_unique<ProcessorType>());
    }

    /** Initializes this chain to create a given number of chained processors, each one with the same constructor
        arguments
     */
    template <typename... ProcessorConstructorArgs>
    void init (size_t numProcessors, ProcessorConstructorArgs&&... args)
    {
        processors.clear();
        processors.reserve (numProcessors);

        for (size_t i = 0; i < numProcessors; ++i)
            processors.push_back (std::make_unique<ProcessorType> (std::forward<ProcessorConstructorArgs> (args)...));
    }

    /** Initializes this chain to create a given number of chained processors, each one constructed with the
        corresponding constructor element from the Initializers, which can be any sort of array, vector or initializer
        list that has a begin, end and size method. The number of processors to create is derived from the number of
        initializers passed
     */
    template <typename Initializers>
    void init (const Initializers& initializers)
    {
        processors.clear();
        processors.reserve (initializers.size());

        for (auto initializer = initializers.begin(); initializer != initializers.end(); ++initializer)
            processors.push_back (std::make_unique<ProcessorType> (*initializer));
    }

    /** Get a reference to the processor at that index */
    ProcessorType& get (size_t index)
    {
        return *processors[index];
    }

    /** Get a reference to the processor at that index */
    const ProcessorType& get (size_t index) const
    {
        return *processors[index];
    }

    /** Prepares all processors in the chain */
    void prepare (const juce::dsp::ProcessSpec& spec)
    {
        for (auto& processor : processors)
            processor->prepare (spec);
    }

    /** Resets all processors in the chain */
    void reset()
    {
        for (auto& processor : processors)
            processor->reset();
    }

    template <class SampleType>
    void process (const juce::dsp::ProcessContextReplacing<SampleType>& context)
    {
        if (context.isBypassed)
            return;

        for (auto& processor : processors)
            processor->process (context);
    }

    /** Returns a begin iterator to allow a range-based loop over all processors */
    auto begin() { return processors.begin(); }

    /** Returns an end iterator to allow a range-based loop over all processors */
    auto end()   { return processors.end(); }

private:
    std::vector<std::unique_ptr<ProcessorType>> processors;
};

Example usage in the context of a juce::dsp::ProcesorChainlooks like this:

// Somewhere in the member section of your AudioProcessor
using MyChain = juce::dsp::ProcessorChain<juce::dsp::Gain<float>,
                                          DynamicChain<juce::dsp::IIR::Filter<float>>,
                                          juce::dsp::Gain<float>>;

MyChain myChain;

// Somewhere in your AudioProcessor implementation
void MyAudioProcessor::prepareToPlay	(double sampleRate, int maximumExpectedSamplesPerBlock) override
{
    juce::dsp::ProcessSpec spec { .sampleRate = sampleRate,
                                  .maximumBlockSize = static_cast<juce::uint32> (maximumExpectedSamplesPerBlock),
                                  .numChannels = static_cast<juce::uint32> (getMainBusNumInputChannels()) };

    // Using a structured binding for a more expressive syntax than e.g. myChain.get<1>()
    auto& [preGain, dynamicIIRChain, postGain] = myChain;

    // Expecting myCutoffFreq and myOrder to be some constants for your filter design declared somewhere
    dynamicIIRChain.init (juce::dsp::FilterDesign::designIIRHighpassHighOrderButterworthMethod (myCutoffFreq, sampleRate, myOrder));

    myChain.prepare (spec);
}

Hope that helps. Code is untested and should primarily illustrate the concept :wink:

1 Like

Much love❤️ I’ll test it out

@PluginPenguin Is there a way to combine two filters like this:


Or i have to make it into two dynamicChains, and after calling prepare(), i only have to call process() right? Thanks

While it is nice that this approach retains the API of ProcessorBase/ProcessorChain, the benefit of the ProcessorChain was to be constructed at compile time to allow as much optiisation as possible.
This advantage is not the case with this setup.

Still better than AudioProcessorGraph though :wink:

@PluginPenguin I get AssertionFailure for not using ProcessorDuplicator:

If you want to chain two such filters, you should create a processor chain with to such dynamic chains, one for the high pass and one for the low pass, like

using HigherOrderIIR = DynamicProcessorChain<juce::dsp::IIR::Filter<float>>
using MyChain = juce::dsp::ProcessorChain< HigherOrderIIR, HigherOrderIIR>;

MyChain myChain;

void prepareToPlay	(double sampleRate, int maximumExpectedSamplesPerBlock) override
{
    auto& [highPassFilter, lowPassFilter] = myChain;

    highPassFilter.init (juce::dsp::FilterDesign::designIIRHighpassHighOrderButterworthMethod (hfrequency, sampleRate, horder);
    lowPassFilter.init ((juce::dsp::FilterDesign::designIIRHighpassHighOrderButterworthMethod (lfrequency, sampleRate, lorder);

    myChain.prepare (spec);
}

Note that I called prepare on the outer juce::dsp::ProcessorChain instance and so you have to call process on the outer chain, this will invoke them in the right order for all sub-processors. This is one of the core concepts of a processor chain :wink:

One more thing, I noted that you passed different sample rate variables to your highpass and lowpass design functions, which seems like a mistake. You should pass the same sample rate to all filter design functions and the process spec passed to prepare.

This is because the juce filters are only intended for single channel usage. You seem to use them in a multi channel context. You have to wrap the filter in a a juce::dsp::ProcessorDuplicator as the comment says:

using HigherOrderIIR = DynamicProcessorChain<juce::dsp::ProcessorDuplicator<juce::dsp::IIR::Filter<float>, juce::dsp::IIR::Coefficients<float>>>
using MyChain = juce::dsp::ProcessorChain< HigherOrderIIR, HigherOrderIIR>;

MyChain myChain;

I agree to some extent – maximum optimization is one point that makes the processor chain approach great, but I also like how descriptive the API is when it comes to declaring chains of processors as types.
Still, if you are aiming for maximum optimization, it’s not too hard to build a IIR filter chain that takes the filter order as template parameter and keeps a compile time static sized std::array of filter instances.

2 Likes

It worked!, thanks a lot