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::ProcessorWrapper, the structures
dsp::ProcessorState, and all the classes derived from this available in the DSP module :
- A new take on the new filters classes
dsp::FIRwhich use extensively the internal structure
- The API choices for the classes
- All the more or less “processor examples” classes provided in the folder juce_dsp/processors, which are the new classes
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
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.
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
preparefunction call instead of the classic
prepareToPlay, using a
ProcessSpecobject 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
SIMDRegisterclass, 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
AudioBlockobject. The second one is the use of a
ProcessContextobject, either a
ProcessContextNonReplacing, 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
ProcessContextclasses have a public property called
isBypassedwhich can be used for… bypassing the processing chain, and they give some pointers to the assigned input and output
- It is now possible to stack several compatible processing classes in a
ProcessingChainobject 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 a
ProcessorChainobject 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
Oscillator. 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::Filteris a duplicate of the original
IIRFilterclass with new capabilities (the compatibility with the
FilterDesignclasses, 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 as
FIRhave been designed the same way. But let’s face the main issue there, in order for that class to be compatible with templating,
ProcessorDuplicator, and the whole
ProcessorChainstuff 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 !
IIR::Filterwas the perfect example of a mono processor class which might need to be augmented with multi-channel capabilities using the
ProcessorDuplicatorclass, so that it could be treated as a whole
ProcessorChaincompatible 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 the
ProcessorDuplicatorclass 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 of
- One odd thing, the demo application uses the class
ProcessorWrapper, but the “example” classes don’t use it (the classes
dsp::Waveshaper), and still they are compatible with
ProcessorChain! (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 from
ProcessorWrapper, 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::FastMathApproximationsclasses or anything based on
- The class
dsp::Convolutionis compatible with
ProcessorChain, but that’s not the case for
Oversamplingsince 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.