Orange Gate v2 is about finished. This one’s got a compressor in it. The basic idea behind Orange Gate is that the gate and the compressor are driven by a filtered input. Both the gate and compressor have a pre-filter and a post-filter spectrum analyzer that give you some extra feedback about your trigger signal. That’s the context, let’s talk about how the spectrum analyzers tick.
Motivation
Orange Gate v1 uses the spectrum analyzer code from Foley’s Finest Frequalizer (github). This works great except that it creates a dedicated thread for each analyzer. In the grand scheme of life this is pretty minor, but it annoys me. I don’t want to have to spin up four threads for each instance of Orange Gate v2. So, here’s the plan: use juce::TimeSliceThread and juce::TimeSliceClient to create a dedicated FFT processing thread that is shared by each instance of Orange Gate.
Design
The core concept is relatively simple:
– The audio thread records some amount of data.
– The time slice thread reads the data and performs an FFT on it.
– The message thread grabs the processed data and displays it.
This means we have three threads and two thread boundaries to worry about. The boundary between the audio thread and the time slice thread is a touchy one. The audio thread can’t stop for any reason and the spectrum analyzers won’t be accurate if the time slice thread isn’t polling for new samples regularly enough. The boundary between the time slice thread and JUCE’s message thread is less critical, but it would still be cool if the message thread didn’t stall out.
Audio thread | TimeSliceThread
Before I started, I anticipated this one would be the most difficult part. Luckily, JUCE provideth. juce::AbstractFifo keeps track of read and write indices for a buffer of any size. ScopedWrite and ScopedRead make exchanging data from the buffer between threads extremely simple. In the end it was as easy as creating a buffer of floats and letting the AbstractFifo coordinate access to it.
TimeSliceThread | Message Thread
This barrier is slightly different than the audio thread given that you need to pass an entire buffer for the fft curve through. You can’t get away with just shyly poking a couple values through like you can in the audio processor. More data is being moved at once, but way less often. Unlike the audio processor, neither thread is high priority which makes it a little easier to deal with. A mutex protecting the shared curve data provides all the protection we need.
Creating the Spectrum Curve
The SpectrumAnalyzer is a juce::TimeSliceClient. In its useTimeSlice() method it checks for new data in the AudioBufferFifo and writes it into each of its contained SpectrumAnalyzerBuffers. Moving sample data from the AudioBufferFifo into the SpectrumAnalazyerBuffer crosses the first thread boundary.
If you run sequential FFT calculations over a simple signal that increases in pitch, you’ll see it step upwards as each new calculation is made. For this reason, Orange Gate has several SpectrumAnalyzerBuffers that calculate FFTs for overlapping sections of the input signal.
The white shaded region above represents the original section of the signal being analyzed. Each orange strip represents an overlapping region of the signal that is stored in its own SpectrumAnalyzerBuffer. As each individual buffer fills up, an FFT is performed and the result is pushed into a SpectrumAnalyzerCurve.
The SpectrumAnalyzerCurve calculates an average of the past several curves to smooth things out. It then smooths the curve further by applying a set of SpectrumAnalyzerBinSmoothers (similar to juce::SmoothedValue) that restrict each bin’s up and down movements. Tying the smoothing logic of each bin to occur at each curve calculation has the added advantage that time is kept track of based on the audio being processed rather than the repaint frequency of the interface. The user interface can simply access the pre-calculated curve and display it, without needing to worry about anything else.
When its time to paint, the SpectrumAnalyzerComponent grabs the curve and converts it into a juce::Path. There’s the second thread boundary. The data is smoothed once again using juce::Path::createPathWithRoundedCorners to remove any jagged edges (especially important around the lower frequencies). I had some concerns about the amount of time being spent smoothing the curves at each step, but it looks so nice that I don’t care. Either way none of the smoothing logic should be happening when the UI isn’t open.
The end result is as many spectrum analyzers as you want, all calculated on a shared thread. This reduces the total number of threads and the total load on the user’s system. All the smoothing makes for some pretty fluid visualizers too. There’s still a couple things left to fix in the code, like calculating an appropriate return value from useTimeSlice() and other miscellaneous things. Hopefully, I’ll get around to those soon.
code: github