Hello everybody !
Today, I release the last topic about the new DSP module classes. If you want to read the previous ones, you can find them here : https://musicalentropy.github.io/JUCE-DSP-module/
Let’s not forget the topic about the class dsp::LadderFilter
developed just before the ADC workshops as well : New LadderFilter class
We are going to talk about the classes dsp::ProcessContext
, dsp::ProcessorChain
, dsp::ProcessorDuplicator
, dsp::ProcessorWrapper
, the structures dsp::ProcessSpec
and dsp::ProcessorState
, and all the classes derived from this available in the DSP module :
- A new take on the new filters classes
dsp::IIR
,dsp::StateVariableFilter
,dsp::LadderFilter
anddsp::FIR
which use extensively the internal structuredsp::ProcessorState
- The API choices for the classes
dsp::Convolution
anddsp::Oversampling
- All the more or less “processor examples” classes provided in the folder juce_dsp/processors, which are the new classes
dsp::Bias
,dsp::Gain
,dsp::Oscillator
,dsp::Reverb
anddsp::Waveshaper
Obviously all these classes use a lot the mighty dsp::AudioBlock
which is probably the one we have talked the most since the release of the DSP module, and to be clear again about it, I think it is awesome and I use it all the time nowadays. You can read again in that topic why : [DSP module discussion] New AudioBlock class
Now, I have to confess I don’t use any of the dsp::Processor
classes in my work, I’ll tell you why later in the discussion. But I would like to get some feedback from you guys, and like in the other DSP module topics, I’ll provide some documentation here and some context about the development.
Problems solved by these classes
So, the new Processor classes are the JUCE team attempt to standardize audio classes which are not the AudioProcessor class, in a way which is compatible with the concept of audio graphs. They were mostly the work of @fabian, @jules and @skot I think.
The idea was to give a new take to that graph concept, which was as you might know already covered in the past with the classes AudioProcessorGraph
and all the classes AudioSource
. Why doing this again ? According to @jules himself, these classes are not that good by the current JUCE team standards. Moreover, they wanted something that could be used with a single process call after all the necessary initialization, with easy multi channel capabilities that became possible thanks to the development of the class AudioBlock
.
I guess it was also an attempt to provide an elegant solution to that first most common programming mistake that we see on the forum as well Indeed, having classes that are supposed to handle by themselved AudioBlock
objects where the user needs to provide the number of channels in the prepare
function means the developer must ask himself the multi-channel question early during the coding process. Moreover, the ProcessorDuplicator
class allows to generate a multi-channel class with multi-channel capabilities based on a mono processor class if it has designed to be augmented in a given way we will talk about later.
Documentation
How to use them ? The first thing to know is that you can find most of the examples you need in the JUCE examples folder, specifically with the demo application DSPDemo and the demo plug-in DSP module plug-in demo. You’ll notice the following things :
- All the new classes can be initialized using a
prepare
function call instead of the classicprepareToPlay
, using aProcessSpec
object providing the sample rate and the maximum audio buffer size as usual, but also the number of channels that the audio class is supposed to handle, which is the new thing here, and which is very important. - A lot of processing classes have become more or less automatically templated. That means that it is now possible to use them with 64 bits samples (double) and not only 32 bits (float). Thanks to the addition of the
SIMDRegister
class, most of the DSP module processing classes are SIMD compatible as well with only a change in the template type argument ! - To process any sample, you need now to use additional abstractions now. The first one is the conversion of an array of samples or an
AudioBuffer
into anAudioBlock
object. The second one is the use of aProcessContext
object, either aProcessContextReplacing
or aProcessContextNonReplacing
, which allows the processing graph to use either one audio buffer for the whole process, or two different one to keep the input samples unchanged and store the output samples in a separate buffer. - By the way, the
ProcessContext
classes have a public property calledisBypassed
which can be used for… bypassing the processing chain, and they give some pointers to the assigned input and outputAudioBlock
objects. - It is now possible to stack several compatible processing classes in a
ProcessingChain
object simply by using their name in the template definition of the object. So if you want to code an overdrive plug-in based on JUCE only classes, you can initialize aProcessorChain
object this way :
dsp::ProcessorChain<GainProcessor, BiasProcessor, DriveProcessor, DCFilter, GainProcessor> overdrive;
Obviously you need to do this before :
using GainProcessor = dsp::Gain<float>;
using BiasProcessor = dsp::Bias<float>;
using DriveProcessor = dsp::WaveShaper<float>;
using DCFilter = dsp::ProcessorDuplicator<dsp::IIR::Filter<float>, dsp::IIR::Coefficients<float>>;
- Using lambda functions, it is possible to initialize some simple waveshapers and oscillators in your headers, using the DSP module processor classes
Waveshaper
andOscillator
. They serve that simplicity purpose as a priority, and as some users reported in the forums, they are not supposed to be used in complex commercial plug-ins where you might need faster and anti-aliased equivalents. - The new class
dsp::IIR::Filter
is a duplicate of the originalIIRFilter
class with new capabilities (the compatibility with theFilterDesign
classes, the possibility to handle filters at any order and not just biquads, the functions to calculate the frequency response for both magnitude and phase). Other new classes such asStateVariableFilter
andFIR
have been designed the same way. But let’s face the main issue there, in order for that class to be compatible with templating,SIMDRegister
,ProcessorDuplicator
, and the wholeProcessorChain
stuff at the same time, the use of the new IIR filters class is a lot more complex than the use of the previous one. I can tell @fabian and @jules had a hard time figuring out how to code these beast the right way as well. And I personally think it is impossible for a beginner to find out by himself this kind of syntax to init its behaviour :
*lowPassFilter.state = *dsp::IIR::Coefficients<float>::makeLowPass (getSampleRate(), lowPassFilterFreqParam->get());
That’s why examples exist of course !
- Indeed,
IIR::Filter
was the perfect example of a mono processor class which might need to be augmented with multi-channel capabilities using theProcessorDuplicator
class, so that it could be treated as a wholeProcessorChain
compatible class. But for this to happen, it sounds logical that we might need some data to be different for every channel (the memory for previous samples) and some data to be the same everywhere and assignable in one go (the coefficients of the filters and all the associated processing internal variables). For this to happen, the current implementation of theProcessorDuplicator
class needs to have an access to a given “State” class which is there to provide the common data for all the mono processor instances. That means that your mono processing class must provide a “State” class as well, which can be provided in the templated definition ofProcessorDuplicator
class. - One odd thing, the demo application uses the class
ProcessorWrapper
, but the “example” classes don’t use it (the classesdsp::Bias
,dsp::Gain
,dsp::Oscillator
,dsp::Reverb
anddsp::Waveshaper
), and still they are compatible withProcessorChain
! (it’s magical)
If you need any information about all this stuff, just looking into the base code, the JUCE website classes documentation, and inti these examples should give you all the answers to your questions, and some code to copy and paste as well. If you don’t understand anything else, feel free to send your questions here of course.
Let’s add some additional remarks :
- If you want to create your own audio processing classes so they are compatible with
ProcessorChain
, the obvious choice is to derive fromProcessorWrapper
, but you can also follow the “examples” processor classes included in the DSP module - I think you can learn a lot about templating and other topics simply by looking into the source code of these classes so don’t hesitate to do so !
- It is possible to use the
dsp::FastMathApproximations
classes or anything based ondsp::LookupTable
with theOscillator
andWaveshaper
classes - The class
dsp::Convolution
is compatible withProcessorChain
, but that’s not the case forOversampling
since its use is singular in comparison with simple processing classes.
Why your feedback is going to be important
So I would like to know what you think about all of this. What’s your experience with the Processor classes ? Did it change your habits ? Have you been influenced by the way these classes have been written in the way you code now ?
I’ll write something very soon about the audio parameters in JUCE since the JUCE team has stated that they are going to start working on them again recently : We are removing AudioProcessor-based parameter management . I think this upcoming development is going to have some interaction with the way the audio processor classes are designed, and that it might imply some updates in the DSP module Processor classes. That’s why your feedback is important here so that the JUCE team will have more information to guide their take on audio parameters.
Tell me if you have any remark, and @fabian and @jules don’t hesitate to correct me if needed.