@audio/filter 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +40 -0
- package/index.d.ts +121 -0
- package/index.js +16 -0
- package/package.json +89 -0
- package/readme.md +1164 -0
package/readme.md
ADDED
|
@@ -0,0 +1,1164 @@
|
|
|
1
|
+
# @audio/filter [](https://github.com/audiojs/filter/actions/workflows/ci.yml) [](https://npmjs.org/package/@audio/filter) [](https://github.com/krishnized/license)
|
|
2
|
+
|
|
3
|
+
Canonical audio filter implementations.<br>
|
|
4
|
+
|
|
5
|
+
<table><tr><td valign="top">
|
|
6
|
+
|
|
7
|
+
**[Weighting](#weighting)**<br>
|
|
8
|
+
<sub>[A-weighting](#a-weighting) · [C-weighting](#c-weighting) · [K-weighting](#k-weighting) · [ITU-R 468](#itu-r-468) · [RIAA](#riaa)</sub>
|
|
9
|
+
|
|
10
|
+
**[Auditory](#auditory)**<br>
|
|
11
|
+
<sub>[Gammatone](#gammatone) · [Octave bank](#octave-bank) · [ERB bank](#erb-bank) · [Bark bank](#bark-bank) · [Mel bank](#mel-bank)</sub>
|
|
12
|
+
|
|
13
|
+
**[Analog](#analog)**<br>
|
|
14
|
+
<sub>[Moog ladder](#moog-ladder) · [Diode ladder](#diode-ladder) · [Korg35](#korg35) · [Oberheim](#oberheim)</sub>
|
|
15
|
+
|
|
16
|
+
</td><td valign="top">
|
|
17
|
+
|
|
18
|
+
**[Speech](#speech)**<br>
|
|
19
|
+
<sub>[Formant](#formant) · [Vocoder](#vocoder) · [LPC](#lpc)</sub>
|
|
20
|
+
|
|
21
|
+
**[EQ](#eq)**<br>
|
|
22
|
+
<sub>[Graphic EQ](#graphic-eq) · [Parametric EQ](#parametric-eq) · [Crossover](#crossover) · [Crossfeed](#crossfeed) · [Shelving](#shelving) · [Baxandall](#baxandall) · [Tilt EQ](#tilt-eq)</sub>
|
|
23
|
+
|
|
24
|
+
**[Effect](#effect)**<br>
|
|
25
|
+
<sub>[DC blocker](#dc-blocker) · [Comb](#comb-filter) · [Allpass](#allpass) · [Pre-emphasis](#pre-emphasis--de-emphasis) · [Lowpass](#lowpass) · [Highpass](#highpass) · [Bandpass](#bandpass) · [Notch](#notch) · [Resonator](#resonator) · [Pink noise](#pink-noise) · [Spectral tilt](#spectral-tilt) · [Variable bandwidth](#variable-bandwidth)</sub>
|
|
26
|
+
|
|
27
|
+
</td></tr></table>
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
npm install @audio/filter
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```js
|
|
36
|
+
// import everything
|
|
37
|
+
import * as filter from '@audio/filter'
|
|
38
|
+
|
|
39
|
+
// import by domain
|
|
40
|
+
import { aWeighting, kWeighting } from '@audio/weighting'
|
|
41
|
+
import { gammatone, melBank } from '@audio/auditory'
|
|
42
|
+
import { moogLadder, oberheim } from '@audio/filter'
|
|
43
|
+
import { vocoder, lpcAnalysis } from '@audio/speech'
|
|
44
|
+
import { parametricEq, crossover, baxandall, tilt, lowShelf, highShelf } from '@audio/eq'
|
|
45
|
+
import { dcBlocker, notch, lowpass, highpass, bandpass, resonator } from '@audio/filter'
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
## API
|
|
50
|
+
|
|
51
|
+
Most filters share one shape:
|
|
52
|
+
|
|
53
|
+
```js skip
|
|
54
|
+
filter(buffer, params) // → buffer (modified in-place)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Takes an `Array`/`Float32Array`/`Float64Array`, modifies it in-place, returns it. Pass the same params object on every call to persist state across blocks automatically:
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
import { moogLadder } from '@audio/filter'
|
|
61
|
+
|
|
62
|
+
let params = { fc: 1000, resonance: 0.5, fs: 44100 }
|
|
63
|
+
for (let buf of stream) moogLadder(buf, params)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Three families, by shape:
|
|
67
|
+
|
|
68
|
+
- **In-place, single buffer** — `filter(buffer, params) → buffer`. The majority: weighting, analog, effect, most of EQ.
|
|
69
|
+
- **In-place, dual buffer** — `filter(bufA, bufB, params) → { ... }`. `crossfeed(left, right, params)`, `vocoder(carrier, modulator, params)` — two channels/signals interact, so neither buffer alone is the whole story.
|
|
70
|
+
- **Designers/analyzers** — take no buffer, return descriptor/SOS data instead of processing anything: `octaveBank`, `erbBank`, `barkBank`, `melBank`, `crossover`, `lpcAnalysis`. Feed their output to `digital-filter`'s `filter()` or to `lpcSynthesize`.
|
|
71
|
+
|
|
72
|
+
For frequency analysis, weighting filters expose a `.coefs(fs)` method returning a second-order sections (SOS) array — `[{b0, b1, b2, a1, a2}, ...]`, one biquad per section — for use with `digital-filter`:
|
|
73
|
+
|
|
74
|
+
```js
|
|
75
|
+
import { aWeighting } from '@audio/weighting'
|
|
76
|
+
import { freqz, mag2db } from 'digital-filter'
|
|
77
|
+
|
|
78
|
+
let sos = aWeighting.coefs(44100)
|
|
79
|
+
let resp = freqz(sos, 2048, 44100)
|
|
80
|
+
let db = mag2db(resp.magnitude)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
## Weighting
|
|
85
|
+
|
|
86
|
+
Standard measurement curves. Each is defined by a standards body to a specific curve shape and normalization.
|
|
87
|
+
|
|
88
|
+

|
|
89
|
+
|
|
90
|
+
| filter | standard | normalized |
|
|
91
|
+
|---|---|---|
|
|
92
|
+
| `aWeighting` | IEC 61672-1:2013 | 0 dB at 1 kHz |
|
|
93
|
+
| `cWeighting` | IEC 61672-1:2013 | 0 dB at 1 kHz |
|
|
94
|
+
| `kWeighting` | ITU-R BS.1770-4:2015 | — |
|
|
95
|
+
| `itu468` | ITU-R BS.468-4:1986 | +12.2 dB at 6.3 kHz |
|
|
96
|
+
| `riaa` | RIAA 1954 | 0 dB at 1 kHz |
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
### A-weighting
|
|
100
|
+
|
|
101
|
+
Models how the ear perceives loudness — attenuates low and very high frequencies.
|
|
102
|
+
|
|
103
|
+
**Transfer function**: $H(s) = \frac{Ks^4}{(s+\omega_1)^2(s+\omega_2)(s+\omega_3)(s+\omega_4)^2}$<br>
|
|
104
|
+
**Poles**: $\omega_1 = 2\pi \cdot 20.6\,\text{Hz}$, $\omega_2 = 2\pi \cdot 107.7\,\text{Hz}$, $\omega_3 = 2\pi \cdot 737.9\,\text{Hz}$, $\omega_4 = 2\pi \cdot 12194\,\text{Hz}$<br>
|
|
105
|
+
**Implementation**: matched z-transform ($z_k = e^{s_k/f_s}$), 3 SOS sections — no frequency warping near Nyquist<br>
|
|
106
|
+
**Normalization**: 0 dB at 1 kHz (IEC requirement)
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
import { aWeighting } from '@audio/weighting'
|
|
110
|
+
|
|
111
|
+
let p = { fs: 44100 }
|
|
112
|
+
for (let buf of stream) aWeighting(buf, p) // A-weighted stream
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Standard**: IEC 61672-1:2013[^1]<br>
|
|
116
|
+
**Use when**: measuring SPL, noise, OSHA compliance, audio quality<br>
|
|
117
|
+
**Not for**: loudness in broadcast (use K-weighting), noise annoyance (use ITU-468)
|
|
118
|
+
|
|
119
|
+

|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
### C-weighting
|
|
123
|
+
|
|
124
|
+
Like A-weighting but flatter — less rolloff at low and high frequencies.
|
|
125
|
+
|
|
126
|
+
**Transfer function**: $H(s) = \frac{Ks^2}{(s+\omega_1)^2(s+\omega_4)^2}$<br>
|
|
127
|
+
**Poles**: $\omega_1 = 2\pi \cdot 20.6\,\text{Hz}$, $\omega_4 = 2\pi \cdot 12194\,\text{Hz}$ (same as A-weighting outer poles)<br>
|
|
128
|
+
**Implementation**: matched z-transform, 2 SOS sections
|
|
129
|
+
|
|
130
|
+
```js
|
|
131
|
+
import { cWeighting } from '@audio/weighting'
|
|
132
|
+
|
|
133
|
+
cWeighting(buffer, { fs: 44100 })
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Standard**: IEC 61672-1:2013[^1]<br>
|
|
137
|
+
**Use when**: peak sound level measurement, where A-weighting over-penalizes bass<br>
|
|
138
|
+
**Compared to A**: rolls off below 31.5 Hz and above 8 kHz; flat 31.5 Hz–8 kHz
|
|
139
|
+
|
|
140
|
+

|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
### K-weighting
|
|
144
|
+
|
|
145
|
+
The loudness measurement curve — a high shelf plus a highpass. Used to compute LUFS.
|
|
146
|
+
|
|
147
|
+
**Stage 1**: pre-filter — high shelf +4 dB above ~1.7 kHz (head diffraction simulation)<br>
|
|
148
|
+
**Stage 2**: RLB highpass — 2nd-order Butterworth at ~38 Hz (removes sub-bass)<br>
|
|
149
|
+
**Coefficients**: one fs-general analytic formula (BS.1770 Annex 1 analog prototype, pre-warped per `fs`) — reproduces the spec's published 48 kHz table to ~1e-11 and stays exact by construction at any other sample rate, not a lesser approximation elsewhere
|
|
150
|
+
|
|
151
|
+
```js
|
|
152
|
+
import { kWeighting } from '@audio/weighting'
|
|
153
|
+
|
|
154
|
+
kWeighting(buffer, { fs: 48000 }) // BS.1770 spec sample rate
|
|
155
|
+
kWeighting(buffer, { fs: 44100 }) // same formula, exact by construction
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**Standard**: ITU-R BS.1770-4:2015[^2], EBU R128<br>
|
|
159
|
+
**Use when**: computing integrated loudness (LUFS/LKFS), broadcast loudness normalization<br>
|
|
160
|
+
**Not for**: A-weighted SPL measurement (different shape, different standard)
|
|
161
|
+
|
|
162
|
+

|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
### ITU-R 468
|
|
166
|
+
|
|
167
|
+
Peaked noise weighting — peaks at +12.2 dB near 6.3 kHz — models how humans actually perceive noise annoyance.
|
|
168
|
+
|
|
169
|
+
**Shape**: rises steeply from 31.5 Hz, peaks at +12.2 dB at 6.3 kHz, rolls off above 10 kHz<br>
|
|
170
|
+
**Implementation**: exact matched z-transform of the analog BS.468-4 rational realization (pre-verified poles, 0.05 dB analog accuracy); discretization adds error near Nyquist at low sample rates (e.g. ~16 dB at 20 kHz at 44.1 kHz) — see `test/weighting.js` for the measured per-sample-rate tolerances
|
|
171
|
+
|
|
172
|
+
```js
|
|
173
|
+
import { itu468 } from '@audio/weighting'
|
|
174
|
+
|
|
175
|
+
itu468(buffer, { fs: 48000 })
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Standard**: ITU-R BS.468-4:1986[^3] (original CCIR 468, 1968)<br>
|
|
179
|
+
**Rationale**: human hearing is more sensitive to short noise bursts than sine tones; 468 weights accordingly<br>
|
|
180
|
+
**Use when**: measuring noise in broadcast equipment, tape noise, hum and hiss<br>
|
|
181
|
+
**Compared to A-weighting**: 6.3 kHz peak makes it harsher on hiss; preferred in European broadcast
|
|
182
|
+
|
|
183
|
+

|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
### RIAA
|
|
187
|
+
|
|
188
|
+
Playback equalization for vinyl records — a shelving curve with three time constants.
|
|
189
|
+
|
|
190
|
+
**Transfer function**: $H(s) = \frac{1 + sT_2}{(1 + sT_1)(1 + sT_3)}$<br>
|
|
191
|
+
**Time constants**: $T_1 = 3180\,\mu\text{s}$ (50.05 Hz pole), $T_2 = 318\,\mu\text{s}$ (500.5 Hz zero), $T_3 = 75\,\mu\text{s}$ (2122 Hz pole)<br>
|
|
192
|
+
**Implementation**: 1 SOS section via bilinear transform, normalized 0 dB at 1 kHz
|
|
193
|
+
|
|
194
|
+
```js
|
|
195
|
+
import { riaa } from '@audio/weighting'
|
|
196
|
+
|
|
197
|
+
riaa(phonoSignal, { fs: 44100 }) // correct vinyl playback
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**Standard**: RIAA (1954)[^4]<br>
|
|
201
|
+
**Purpose**: playback de-emphasis undoes the mastering pre-emphasis applied during vinyl cutting<br>
|
|
202
|
+
**Shape**: boosts bass ~+20 dB at 20 Hz, rolls off treble; at playback restores flat response<br>
|
|
203
|
+
**Note**: classic 3-time-constant RIAA curve ($T_1$/$T_2$/$T_3$ above); IEC 60098 adds a 4th, ~7950 µs (~20 Hz) subsonic time constant that this implementation deliberately omits
|
|
204
|
+
|
|
205
|
+

|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
## Auditory
|
|
209
|
+
|
|
210
|
+
Models of the human auditory system — how the cochlea and brain decompose sound into frequency channels. Used in psychoacoustics, music information retrieval, and hearing aid design.
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
### Gammatone
|
|
214
|
+
|
|
215
|
+
The cochlear filter — bandpass tuned to one frequency, decaying oscillation, mimics an inner hair cell.
|
|
216
|
+
|
|
217
|
+
**Model**: cascade of complex one-pole filters; 4th-order is the standard cochlear approximation<br>
|
|
218
|
+
**Bandwidth**: $\text{ERB} = 24.7\left(\frac{4.37 f_c}{1000} + 1\right)\,\text{Hz}$<br>
|
|
219
|
+
**Implementation**: complex resonator with gain normalization to 0 dB at $f_c$
|
|
220
|
+
|
|
221
|
+
```js
|
|
222
|
+
import { gammatone } from '@audio/auditory'
|
|
223
|
+
|
|
224
|
+
let params = { fc: 1000, fs: 44100 }
|
|
225
|
+
gammatone(buffer, params) // bandpass at 1 kHz with cochlear envelope
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
**Origin**: Patterson et al. (1992)[^5]<br>
|
|
229
|
+
**Use when**: cochlear modeling, auditory scene analysis, psychoacoustic feature extraction<br>
|
|
230
|
+
**Compared to Butterworth bandpass**: gammatone has asymmetric temporal envelope matching biological data
|
|
231
|
+
|
|
232
|
+

|
|
233
|
+
|
|
234
|
+
Reuse `params` across blocks — state in `params._s`, gain cached in `params._gain`.
|
|
235
|
+
|
|
236
|
+

|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
### Octave bank
|
|
240
|
+
|
|
241
|
+
ISO/IEC fractional-octave filter bank — the standard for acoustic measurement and spectrum analysis.
|
|
242
|
+
|
|
243
|
+
**Center frequencies**: ISO 266 series — $f_c = 1000 \cdot G^{k/n}$, $G = 10^{3/10}$<br>
|
|
244
|
+
**Bandwidth**: each band spans $f_c \cdot G^{-1/(2n)}$ to $f_c \cdot G^{+1/(2n)}$<br>
|
|
245
|
+
**1/1 octave**: 10 bands (31.5–16 kHz) — coarse; **1/3 octave**: 28 bands (default fmin/fmax 31.25/16000 Hz) — standard; **1/6+**: psychoacoustics<br>
|
|
246
|
+
**Returns**: array of `{ fc, coefs }` — each band is a biquad bandpass section
|
|
247
|
+
|
|
248
|
+
```js
|
|
249
|
+
import { octaveBank } from '@audio/auditory'
|
|
250
|
+
import { filter } from 'digital-filter'
|
|
251
|
+
|
|
252
|
+
let bands = octaveBank(3, 44100) // 1/3-octave, 28 bands
|
|
253
|
+
for (let band of bands) {
|
|
254
|
+
let buf = Float64Array.from(signal)
|
|
255
|
+
filter(buf, { coefs: band.coefs })
|
|
256
|
+
spectrum.push({ fc: band.fc, energy: rms(buf) })
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
**Standard**: IEC 61260-1:2014[^6], ANSI S1.11:2004<br>
|
|
261
|
+
**Use when**: acoustic measurement, noise assessment, spectrum visualization
|
|
262
|
+
|
|
263
|
+

|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
### ERB bank
|
|
267
|
+
|
|
268
|
+
Equivalent Rectangular Bandwidth scale — how the auditory system actually spaces its channels.
|
|
269
|
+
|
|
270
|
+
**ERB formula**: $\text{ERB}(f_c) = 24.7\left(\frac{4.37 f_c}{1000} + 1\right)$<br>
|
|
271
|
+
**Spacing**: ~1 ERB between adjacent channels — logarithmic above 1 kHz, more linear below<br>
|
|
272
|
+
**Returns**: array of `{ fc, erb }` descriptors; apply `gammatone` at each `fc` for the filter bank
|
|
273
|
+
|
|
274
|
+
```js
|
|
275
|
+
import { erbBank, gammatone } from '@audio/auditory'
|
|
276
|
+
|
|
277
|
+
let bands = erbBank(44100)
|
|
278
|
+
let states = bands.map(b => ({ fc: b.fc, fs: 44100 }))
|
|
279
|
+
|
|
280
|
+
for (let buf of stream) {
|
|
281
|
+
let channels = bands.map((_, i) => {
|
|
282
|
+
let b = Float64Array.from(buf)
|
|
283
|
+
gammatone(b, states[i])
|
|
284
|
+
return b
|
|
285
|
+
})
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**Origin**: Moore & Glasberg (1983, 1990)[^7]<br>
|
|
290
|
+
**Use when**: speech processing, hearing models, auditory feature extraction<br>
|
|
291
|
+
**Compared to Bark**: ERB is more accurate above 500 Hz; Bark is the psychoacoustic masking model
|
|
292
|
+
|
|
293
|
+

|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
### Bark bank
|
|
297
|
+
|
|
298
|
+
Zwicker's 24 critical bands — the psychoacoustic foundation of perceptual audio coding.
|
|
299
|
+
|
|
300
|
+
**Scale**: 24 bands spanning 20 Hz–15.5 kHz (Zwicker's table starts at 0 Hz; this implementation keeps a practical 20 Hz first edge, honoring a caller-supplied `fmin` below 20 Hz as the first edge instead of clamping it); named after Heinrich Barkhausen<br>
|
|
301
|
+
**Band widths**: ~100 Hz wide below 500 Hz; ~20% of center frequency above<br>
|
|
302
|
+
**Returns**: array of `{ bark, fLow, fHigh, fc, coefs }` — each band is a biquad bandpass section
|
|
303
|
+
|
|
304
|
+
```js
|
|
305
|
+
import { barkBank } from '@audio/auditory'
|
|
306
|
+
import { filter } from 'digital-filter'
|
|
307
|
+
|
|
308
|
+
let bands = barkBank(44100) // 24 critical bands
|
|
309
|
+
for (let band of bands) {
|
|
310
|
+
let buf = Float64Array.from(signal)
|
|
311
|
+
filter(buf, { coefs: band.coefs })
|
|
312
|
+
excitation[band.bark] = rms(buf)
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
**Origin**: Zwicker (1961)[^8]<br>
|
|
317
|
+
**Use when**: perceptual audio coding (MP3/AAC use Bark-like groupings), loudness models, masking<br>
|
|
318
|
+
**Compared to ERB**: Bark bands are wider and fewer; ERB is more accurate for hearing science
|
|
319
|
+
|
|
320
|
+

|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
### Mel bank
|
|
324
|
+
|
|
325
|
+
Mel-frequency triangular filter bank — the standard front-end for speech recognition and music information retrieval.
|
|
326
|
+
|
|
327
|
+
**Scale**: $\text{mel}(f) = 2595 \log_{10}(1 + f/700)$ (O'Shaughnessy variant)[^18]<br>
|
|
328
|
+
**Bands**: equally spaced in mel scale; each band is a triangle spanning 3 adjacent mel points<br>
|
|
329
|
+
**Returns**: array of `{ fc, fLow, fHigh, mel }` — band descriptors for MFCC computation
|
|
330
|
+
|
|
331
|
+
```js
|
|
332
|
+
import { melBank } from '@audio/auditory'
|
|
333
|
+
|
|
334
|
+
let bands = melBank(44100) // 26 bands (default)
|
|
335
|
+
let telephonyBands = melBank(16000, { nFilters: 40 }) // 40 bands, telephony rate
|
|
336
|
+
let voiceBands = melBank(44100, { fmin: 300, fmax: 8000 })
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
**Use when**: MFCC feature extraction, speech recognition, music genre classification, audio fingerprinting<br>
|
|
340
|
+
**Compared to ERB/Bark**: mel is the most widely used in ML; ERB is more physiologically accurate
|
|
341
|
+
|
|
342
|
+

|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
## Analog
|
|
346
|
+
|
|
347
|
+
Discrete-time models of analog circuits — each named after the hardware it replicates. Nonlinear, stateful, process in-place. The filters in synthesizers.
|
|
348
|
+
|
|
349
|
+
**Resonance means something different per filter** — all four take `resonance` in 0–1, but the mapping and self-oscillation point differ: `moogLadder` maps $k = 4 \cdot \text{resonance}$, self-oscillates exactly at resonance=1 (Stilson & Smith 1996, <0.5% frequency error); `diodeLadder` uses the same $k = 4 \cdot \text{resonance}$ but self-oscillation shifts to ≈1.15–1.2 because its extra per-stage $\tanh$ adds damping; `korg35` maps $k = 2 \cdot \text{resonance}$ and never self-oscillates at any value — 2 real poles can't reach the −180° loop phase Barkhausen's criterion needs at any finite, audible frequency, so resonance only adds damping/saturation character; `oberheim` maps $R = 1 - \text{resonance}$ (inverted — resonance=1 gives $R=0$, maximum SVF resonance), with bandpass peak gain exactly $1/(2R)$.<br>
|
|
350
|
+
**`drive`** (default 1, all four filters) is input gain into each filter's $\tanh$ saturation path — `drive: 0` mutes the signal ($\tanh(0) = 0$) rather than disabling saturation and falling back to a linear filter.
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
### Moog ladder
|
|
354
|
+
|
|
355
|
+
Robert Moog's 4-pole transistor ladder, 1965 — the most imitated filter in electronic music.
|
|
356
|
+
|
|
357
|
+
**Circuit**: 4 cascaded one-pole transistor ladder sections, global feedback from output to input<br>
|
|
358
|
+
**Implementation**: Zero-delay feedback (ZDF) via trapezoidal integration — Zavalishin (2012)[^9], Ch. 6<br>
|
|
359
|
+
**Response**: $-24\,\text{dB/oct}$ lowpass; resonance peak at $f_c$; self-oscillation (sine wave) at resonance=1<br>
|
|
360
|
+
**Nonlinearity**: $\tanh$ saturation at input (transistor ladder characteristic)
|
|
361
|
+
|
|
362
|
+
```js
|
|
363
|
+
import { moogLadder } from '@audio/filter'
|
|
364
|
+
|
|
365
|
+
let params = { fc: 800, resonance: 0.7, fs: 44100 }
|
|
366
|
+
moogLadder(buffer, params)
|
|
367
|
+
|
|
368
|
+
// Self-oscillation — runs indefinitely from a single impulse
|
|
369
|
+
let silent = new Float64Array(4096); silent[0] = 0.01
|
|
370
|
+
moogLadder(silent, { fc: 1000, resonance: 1, fs: 44100 })
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
**Patent**: Moog (1965) US3475623[^10]<br>
|
|
374
|
+
**vs Diode ladder**: Moog saturates only at input; diode saturates at each stage — different character at high resonance
|
|
375
|
+
|
|
376
|
+

|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
### Diode ladder
|
|
380
|
+
|
|
381
|
+
Roland TB-303 / EMS VCS3 style — per-stage saturation gives the characteristic acid "squelch".
|
|
382
|
+
|
|
383
|
+
**Circuit**: Roland TB-303, EMS VCS3, EDP Wasp<br>
|
|
384
|
+
**Key difference from Moog**: stages are bidirectionally coupled — no unity-gain buffers between them (unlike Moog), so each stage loads its neighbors; solved as one tridiagonal system per sample (Thomas algorithm) instead of Moog's simple forward cascade<br>
|
|
385
|
+
**Character**: preserves more bass at high resonance than Moog — closed-form DC gain $1/(1 + 4\cdot\text{resonance})$ vs Moog's $1/(1 + 4\cdot\text{resonance}\cdot(1+G+G^2+G^3+G^4))$, $G<1$ — and more "squelchy"/aggressive due to the per-stage $\tanh$<br>
|
|
386
|
+
**Implementation**: ZDF — Zavalishin (2012)[^9]; Pirkle (2019)[^11], Ch. 10<br>
|
|
387
|
+
**Stability**: bounded (per-stage $\tanh$-saturated) across the documented 0–1 range and well beyond; self-oscillates near resonance≈1.15–1.2 — higher than Moog's exact 1, since the extra per-stage $\tanh$ adds damping
|
|
388
|
+
|
|
389
|
+
```js
|
|
390
|
+
import { diodeLadder } from '@audio/filter'
|
|
391
|
+
|
|
392
|
+
let params = { fc: 500, resonance: 0.8, fs: 44100 }
|
|
393
|
+
diodeLadder(buffer, params)
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+

|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
### Korg35
|
|
400
|
+
|
|
401
|
+
Korg MS-10/MS-20, 1978 — 2-pole filter with lowpass and highpass outputs from nonlinear feedback.
|
|
402
|
+
|
|
403
|
+
**Topology**: 2 cascaded one-pole sections with nonlinear feedback; HP = input − LP<br>
|
|
404
|
+
**Response**: $-12\,\text{dB/oct}$; resonance 0–1 adds damping/saturation character — no resonant peak, no self-oscillation at any setting (2 real poles in the loop can't reach the −180° phase Barkhausen's criterion needs at any finite, audible frequency, unlike Moog/Diode's 4-pole loops)<br>
|
|
405
|
+
**Complementarity**: LP+HP=input holds exactly only at resonance=0 (verified to 1e-16); at resonance>0 the HP tap's extra feedback term makes LP+HP diverge from the input (measured −7.5 dB to +1.4 dB error at resonance=0.8 across 200 Hz–5 kHz)
|
|
406
|
+
|
|
407
|
+
```js
|
|
408
|
+
import { korg35 } from '@audio/filter'
|
|
409
|
+
|
|
410
|
+
korg35(buffer, { fc: 1000, resonance: 0.5, type: 'lowpass', fs: 44100 })
|
|
411
|
+
korg35(buffer, { fc: 1000, resonance: 0.5, type: 'highpass', fs: 44100 })
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
**Circuit**: Korg MS-10/MS-20 (1978)<br>
|
|
415
|
+
**Analysis**: Stilson & Smith (1996)[^12]; Zavalishin (2012)[^9], Ch. 5<br>
|
|
416
|
+
**vs Moog ladder**: 2-pole ($-12\,\text{dB/oct}$) vs 4-pole ($-24\,\text{dB/oct}$); Korg35 has both LP and HP taps from one circuit, but no self-oscillation
|
|
417
|
+
|
|
418
|
+

|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
### Oberheim
|
|
422
|
+
|
|
423
|
+
Oberheim SEM (1974) — 2-pole state-variable filter with four modes from one circuit.
|
|
424
|
+
|
|
425
|
+
**Topology**: 2 trapezoidal integrators with nonlinear feedback; multimode output (LP/HP/BP/notch)<br>
|
|
426
|
+
**Response**: $-12\,\text{dB/oct}$; warm, musical resonance; continuous mode morphing<br>
|
|
427
|
+
**Implementation**: ZDF — Zavalishin (2012)[^9], Ch. 4–5; $\tanh$ saturation on integrator states
|
|
428
|
+
|
|
429
|
+
```js
|
|
430
|
+
import { oberheim } from '@audio/filter'
|
|
431
|
+
|
|
432
|
+
oberheim(buffer, { fc: 1000, resonance: 0.5, type: 'lowpass', fs: 44100 })
|
|
433
|
+
oberheim(buffer, { fc: 1000, resonance: 0.5, type: 'highpass', fs: 44100 })
|
|
434
|
+
oberheim(buffer, { fc: 1000, resonance: 0.5, type: 'bandpass', fs: 44100 })
|
|
435
|
+
oberheim(buffer, { fc: 1000, resonance: 0.5, type: 'notch', fs: 44100 })
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
**Circuit**: Oberheim SEM (1974), Two Voice, Four Voice, Eight Voice<br>
|
|
439
|
+
**vs Moog/Korg**: 2-pole like Korg35 but true state-variable topology; LP/HP/BP/notch from one circuit; warmer resonance character
|
|
440
|
+
|
|
441
|
+

|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
## Speech
|
|
445
|
+
|
|
446
|
+
Filters that model or process the human vocal tract — from vowel synthesis to spectral voice coding.
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
### Formant
|
|
450
|
+
|
|
451
|
+
Parallel resonator bank — each peak models one vocal tract resonance (formant).
|
|
452
|
+
|
|
453
|
+
**Model**: parallel combination of second-order resonators, each modeling one vocal tract mode<br>
|
|
454
|
+
**Formant frequencies**: determined by vocal tract shape; F1 controls vowel openness, F2 controls front/back<br>
|
|
455
|
+
**Typical ranges**: F1: 250–850 Hz, F2: 850–2500 Hz, F3: 1700–3500 Hz<br>
|
|
456
|
+
**Implementation**: uses `resonator` internally — constant peak-gain bandpass per formant<br>
|
|
457
|
+
**Defaults**: F1=730 Hz, F2=1090 Hz, F3=2440 Hz (open vowel /a/)
|
|
458
|
+
|
|
459
|
+
```js
|
|
460
|
+
import { formant } from '@audio/speech'
|
|
461
|
+
|
|
462
|
+
formant(excitation, { fs: 44100 }) // vowel /a/ (default)
|
|
463
|
+
|
|
464
|
+
formant(excitation, {
|
|
465
|
+
formants: [{ fc: 270, bw: 60, gain: 1 }, { fc: 2290, bw: 90, gain: 0.5 }],
|
|
466
|
+
fs: 44100
|
|
467
|
+
}) // vowel /i/
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
**Use when**: speech synthesis, singing synthesis, vocal effects, acoustic phonetics<br>
|
|
471
|
+
**Not a substitute for**: LPC synthesis, which estimates formants automatically from a speech signal
|
|
472
|
+
|
|
473
|
+

|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
### Vocoder
|
|
477
|
+
|
|
478
|
+
Channel vocoder — transfers the spectral envelope of one sound onto the pitched content of another.
|
|
479
|
+
|
|
480
|
+
Note: takes two separate buffers, returns a new buffer (does not modify in-place).
|
|
481
|
+
|
|
482
|
+
**Principle**: analyze modulator into N bands → extract envelope per band → multiply with filtered carrier → sum<br>
|
|
483
|
+
**Implementation**: N parallel bandpass filters on both signals; envelope follower per modulator band<br>
|
|
484
|
+
**Band count**: 8 = robotic effect; 16 = classic vocoder sound; 32+ = more speech intelligibility
|
|
485
|
+
|
|
486
|
+
```js
|
|
487
|
+
import { vocoder } from '@audio/speech'
|
|
488
|
+
|
|
489
|
+
// carrier: pitched source (sawtooth, buzz, noise...)
|
|
490
|
+
// modulator: signal whose spectral shape to impose (voice, instrument...)
|
|
491
|
+
let output = vocoder(carrier, modulator, { bands: 16, fs: 44100 })
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
**Inventor**: Dudley (1939)[^13], Bell Labs<br>
|
|
495
|
+
**Use when**: voice effects, talkbox simulation, cross-synthesis, spectral morphing
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
### LPC
|
|
499
|
+
|
|
500
|
+
Linear Predictive Coding — estimates the vocal tract transfer function from a speech signal.
|
|
501
|
+
|
|
502
|
+
**Analysis**: autocorrelation method + Levinson-Durbin recursion → LPC coefficients + residual<br>
|
|
503
|
+
**Synthesis**: all-pole filter reconstructs signal from residual excitation<br>
|
|
504
|
+
**Round-trip**: `lpcAnalysis` → `lpcSynthesize` recovers the original signal exactly
|
|
505
|
+
|
|
506
|
+
```js
|
|
507
|
+
import { lpcAnalysis, lpcSynthesize } from '@audio/speech'
|
|
508
|
+
|
|
509
|
+
// Analysis: extract vocal tract model
|
|
510
|
+
let { coefs, gain, residual } = lpcAnalysis(speechFrame, { order: 12 })
|
|
511
|
+
|
|
512
|
+
// Synthesis: reconstruct from residual
|
|
513
|
+
lpcSynthesize(residual, { coefs, gain }) // residual → reconstructed speech
|
|
514
|
+
|
|
515
|
+
// Modify pitch: replace residual with different excitation
|
|
516
|
+
let buzz = generatePulseTrainAtNewPitch()
|
|
517
|
+
lpcSynthesize(buzz, { coefs, gain }) // speech at new pitch
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
**Origin**: Atal & Hanauer (1971)[^19]; foundation of CELP, GSM, and modern speech codecs<br>
|
|
521
|
+
**Use when**: speech coding, pitch modification, voice conversion, formant estimation, speech analysis
|
|
522
|
+
|
|
523
|
+

|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
## EQ
|
|
527
|
+
|
|
528
|
+
Equalization and frequency routing — from parametric studio EQ to speaker crossover networks.
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
### Graphic EQ
|
|
532
|
+
|
|
533
|
+
10-band ISO octave equalizer — fixed center frequencies, gain per band.
|
|
534
|
+
|
|
535
|
+
**Implementation**: cascaded (serial) biquad peaking filters, one per band, applied in sequence — the cascade's dB result coincides with a naive additive prediction since magnitude responses of a cascade multiply<br>
|
|
536
|
+
**Band spacing**: 1-octave intervals, ISO 266 nominal series<br>
|
|
537
|
+
**Bands**: 31.5, 63, 125, 250, 500, 1000, 2000, 4000, 8000, 16000 Hz
|
|
538
|
+
|
|
539
|
+
```js
|
|
540
|
+
import { graphicEq } from '@audio/eq'
|
|
541
|
+
|
|
542
|
+
graphicEq(buffer, {
|
|
543
|
+
gains: { 125: -3, 1000: +6, 8000: +2 },
|
|
544
|
+
fs: 44100
|
|
545
|
+
})
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
**Standard**: ISO 266:1997 / IEC 61260-1 nominal center frequencies<br>
|
|
549
|
+
**Use when**: quick tonal shaping, DJ mixers, consumer audio, live sound<br>
|
|
550
|
+
**vs Parametric EQ**: fixed centers but simpler — no per-band frequency or Q control
|
|
551
|
+
|
|
552
|
+

|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
### Parametric EQ
|
|
556
|
+
|
|
557
|
+
N-band EQ with fully adjustable frequency, Q, and gain per band.
|
|
558
|
+
|
|
559
|
+
**Implementation**: cascaded biquad sections — one per band; `peak` uses peaking EQ biquad, shelves use Zölzer shelf design[^16]<br>
|
|
560
|
+
**Band types**: `peak` (bell curve at $f_c$, Q defaults to 1), `lowshelf`/`highshelf` (boost/cut below/above $f_c$, Q defaults to 0.707 — matching standalone `lowShelf`/`highShelf`)<br>
|
|
561
|
+
**Reuse**: filters rebuild automatically whenever any band's `fc`/`Q`/`gain`/`type` or `fs` changes on a reused `params` object — mutate `params.bands` in place, no dirty flag needed
|
|
562
|
+
|
|
563
|
+
```js
|
|
564
|
+
import { parametricEq } from '@audio/eq'
|
|
565
|
+
|
|
566
|
+
parametricEq(buffer, {
|
|
567
|
+
bands: [
|
|
568
|
+
{ fc: 80, Q: 0.7, gain: +4, type: 'lowshelf' },
|
|
569
|
+
{ fc: 1000, Q: 2.0, gain: -3, type: 'peak' },
|
|
570
|
+
{ fc: 8000, Q: 0.7, gain: +2, type: 'highshelf' },
|
|
571
|
+
],
|
|
572
|
+
fs: 44100
|
|
573
|
+
})
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
**Use when**: studio mixing, mastering, precise tonal correction<br>
|
|
577
|
+
**vs Graphic EQ**: fully adjustable $f_c$, Q, and gain per band; no fixed centers
|
|
578
|
+
|
|
579
|
+

|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
### Crossover
|
|
583
|
+
|
|
584
|
+
Linkwitz-Riley crossover network — splits audio into N frequency bands with flat magnitude sum.
|
|
585
|
+
|
|
586
|
+
**Filter type**: cascade of two Butterworth filters of half the specified order<br>
|
|
587
|
+
**Property**: bands sum to flat magnitude response with correct phase alignment for every order — for order ≡ 0 (mod 4) (LR4, LR8, ...) the bands already sum flat; for order ≡ 2 (mod 4) (LR2, LR6, ...) `crossover()` internally inverts the polarity of alternate bands' numerator coefficients so the sum is flat by construction (Linkwitz & Riley 1976)<br>
|
|
588
|
+
**Orders**: LR2 ($-12\,\text{dB/oct}$), LR4 ($-24\,\text{dB/oct}$, most common), LR8 ($-48\,\text{dB/oct}$)<br>
|
|
589
|
+
**Returns**: `SOS[]` — one SOS per band
|
|
590
|
+
|
|
591
|
+
```js
|
|
592
|
+
import { crossover } from '@audio/eq'
|
|
593
|
+
import { filter } from 'digital-filter'
|
|
594
|
+
|
|
595
|
+
let bands = crossover([500, 5000], 4, 44100) // 3 bands: lo / mid / hi
|
|
596
|
+
|
|
597
|
+
let lo = Float64Array.from(buffer); filter(lo, { coefs: bands[0] })
|
|
598
|
+
let mid = Float64Array.from(buffer); filter(mid, { coefs: bands[1] })
|
|
599
|
+
let hi = Float64Array.from(buffer); filter(hi, { coefs: bands[2] })
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
**Designers**: Linkwitz & Riley (1976)[^14]<br>
|
|
603
|
+
**Use when**: speaker system design, multi-band dynamics, band splitting for separate processing
|
|
604
|
+
|
|
605
|
+

|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
### Crossfeed
|
|
609
|
+
|
|
610
|
+
Headphone crossfeed — mixes a filtered copy of each channel into the other to reduce in-head localization.
|
|
611
|
+
|
|
612
|
+
Takes two separate channel buffers, modifies both in-place.
|
|
613
|
+
|
|
614
|
+
**Problem**: speaker playback has inter-channel crosstalk and head shadowing; headphones remove these, causing an unnatural "in-head" stereo image<br>
|
|
615
|
+
**Solution**: mix each channel as `direct·(1 − level/2) + cross·(level/2)` — direct and cross sum to unity, so mono/correlated content stays at ~unity gain across the documented level range (Bauer 1961 / BS2B lineage)<br>
|
|
616
|
+
**fc**: models the head-shadow lowpass (~700 Hz is typical); **level**: 0.3 = mild, 0.5 = strong<br>
|
|
617
|
+
**Returns**: `{ left, right }` (both also modified in-place)
|
|
618
|
+
|
|
619
|
+
```js
|
|
620
|
+
import { crossfeed } from '@audio/eq'
|
|
621
|
+
|
|
622
|
+
crossfeed(left, right, { fc: 700, level: 0.3, fs: 44100 })
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
**Origin**: Bauer (1961)[^15]; BS2B (Bauer Stereophonic-to-Binaural) algorithm
|
|
626
|
+
|
|
627
|
+

|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
### Shelving
|
|
631
|
+
|
|
632
|
+
Standalone low-shelf and high-shelf filters — boost or cut below/above a corner frequency.
|
|
633
|
+
|
|
634
|
+
**Low shelf**: $H(s) = A \cdot \frac{s/\omega_c + \sqrt{A}}{s/(\omega_c\sqrt{A}) + 1}$ — RBJ biquad shelf design<br>
|
|
635
|
+
**High shelf**: same topology, mirrored in frequency<br>
|
|
636
|
+
**Q / slope**: $Q = 0.707$ gives maximally-flat transition; lower Q gives a gentler, wider slope
|
|
637
|
+
|
|
638
|
+
```js
|
|
639
|
+
import { lowShelf, highShelf } from '@audio/eq'
|
|
640
|
+
|
|
641
|
+
lowShelf(buffer, { fc: 200, gain: +6, Q: 0.707, fs: 44100 }) // bass boost
|
|
642
|
+
highShelf(buffer, { fc: 4000, gain: -3, Q: 0.707, fs: 44100 }) // treble cut
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
**Defaults**: `lowShelf` fc defaults to 200 Hz; `highShelf` fc defaults to 4000 Hz (no shared default — each function's own)<br>
|
|
646
|
+
**Use when**: correcting speaker/room low-end buildup, air-band top-end addition, mastering bus<br>
|
|
647
|
+
**vs Parametric EQ**: shelf is a single-band operation with a cleaner API — use when you don't need bell curves
|
|
648
|
+
|
|
649
|
+
 
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
### Baxandall
|
|
653
|
+
|
|
654
|
+
Bass/treble tone control — the canonical two-knob EQ in amplifiers, mixers, and guitar pedals since 1952.
|
|
655
|
+
|
|
656
|
+
**Bass**: low shelf around `fBass` (default 250 Hz)<br>
|
|
657
|
+
**Treble**: high shelf around `fTreble` (default 4 kHz)<br>
|
|
658
|
+
**Independence**: bass and treble controls are cascaded, not interactive — each shelf is independent
|
|
659
|
+
|
|
660
|
+
```js
|
|
661
|
+
import { baxandall } from '@audio/eq'
|
|
662
|
+
|
|
663
|
+
baxandall(buffer, { bass: +6, treble: -3, fs: 44100 }) // default pivot freqs
|
|
664
|
+
baxandall(buffer, { bass: +4, treble: +2, fBass: 300, fTreble: 6000, fs: 44100 }) // custom pivots
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
**Origin**: Peter Baxandall (1952)[^20]<br>
|
|
668
|
+
**Use when**: amp/mixer tone stack simulation, consumer audio tone controls, guitar pedal EQ<br>
|
|
669
|
+
**vs Parametric EQ**: intentionally limited to two knobs — the constraint is the point
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
### Tilt EQ
|
|
673
|
+
|
|
674
|
+
See-saw around a pivot frequency — one knob trades bass for treble symmetrically.
|
|
675
|
+
|
|
676
|
+
**Positive gain**: bass up / treble down — warms up a bright signal<br>
|
|
677
|
+
**Negative gain**: treble up / bass down — brightens a dull signal<br>
|
|
678
|
+
**Pivot**: frequency that stays at 0 dB (default 1 kHz)
|
|
679
|
+
|
|
680
|
+
```js
|
|
681
|
+
import { tilt } from '@audio/eq'
|
|
682
|
+
|
|
683
|
+
tilt(buffer, { gain: +4, pivot: 1000, fs: 44100 }) // warm up
|
|
684
|
+
tilt(buffer, { gain: -3, pivot: 1000, fs: 44100 }) // brighten
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
**Use when**: quick tonal correction on a mix bus or stereo source with a single parameter<br>
|
|
688
|
+
**vs Baxandall**: tilt is one knob not two — bass and treble always move equal and opposite
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
## Effect
|
|
692
|
+
|
|
693
|
+
Signal conditioning and spectral shaping — single-purpose filters with well-defined transfer functions.
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
### DC blocker
|
|
697
|
+
|
|
698
|
+
Removes DC offset — the simplest useful filter.
|
|
699
|
+
|
|
700
|
+
$H(z) = \dfrac{1 - z^{-1}}{1 - Rz^{-1}}$
|
|
701
|
+
|
|
702
|
+
**Topology**: zero at $z = 1$ (DC), pole at $z = R$<br>
|
|
703
|
+
**Cutoff**: $f_c \approx \frac{(1-R) f_s}{2\pi}$ — $R = 0.995$ gives ~35 Hz at 44.1 kHz
|
|
704
|
+
|
|
705
|
+
```js
|
|
706
|
+
import { dcBlocker } from '@audio/filter'
|
|
707
|
+
|
|
708
|
+
let params = { R: 0.995 }
|
|
709
|
+
dcBlocker(buffer, params)
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
**Use when**: removing DC bias before processing, preventing lowpass filter saturation
|
|
713
|
+
|
|
714
|
+

|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
### Comb filter
|
|
718
|
+
|
|
719
|
+
Adds a delayed copy of the signal to itself — notches and peaks at harmonics of $f_s / D$.
|
|
720
|
+
|
|
721
|
+
**Feedforward**: $H(z) = 1 + g \cdot z^{-D}$ — dips at $f = \frac{(2k+1) f_s}{2D}$, depth depends on $g$: a true null only at $|g|=1$; the default $g=0.5$ gives a $-6\,\text{dB}$ dip, not a notch<br>
|
|
722
|
+
**Feedback**: $H(z) = \dfrac{1}{1 - g \cdot z^{-D}}$ — peaks at $f = \frac{k \cdot f_s}{D}$
|
|
723
|
+
|
|
724
|
+
```js
|
|
725
|
+
import { comb } from '@audio/filter'
|
|
726
|
+
|
|
727
|
+
comb(buffer, { delay: 100, gain: 0.6, type: 'feedback' })
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
**Use when**: flanging, chorus (with modulated delay), Karplus-Strong string synthesis, room mode modeling
|
|
731
|
+
|
|
732
|
+

|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
### Allpass
|
|
736
|
+
|
|
737
|
+
Unity magnitude at all frequencies — shifts phase only. First and second order.
|
|
738
|
+
|
|
739
|
+
**First order**: $H(z) = \dfrac{a + z^{-1}}{1 + a z^{-1}}$ — pole at $z = -a$, 180° phase shift at Nyquist<br>
|
|
740
|
+
**Second order**: $H(z) = \dfrac{(1-\alpha) - 2\cos(\omega_0)z^{-1} + (1+\alpha)z^{-2}}{(1+\alpha) - 2\cos(\omega_0)z^{-1} + (1-\alpha)z^{-2}}$, $\alpha = \sin(\omega_0)/(2Q)$ — RBJ cookbook allpass; 360° phase shift around $\omega_0$, width of the transition controlled by $Q$
|
|
741
|
+
|
|
742
|
+
```js
|
|
743
|
+
import { allpass } from '@audio/filter'
|
|
744
|
+
|
|
745
|
+
allpass.first(buffer, { a: 0.5 }) // coefficient a
|
|
746
|
+
allpass.second(buffer, { fc: 1000, Q: 1, fs: 44100 }) // center fc, quality Q
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
**Use when**: phase equalization, reverb building blocks (Schroeder reverb), stereo widening
|
|
750
|
+
|
|
751
|
+

|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
### Pre-emphasis / de-emphasis
|
|
755
|
+
|
|
756
|
+
First-order highpass (emphasis) and its inverse (de-emphasis) — used before and after coding or transmission.
|
|
757
|
+
|
|
758
|
+
$H(z) = 1 - \alpha z^{-1}$ (emphasis) / $H(z) = \dfrac{1}{1 - \alpha z^{-1}}$ (de-emphasis)
|
|
759
|
+
|
|
760
|
+
**Rolloff**: emphasis boosts above $f_c = \frac{(1-\alpha) f_s}{2\pi}$ — $\alpha = 0.97$ gives ~210 Hz at 44.1 kHz<br>
|
|
761
|
+
**Inverse pair**: `deemphasis` exactly cancels `emphasis` — $H_e(z) \cdot H_d(z) = 1$ (round-trip error ~5.5e-17)
|
|
762
|
+
|
|
763
|
+
```js
|
|
764
|
+
import { emphasis, deemphasis } from '@audio/filter'
|
|
765
|
+
|
|
766
|
+
emphasis(buffer, { alpha: 0.97 }) // before encoding
|
|
767
|
+
deemphasis(buffer, { alpha: 0.97 }) // after decoding — exact inverse
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
**Use when**: speech coding (GSM, AMR uses $\alpha = 0.97$), tape recording, FM broadcasting
|
|
771
|
+
|
|
772
|
+
 
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
### Lowpass
|
|
776
|
+
|
|
777
|
+
Removes everything above cutoff frequency — the most common filter in audio.
|
|
778
|
+
|
|
779
|
+
**Order 2** (default): RBJ biquad lowpass — $-12\,\text{dB/oct}$<br>
|
|
780
|
+
**Order 4+**: Butterworth cascaded SOS — $-6n\,\text{dB/oct}$ where $n$ = order; requires registering `digital-filter`'s Butterworth designer once (kept out of the default import so `lowpass`/`highpass` stay lean when you only need order 2)
|
|
781
|
+
|
|
782
|
+
```js
|
|
783
|
+
import { lowpass } from '@audio/filter'
|
|
784
|
+
import butterworth from 'digital-filter/iir/butterworth.js'
|
|
785
|
+
|
|
786
|
+
lowpass.useButterworth(butterworth) // once, before any order > 2 call
|
|
787
|
+
|
|
788
|
+
lowpass(buffer, { fc: 2000, fs: 44100 }) // 2nd-order (default) — no registration needed
|
|
789
|
+
lowpass(buffer, { fc: 2000, order: 4, fs: 44100 }) // 4th-order Butterworth
|
|
790
|
+
lowpass(buffer, { fc: 2000, Q: 1.5, fs: 44100 }) // resonant
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
**Use when**: anti-aliasing, smoothing, removing hiss, synth subtractive filtering<br>
|
|
794
|
+
**vs Moog ladder**: lowpass is a clean linear filter; Moog adds nonlinear saturation and self-oscillation
|
|
795
|
+
|
|
796
|
+

|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
### Highpass
|
|
800
|
+
|
|
801
|
+
Removes everything below cutoff frequency — DC removal, rumble elimination.
|
|
802
|
+
|
|
803
|
+
**Order 2** (default): RBJ biquad highpass — $-12\,\text{dB/oct}$<br>
|
|
804
|
+
**Order 4+**: Butterworth cascaded SOS — $-6n\,\text{dB/oct}$ where $n$ = order; same registration as Lowpass above
|
|
805
|
+
|
|
806
|
+
```js
|
|
807
|
+
import { highpass } from '@audio/filter'
|
|
808
|
+
import butterworth from 'digital-filter/iir/butterworth.js'
|
|
809
|
+
|
|
810
|
+
highpass.useButterworth(butterworth) // once, before any order > 2 call
|
|
811
|
+
|
|
812
|
+
highpass(buffer, { fc: 80, fs: 44100 }) // rumble filter
|
|
813
|
+
highpass(buffer, { fc: 80, order: 4, fs: 44100 }) // steeper rolloff
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
**Use when**: removing rumble, mic handling noise, subsonic content before dynamics processing<br>
|
|
817
|
+
**vs DC blocker**: highpass has adjustable cutoff and slope; DC blocker is simpler, lighter, fixed near 0 Hz
|
|
818
|
+
|
|
819
|
+

|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
### Bandpass
|
|
823
|
+
|
|
824
|
+
Passes frequencies around center frequency, rejects the rest — constant 0 dB peak gain.
|
|
825
|
+
|
|
826
|
+
**Implementation**: RBJ biquad bandpass (constant peak, Q controls width)
|
|
827
|
+
|
|
828
|
+
```js
|
|
829
|
+
import { bandpass } from '@audio/filter'
|
|
830
|
+
|
|
831
|
+
bandpass(buffer, { fc: 1000, Q: 5, fs: 44100 }) // narrow
|
|
832
|
+
bandpass(buffer, { fc: 1000, Q: 0.5, fs: 44100 }) // wide
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
**Use when**: isolating frequency regions, radio effect, walkie-talkie simulation, band splitting<br>
|
|
836
|
+
**vs Resonator**: bandpass is RBJ biquad (standard); resonator has constant peak gain — use resonator for modal synthesis
|
|
837
|
+
|
|
838
|
+

|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
### Resonator
|
|
842
|
+
|
|
843
|
+
Constant peak-gain bandpass — peak amplitude stays fixed regardless of bandwidth.
|
|
844
|
+
|
|
845
|
+
$H(z) = \dfrac{\frac{1-R^2}{2}(1 - z^{-2})}{1 - 2R\cos(\omega_0)z^{-1} + R^2 z^{-2}}$
|
|
846
|
+
|
|
847
|
+
**Pole radius**: $R = e^{-\pi \cdot bw / f_s}$ — controls bandwidth; $bw \to 0$ gives infinite Q<br>
|
|
848
|
+
**Peak gain**: always 0 dB by construction — the two zeros at $z = \pm 1$ shape the numerator so $(1-R^2)/2$ normalizes the pole peak regardless of `fc`/`bw` (verified ±0.01 dB across `fc` ∈ {50, 440, 5000, 15000} Hz, `bw` ∈ {5, 20, 200} Hz)<br>
|
|
849
|
+
**Origin**: Julius O. Smith III, "Introduction to Digital Filters" — Two-Pole, "Constant Peak-Gain Resonator"[^17]
|
|
850
|
+
|
|
851
|
+
```js
|
|
852
|
+
import { resonator } from '@audio/filter'
|
|
853
|
+
|
|
854
|
+
resonator(buffer, { fc: 440, bw: 20, fs: 44100 })
|
|
855
|
+
```
|
|
856
|
+
|
|
857
|
+
**Use when**: additive synthesis (bells, gongs), modal synthesis, formant bank building<br>
|
|
858
|
+
**vs Peaking EQ**: resonator has fixed 0 dB peak; peaking EQ has variable gain — use resonator for synthesis, EQ for mixing
|
|
859
|
+
|
|
860
|
+

|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
### Notch
|
|
864
|
+
|
|
865
|
+
Band-reject filter — unity gain everywhere except a deep null at `fc`.
|
|
866
|
+
|
|
867
|
+
$H(z) = \dfrac{1 - 2\cos(\omega_0)z^{-1} + z^{-2}}{1 - 2\cos(\omega_0)z^{-1}(1-\alpha) + (1-2\alpha)z^{-2}}$
|
|
868
|
+
|
|
869
|
+
**Q**: controls notch width — $Q = 30$ is narrow (hum removal); $Q = 5$ is wider (resonance suppression)<br>
|
|
870
|
+
**Zeros**: on the unit circle at $\pm\omega_0$ — exact null, independent of Q
|
|
871
|
+
|
|
872
|
+
```js
|
|
873
|
+
import { notch } from '@audio/filter'
|
|
874
|
+
|
|
875
|
+
notch(buffer, { fc: 50, Q: 30, fs: 44100 }) // remove 50 Hz mains hum
|
|
876
|
+
notch(buffer, { fc: 1000, Q: 10, fs: 44100 }) // suppress a resonance
|
|
877
|
+
```
|
|
878
|
+
|
|
879
|
+
**Use when**: mains hum removal (50/60 Hz), feedback cancellation, room mode suppression<br>
|
|
880
|
+
**vs Parametric EQ with negative gain**: notch reaches −∞ dB exactly at fc; peaking EQ has finite attenuation
|
|
881
|
+
|
|
882
|
+

|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
### Pink noise
|
|
886
|
+
|
|
887
|
+
Shapes white noise to $1/f$ spectrum — equal energy per octave.
|
|
888
|
+
|
|
889
|
+
**Spectrum**: power spectral density $S(f) \propto 1/f$ — $-3\,\text{dB/oct}$ slope, equal energy per octave<br>
|
|
890
|
+
**Implementation**: Paul Kellet's refined pink-noise filter — 7 cascaded first-order IIR stages with published fixed coefficients (musicdsp.org), no stochastic counters
|
|
891
|
+
|
|
892
|
+
```js
|
|
893
|
+
import { pinkNoise } from '@audio/filter'
|
|
894
|
+
|
|
895
|
+
let buf = new Float64Array(1024)
|
|
896
|
+
for (let i = 0; i < buf.length; i++) buf[i] = Math.random() * 2 - 1
|
|
897
|
+
pinkNoise(buf, {}) // white → pink (−3 dB/oct spectral slope)
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
**Use when**: noise testing, psychoacoustic masking reference, procedural audio, natural-sounding noise<br>
|
|
901
|
+
**vs White noise**: white noise has equal energy per Hz ($-0\,\text{dB/oct}$); pink is perceptually flat
|
|
902
|
+
|
|
903
|
+

|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
### Spectral tilt
|
|
907
|
+
|
|
908
|
+
Applies a constant dB/octave slope — tilts the entire spectrum.
|
|
909
|
+
|
|
910
|
+
**Model**: cascade of 8 octave-spaced first-order shelving sections approximating a fractional power-law spectrum $S(f) \propto f^\alpha$; positive slope boosts highs, negative slope cuts them<br>
|
|
911
|
+
**slope**: $\alpha = -3\,\text{dB/oct}$ gives pink noise character; $-6\,\text{dB/oct}$ gives brownian/red noise<br>
|
|
912
|
+
**Accuracy**: measured slope tracks the requested dB/oct to within ~20% (inherent to the 8-stage shelving cascade) — e.g. `slope: +3` measures ≈+2.4 dB/oct, `slope: -6` measures ≈-4.9 dB/oct
|
|
913
|
+
|
|
914
|
+
```js
|
|
915
|
+
import { spectralTilt } from '@audio/filter'
|
|
916
|
+
|
|
917
|
+
spectralTilt(buffer, { slope: -3, fs: 44100 }) // −3 dB/oct: pink noise character
|
|
918
|
+
spectralTilt(buffer, { slope: +3, fs: 44100 }) // +3 dB/oct: pre-emphasis for coding
|
|
919
|
+
```
|
|
920
|
+
|
|
921
|
+
**Use when**: matching microphone/speaker frequency responses, spectral coloring, noise synthesis
|
|
922
|
+
|
|
923
|
+

|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
### Variable bandwidth
|
|
927
|
+
|
|
928
|
+
Lowpass with continuously variable bandwidth — smooth parameter automation without discontinuities.
|
|
929
|
+
|
|
930
|
+
**Implementation**: `fc`/`Q` are exponentially smoothed toward their target values with a 5 ms time constant, and biquad coefficients are recomputed from the smoothed values every sample (not once per buffer)<br>
|
|
931
|
+
**Property**: no discontinuity when $f_c$ or $Q$ change — a mid-buffer step change lands ~9x closer to the plain-lowpass baseline than an abrupt coefficient jump; once converged, steady-state output equals plain lowpass/highpass/bandpass
|
|
932
|
+
|
|
933
|
+
```js
|
|
934
|
+
import { variableBandwidth } from '@audio/filter'
|
|
935
|
+
|
|
936
|
+
variableBandwidth(buffer, { fc: 2000, Q: 1.0, fs: 44100 })
|
|
937
|
+
```
|
|
938
|
+
|
|
939
|
+
**Use when**: LFO-modulated filter cutoff, automated EQ sweeps, smooth filter animation<br>
|
|
940
|
+
**vs Direct biquad**: recalculating biquad coefficients per sample causes zipper noise; variable bandwidth avoids this
|
|
941
|
+
|
|
942
|
+

|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
## Filter selection guide
|
|
946
|
+
|
|
947
|
+
| I need to... | Use |
|
|
948
|
+
|---|---|
|
|
949
|
+
| Measure SPL or noise level | `aWeighting` (general), `cWeighting` (peak), `itu468` (broadcast noise) |
|
|
950
|
+
| Measure loudness (LUFS/LU) | `kWeighting` |
|
|
951
|
+
| Decode vinyl audio | `riaa` |
|
|
952
|
+
| Model the cochlea / auditory system | `gammatone`, `erbBank` |
|
|
953
|
+
| Analyze a spectrum in octave bands | `octaveBank` |
|
|
954
|
+
| Psychoacoustic analysis / masking model | `barkBank` |
|
|
955
|
+
| MFCC / speech recognition features | `melBank` |
|
|
956
|
+
| Synth filter — warmth and resonance | `moogLadder` |
|
|
957
|
+
| Synth filter — acid / squelch | `diodeLadder` |
|
|
958
|
+
| Synth filter — 2-pole LP + HP | `korg35` |
|
|
959
|
+
| Synth filter — multimode SVF | `oberheim` |
|
|
960
|
+
| Synthesize vowel sounds | `formant` |
|
|
961
|
+
| Transfer one sound's spectral shape to another | `vocoder` |
|
|
962
|
+
| Analyze/resynthesize speech, change pitch | `lpcAnalysis` / `lpcSynthesize` |
|
|
963
|
+
| Studio EQ at fixed ISO frequencies | `graphicEq` |
|
|
964
|
+
| Studio EQ with full per-band control | `parametricEq` |
|
|
965
|
+
| Split audio for multi-way speakers | `crossover` |
|
|
966
|
+
| Improve headphone stereo imaging | `crossfeed` |
|
|
967
|
+
| Bass/treble tone control | `baxandall` |
|
|
968
|
+
| One-knob tonal tilt | `tilt` |
|
|
969
|
+
| Standalone bass or treble shelf | `lowShelf` / `highShelf` |
|
|
970
|
+
| Remove DC offset | `dcBlocker` |
|
|
971
|
+
| Remove mains hum / suppress resonance | `notch` |
|
|
972
|
+
| Clean lowpass / anti-alias | `lowpass` |
|
|
973
|
+
| Remove rumble / subsonic content | `highpass` |
|
|
974
|
+
| Isolate a frequency band | `bandpass` |
|
|
975
|
+
| Create resonant combing | `comb` |
|
|
976
|
+
| Phase-shift without changing magnitude | `allpass.first`, `allpass.second` |
|
|
977
|
+
| Pre-process for audio coding | `emphasis` / `deemphasis` |
|
|
978
|
+
| Modal synthesis (bells, drums, rooms) | `resonator` |
|
|
979
|
+
| Generate pink / brown noise | `pinkNoise` + `spectralTilt` |
|
|
980
|
+
| Tilt spectrum for noise synthesis | `spectralTilt` |
|
|
981
|
+
| Smooth automated filter sweeps | `variableBandwidth` |
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
## FAQ
|
|
985
|
+
|
|
986
|
+
**Why does my filter click when I change `fc` or `Q`?**
|
|
987
|
+
Biquad coefficients change discontinuously between samples. Use `variableBandwidth` for smooth automated sweeps, or crossfade.
|
|
988
|
+
|
|
989
|
+
**Why does my Moog/Diode filter blow up?**
|
|
990
|
+
`resonance=1` on Moog is intentional self-oscillation. Diode ladder is bounded across its full resonance range (per-stage $\tanh$-saturated); it self-oscillates near resonance≈1.15–1.2, not exactly at 1 like Moog. Limit input gain before high resonance.
|
|
991
|
+
|
|
992
|
+
**Does mutating `params` between calls reset state?**
|
|
993
|
+
No — mutating the same object (`params.fc = newFc`) preserves state. Replacing the object (`params = { fc: newFc }`) loses it.
|
|
994
|
+
|
|
995
|
+
**Why does `.coefs(fs)` return an SOS array instead of one biquad?**
|
|
996
|
+
A-weighting needs 3 second-order sections; a single biquad can't represent a 6-pole response. Pass SOS arrays to `digital-filter`'s `filter()` or `freqz()`.
|
|
997
|
+
|
|
998
|
+
**What sample rate should I use for accurate A-weighting?**
|
|
999
|
+
96 kHz for IEC Class 1 across the full 20 Hz–20 kHz range. At 48 kHz error grows above 10 kHz (~1 dB at 10 kHz, ~4 dB at 20 kHz).
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
## Recipes
|
|
1003
|
+
|
|
1004
|
+
**Chain filters**
|
|
1005
|
+
```js
|
|
1006
|
+
import { dcBlocker } from '@audio/filter'
|
|
1007
|
+
import { moogLadder } from '@audio/filter'
|
|
1008
|
+
|
|
1009
|
+
let p1 = { fc: 200, fs: 44100 }
|
|
1010
|
+
let p2 = { R: 0.995 }
|
|
1011
|
+
for (let buf of stream) {
|
|
1012
|
+
dcBlocker(buf, p2) // DC removal first
|
|
1013
|
+
moogLadder(buf, p1)
|
|
1014
|
+
}
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
**Stereo — independent state per channel**
|
|
1018
|
+
```js
|
|
1019
|
+
import { moogLadder } from '@audio/filter'
|
|
1020
|
+
|
|
1021
|
+
let pL = { fc: 1000, fs: 44100 }
|
|
1022
|
+
let pR = { fc: 1000, fs: 44100 }
|
|
1023
|
+
for (let [L, R] of stereoStream) {
|
|
1024
|
+
moogLadder(L, pL)
|
|
1025
|
+
moogLadder(R, pR)
|
|
1026
|
+
}
|
|
1027
|
+
```
|
|
1028
|
+
|
|
1029
|
+
**Frequency analysis**
|
|
1030
|
+
```js
|
|
1031
|
+
import { aWeighting } from '@audio/weighting'
|
|
1032
|
+
import { freqz, mag2db } from 'digital-filter'
|
|
1033
|
+
|
|
1034
|
+
let sos = aWeighting.coefs(44100)
|
|
1035
|
+
let { magnitude } = freqz(sos, 4096, 44100)
|
|
1036
|
+
let db = mag2db(magnitude) // dB at 4096 frequencies, 20 Hz–Nyquist
|
|
1037
|
+
```
|
|
1038
|
+
|
|
1039
|
+
**Multi-band split**
|
|
1040
|
+
```js
|
|
1041
|
+
import { crossover } from '@audio/eq'
|
|
1042
|
+
import { filter } from 'digital-filter'
|
|
1043
|
+
|
|
1044
|
+
let bands = crossover([500, 5000], 4, 44100) // lo / mid / hi
|
|
1045
|
+
let [lo, mid, hi] = bands.map(coefs => {
|
|
1046
|
+
let buf = Float64Array.from(input) // copy — filter is in-place
|
|
1047
|
+
filter(buf, { coefs })
|
|
1048
|
+
return buf
|
|
1049
|
+
})
|
|
1050
|
+
// process independently, then sum
|
|
1051
|
+
```
|
|
1052
|
+
|
|
1053
|
+
**Notch out mains hum**
|
|
1054
|
+
```js
|
|
1055
|
+
import { notch } from '@audio/filter'
|
|
1056
|
+
|
|
1057
|
+
let p = { fc: 50, Q: 30, fs: 44100 }
|
|
1058
|
+
for (let buf of stream) notch(buf, p) // removes 50 Hz hum, flat elsewhere
|
|
1059
|
+
```
|
|
1060
|
+
|
|
1061
|
+
**Automate cutoff without clicks**
|
|
1062
|
+
```js
|
|
1063
|
+
import { variableBandwidth } from '@audio/filter'
|
|
1064
|
+
|
|
1065
|
+
let p = { fc: 200, Q: 1.0, fs: 44100 }
|
|
1066
|
+
for (let buf of stream) {
|
|
1067
|
+
p.fc = 200 + lfo() * 1800 // mutate in-place — state preserved
|
|
1068
|
+
variableBandwidth(buf, p)
|
|
1069
|
+
}
|
|
1070
|
+
```
|
|
1071
|
+
|
|
1072
|
+
|
|
1073
|
+
## Pitfalls
|
|
1074
|
+
|
|
1075
|
+
**New params object on every call — state resets each block**
|
|
1076
|
+
```js skip
|
|
1077
|
+
// Wrong
|
|
1078
|
+
for (let buf of stream) moogLadder(buf, { fc: 1000, fs: 44100 })
|
|
1079
|
+
|
|
1080
|
+
// Right — create once, reuse
|
|
1081
|
+
let p = { fc: 1000, fs: 44100 }
|
|
1082
|
+
for (let buf of stream) moogLadder(buf, p)
|
|
1083
|
+
```
|
|
1084
|
+
|
|
1085
|
+
**Shared params for stereo — channels corrupt each other's state**
|
|
1086
|
+
```js skip
|
|
1087
|
+
// Wrong
|
|
1088
|
+
let p = { fc: 1000, fs: 44100 }
|
|
1089
|
+
for (let [L, R] of stream) { moogLadder(L, p); moogLadder(R, p) }
|
|
1090
|
+
|
|
1091
|
+
// Right — one object per channel
|
|
1092
|
+
let pL = { fc: 1000, fs: 44100 }, pR = { fc: 1000, fs: 44100 }
|
|
1093
|
+
for (let [L, R] of stream) { moogLadder(L, pL); moogLadder(R, pR) }
|
|
1094
|
+
```
|
|
1095
|
+
|
|
1096
|
+
**Filtering the same buffer twice for multi-band — second band sees pre-filtered input**
|
|
1097
|
+
```js skip
|
|
1098
|
+
// Wrong
|
|
1099
|
+
filter(buffer, { coefs: bands[0] })
|
|
1100
|
+
filter(buffer, { coefs: bands[1] }) // input already filtered!
|
|
1101
|
+
|
|
1102
|
+
// Right — copy per band
|
|
1103
|
+
let bufs = bands.map(b => { let c = Float64Array.from(buffer); filter(c, { coefs: b.coefs }); return c })
|
|
1104
|
+
```
|
|
1105
|
+
|
|
1106
|
+
**Omitting `fs` — silently uses 44100 Hz math on 48000 Hz audio**
|
|
1107
|
+
```js skip
|
|
1108
|
+
// Wrong — wrong cutoffs at 48 kHz
|
|
1109
|
+
moogLadder(buffer, { fc: 1000 })
|
|
1110
|
+
|
|
1111
|
+
// Right
|
|
1112
|
+
moogLadder(buffer, { fc: 1000, fs: 48000 })
|
|
1113
|
+
```
|
|
1114
|
+
|
|
1115
|
+
|
|
1116
|
+
## See also
|
|
1117
|
+
|
|
1118
|
+
- [effect](https://github.com/audiojs/effect) — audio effects: phaser, flanger, chorus, wah, compressor, reverb, delay, and more
|
|
1119
|
+
- [digital-filter](https://github.com/audiojs/digital-filter) — general-purpose filter design: Butterworth, Chebyshev, Bessel, Elliptic, FIR, and more
|
|
1120
|
+
- [decode](https://github.com/audiojs/decode) — decode audio files to PCM buffers
|
|
1121
|
+
- [speaker](https://github.com/audiojs/speaker) — output PCM audio to system speakers
|
|
1122
|
+
- [Web Audio API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API) — browser built-in audio; basic biquad shapes only, requires `AudioContext`
|
|
1123
|
+
|
|
1124
|
+
|
|
1125
|
+
|
|
1126
|
+
[^1]: IEC 61672-1:2013, *Electroacoustics — Sound level meters — Part 1: Specifications*. Supersedes IEC 651:1979.
|
|
1127
|
+
|
|
1128
|
+
[^2]: ITU-R BS.1770-4:2015, *Algorithms to measure audio programme loudness and true-peak audio level*. Adopted by EBU R128.
|
|
1129
|
+
|
|
1130
|
+
[^3]: ITU-R BS.468-4:1986, *Measurement of audio-frequency noise voltage level in sound broadcasting*. Originally CCIR 468, 1968.
|
|
1131
|
+
|
|
1132
|
+
[^4]: RIAA standard (1954). Cf. IEC 60098:1987, *Analogue audio disk records and reproducing equipment*, whose 4-time-constant variant adds a subsonic pole this implementation does not.
|
|
1133
|
+
|
|
1134
|
+
[^5]: Patterson, R.D., Robinson, K., Holdsworth, J., McKeown, D., Zhang, C. & Allerhand, M. (1992). "Complex sounds and auditory images." *Auditory Physiology and Perception*, Pergamon, pp. 429–446.
|
|
1135
|
+
|
|
1136
|
+
[^6]: IEC 61260-1:2014, *Electroacoustics — Octave-band and fractional-octave-band filters — Part 1: Specifications*. ANSI S1.11:2004.
|
|
1137
|
+
|
|
1138
|
+
[^7]: Moore, B.C.J. & Glasberg, B.R. (1983). "Suggested formulae for calculating auditory-filter bandwidths and excitation patterns." *JASA* 74(3), pp. 750–753. Updated 1990.
|
|
1139
|
+
|
|
1140
|
+
[^8]: Zwicker, E. (1961). "Subdivision of the audible frequency range into critical bands." *JASA* 33(2), p. 248.
|
|
1141
|
+
|
|
1142
|
+
[^9]: Zavalishin, V. (2012). *The Art of VA Filter Design*. Native Instruments.
|
|
1143
|
+
|
|
1144
|
+
[^10]: Moog, R.A. (1965). *Voltage controlled electronic music modules*. Patent US3475623.
|
|
1145
|
+
|
|
1146
|
+
[^11]: Pirkle, W.C. (2019). *Designing Audio Effect Plugins in C++*, 2nd ed. Routledge.
|
|
1147
|
+
|
|
1148
|
+
[^12]: Stilson, T. & Smith, J.O. (1996). "Analyzing the Moog VCF with considerations for digital implementation." *Proc. ICMC*.
|
|
1149
|
+
|
|
1150
|
+
[^13]: Dudley, H. (1939). "The vocoder." *Bell Laboratories Record* 17, pp. 122–126. Patent US2151091.
|
|
1151
|
+
|
|
1152
|
+
[^14]: Linkwitz, S. & Riley, R. (1976). "Active Crossover Networks for Non-Coincident Drivers." *JAES* 24(1), pp. 2–8.
|
|
1153
|
+
|
|
1154
|
+
[^15]: Bauer, B.B. (1961). "Stereophonic Earphones and Binaural Loudspeakers." *JAES* 9(2), pp. 148–151.
|
|
1155
|
+
|
|
1156
|
+
[^16]: Zölzer, U. (2011). *DAFX: Digital Audio Effects*, 2nd ed. Wiley.
|
|
1157
|
+
|
|
1158
|
+
[^17]: Smith, J.O. III. *Introduction to Digital Filters with Audio Applications*. "Two-Pole" — "Constant Peak-Gain Resonator". CCRMA, Stanford University. https://ccrma.stanford.edu/~jos/filters/
|
|
1159
|
+
|
|
1160
|
+
[^18]: O'Shaughnessy, D. (2000). *Speech Communications: Human and Machine*, 2nd ed. IEEE Press.
|
|
1161
|
+
|
|
1162
|
+
[^19]: Atal, B.S. & Hanauer, S.L. (1971). "Speech Analysis and Synthesis by Linear Prediction of the Speech Wave." *JASA* 50(2B), pp. 637–655.
|
|
1163
|
+
|
|
1164
|
+
[^20]: Baxandall, P.J. (1952). "Transistor Tone-Control Design." *Wireless World* 58(10), pp. 402–405.
|