-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbiquad_cookbook.py
More file actions
600 lines (479 loc) · 20.8 KB
/
biquad_cookbook.py
File metadata and controls
600 lines (479 loc) · 20.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
# -*- coding: utf-8 -*-
"""
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
http://www.musicdsp.org/files/Audio-EQ-Cookbook.txt
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?
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.
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, 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 __future__ import division
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):
"""
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, state-space, numer-denom)
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)
elif output in ('sos'):
raise NotImplementedError('second-order sections not yet implemented')
else:
raise ValueError('Unknown output type {0}'.format(output))
def lowpass(Wn, Q=1/sqrt(2), analog=False, output='ba'):
"""
Generic biquad lowpass filter design
Design a 2nd-order analog or digital lowpass filter with variable Q and
return the filter coefficients.
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'):
"""
Generic biquad highpass filter design
Design a 2nd-order analog or digital highpass filter with variable Q and
return the filter coefficients.
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'):
"""
Biquad bandpass filter design
Design a 2nd-order analog or digital bandpass filter with variable Q and
return the filter coefficients.
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'):
"""
Biquad notch filter design
Design a 2nd-order analog or digital notch filter with variable Q and
return the filter coefficients.
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'):
"""
Biquad allpass filter design
Design a 2nd-order analog or digital allpass filter with variable Q and
return the filter coefficients.
Transfer function: H(s) = (s**2 - s/Q + 1) / (s**2 + s/Q + 1)
Wn is center frequency
"""
# 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'):
"""
Biquad peaking filter design
Design a 2nd-order analog or digital peaking filter with variable Q and
return the filter coefficients. Used in graphic or parametric EQs.
Transfer function: H(s) = (s**2 + s*(Az/Q) + 1) / (s**2 + s/(Ap*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).
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'):
"""
Biquad shelving filter design
Design a 2nd-order analog or digital shelving filter with variable slope
and return the filter coefficients.
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
from matplotlib.pyplot import (title, grid, show, plot, xlabel,
ylabel, xscale, figure, yticks, xlim,
margins)
for ftype in ('half', 'constantq'):
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)
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)
plot(w, 20 * log10(abs(h)), 'b', alpha=0.5)
xscale('log')
title(ftype + ' frequency response')
xlim(0.1, 1000)
xlabel('Frequency [radians / second]')
ylabel('Amplitude [dB]')
yticks(range(-24, 25, 3))
margins(0, 0.1)
grid(True, color = '0.7', linestyle='-', which='major', axis='both')
grid(True, color = '0.9', linestyle='-', which='minor', axis='both')
show()