dsp::ProcessorChain only works with ProcessContextReplacing?


#1

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.

Am I missing something?

Thanks,
Dan


#2

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


#3

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.


#4

Ok, where is that used though?

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.


#5

I’ve definitely seen that presentation and I agree, it’s great :slight_smile:


#6

@jules @IvanC when you guys designed the ProcessorChain class, were you borrowing some inspiration from this talk?

Also, can you break down how the templating causes each Processor’s ::process() method being called sequentially?


#7

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.


#8

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!


#9

Why do you have to wait until 2020?


#10

Because concepts didn’t make it into the C++2017 standard, and AFAIK it’s not even definitely agreed on for 2020 yet.


#11

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!


#12

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;

#13

Ouch! Thanks for reporting, we’ll try to figure out the intricacies of that one asap!


#14

FYI a fix should be on develop shortly…


#16

Thanks! So far seems to work now.