Parametric EQ (ZDF SVF/TPT) built with C++20 and JUCE

DSP, Plugin and Host development discussion.
RELATED
PRODUCTS

Post

Music Engineer wrote: Wed Apr 29, 2026 9:51 am .but is it a low- or high-shelf? Or does that depend on the gain setting - and does that even make a difference or can a high-shelf always be expressed as low-shelf + gain and vice versa?
For symmetric "cookbook-style" shelves the only differencse between low-shelf and high-shelf are the direction in which the gain-parameter tilts the thing and whether you normalize at DC or Nyquist... and you can normalize some other point such as the mid-point as well.

ps. As soon as you make the shelf asymmetric in one way or another, things get more complicated, but at it's core a symmetric "cookbook shelf" is just a "tilt" + normalization at one frequency or another.

Post

Yes, ok. I did some tests with my SVF implementation and the low and high shelf are indeed the same up to a (positive(!)) renormalization gain factor. However, I noticed that this "A" parameter that I use in my API (which originates from the RBJ formulas) is only the square root of maximum magnitude response (for a boost setting, i.e. positive dB-gain). Hmm...that's a bit inconvenient! It violates the principle of least astonishment. Maybe I need to change that.

I also recall that in some other context (namely: as damping filters in a Karplus-Strong comb like setup), I was using first order high shelvers and noticed that when I set the dB gain to zero (i.e. set the filter up neutrally), I seemed to get a (nontrivial) allpass rather than a trivial one, i.e. a "wire", i.e. H(z) = 1, and so I thought that maybe the high- and low- shelvers might be equivalent only up to sign flip and/or an allpass component. But I couldn't reproduce this allpass behavior yesterday so maybe I was just doing something else wrong back then. Also - considering that all the RBJ filters are supposed to be minimum phase, they actually must be equivalent as soon as the magnitude responses are the same which can be easily confirmed for the low- and high shelfs visually when plotting them.

By the way, my design formulas for 1st order low- and high-shelvers, which I took from the DAFX book, do not have this nice feature of being equivalent to one another up to a positive renormalization gain - instead, one also has to adjust the frequency. I assume that those design formulas do not use the gain midpoint as reference frequency. Maybe I would be better off specializing my (Orfanidis based) Butterworth shelver designs to the first order case...or maybe just add/subtract a 1st order lowpass (either IIT or BLT based) and apply a compensation gain. I think, the DAFX designs for 1st order shelvers are based on adding/subtracting a 1st order allpass from the input - and the shelver frequency is defined as the allpass 90° frequency or something...and maybe that's an inconvenient parametrization from a user point of view because that point does not necessarily coincide with the half-gain point. ...something more to figure out...
Last edited by Music Engineer on Sat May 02, 2026 2:48 pm, edited 4 times in total.
My website: rs-met.com, My presences on: YouTube, GitHub, Facebook

Post

Btw, I investigated why Kilohearts plugins are so lightweight and found that they zero out signals below a specific threshold to skip processing.
Specifically, my testing shows they truncate signals below -100dB to zero.
This is relevant because many DAWs continue to send sample blocks even when transport is stopped.
I'm sharing this in case it's helpful for other developers.

Post

I might have been mistaken.
I tested this by attenuating a 0dB signal to -100dB, passing it through the plugin, and then boosting it by +100dB.
While Kilohearts and my plugin suffered from dropouts, Ableton’s EQ Eight stayed clean.
There must be some other under-the-hood processing going on.

Post

Zalthyrexor wrote: Fri May 01, 2026 9:00 pm This is relevant because many DAWs continue to send sample blocks even when transport is stopped.
You should be processing whether or not transport is running. It's perfectly normal to put signals through a DAW (or play virtual instruments, whatever) without running the transport.

That said, some plugin suspend on silence. Personally I hate this, but if you insist on doing it, I'd at least suggest putting the threshold a lot lower than -100dB (around -200dB or so might be a more reasonable threshold... or even just compared to zeroes directly; often silent DAW tracks are canonically zero) because that's still within the dynamic range of even 16-bit master (with proper dither, especially with noise shaping) and you'd probably want some headroom as well.

Post

Note also that plugin APIs actually do have a way handling this "turn off processing when input is silent" business. It is based on the plugin reporting to the host, how long their "processing tail" is. In a JUCE-based plugin, you would provide this info by implementing getTailLengthSeconds():

https://docs.juce.com/master/classjuce_ ... 30784d7550

For an IIR filter, you could compute the (maximum) length of your processing tail by computing some number that is proportional to the maximum filter Q that you allow because the ringout time for an IIR filter is proportional to the filter Q...(if I'm not mistaken. I'm not sure though. Maybe there's some log or exp and/or something else involved. But the general trend: higher Q -> longer ringing is true. I could work out a formula if there is sufficient interest). Or, if you don't want to overthink it, you could just report a number that is safe by being much longer than the actual tail length - like a full second or even two. That should be more than enough for a typical EQ. ...although, thinking about it, very narrow notches are sometimes a thing in an EQ - and these do indeed have rather high Q. So - yeah - I guess a proper formula based on Q would be better. :shrug: Oh - and the ringing time would also be inversely proportional to frequency when the Q is fixed. It's actually inversely proportional to the absolute bandwidth - at least in a bandpass and notch. For brickwall filters, it has also to do with the steepness of the slope - but that's not relevant here.

When the host knows the length of your processing tail, it may decide to bypass your plugin when it notices that it would feed you only with zeros anyway - but only after it has allowed for the tail to ring out. Think of reverbs and delays etc. - they don't immediately switch off the output when the input goes silent. They have a ringout tail which could be several seconds long. So, to be safe, hosts would by default assume that the tail length could potentially be infinite such that the audio processing callback needs to always be called, unless you explicitly report them something else by implementing (i.e. overriding) this method. Filters also have such a tail, although it tends to be much shorter - unless you have extremely high Q, as said - which is indeed a thing, for example in modal filters.

I think, this is the "proper" way to do it but it would shift the responsibility of actually bypassing the processing to the host and some hosts may not support it.
My website: rs-met.com, My presences on: YouTube, GitHub, Facebook

Post Reply

Return to “DSP and Plugin Development”