Skip to content

Instantly share code, notes, and snippets.

@endolith
Last active November 19, 2023 08:53
Show Gist options
  • Save endolith/5455375 to your computer and use it in GitHub Desktop.
Save endolith/5455375 to your computer and use it in GitHub Desktop.
Python implementation of "Cookbook formulae for audio EQ biquad filter coefficients"
"""
Audio EQ biquad filter design from cookbook formulae.
Created on Thu Mar 28 18:51:00 2013
UNFINISHED AND BUGGY
Python/SciPy implementation of the filters described in
"Cookbook formulae for audio EQ biquad filter coefficients"
by Robert Bristow-Johnson
https://www.musicdsp.org/en/latest/Filters/197-rbj-audio-eq-cookbook.html
These functions will output analog or digital transfer functions, deriving
the latter using the bilinear transform, as is done in the reference.
Overall gain parameters are not included.
"BLT frequency warping has been taken into account for
both significant frequency relocation (this is the normal "prewarping" that
is necessary when using the BLT) and for bandwidth readjustment (since the
bandwidth is compressed when mapped from analog to digital using the BLT)."
TODO: combine lowpass and highpass? and bandpass?
TODO: generate analog poles/zeros prototypes and convert them or output them
directly? Would be useful to have an analog shelving filter designer, for
instance.
TODO: Use ordinary frequency instead of rad/s for analog filters? angular
matches scipy, but these are usually used in audio. Compare with CSound
functions, etc.
http://www.dsprelated.com/showcode/170.php defines it as
function [b, a] = shelving(G, fc, fs, Q, type)
TODO: sane defaults for Q for all filters
TODO: Try to think of better names than "outer", "constantq", "skirt", etc
TODO: Bandwidth is wrong for high-frequency peaking digital filters,
despite using the equations in the cookbook
TODO: functions should accept Q, BW, or S directly, since these are not
trivially derived otherwise?
Q (the EE kind of definition, except for peakingEQ in which A*Q is
the classic EE Q. That adjustment in definition was made so that
a boost of N dB followed by a cut of N dB for identical Q and
f0/Fs results in a precisely flat unity gain filter or "wire".)
_or_ BW, the bandwidth in octaves (between -3 dB frequencies for BPF
and notch or between midpoint (dBgain/2) gain frequencies for
peaking EQ)
_or_ S, a "shelf slope" parameter (for shelving EQ only). When S = 1,
the shelf slope is as steep as it can be and remain monotonically
increasing or decreasing gain with frequency. The shelf slope, in
dB/octave, remains proportional to S for all other values for a
fixed f0/Fs and dBgain.
Then compute a few intermediate variables:
alpha = sin(w0)/(2*Q) (case: Q)
= sin(w0)*sinh( ln(2)/2 * BW * w0/sin(w0) ) (case: BW)
= sin(w0)/2 * sqrt( (A + 1/A)*(1/S - 1) + 2 ) (case: S)
FYI: The relationship between bandwidth and Q is
1/Q = 2*sinh(ln(2)/2*BW*w0/sin(w0)) (digital filter w BLT)
or 1/Q = 2*sinh(ln(2)/2*BW) (analog filter prototype)
"""
from math import pi, tan, sinh
from math import log as ln
from cmath import sqrt
import numpy as np
from scipy.signal import tf2zpk, tf2ss, lp2lp, bilinear
def _transform(b, a, Wn, analog, output):
"""
Convert analog prototype filter to desired output format.
Shift prototype filter to desired frequency, convert to digital with
pre-warping, and return in various formats.
"""
Wn = np.asarray(Wn)
if not analog:
if np.any(Wn < 0) or np.any(Wn > 1):
raise ValueError("Digital filter critical frequencies "
"must be 0 <= Wn <= 1")
fs = 2.0
warped = 2 * fs * tan(pi * Wn / fs)
else:
warped = Wn
# Shift frequency
b, a = lp2lp(b, a, wo=warped)
# Find discrete equivalent if necessary
if not analog:
b, a = bilinear(b, a, fs=fs)
# Transform to proper out type (pole-zero, numer-denom, state-space)
if output in ('zpk', 'zp'):
return tf2zpk(b, a)
elif output in ('ba', 'tf'):
return b, a
elif output in ('ss', 'abcd'):
return tf2ss(b, a)
else:
raise ValueError('Unknown output type {0}'.format(output))
def lowpass(Wn, Q=1/sqrt(2), analog=False, output='ba'):
"""
Design an analog or digital biquad lowpass filter with variable Q.
Analog prototype: H(s) = 1 / (s**2 + s/Q + 1)
Parameters
----------
Wn : float
Corner frequency of the filter.
For digital filters, `Wn` is normalized from 0 to 1, where 1 is the
Nyquist frequency, pi radians/sample. (`Wn` is thus in
half-cycles / sample.)
For analog filters, `Wn` is an angular frequency (e.g. rad/s).
Q : float
Quality factor of the filter. Examples:
* 1/sqrt(2) (default) is a Butterworth filter, with maximally-flat
passband
* 1/sqrt(3) is a Bessel filter, with maximally-flat group delay.
* 1/2 is a Linkwitz-Riley filter, used to make lowpass and highpass
sections that sum flat to unity gain.
analog : bool, optional
When True, return an analog filter, otherwise a digital filter is
returned.
output : {'ba', 'zpk', 'ss'}, optional
Type of output: numerator/denominator ('ba'), pole-zero ('zpk'), or
state-space ('ss').
Default is 'ba'.
Returns
-------
b, a : ndarray, ndarray
Numerator (`b`) and denominator (`a`) polynomials of the IIR filter.
Only returned if ``output='ba'``.
z, p, k : ndarray, ndarray, float
Zeros, poles, and system gain of the IIR filter transfer
function. Only returned if ``output='zpk'``.
"""
# H(s) = 1 / (s**2 + s/Q + 1)
b = np.array([1])
a = np.array([1, 1/Q, 1])
return _transform(b, a, Wn, analog, output)
def highpass(Wn, Q=1/sqrt(2), analog=False, output='ba'):
"""
Design an analog or digital biquad highpass filter with variable Q.
Analog prototype: H(s) = s**2 / (s**2 + s/Q + 1)
Parameters
----------
Wn : float
Corner frequency of the filter.
For digital filters, `Wn` is normalized from 0 to 1, where 1 is the
Nyquist frequency, pi radians/sample. (`Wn` is thus in
half-cycles / sample.)
For analog filters, `Wn` is an angular frequency (e.g. rad/s).
Q : float
Quality factor of the filter. Examples:
* 1/sqrt(2) (default) is a Butterworth filter, with maximally-flat
passband
* 1/sqrt(3) is a Bessel filter, with maximally-flat group delay.
* 1/2 is a Linkwitz-Riley filter, used to make lowpass and highpass
sections that sum flat to unity gain.
analog : bool, optional
When True, return an analog filter, otherwise a digital filter is
returned.
output : {'ba', 'zpk', 'ss'}, optional
Type of output: numerator/denominator ('ba'), pole-zero ('zpk'), or
state-space ('ss').
Default is 'ba'.
Returns
-------
b, a : ndarray, ndarray
Numerator (`b`) and denominator (`a`) polynomials of the IIR filter.
Only returned if ``output='ba'``.
z, p, k : ndarray, ndarray, float
Zeros, poles, and system gain of the IIR filter transfer
function. Only returned if ``output='zpk'``.
"""
# H(s) = s**2 / (s**2 + s/Q + 1)
b = np.array([1, 0, 0])
a = np.array([1, 1/Q, 1])
return _transform(b, a, Wn, analog, output)
def bandpass(Wn, Q=1, type='skirt', analog=False, output='ba'):
"""
Design an analog or digital biquad bandpass filter with variable Q.
Parameters
----------
Wn : float
Center frequency of the filter.
For digital filters, `Wn` is normalized from 0 to 1, where 1 is the
Nyquist frequency, pi radians/sample. (`Wn` is thus in
half-cycles / sample.)
For analog filters, `Wn` is an angular frequency (e.g. rad/s).
Q : float
Quality factor of the filter. Examples:
* sqrt(2) is 1 octave wide
type : {'skirt', 'peak'}, optional
The type of filter.
``skirt``
Type 1 (default), has a constant skirt gain, with peak gain = Q
Transfer function: H(s) = s / (s**2 + s/Q + 1)
``peak``
Type 2, has a constant peak gain of 0 dB, and the skirt changes
with the Q.
Transfer function: H(s) = (s/Q) / (s**2 + s/Q + 1)
analog : bool, optional
When True, return an analog filter, otherwise a digital filter is
returned.
output : {'ba', 'zpk', 'ss'}, optional
Type of output: numerator/denominator ('ba'), pole-zero ('zpk'), or
state-space ('ss').
Default is 'ba'.
Returns
-------
b, a : ndarray, ndarray
Numerator (`b`) and denominator (`a`) polynomials of the IIR filter.
Only returned if ``output='ba'``.
z, p, k : ndarray, ndarray, float
Zeros, poles, and system gain of the IIR filter transfer
function. Only returned if ``output='zpk'``.
"""
if type in (1, 'skirt'):
# H(s) = s / (s**2 + s/Q + 1)
b = np.array([0, 1, 0])
elif type in (2, 'peak'):
# H(s) = (s/Q) / (s**2 + s/Q + 1)
b = np.array([0, 1/Q, 0])
else:
raise ValueError('"%s" is not a known bandpass type' % type)
a = np.array([1, 1/Q, 1])
return _transform(b, a, Wn, analog, output)
def notch(Wn, Q=10, analog=False, output='ba'):
"""
Design an analog or digital biquad notch filter with variable Q.
The notch differs from a peaking cut filter in that the gain at the
notch center frequency is 0, or -Inf dB.
Transfer function: H(s) = (s**2 + 1) / (s**2 + s/Q + 1)
Parameters
----------
Wn : float
Center frequency of the filter.
For digital filters, `Wn` is normalized from 0 to 1, where 1 is the
Nyquist frequency, pi radians/sample. (`Wn` is thus in
half-cycles / sample.)
For analog filters, `Wn` is an angular frequency (e.g. rad/s).
Q : float
Quality factor of the filter. Examples:
* sqrt(2) is 1 octave wide
analog : bool, optional
When True, return an analog filter, otherwise a digital filter is
returned.
output : {'ba', 'zpk', 'ss'}, optional
Type of output: numerator/denominator ('ba'), pole-zero ('zpk'), or
state-space ('ss').
Default is 'ba'.
Returns
-------
b, a : ndarray, ndarray
Numerator (`b`) and denominator (`a`) polynomials of the IIR filter.
Only returned if ``output='ba'``.
z, p, k : ndarray, ndarray, float
Zeros, poles, and system gain of the IIR filter transfer
function. Only returned if ``output='zpk'``.
"""
# H(s) = (s**2 + 1) / (s**2 + s/Q + 1)
b = np.array([1, 0, 1])
a = np.array([1, 1/Q, 1])
return _transform(b, a, Wn, analog, output)
def allpass(Wn, Q=1, analog=False, output='ba'):
"""
Design an analog or digital biquad allpass filter with variable Q.
Transfer function: H(s) = (s**2 - s/Q + 1) / (s**2 + s/Q + 1)
Parameters
----------
Wn : float
Center frequency of the filter.
For digital filters, `Wn` is normalized from 0 to 1, where 1 is the
Nyquist frequency, pi radians/sample. (`Wn` is thus in
half-cycles / sample.)
For analog filters, `Wn` is an angular frequency (e.g. rad/s).
Q : float
Quality factor of the filter.
analog : bool, optional
When True, return an analog filter, otherwise a digital filter is
returned.
output : {'ba', 'zpk', 'ss'}, optional
Type of output: numerator/denominator ('ba'), pole-zero ('zpk'), or
state-space ('ss').
Default is 'ba'.
Returns
-------
b, a : ndarray, ndarray
Numerator (`b`) and denominator (`a`) polynomials of the IIR filter.
Only returned if ``output='ba'``.
z, p, k : ndarray, ndarray, float
Zeros, poles, and system gain of the IIR filter transfer
function. Only returned if ``output='zpk'``.
"""
# H(s) = (s**2 - s/Q + 1) / (s**2 + s/Q + 1)
b = np.array([1, -1/Q, 1])
a = np.array([1, 1/Q, 1])
return _transform(b, a, Wn, analog, output)
def peaking(Wn, dBgain, Q=None, BW=None, type='half', analog=False,
output='ba'):
"""
Design an analog or digital biquad peaking filter with variable Q.
Transfer function: H(s) = (s**2 + s*(Az/Q) + 1) / (s**2 + s/(Ap*Q) + 1)
Used in graphic or parametric EQs.
Parameters
----------
Wn : float
Center frequency of the filter.
For digital filters, `Wn` is normalized from 0 to 1, where 1 is the
Nyquist frequency, pi radians/sample. (`Wn` is thus in
half-cycles / sample.)
For analog filters, `Wn` is an angular frequency (e.g. rad/s).
dBgain : float
The gain at the center frequency, in dB. Positive for boost,
negative for cut.
Q : float
Quality factor of the filter. Examples:
* Q = sqrt(2) (default) produces a bandwidth of 1 octave
ftype : {'half', 'constant'}, optional
Where on the curve to measure the bandwidth of the filter.
``half``
Bandwidth is defined using the points on the curve at which the
gain in dB is half of the peak gain. This is the method used in
"Cookbook formulae for audio EQ biquad filter coefficients"
``constant``
Bandwidth is defined using the points -3 dB down from the peak
gain (or +3 dB up from the cut gain), maintaining constant Q
regardless of center frequency or boost gain. This is
symmetrical in dB, so that a boost and cut with identical
parameters sum to unity gain.
This is the method used in "Constant-Q" hardware equalizers.
[ref: http://www.rane.com/note101.html]
Klark Teknik calls this "symmetrical Q"
http://www.klarkteknik.com/faq-06.php
constant Q asymmetrical
constant Q for both boost and cut, which makes them asymmetrical
(not implemented)
Half-gain Hybrid
Defined symmetrical at half gain point except for 3 dB or less
(not implemented)
analog : bool, optional
When True, return an analog filter, otherwise a digital filter is
returned.
output : {'ba', 'zpk', 'ss'}, optional
Type of output: numerator/denominator ('ba'), pole-zero ('zpk'), or
state-space ('ss').
Default is 'ba'.
Notes
-----
Due to bilinear transform, this is always 0 dB at fs/2, but it would be
better if the curve fell off symmetrically.
Orfanidis describes a digital filter that more accurately matches the
analog filter, but it is far more complicated.
Orfanidis, Sophocles J., "Digital Parametric Equalizer Design with
Prescribed Nyquist-Frequency Gain"
"""
if Q is None and BW is None:
BW = 1 # octave
if Q is None:
# w0 = Wn
# Q = 1/(2*sinh(ln(2)/2*BW*w0/sin(w0))) # digital filter w BLT
Q = 1/(2*sinh(ln(2)/2*BW)) # analog filter prototype
# TODO: In testing, neither of these is even close to correct near
# fs/2, and the difference between them is very small
if type in ('half'):
A = 10.0**(dBgain/40.0) # for peaking and shelving EQ filters only
Az = A
Ap = A
elif type in ('constantq'):
A = 10.0**(dBgain/20.0)
if dBgain > 0: # boost
Az = A
Ap = 1
else: # cut
Az = 1
Ap = A
else:
raise ValueError('"%s" is not a known peaking type' % type)
# H(s) = (s**2 + s*(Az/Q) + 1) / (s**2 + s/(Ap*Q) + 1)
b = np.array([1, Az/Q, 1])
a = np.array([1, 1/(Ap*Q), 1])
return _transform(b, a, Wn, analog, output)
def shelf(Wn, dBgain, S=1, btype='low', ftype='half', analog=False,
output='ba'):
"""
Design an analog or digital biquad shelving filter with variable slope.
Parameters
----------
Wn : float
Turnover frequency of the filter, defined by the `ftype` parameter.
For digital filters, `Wn` is normalized from 0 to 1, where 1 is the
Nyquist frequency, pi radians/sample. (`Wn` is thus in
half-cycles / sample.)
For analog filters, `Wn` is an angular frequency (e.g. rad/s).
dBgain : float
The gain at the center frequency, in dB. Positive for boost,
negative for cut.
Q : float
Quality factor of the filter. Examples:
* Q fdsafda
ftype : {'half', 'outer', 'inner'}, optional
fpoint?
fdef?
Definition of the filter's turnover frequency
``half``
Wn is defined as the point on the curve at which the
gain in dB is half of the shelf gain, or midway between the
filter's pole and zero. This method is used in
"Cookbook formulae for audio EQ biquad filter coefficients"
``outer``
Wn is defined as the point 3 dB up or down from the shelf's
plateau.
This is symmetrical in dB, so that a boost and cut with identical
parameters sum to unity gain.
This is defined using the location of the outer pole or zero of
the filter (the lower of the two for a low shelf, higher of the
two for a high shelf), so will not be exactly 3 dB at lower shelf
gains. This method is used in ____ hardware audio equalizers.
``inner``
Wn is defined as the point 3 dB up or down from unity gain.
This is symmetrical in dB, so that a boost and cut with identical
parameters sum to unity gain.
btype : {'low', 'high'}, optional
Band type of the filter, low shelf or high shelf.
ftype is the meaning of f, either midpoint of slope, fstop or fturnover
turnover frequency at large boost/cuts, this is 3 dB away from unity gain
stop frequency at large boost/cuts, this is 3 dB away from plateau
tonmeister defines outer as fstop and inner as fturnover
as does http://www.soundonsound.com/sos/dec05/articles/qa1205_3.htm
Understanding Audio defines turnover as outer
as does ems.music.utexas.edu/dwnld/mus329j10/Filter%20Basics.ppt
also calls it knee
R is transition ratio fstop/fturnover. at R=1, fstop = fturnover
If the transition ratio is less than 1, then the filter is a low shelving
filter. If the transition ratio is greater than 1, then the filter is a
high shelving filter.
highShelf:
H(s) = A * (A*s**2 + (sqrt(A)/Q)*s + 1)/( s**2 + (sqrt(A)/Q)*s + A)
lowShelf:
H(s) = A * ( s**2 + (sqrt(A)/Q)*s + A)/(A*s**2 + (sqrt(A)/Q)*s + 1)
2*sqrt(A)*alpha = sin(w0) * sqrt( (A**2 + 1)*(1/S - 1) + 2*A )
is a handy intermediate variable for shelving EQ filters.
The relationship between shelf slope and Q is
1/Q = sqrt((A + 1/A)*(1/S - 1) + 2)
f0 shelf midpoint frequency
_or_ S, a "shelf slope" parameter (for shelving EQ only). When S = 1,
the shelf slope is as steep as it can be and remain monotonically
increasing or decreasing gain with frequency. The shelf slope, in
dB/octave, remains proportional to S for all other values for a
fixed f0/Fs and dBgain.
"""
Q = None # TODO: Maybe this should be a function parameter?
if ftype in ('mid', 'half'):
A = 10.0**(dBgain/40.0) # for peaking and shelving EQ filters only
if Q is None:
Q = 1/sqrt((A + 1/A)*(1/S - 1) + 2)
Az = A
Ap = A
elif ftype in ('outer'):
A = 10.0**(dBgain/20.0)
if Q is None:
Q = 1/sqrt((A + 1/A)*(1/S - 1) + 2)
if dBgain > 0: # boost
Az = A
Ap = 1
else: # cut
Az = 1
Ap = A
elif ftype in ('inner'):
A = 10.0**(dBgain/20.0)
if Q is None:
Q = 1/sqrt((A + 1/A)*(1/S - 1) + 2)
if dBgain > 0: # boost
Az = 1
Ap = A
else: # cut
Az = A
Ap = 1
else:
raise ValueError('"%s" is not a known shelf type' % ftype)
if btype == 'low':
# H(s) = A * ( s**2 + (sqrt(A)/Q)*s + A)/(A*s**2 + (sqrt(A)/Q)*s + 1)
b = Ap * np.array([1, sqrt(Az)/Q, Az])
a = np.array([Ap, sqrt(Ap)/Q, 1])
elif btype == 'high':
# H(s) = A * (A*s**2 + (sqrt(A)/Q)*s + 1)/( s**2 + (sqrt(A)/Q)*s + A)
b = Ap * np.array([Az, sqrt(Az)/Q, 1])
a = np.array([1, sqrt(Ap)/Q, Ap])
else:
raise ValueError('"%s" is not a known shelf type' % btype)
return _transform(b, a, Wn, analog, output)
if __name__ == "__main__":
from scipy.signal import freqs, freqz
from numpy import log10
import matplotlib.pyplot as plt
for ftype in ('half', 'constantq'):
plt.figure()
for boost in range(-24, 0, 3):
b, a = peaking(10, dBgain=boost, Q=sqrt(2), type=ftype,
analog=True)
w, h = freqs(b, a, 10000)
plt.plot(w, 20 * log10(abs(h)), 'r', alpha=0.5)
for boost in range(0, 25, 3):
b, a = peaking(10, dBgain=boost, Q=sqrt(2), type=ftype,
analog=True)
w, h = freqs(b, a, 10000)
plt.plot(w, 20 * log10(abs(h)), 'b', alpha=0.5)
plt.xscale('log')
plt.title(f'Peaking filter, "{ftype}" frequency response')
plt.xlim(0.1, 1000)
plt.xlabel('Frequency [radians / second]')
plt.ylabel('Amplitude [dB]')
plt.yticks(range(-24, 25, 3))
plt.margins(0, 0.1)
plt.grid(True, color='0.7', linestyle='-', which='major', axis='both')
plt.grid(True, color='0.9', linestyle='-', which='minor', axis='both')
plt.show()
MIT License
Copyright (c) 2019 endolith
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@oyvindln
Copy link

Would it be possible to put a foss license on this?

@endolith
Copy link
Author

@endolith
Copy link
Author

endolith commented Mar 14, 2021

I cleaned it up a little, but too many other things I want to work on. (But people commenting on gists gets my attention to them also.) There are a bunch of other examples in the develop branch

@oyvindln
Copy link

Awesome!

@tae-jun
Copy link

tae-jun commented Aug 10, 2022

Hi, could you please let me know where ftype="inner" of shelf() came from?
I'm trying to implement your inner low shelving filter in PyTorch to make it differentiable.
But I don't have coefficients (after prewarping and bilinear transform for digital), so I'm struggling.
It would be very appreciated if you help me!

Thank you :)

@endolith
Copy link
Author

@tae-jun What do you mean by "where it came from"?

@tae-jun
Copy link

tae-jun commented Aug 10, 2022

@endolith Thanks for your quick response!
I meant any resources that explain the theoretical background of the code (ftype="inner") like the RBJ Audio Cookbook (Textbook, blog, etc.)
But if understood correctly, there is no explanation of ftype="inner" in RBJ Cookbook.
So I'm asking where I can check background knowledge to implement ftype="inner"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment