I haven’t tested it, but from looking at the implementation of dsp::ProcessorChain, it looks like it would only work with ProcessContextReplacing.
With ProcessContextNonReplacing, since all processors in the chain have the same context being passed to their process method, each would process the same input block and write its results to the same output block, regardless of what any previous processor already wrote there. So only the last processor in the chain would have any effect.
This video is long and not 100% relevant, but he does talk about how he sets up templates with variadic arguments to choose which template gets executed when a couple templates have almost identical signatures. it’s a whole bunch of the compiler doing ALL of the heavy lifting.
I believe that’s what’s happening in that ProcessorChain class. Jump to 12:36. 14:00 will be the start of your moment of zen
Yes, you’re missing the fact that the context objects have a method usesSeparateInputAndOutputBlocks() so an algorithm can find out whether it needs to replace or not. The method is constexpr so that the algorithm class can provide in-place and out-of-place versions which are selected at compile time.
For example, from looking at the implementations of the classes, it looks like ProcessorChain<Bias, Gain>::process (ProcessContext& context) would just hand the context to Bias::process and then hand it to Gain::process.
With a ProcessContextNonReplacing context, the addition that Bias::process does to the output buffer would just be overwritten by Gain::process, which writes the output buffer with the input buffer (which wasn’t modified) multiplied by the gain.
I haven’t spent as much time with variadic template programming and the dsp module, as I wanted to, but I think it works like this:
Each Chain template consists of Base, which is a single processor wrapper of type ChainBase and the processors Chain, holding a Chain of all following processors.
You can see that e.g. in the process:
template <typename ProcessContext>
void process (ProcessContext& context) noexcept
{
Base::processor.process (context); // calling this process method
processors.process (context); // calling the following process (which is a chain in itself)
}
But I agree with @danradix, I can’t see the non replacing flag being used anywhere in the dsp module…
I also think, it should be handled differently for the first processor in a chain and the following doing an in-place process.
I haven’t watched that talk yet, but nothing that we did with ProcessorChain is particularly groundbreaking, it’s just the natural way you’d design a compile-time processing framework according to modern C++ idioms.
If only we’d been able to use C++ Concepts for it, the whole thing would have been much simpler to read and use though. Hopefully in 2020 we’ll be able to rewrite it with those!
Oh, “concepts” is like a new language feature, like the “auto” keyword.
You’re not talking about concepts literally, as in ideas (such as: stop
using ‘delete’, start using smart pointers when you need to use ‘new’).
Got it!
I think that what Dan is pointing out is that ProcessorChain does not use this method.
I’ll demonstrate with an example how this causes the “non-replacing-context multiplication” of 2*3*5 to result in 10 rather than the standard multiplication’s result of 30:
// single sample and channel buffers.
AudioBuffer<double> input (1, 1), output (1, 1);
// initialize input to 2
input.setSample (0, 0, 2.0);
// multiply it by 3 and then by 5
ProcessorChain<Gain<double>, Gain<double> > chain;
chain.get<0>().setGainLinear (3.0);
chain.get<1>().setGainLinear (5.0);
// but use a non-replacing context
chain.prepare ({ 44100.0, 256, 1} );
AudioBlock<double> inputBlock (input), outputBlock (output);
ProcessContextNonReplacing<double> ctx (inputBlock, outputBlock);
chain.process (ctx);
// non-replacing-context multiplication: 2 * 3 * 5 = 10
cout << output.getSample (0, 0) << endl;