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
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.
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
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
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.