Hello guys !
During the development of the DSP module, a discussion subject came back very often which is about the structure of processing code, since we designed a few audio effects classes.
As all plug-ins developers here already know, JUCE doesn’t provide a full “structure by design” for processing code and audio plug-ins. I mean that you can’t just use the JUCE classes and provided functions to have a fully functional plug-in which can be commercially released. That’s not necessarily a bad thing since you get a lot of freedom this way on how you want to code your API, but it might be confusing for newcomers or when designing an audio processing class for JUCE in my opinion.
So, to develop a plug-in, you usually need to follow some of the steps below :
- Create a new “audio plug-in project” in the Projucer, to get the PluginProcessor h+cpp created, and heriting from AudioProcessor class
- Add some parameters in the constructor using the JUCE AudioProcessorParameter classes, or custom ones to handle properly the parameter changes (and automation, presets, undo/redo, load/save from the DAW etc.)
- Fill the content of the AudioProcessor functions called prepareToPlay, releaseResources and processBlock.
I’ll be just talking about the audio/DSP side of things here, so let’s forget in this thread that we have a PluginEditor as well.
Now imagine you don’t want to code everything in these last three functions, but that you have instead processing classes + objects with functions to call everywhere. For example, you could use the JUCE filtering classes, any audio effect class you made etc. You want to init them properly in the prepareToPlay function, to set their internal sample rates. You want also to update other of their internal variables when the user moves a knob. And sometimes you just want to reset something, without updating anything else (imagine a meter component where the maximum amplitude can be reset with a mouse-click).
For these very reasons, I tend to have for every of my audio processors some additional functions, and at the end I got this :
- A constructor + a destructor
- The prepareToPlay function to set the internal variable sampleRate and the internal variable maximum size of the audio buffer. Sometimes, it is useful to use the internal number of channels information here.
- The releaseResources function that I don’t use very often, which can be called in a destructor or when the processor is made inactive
- A function to make the processor active or inactive
- Some functions to set/get the interface parameters (probably some AudioProcessorParameters or anything atomic, like a cutoff frequency)
- A reset function to reset some internal state variables (like a filter state variable such as the last value of the output)
- An update function to set the new values of the internal variables (like a filter coefficient) when the interface parameters have changed
- The processBlock function to do the actual processing, using the internal state variables and internal variables
Since I don’t like to write the same code several times (DRY principle), the internal variables can only be updated in the update function, not in the constructor or anywhere else. My set function sets also a boolean called mustUpdateParameters to true. This boolean is set to false in the update function. The prepareToPlay function does extra initialisation and calls update + reset at its end all the time. And the processBlock function checks the state of mustUpdateParameters and calls update if needed. And we can also optimize a little what happens in the update function, to prevent all the updating code to be called all the time, but only when a specific parameter has changed for every call…
So that’s what I do. What do you think of this approach ? Do you use something like that as well ? Any suggestion of better practices ?
I hope I will get a lot of feedback here since all your answers will probably be read and studied for the next JUCE developments by me and the JUCE team