I think this is a basic question on thread-safety and a common use-case but I’m still new to this.
I’m writing a simple prototype filter with juce::dsp classes. I wrote a Biquad class which contains a juce::IIR::Filter<float> object and updates its coefficients if some relevant parameter has been changed. The coefficient updates always happen on the audio thread before doing the actual processing. If some coeffs had to be changed, a change message is sent asynchronously on the main thread after their update.
Now I want to draw the frequency response of that filter on a juce component. In my first, naive implementation, I wrote a FrequencyResponseComponent which keeps a const Biquad& reference to the biquad, and is a listener to it. Whenever a change message is received, the callback does the following:
make a local copy of the biquad coeffs: auto currentCoeffs = biquad.coeffs;
call coeffs.getMagnitudeForFrequencyArray on a set of frequencies to fill an array double *of magnitudes. Then call repaint().
In repaint, draw using the array of magnitudes.
Of course I get crashes because of step 1 since it’s possible that the biquad.coeffs were being updated on the audio-thread when I made that local copy. To solve this I want my FrequencyResponseComponent to read from a set of biquad coeffs which are always valid, and correspond to the latest valid set of coeffs available at the time of the change callback. Is this a case where I should implement some kind of lock-free FIFO / circular buffer of biquad coeffs ?
If so, what size should this FIFO be ? I’m assuming it’s 2 * [number-of-coeffs-in-a-biquad] so there’s always space for the Biquad object to write new coeffs without overwriting the section of coeffs that is being read, but I’m not sure. What if the FrequencyResponseComponent gets slow because the main thread is blocked for some reason? Then the Biquad cannot continue writing new coeffs, and once the app responds again the set of coeffs that is retrieved from the ringbuffer might be an old one.
Anyway newbie questions I guess, I’m not 100% sure how this works or if I should even be using a ring buffer for this in the first place. I just don’t want to block the audio thread so I’m refraining from using mutexes when updating the coeffs, or allocating.
For your use case, the real-time thread changes the coeff and a non-real-time thread reads the coeff (as a whole). As a result, double-buffering might be a good choice. Please refer to the following video for more info.
However, from my personal experience, a better way is to let the juce component have its own filter (and listen to para changes of course). Then you don’t have to worry about the thread safety issue & whether the DAW calls the audio thread when bypassed.
Thanks ! Will have a look into that. Your second option means the main thread will re-do the coefficient calculation on its own ? Never thought about this, I guess it’s not so much a problem if there are only a few filters.
Should I have done it the other way around ? I wrote it like this because it actually seemed simpler to avoid blocking the audio-thread, but if there’s a more common pattern I’m all ears.
I would suggest the second option. Yes, the audio thread and the non-real-time thread both calculate their own coeffs. If you are worried about the workload on the message thread, do it on a background thread and use mutex whenever necessary (e.g., try lock the message thread and lock the background thread).
I have used the first option before. However, the first option
will increase the workload on the real-time thread a little bit
will lock the non-real-time thread during writing
cannot update the GUI when the processBlock is not called (and your users will think your plugin is dead). For example, Logic is very smart:
cannot suit other use cases, e.g., if you also allow the user to choose linear phase filters.
Oh okay, very interesting, thanks! Didn’t think about this “users will think your plugin is dead” situation but yeah this makes total sense, you still want the frequency response to be updated even if the transport is stopped in the DAW (which I assume stops the processBlock callbacks). I’ll try reversing my code (i.e. make the main thread update the coeffs and the audio thread just reading them) using advice from the video you linked. If this is too complex I’ll just keep two copies of coeffs in both threads as you advised.