12-Band Visual Equalizer

Key Technical Features:

  • Custom juce::Button inheritance for draggable controls
  • Template-based lock-free FIFO for analyzer data
  • Dirty flag parameter synchronization between GUI and DSP threads
  • Thread and real-time safe DSP architecture strategies
  • FFT-driven spectrum analysis

Frontend Challenges

FabFilter's Pro-Q 3 is my go-to EQ, but its CPU cost always made me hesitate. This project recreates its core functionality with minimal overhead: an EQ you never have to think twice about using, but without the steep price tag of the Pro-Q.

With that in mind, these were the core requirements I decided on for the interface:

  • Spectrum analyzer to show the audio pre and post the effects of the EQs, with an option to turn off entirely
  • Background visuals and lines that maintain the logarithmic scale that is standard in audio for Hz and dB
  • Real-time response curve updates that show the collective effect of the equalizer processing
  • Selected EQs always show parameters in the bottom center and update on change in selected EQ
  • Most importantly, color-coded draggable buttons, to adjust frequency and gain within the visual interface

While JUCE does have a massive library of functionality to assist in audio processing and its associated GUI needs, there were still many things from this list alone that would need to be created for this. JUCE's GUI editor makes sliders and faders very easy, and offers a full rendering library similar to, and, in my opinion, better than, Java's AWT and Swing libraries. The key was getting proper line drawing for the analyzers and the response curve, buffering a drawn image for the background on startup, having the selected EQ's sliders and their attachments detach and reattach pointers to the associated parameters properly, and putting as much work as possible into a custom class that will inherit from JUCE's Button class for the best possible feel. I knew first-hand that a majority of the interactions of this interface will be with the draggable buttons. They had to be perfect, and I believe they are a great success in feeling great and allowing real-time parameter changes. Plus, I got to add my favorite addition to any interface: tooltips.

A quick scroll through each of the EQ modes with the spectrum analyzer in action

Backend Challenges

Real-Time Safety

The general workflow established in the JUCE Framework is not much different than any other audio processing that would be done closer to the hardware. It just offers some conveniences. These conveniences primarily come from JUCE's internal handling of audio samples, which it passes to a process block function as a buffer of floating point values for quick processing in batches. This allows for all processing to be handled in a single function that will be reliably called by the processor thread, but it does not guarantee that the processes in the function will be real-time safe. This is a particularly serious problem. The audio samples will not stop if your software does not keep up. If the software can't keep up, it will result in clicks and pops that can permanently damage users' audio equipment. In a plugin that is meant to house several separate equalizers, in this case 12, real-time safety and lock-free solutions have to be considered in all decisions that can relate to the operations of the processor. This consideration, paired with the stipulations set up for parameters in VST3 plugins from the Steinberg SDK, meant that all 12 EQs' parameters need to be created on startup. This is a blessing and a curse. The parameters can give the illusion of being dynamic to the user while remaining allocated once at initialization, but it also meant a bit more work within the process block to check whether the EQs were initialized to decide whether to process them or not.

Thread-Aware Parameter Updates

The next primary concerns were handling user changes to the EQs and handling updating the interface in sync with those changes. JUCE's built-in threads to keep GUI and the processor separated work to allow for an unburdened processor thread, but only if this is addressed properly in the GUI thread to handle as much as possible. The solution applied had the lowest cost on the processor, but required careful synchronization between threads. The decision was to use stack-allocated synchronization structs with atomic values and dirty flags for each EQ, allowing the processor to update filters on its own schedule without blocking. A separate set of coefficient references is maintained for the GUI to query without constantly accessing the processor thread.

FFTs and Spectral Analysis

From here the final primary concern needed was to copy each buffer for the Fast Fourier Transforms needed for the spectrum analyzer. This was done using some of the built-in functions of JUCE along with a templated, lock-free, and rotating first-in-first-out buffer that would push to the GUI. This allowed for the bulk of the work to be handled by the GUI, with just a copy and rotation based on windowing to be done in the processor's FIFO buffer to pass along the data for the spectrum analyzer.

User Feedback & Iteration

After having some users test the functionality, addressing their feedback brought new additions that still maintain the small CPU overhead required in the initial vision. These are pre and post equalizer gains on the audio to account for cuts in frequency or too hot of an input signal. Lastly, there has been an addition of smoothed corners in the analyzer line drawing.

Performance

  • ~2-3% CPU per instance (vs. 4-7% for Pro-Q)
  • Lock-free parameter synchronization with dirty flags
  • Real-time safety maintained throughout