Is this a good way to use multiple dsp ProcessorChains

so say i have a plugin that has 2 seperate chains,
[chain 1] and [chain 2]

but, i want to do the dry/wet mixing before chain 2 processes signals…
so its looks like IN → P1 → DryWetMixer → P2 → OUT

currently i have

	juce::dsp::ProcessorChain
		< Gain, Waveshaper, FilterWithCoeff, FilterWithCoeff > gummiFilterChain_preMix;

	Mixdown mixer;

	juce::dsp::ProcessorChain
		< FilterWithCoeff, FilterWithCoeff, Gain > gummiFilterChain_postMix;

of course, those are references to the different classes in dsp to save time and make it more readable using

    using Gain = juce::dsp::Gain<float>;
	using Filter = juce::dsp::IIR::Filter<float>;
	using Coefficients = juce::dsp::IIR::Coefficients<float>;
	using Waveshaper = juce::dsp::WaveShaper<float, Function>;
	using Mixdown = juce::dsp::DryWetMixer<float>;
	using FilterWithCoeff = juce::dsp::ProcessorDuplicator<Filter, Coefficients>;

in my process func

    auto& inBlock = context.getInputBlock();
	auto& outBlock = context.getOutputBlock();

	mixer.pushDrySamples(inBlock);
	gummiFilterChain_preMix.process(context);
	mixer.mixWetSamples(outBlock);
    gummiFilterChain_postMix.process(context);

is this a good way to do things? or is there a safer and/or cleaner way?

This will work fine and looks clean to me in case that context is a juce::dsp::ProcessContextReplacing. For a non-replacing context the second chains process call would overwrite everything in the out block again. So what’s the signature of the function this code snippet is called in?

A slightly more elegant pattern that we tend to use in our dsp code more and more is to build a wrapper processor that holds the chain and the dry wet mixer. Something like e.g. (written here in the post, not tested)

template <class InnerProcessor>
class MixerProcessor : public InnerProcessor
{
public:
    void prepare (const juce::dsp::ProcessSpec& spec)
    {
        InnerProcessor::prepare (spec);
        dryWetMixer.prepare (spec);
    }

    template <class ProcessContext>
    void process (const ProcessContext& context)
    {
        dryWetMixer.pushDrySamples (context.getInputBlock());

        InnerProcessor::process (context);

        dryWetMixer.mixWetSamples (context.getOutputBlock());
    }

private:
    juce::dsp::DryWetMixer<float> dryWetMixer;

}

Then wrap your first chain into that wrapper processor, like:

using PreMixChain = juce::dsp::ProcessorChain<Gain, Waveshaper, FilterWithCoeff, FilterWithCoef >;
using PostMixChain = juce::dsp::ProcessorChain<FilterWithCoeff, FilterWithCoeff, Gain>;

using MixedPreMixChain = MixerProcessor <PreMixChain>;

and wrap both the pre and post chain in an outer chain like

using CompleteChain = juce::dsp::ProcessorChain<MixedPreMixChain, PostMixChain>;

CompleteChain chain;

With this, all you have to do now in your process function is to call process on this one outer chain. This will work with any type of context, replacing and non-replacing ones.

For simple things this might be a bit over-engineered, but once the signal processing flow gets really complicated, this evolved to be a good pattern to keep the code clean. You can build wrapper processors for e.g. oversampling, constant block rate processing and a lot of other things the same way and keep everything nicely nested. The only downside is that accessing the actual processors might become a bit more less straightforward because of the nested chains. We solve this by usually declaring private member references to all processors we need direct access to so that the whole chain.get<> stuff is done centralized in one place and less error prone. Hope that helped

1 Like

great, thanks for the insight on using wrappers too! And yes, in this context the “context” is replacing.