Architectural Best Practices - Workflow Tips & Tricks

Hello,

Would like to get people’s take on the cleanest, most extensible way to build an app that strings together audio generators/processors.

At the moment I’ve created a project using Audio Processors/Editors and an AudioProcessorGraph to chain them all together. It’s working fine and does what I’d expect but it’s really slow going. Every time I add a new feature/processor I have to create 4 files (two .cpps and two headers for the processor and editors respectively) with many, many lines of boilerplate. For example a trivial Panning dsp routine and attendant gui pan pot is several hundred lines of code long.

I have an ADSR, panning, and a simple wavetable osc and the project already feels bloated and is tiring to traverse. I’m a functional but not expert C++ coder so maybe I’m missing opportunities for optimization (consolidate function defs into headers?).

Ultimately I’d really like a quick, straightforward way to add new processing elements (filters, oscillators, envelopes, etc) with mapped gui inputs. Has anyone found a better, quicker way to iterate or is this just the nature of the beast? Have had a look at the doc examples on GitHub but nothing struck me as an obviously better solution.

Maybe I should prototype/experiment in a simpler language like SOUL and then port that code over? Or even write a template generator that scaffolds boilerplate for me? Any workflow tips and tricks you all have developed?

Thoughts?

1 Like

Just add your additional features into your EXISTING files instead of constantly creating new ones. Any basic JUCE project should have just one plugin processor and one editor. For your pan pot, just add the GUI rotary dial to your editor and add the DSP code (which could be a single pan() function) to your processor.

For more complex DSP algorithms that you want to code in separate files, this should at most be 2 additional files – customAlgorithm.h and customAlgorithm.cpp – simply define your algorithm in a class, and then in your project’s processor you can create an instance of this class as a member.

Ah okay, that’s make sense. For some reason got it in my head the AudioProcessorGraph approach would make things more modular. Probably only useful for more ambitious/complicated projects.

Better to just combine all my dsp into a single process block instead of chaining separate AudioProcessors together. I’m a bit neurotic about separating concerns so I may well break panning, envelopes, wavetables, filters, etc out into separate files but like you said they can be defined as their own classes with a process method and instantiated as member objects in the main plugin processor. No need to complicate things by making them into AudioProcessors.

I think I may have overestimated how complicated chaining/combining dsp in the same process block is and tried to solve for a complication that doesn’t actually exist.

Thanks for the perspective. I really appreciate it.

1 Like

of course! I’m a beginner too, and I haven’t really played around with AudioProcessorGraph – it certainly does look like it makes modularity approachable, but for a simple project I think it would only complicate things.

You can still bypass certain effects / nodes in your processing chain even if they’re all in the same AudioProcessor – just create a bool member variable that’s linked to a GUI ToggleButton, and then only do that DSP if the bool is true.

One thing I’ve been doing is putting the code in just a header file…no .cpp file. This works for about 95% of my classes. Yes, I know it means that everything has to get compiled every time and therefore slightly slower builds, but to me it’s worth it.

1 Like

It does not seem weird to separate your code like this, I’d advocate for it too. Do you have some kind of top processor class that your processors inherit to avoid rewriting the same code for each? There’s a good example for that in this tutorial : https://docs.juce.com/master/tutorial_audio_processor_graph.html

Anyway it’s good to have your separate modules like you’re doing, this way you can reuse them in other projects more easily. Like that you can also declare the parameters for each processor and easily activate/deactivate some processors for testing purposes.

Like @pizzafilms said, you can define your custom processors in header files without a separate cpp for simplicity.

For the editor I don’t really know what would be the best approach though…

Generally speaking it is good to split functionality over multiple source files. Some of our more complex plugins consist of more than 100 product specific source files and even more in our in-house shared codebase used for all of our products.

Still, if you find yourself writing many lines of repetitive boilerplate code your feeling that you can do better is probably right. Some thoughts:

GUI

Do you really need always a specific pair of dsp and GUI widget? Or can you put repetitive stuff in base classes you inherit from? When it comes to GUI, you might want to use knobs/sliders basically styled the same for multiple parameters. Go and create one single base class for that type of slider and extend it to that specific control it refers to by either adding some simple styling functions to it (maybe simply pass a bunch of colours to the constructor) or subclassing your base class. Use the AudioProcessorValueTreeState in conjunction with SliderAttachments and it will make sure your slider ranges and communication is set up with one single step (I even put that one step further and created a class that groups the widget and the attachment in one, you find it here). Also defining a LookAndFeel subclass that manages the basic style for your whole plugin is a good thing.

Processing Code

If your plugin does not require re-ordering processors at run time, better use the compile time fixed juce::dsp::ProcessorChain class to create a chain of processing modules. Much less overhead. Building new processors then means inheriting the simple interface of juce::dsp::ProcessorBase and if it’s simple enough go header only. You will see compile times go up a bit but code overhead go down a lot and you might also see some runtime performance improvement.

For some inspiration you can have a look at some open source plugin projects out there, I have recently released a simple small plugin built with most of the points mentioned above, you find it here.

In that project I have e.g. only one type of slider knob style, so I just put that directly into the globally used look and feel. Furthermore I use my AttachedWidget class for managing connection to the apvts and also some AAX style parameter automation highlighting and in the end adding a slider for one of my processing modules then was only as much as writing
jb::AttachedWidget<juce::Slider, /** some AAX specific highlighting stuff here */> mySlider;
Note that this was my individual way of doing it, for your plugin some other approach might be suited better, but maybe you get the idea of using various design patterns to avoid repetitive code.

3 Likes

I absolutely recommend to not use per project header/source files and rather take a look at JUCE Module Format and build your own modules. This is probably the most extensible way to manage a bigger and future proof codebase that can be maintained and used in multiple sub projects.

What happens if you want to build a demo, fx, beta, develop, test version of your application? What if you want to isolate dsp stuff to test it? You will start copy and pasting your stuff between projects and start to use weird macro stuff to customize it. Instead, build a base implementation in a module and specify it in your project specific source files. Not with inheritance but with composition.

You could use a module structure like this. Replace company with your prefix. Like “juce”.

company_core
Commonly used functions that can and will be used in multiple future projects.

company_dsp
Commonly used signal processing functions. No UI. No project specific code. Your basic processors should be defined here.

company_gui
Commonly used components. No assets, no project specific design / colours / components.

company_project
/dsp/
/gui/
/utils/

Project/app/plugin specific code. Assets and designs went here. Use multiple .cpp to speed up compilation of assets.

And then finally your project files which should be rather build specific than project specific.
Why? Because you may have one project called “My Effect” but have multiple configurations of it.

Let’s say you want to build a version without a UI that can be used in a command line. For example to convert or test presets. Or you want to build a demo version with all unnecessary assets stripped.

Some good rules:

  1. Avoid project specific macros in general code. And if, use the juce module settings macros.
  2. Avoid copy / paste of functions. Sometimes it’s better to split a function into multiple sub functions instead of building two project specific variations.
  3. Separate plugin UI code from DSP. Your app must always be executable as black box without GUI. Your dsp should know nothing about your UI. The final implementation should solve this with composition and something like an additional wrapper.

It needs time to find a structure that fits your needs. But it will pay off to think about it before you build one huge Jucer project with hundreds of classes.

11 Likes

@parawave this is a fantastic post, and should probably be pinned somewhere.

I wish I’d read it when starting out with JUCE a few years ago, so I wouldn’t have had to go through other less efficient solutions I experimented with.

Eventually I settled for something pretty much identical to your guidelines, and I’m extremely happy with how it scales to multiple projects/configurations/tests/etc.

1 Like

I’m gonna go ahead and bump this post for anyone looking for it… I had read it last month and spent 10 minutes searching for it. Thanks to all.

I wonder if JUCE team would be amenable to the idea of creating an admin only tag for useful, “everyone should know this” type posts that they can apply when relevant and thus make it easier for people to return to/discover such info?

1 Like