Skip to content

audiojs/audio-filter

Repository files navigation

audio-filter ci npm

Canonical audio filter implementations.

Weighting
A-weighting · C-weighting · K-weighting · ITU-R 468 · RIAA

Auditory
Gammatone · Octave bank · ERB bank · Bark bank · Mel bank

Analog
Moog ladder · Diode ladder · Korg35 · Oberheim

Speech
Formant · Vocoder · LPC

EQ
Graphic EQ · Parametric EQ · Crossover · Crossfeed · Shelving · Baxandall · Tilt EQ

Effect
DC blocker · Comb · Allpass · Pre-emphasis · Notch · Resonator · Pink noise · Spectral tilt · Variable bandwidth

Install

npm install audio-filter
// import everything
import * as filter from 'audio-filter'

// import by domain
import { aWeighting, kWeighting } from 'audio-filter/weighting'
import { gammatone, melBank } from 'audio-filter/auditory'
import { moogLadder, oberheim } from 'audio-filter/analog'
import { vocoder, lpcAnalysis } from 'audio-filter/speech'
import { parametricEq, crossover, baxandall, tilt } from 'audio-filter/eq'
import { dcBlocker, notch, resonator } from 'audio-filter/effect'

API

All filters share one shape:

filter(buffer, params)   // → buffer (modified in-place)

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:

let params = { fc: 1000, resonance: 0.5, fs: 44100 }
for (let buf of stream) moogLadder(buf, params)

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:

import { freqz, mag2db } from 'digital-filter/core'

let sos  = aWeighting.coefs(44100)
let resp = freqz(sos, 2048, 44100)
let db   = mag2db(resp.magnitude)

Weighting

Standard measurement curves. Each is defined by a standards body to a specific curve shape and normalization.

Weighting filters comparison

filter standard normalized
aWeighting IEC 61672-1:2013 0 dB at 1 kHz
cWeighting IEC 61672-1:2013 0 dB at 1 kHz
kWeighting ITU-R BS.1770-4:2015
itu468 ITU-R BS.468-4:1986 +12.2 dB at 6.3 kHz
riaa RIAA 1954 / IEC 60098 0 dB at 1 kHz

A-weighting

Models how the ear perceives loudness — attenuates low and very high frequencies.

Transfer function: $H(s) = \frac{Ks^4}{(s+\omega_1)^2(s+\omega_2)(s+\omega_3)(s+\omega_4)^2}$
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}$
Implementation: matched z-transform ($z_k = e^{s_k/f_s}$), 3 SOS sections — no frequency warping near Nyquist
Normalization: 0 dB at 1 kHz (IEC requirement)

import { aWeighting } from 'audio-filter/weighting'

let p = { fs: 44100 }
for (let buf of stream) aWeighting(buf, p)   // A-weighted stream

Standard: IEC 61672-1:20131
Use when: measuring SPL, noise, OSHA compliance, audio quality
Not for: loudness in broadcast (use K-weighting), noise annoyance (use ITU-468)

A-weighting

C-weighting

Like A-weighting but flatter — less rolloff at low and high frequencies.

Transfer function: $H(s) = \frac{Ks^2}{(s+\omega_1)^2(s+\omega_4)^2}$
Poles: $\omega_1 = 2\pi \cdot 20.6,\text{Hz}$, $\omega_4 = 2\pi \cdot 12194,\text{Hz}$ (same as A-weighting outer poles)
Implementation: matched z-transform, 2 SOS sections

cWeighting(buffer, { fs: 44100 })

Standard: IEC 61672-1:20131
Use when: peak sound level measurement, where A-weighting over-penalizes bass
Compared to A: rolls off below 31.5 Hz and above 8 kHz; flat 31.5 Hz–8 kHz

C-weighting

K-weighting

The loudness measurement curve — a high shelf plus a highpass. Used to compute LUFS.

Stage 1: pre-filter — high shelf +4 dB above ~1.5 kHz (head diffraction simulation)
Stage 2: RLB highpass — 2nd-order Butterworth at ~38 Hz (removes sub-bass)
Exact coefficients at 48 kHz: specified in BS.1770 Annex 1; this implementation uses them verbatim

import { kWeighting } from 'audio-filter/weighting'

kWeighting(buffer, { fs: 48000 })   // exact ITU-R BS.1770 coefficients
kWeighting(buffer, { fs: 44100 })   // approximated via biquad design

Standard: ITU-R BS.1770-4:20152, EBU R128
Use when: computing integrated loudness (LUFS/LKFS), broadcast loudness normalization
Not for: A-weighted SPL measurement (different shape, different standard)

K-weighting

ITU-R 468

Peaked noise weighting — peaks at +12.2 dB near 6.3 kHz — models how humans actually perceive noise annoyance.

Shape: rises steeply from 31.5 Hz, peaks at +12.2 dB at 6.3 kHz, rolls off above 10 kHz
Implementation: practical IIR approximation via cascaded biquads, within ~1 dB of spec

itu468(buffer, { fs: 48000 })

Standard: ITU-R BS.468-4:19863 (original CCIR 468, 1968)
Rationale: human hearing is more sensitive to short noise bursts than sine tones; 468 weights accordingly
Use when: measuring noise in broadcast equipment, tape noise, hum and hiss
Compared to A-weighting: 6.3 kHz peak makes it harsher on hiss; preferred in European broadcast

ITU-R 468

RIAA

Playback equalization for vinyl records — a shelving curve with three time constants.

Transfer function: $H(s) = \frac{1 + sT_2}{(1 + sT_1)(1 + sT_3)}$
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)
Implementation: 1 SOS section via bilinear transform, normalized 0 dB at 1 kHz

import { riaa } from 'audio-filter/weighting'

riaa(phonoSignal, { fs: 44100 })   // correct vinyl playback

Standard: RIAA 1954, IEC 60098:19874
Purpose: playback de-emphasis undoes the mastering pre-emphasis applied during vinyl cutting
Shape: boosts bass ~+20 dB at 20 Hz, rolls off treble; at playback restores flat response

RIAA equalization

Auditory

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.

Gammatone

The cochlear filter — bandpass tuned to one frequency, decaying oscillation, mimics an inner hair cell.

Model: cascade of complex one-pole filters; 4th-order is the standard cochlear approximation
Bandwidth: $\text{ERB} = 24.7\left(\frac{4.37 f_c}{1000} + 1\right),\text{Hz}$
Implementation: complex resonator with gain normalization to 0 dB at $f_c$

import { gammatone } from 'audio-filter/auditory'

let params = { fc: 1000, fs: 44100 }
gammatone(buffer, params)   // bandpass at 1 kHz with cochlear envelope

Origin: Patterson et al. (1992)5
Use when: cochlear modeling, auditory scene analysis, psychoacoustic feature extraction
Compared to Butterworth bandpass: gammatone has asymmetric temporal envelope matching biological data

Gammatone filter

Reuse params across blocks — state in params._s, gain cached in params._gain.

Gammatone bank (6 center frequencies)

Octave bank

ISO/IEC fractional-octave filter bank — the standard for acoustic measurement and spectrum analysis.

Center frequencies: ISO 266 series — $f_c = 1000 \cdot G^{k/n}$, $G = 10^{3/10}$
Bandwidth: each band spans $f_c \cdot G^{-1/(2n)}$ to $f_c \cdot G^{+1/(2n)}$
1/1 octave: 10 bands (31.5–16 kHz) — coarse; 1/3 octave: 30 bands — standard; 1/6+: psychoacoustics
Returns: array of { fc, coefs } — each band is a biquad bandpass section

import { octaveBank } from 'audio-filter/auditory'
import { filter } from 'digital-filter'

let bands = octaveBank(3, 44100)   // 1/3-octave, 30+ bands
for (let band of bands) {
  let buf = Float64Array.from(signal)
  filter(buf, { coefs: band.coefs })
  spectrum.push({ fc: band.fc, energy: rms(buf) })
}

Standard: IEC 61260-1:20146, ANSI S1.11:2004
Use when: acoustic measurement, noise assessment, spectrum visualization

1/3-octave filter bank

ERB bank

Equivalent Rectangular Bandwidth scale — how the auditory system actually spaces its channels.

ERB formula: $\text{ERB}(f_c) = 24.7\left(\frac{4.37 f_c}{1000} + 1\right)$
Spacing: ~1 ERB between adjacent channels — logarithmic above 1 kHz, more linear below
Returns: array of { fc, erb, bw } descriptors; apply gammatone at each fc for the filter bank

import { erbBank, gammatone } from 'audio-filter/auditory'

let bands  = erbBank(44100)
let states = bands.map(b => ({ fc: b.fc, fs: 44100 }))

for (let buf of stream) {
  let channels = bands.map((_, i) => {
    let b = Float64Array.from(buf)
    gammatone(b, states[i])
    return b
  })
}

Origin: Moore & Glasberg (1983, 1990)7
Use when: speech processing, hearing models, auditory feature extraction
Compared to Bark: ERB is more accurate above 500 Hz; Bark is the psychoacoustic masking model

ERB filter bank

Bark bank

Zwicker's 24 critical bands — the psychoacoustic foundation of perceptual audio coding.

Scale: 24 bands spanning 20 Hz–20 kHz; named after Heinrich Barkhausen
Band widths: ~100 Hz wide below 500 Hz; ~20% of center frequency above
Returns: array of { bark, fLow, fHigh, fc, coefs } — each band is a biquad bandpass section

import { barkBank } from 'audio-filter/auditory'
import { filter } from 'digital-filter'

let bands = barkBank(44100)   // 24 critical bands
for (let band of bands) {
  let buf = Float64Array.from(signal)
  filter(buf, { coefs: band.coefs })
  excitation[band.bark] = rms(buf)
}

Origin: Zwicker (1961)8
Use when: perceptual audio coding (MP3/AAC use Bark-like groupings), loudness models, masking
Compared to ERB: Bark bands are wider and fewer; ERB is more accurate for hearing science

Bark critical band filter bank

Mel bank

Mel-frequency triangular filter bank — the standard front-end for speech recognition and music information retrieval.

Scale: $\text{mel}(f) = 2595 \log_{10}(1 + f/700)$ (O'Shaughnessy variant)9
Bands: equally spaced in mel scale; each band is a triangle spanning 3 adjacent mel points
Returns: array of { fc, fLow, fHigh, mel } — band descriptors for MFCC computation

import { melBank } from 'audio-filter/auditory'

let bands = melBank(44100)                          // 26 bands (default)
let bands = melBank(16000, { nFilters: 40 })        // 40 bands, telephony rate
let bands = melBank(44100, { fmin: 300, fmax: 8000 })

Use when: MFCC feature extraction, speech recognition, music genre classification, audio fingerprinting
Compared to ERB/Bark: mel is the most widely used in ML; ERB is more physiologically accurate

Mel filter bank

Analog

Discrete-time models of analog circuits — each named after the hardware it replicates. Nonlinear, stateful, process in-place. The filters in synthesizers.

Moog ladder

Robert Moog's 4-pole transistor ladder, 1965 — the most imitated filter in electronic music.

Circuit: 4 cascaded one-pole transistor ladder sections, global feedback from output to input
Implementation: Zero-delay feedback (ZDF) via trapezoidal integration — Zavalishin (2012)10, Ch. 6
Response: $-24,\text{dB/oct}$ lowpass; resonance peak at $f_c$; self-oscillation (sine wave) at resonance=1
Nonlinearity: $\tanh$ saturation at input (transistor ladder characteristic)

import { moogLadder } from 'audio-filter/analog'

let params = { fc: 800, resonance: 0.7, fs: 44100 }
moogLadder(buffer, params)

// Self-oscillation — runs indefinitely from a single impulse
let silent = new Float64Array(4096); silent[0] = 0.01
moogLadder(silent, { fc: 1000, resonance: 1, fs: 44100 })

Patent: Moog (1965) US347562311
vs Diode ladder: Moog saturates only at input; diode saturates at each stage — different character at high resonance

Moog ladder resonance sweep

Diode ladder

Roland TB-303 / EMS VCS3 style — per-stage saturation gives the characteristic acid "squelch".

Circuit: Roland TB-303, EMS VCS3, EDP Wasp
Key difference from Moog: $\tanh$ nonlinearity at each of 4 stages, not just input; feedback is a weighted sum of all stage outputs
Character: preserves bass at high resonance; more "squelchy" and aggressive than Moog
Implementation: ZDF — Zavalishin (2012)10; Pirkle (2019)12, Ch. 10
Stability: stable up to resonance=0.95; bounded output

import { diodeLadder } from 'audio-filter/analog'

let params = { fc: 500, resonance: 0.8, fs: 44100 }
diodeLadder(buffer, params)

Diode ladder

Korg35

Korg MS-10/MS-20, 1978 — 2-pole filter with lowpass and complementary highpass outputs.

Topology: 2 cascaded one-pole sections with nonlinear feedback; HP = input − LP
Response: $-12,\text{dB/oct}$; aggressive resonance due to nonlinear feedback; both LP and HP from one circuit

import { korg35 } from 'audio-filter/analog'

korg35(buffer, { fc: 1000, resonance: 0.5, type: 'lowpass',  fs: 44100 })
korg35(buffer, { fc: 1000, resonance: 0.5, type: 'highpass', fs: 44100 })

Circuit: Korg MS-10/MS-20 (1978)
Analysis: Stilson & Smith (1996)13; Zavalishin (2012)10, Ch. 5
vs Moog ladder: 2-pole ($-12,\text{dB/oct}$) vs 4-pole ($-24,\text{dB/oct}$); Korg35 has complementary HP mode

Korg35 LP and HP

Oberheim

Oberheim SEM (1974) — 2-pole state-variable filter with four modes from one circuit.

Topology: 2 trapezoidal integrators with nonlinear feedback; multimode output (LP/HP/BP/notch)
Response: $-12,\text{dB/oct}$; warm, musical resonance; continuous mode morphing
Implementation: ZDF — Zavalishin (2012)10, Ch. 4–5; $\tanh$ saturation on integrator states

import { oberheim } from 'audio-filter/analog'

oberheim(buffer, { fc: 1000, resonance: 0.5, type: 'lowpass',  fs: 44100 })
oberheim(buffer, { fc: 1000, resonance: 0.5, type: 'highpass', fs: 44100 })
oberheim(buffer, { fc: 1000, resonance: 0.5, type: 'bandpass', fs: 44100 })
oberheim(buffer, { fc: 1000, resonance: 0.5, type: 'notch',    fs: 44100 })

Circuit: Oberheim SEM (1974), Two Voice, Four Voice, Eight Voice
vs Moog/Korg: 2-pole like Korg35 but true state-variable topology; LP/HP/BP/notch from one circuit; warmer resonance character

Oberheim SEM

Speech

Filters that model or process the human vocal tract — from vowel synthesis to spectral voice coding.

Formant

Parallel resonator bank — each peak models one vocal tract resonance (formant).

Model: parallel combination of second-order resonators, each modeling one vocal tract mode
Formant frequencies: determined by vocal tract shape; F1 controls vowel openness, F2 controls front/back
Typical ranges: F1: 250–850 Hz, F2: 850–2500 Hz, F3: 1700–3500 Hz
Implementation: uses resonator internally — constant peak-gain bandpass per formant
Defaults: F1=730 Hz, F2=1090 Hz, F3=2440 Hz (open vowel /a/)

import { formant } from 'audio-filter/speech'

formant(excitation, { fs: 44100 })   // vowel /a/ (default)

formant(excitation, {
  formants: [{ fc: 270, bw: 60, gain: 1 }, { fc: 2290, bw: 90, gain: 0.5 }],
  fs: 44100
})   // vowel /i/

Use when: speech synthesis, singing synthesis, vocal effects, acoustic phonetics
Not a substitute for: LPC synthesis, which estimates formants automatically from a speech signal

Formant filter

Vocoder

Channel vocoder — transfers the spectral envelope of one sound onto the pitched content of another.

Note: takes two separate buffers, returns a new buffer (does not modify in-place).

Principle: analyze modulator into N bands → extract envelope per band → multiply with filtered carrier → sum
Implementation: N parallel bandpass filters on both signals; envelope follower per modulator band
Band count: 8 = robotic effect; 16 = classic vocoder sound; 32+ = more speech intelligibility

import { vocoder } from 'audio-filter/speech'

// carrier: pitched source (sawtooth, buzz, noise...)
// modulator: signal whose spectral shape to impose (voice, instrument...)
let output = vocoder(carrier, modulator, { bands: 16, fs: 44100 })

Inventor: Dudley (1939)14, Bell Labs
Use when: voice effects, talkbox simulation, cross-synthesis, spectral morphing

LPC

Linear Predictive Coding — estimates the vocal tract transfer function from a speech signal.

Analysis: autocorrelation method + Levinson-Durbin recursion → LPC coefficients + residual
Synthesis: all-pole filter reconstructs signal from residual excitation
Round-trip: lpcAnalysislpcSynthesize recovers the original signal exactly

import { lpcAnalysis, lpcSynthesize } from 'audio-filter/speech'

// Analysis: extract vocal tract model
let { coefs, gain, residual } = lpcAnalysis(speechFrame, { order: 12 })

// Synthesis: reconstruct from residual
lpcSynthesize(residual, { coefs, gain })   // residual → reconstructed speech

// Modify pitch: replace residual with different excitation
let buzz = generatePulseTrainAtNewPitch()
lpcSynthesize(buzz, { coefs, gain })       // speech at new pitch

Origin: Atal & Hanauer (1971)15; foundation of CELP, GSM, and modern speech codecs
Use when: speech coding, pitch modification, voice conversion, formant estimation, speech analysis

LPC analysis/synthesis

EQ

Equalization and frequency routing — from parametric studio EQ to speaker crossover networks.

Graphic EQ

10-band ISO octave equalizer — fixed center frequencies, gain per band.

Implementation: parallel biquad peaking filters, one per band; gains combined additively
Band spacing: 1-octave intervals — $f_k = 1000 \cdot 2^k,\text{Hz}$
Bands: 31.25, 62.5, 125, 250, 500, 1000, 2000, 4000, 8000, 16000 Hz

import { graphicEq } from 'audio-filter/eq'

graphicEq(buffer, {
  gains: { 125: -3, 1000: +6, 8000: +2 },
  fs: 44100
})

Standard: ISO 266:1997 center frequencies
Use when: quick tonal shaping, DJ mixers, consumer audio, live sound
vs Parametric EQ: fixed centers but simpler — no per-band frequency or Q control

Graphic EQ

Parametric EQ

N-band EQ with fully adjustable frequency, Q, and gain per band.

Implementation: cascaded biquad sections — one per band; peak uses peaking EQ biquad, shelves use Zölzer shelf design16
Band types: peak (bell curve at $f_c$), lowshelf (boost/cut below $f_c$), highshelf (boost/cut above $f_c$)

import { parametricEq } from 'audio-filter/eq'

parametricEq(buffer, {
  bands: [
    { fc: 80,   Q: 0.7, gain: +4,  type: 'lowshelf'  },
    { fc: 1000, Q: 2.0, gain: -3,  type: 'peak'      },
    { fc: 8000, Q: 0.7, gain: +2,  type: 'highshelf' },
  ],
  fs: 44100
})

Use when: studio mixing, mastering, precise tonal correction
vs Graphic EQ: fully adjustable $f_c$, Q, and gain per band; no fixed centers

Parametric EQ

Crossover

Linkwitz-Riley crossover network — splits audio into N frequency bands with flat magnitude sum.

Filter type: cascade of two Butterworth filters of half the specified order
Property: LR4 (order=4) bands sum to flat magnitude response with correct phase alignment
Orders: LR2 ($-12,\text{dB/oct}$), LR4 ($-24,\text{dB/oct}$, most common), LR8 ($-48,\text{dB/oct}$)
Returns: SOS[][] — one SOS array per band

import { crossover } from 'audio-filter/eq'
import { filter } from 'digital-filter'

let bands = crossover([500, 5000], 4, 44100)   // 3 bands: lo / mid / hi

let lo  = Float64Array.from(buffer); filter(lo,  { coefs: bands[0] })
let mid = Float64Array.from(buffer); filter(mid, { coefs: bands[1] })
let hi  = Float64Array.from(buffer); filter(hi,  { coefs: bands[2] })

Designers: Linkwitz & Riley (1976)17
Use when: speaker system design, multi-band dynamics, band splitting for separate processing

4-way crossover

Crossfeed

Headphone crossfeed — mixes a filtered copy of each channel into the other to reduce in-head localization.

Takes two separate channel buffers, modifies both in-place.

Problem: speaker playback has inter-channel crosstalk and head shadowing; headphones remove these, causing an unnatural "in-head" stereo image
Solution: add a lowpass-filtered, attenuated copy of each channel to the opposite channel, simulating crosstalk and head diffraction
fc: models the head-shadow lowpass (~700 Hz is typical); level: 0.3 = mild, 0.5 = strong

import { crossfeed } from 'audio-filter/eq'

crossfeed(left, right, { fc: 700, level: 0.3, fs: 44100 })

Origin: Bauer (1961)18; BS2B (Bauer Stereophonic-to-Binaural) algorithm

Crossfeed

Shelving

Standalone low-shelf and high-shelf filters — boost or cut below/above a corner frequency.

Low shelf: $H(s) = A \cdot \frac{s/\omega_c + \sqrt{A}}{s/(\omega_c\sqrt{A}) + 1}$ — RBJ biquad shelf design
High shelf: same topology, mirrored in frequency
Q / slope: $Q = 0.707$ gives maximally-flat transition; lower Q gives a gentler, wider slope

import { lowShelf, highShelf } from 'audio-filter/eq'

lowShelf(buffer,  { fc: 200,  gain: +6, Q: 0.707, fs: 44100 })   // bass boost
highShelf(buffer, { fc: 4000, gain: -3, Q: 0.707, fs: 44100 })   // treble cut

Use when: correcting speaker/room low-end buildup, air-band top-end addition, mastering bus
vs Parametric EQ: shelf is a single-band operation with a cleaner API — use when you don't need bell curves

Baxandall

Bass/treble tone control — the canonical two-knob EQ in amplifiers, mixers, and guitar pedals since 1952.

Bass: low shelf around fBass (default 250 Hz)
Treble: high shelf around fTreble (default 4 kHz)
Independence: bass and treble controls are cascaded, not interactive — each shelf is independent

import { baxandall } from 'audio-filter/eq'

baxandall(buffer, { bass: +6, treble: -3, fs: 44100 })                           // default pivot freqs
baxandall(buffer, { bass: +4, treble: +2, fBass: 300, fTreble: 6000, fs: 44100 }) // custom pivots

Origin: Peter Baxandall (1952)19
Use when: amp/mixer tone stack simulation, consumer audio tone controls, guitar pedal EQ
vs Parametric EQ: intentionally limited to two knobs — the constraint is the point

Tilt EQ

See-saw around a pivot frequency — one knob trades bass for treble symmetrically.

Positive gain: bass up / treble down — warms up a bright signal
Negative gain: treble up / bass down — brightens a dull signal
Pivot: frequency that stays at 0 dB (default 1 kHz)

import { tilt } from 'audio-filter/eq'

tilt(buffer, { gain: +4, pivot: 1000, fs: 44100 })   // warm up
tilt(buffer, { gain: -3, pivot: 1000, fs: 44100 })   // brighten

Use when: quick tonal correction on a mix bus or stereo source with a single parameter
vs Baxandall: tilt is one knob not two — bass and treble always move equal and opposite

Effect

Signal conditioning and spectral shaping — single-purpose filters with well-defined transfer functions.

DC blocker

Removes DC offset — the simplest useful filter.

$H(z) = \dfrac{1 - z^{-1}}{1 - Rz^{-1}}$

Topology: zero at $z = 1$ (DC), pole at $z = R$
Cutoff: $f_c \approx \frac{(1-R) f_s}{2\pi}$$R = 0.995$ gives ~22 Hz at 44.1 kHz

import { dcBlocker } from 'audio-filter/effect'

let params = { R: 0.995 }
dcBlocker(buffer, params)

Use when: removing DC bias before processing, preventing lowpass filter saturation

DC blocker

Comb filter

Adds a delayed copy of the signal to itself — notches and peaks at harmonics of $f_s / D$.

Feedforward: $H(z) = 1 + g \cdot z^{-D}$ — notches at $f = \frac{(2k+1) f_s}{2D}$
Feedback: $H(z) = \dfrac{1}{1 - g \cdot z^{-D}}$ — peaks at $f = \frac{k \cdot f_s}{D}$

import { comb } from 'audio-filter/effect'

comb(buffer, { delay: 100, gain: 0.6, type: 'feedback' })

Use when: flanging, chorus (with modulated delay), Karplus-Strong string synthesis, room mode modeling

Comb filter

Allpass

Unity magnitude at all frequencies — shifts phase only. First and second order.

First order: $H(z) = \dfrac{a + z^{-1}}{1 + a z^{-1}}$ — pole at $z = -a$, 180° phase shift at Nyquist
Second order: $H(z) = \dfrac{d - 2R\cos(\omega_0)z^{-1} + R^2 z^{-2}}{1 - 2R\cos(\omega_0)z^{-1} + R^2 z^{-2}}$ — 360° phase shift around $\omega_0$

import { allpass } from 'audio-filter/effect'

allpass.first(buffer, { a: 0.5 })                          // coefficient a
allpass.second(buffer, { fc: 1000, Q: 1, fs: 44100 })      // center fc, quality Q

Use when: phase equalization, reverb building blocks (Schroeder reverb), stereo widening

Allpass 2nd order

Pre-emphasis / de-emphasis

First-order highpass (emphasis) and its inverse (de-emphasis) — used before and after coding or transmission.

$H(z) = 1 - \alpha z^{-1}$ (emphasis)  /  $H(z) = \dfrac{1}{1 - \alpha z^{-1}}$ (de-emphasis)

Rolloff: emphasis boosts above $f_c = \frac{(1-\alpha) f_s}{2\pi}$$\alpha = 0.97$ gives ~420 Hz at 44.1 kHz
Inverse pair: deemphasis exactly cancels emphasis$H_e(z) \cdot H_d(z) = 1$

import { emphasis, deemphasis } from 'audio-filter/effect'

emphasis(buffer, { alpha: 0.97 })    // before encoding
deemphasis(buffer, { alpha: 0.97 })  // after decoding — exact inverse

Use when: speech coding (GSM, AMR uses $\alpha = 0.97$), tape recording, FM broadcasting

Pre-emphasis

Resonator

Constant peak-gain bandpass — peak amplitude stays fixed regardless of bandwidth.

$H(z) = \dfrac{1 - R^2}{1 - 2R\cos(\omega_0)z^{-1} + R^2 z^{-2}}$

Pole radius: $R = e^{-\pi \cdot bw / f_s}$ — controls bandwidth; $bw \to 0$ gives infinite Q
Peak gain: always 0 dB by construction — $(1 - R^2)$ normalizes the peak

import { resonator } from 'audio-filter/effect'

resonator(buffer, { fc: 440, bw: 20, fs: 44100 })

Use when: additive synthesis (bells, gongs), modal synthesis, formant bank building
vs Peaking EQ: resonator has fixed 0 dB peak; peaking EQ has variable gain — use resonator for synthesis, EQ for mixing

Resonator

Notch

Band-reject filter — unity gain everywhere except a deep null at fc.

$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}}$

Q: controls notch width — $Q = 30$ is narrow (hum removal); $Q = 5$ is wider (resonance suppression)
Zeros: on the unit circle at $\pm\omega_0$ — exact null, independent of Q

import { notch } from 'audio-filter/effect'

notch(buffer, { fc: 50,   Q: 30, fs: 44100 })   // remove 50 Hz mains hum
notch(buffer, { fc: 1000, Q: 10, fs: 44100 })   // suppress a resonance

Use when: mains hum removal (50/60 Hz), feedback cancellation, room mode suppression
vs Parametric EQ with negative gain: notch reaches −∞ dB exactly at fc; peaking EQ has finite attenuation

Pink noise

Shapes white noise to $1/f$ spectrum — equal energy per octave.

Spectrum: power spectral density $S(f) \propto 1/f$$-3,\text{dB/oct}$ slope, equal energy per octave
Implementation: Voss-McCartney algorithm — sum of white noise sources at octave-spaced update rates; approximated by cascaded first-order IIR filters

import { pinkNoise } from 'audio-filter/effect'

let buf = new Float64Array(1024)
for (let i = 0; i < buf.length; i++) buf[i] = Math.random() * 2 - 1
pinkNoise(buf, {})   // white → pink (−3 dB/oct spectral slope)

Use when: noise testing, psychoacoustic masking reference, procedural audio, natural-sounding noise
vs White noise: white noise has equal energy per Hz ($-0,\text{dB/oct}$); pink is perceptually flat

Pink noise filter

Spectral tilt

Applies a constant dB/octave slope — tilts the entire spectrum.

Model: first-order IIR approximation of fractional power-law spectrum $S(f) \propto f^\alpha$
slope: $\alpha = -3,\text{dB/oct}$ gives pink noise character; $-6,\text{dB/oct}$ gives brownian/red noise

import { spectralTilt } from 'audio-filter/effect'

spectralTilt(buffer, { slope: -3, fs: 44100 })   // −3 dB/oct: brownian noise character
spectralTilt(buffer, { slope: +3, fs: 44100 })   // +3 dB/oct: pre-emphasis for coding

Use when: matching microphone/speaker frequency responses, spectral coloring, noise synthesis

Spectral tilt

Variable bandwidth

Lowpass with continuously variable bandwidth — smooth parameter automation without discontinuities.

Implementation: biquad lowpass with per-sample coefficient update using smooth interpolation
Property: no discontinuity when $f_c$ or $Q$ change — avoids clicks from abrupt coefficient jumps

import { variableBandwidth } from 'audio-filter/effect'

variableBandwidth(buffer, { fc: 2000, Q: 1.0, fs: 44100 })

Use when: LFO-modulated filter cutoff, automated EQ sweeps, smooth filter animation
vs Direct biquad: recalculating biquad coefficients per sample causes zipper noise; variable bandwidth avoids this

Variable bandwidth

Filter selection guide

I need to... Use
Measure SPL or noise level aWeighting (general), cWeighting (peak), itu468 (broadcast noise)
Measure loudness (LUFS/LU) kWeighting
Decode vinyl audio riaa
Model the cochlea / auditory system gammatone, erbBank
Analyze a spectrum in octave bands octaveBank
Psychoacoustic analysis / masking model barkBank
MFCC / speech recognition features melBank
Synth filter — warmth and resonance moogLadder
Synth filter — acid / squelch diodeLadder
Synth filter — 2-pole LP + HP korg35
Synth filter — multimode SVF oberheim
Synthesize vowel sounds formant
Transfer one sound's spectral shape to another vocoder
Analyze/resynthesize speech, change pitch lpcAnalysis / lpcSynthesize
Studio EQ at fixed ISO frequencies graphicEq
Studio EQ with full per-band control parametricEq
Split audio for multi-way speakers crossover
Improve headphone stereo imaging crossfeed
Bass/treble tone control baxandall
One-knob tonal tilt tilt
Standalone bass or treble shelf lowShelf / highShelf
Remove DC offset dcBlocker
Remove mains hum / suppress resonance notch
Create resonant combing comb
Phase-shift without changing magnitude allpass.first, allpass.second
Pre-process for audio coding emphasis / deemphasis
Modal synthesis (bells, drums, rooms) resonator
Generate pink / brown noise pinkNoise + spectralTilt
Tilt spectrum for noise synthesis spectralTilt
Smooth automated filter sweeps variableBandwidth

FAQ

Why does my filter click when I change fc or Q? Biquad coefficients change discontinuously between samples. Use variableBandwidth for smooth automated sweeps, or crossfade.

Why does my Moog/Diode filter blow up? resonance=1 on Moog is intentional self-oscillation. Diode ladder is stable up to 0.95. Limit input gain before high resonance.

Does mutating params between calls reset state? No — mutating the same object (params.fc = newFc) preserves state. Replacing the object (params = { fc: newFc }) loses it.

Why does .coefs(fs) return an SOS array instead of one biquad? 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().

What sample rate should I use for accurate A-weighting? 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).

Recipes

Chain filters

let p1 = { fc: 200, fs: 44100 }
let p2 = { R: 0.995 }
for (let buf of stream) {
  dcBlocker(buf, p2)   // DC removal first
  moogLadder(buf, p1)
}

Stereo — independent state per channel

let pL = { fc: 1000, fs: 44100 }
let pR = { fc: 1000, fs: 44100 }
for (let [L, R] of stereoStream) {
  moogLadder(L, pL)
  moogLadder(R, pR)
}

Frequency analysis

import { freqz, mag2db } from 'digital-filter'

let sos = aWeighting.coefs(44100)
let { magnitude } = freqz(sos, 4096, 44100)
let db = mag2db(magnitude)   // dB at 4096 frequencies, 20 Hz–Nyquist

Multi-band split

let bands = crossover([500, 5000], 4, 44100)   // lo / mid / hi
let [lo, mid, hi] = bands.map(coefs => {
  let buf = Float64Array.from(input)   // copy — filter is in-place
  filter(buf, { coefs })
  return buf
})
// process independently, then sum

Notch out mains hum

let p = { fc: 50, Q: 30, fs: 44100 }
for (let buf of stream) notch(buf, p)   // removes 50 Hz hum, flat elsewhere

Automate cutoff without clicks

let p = { fc: 200, Q: 1.0, fs: 44100 }
for (let buf of stream) {
  p.fc = 200 + lfo() * 1800   // mutate in-place — state preserved
  variableBandwidth(buf, p)
}

Pitfalls

New params object on every call — state resets each block

// Wrong
for (let buf of stream) moogLadder(buf, { fc: 1000, fs: 44100 })

// Right — create once, reuse
let p = { fc: 1000, fs: 44100 }
for (let buf of stream) moogLadder(buf, p)

Shared params for stereo — channels corrupt each other's state

// Wrong
let p = { fc: 1000, fs: 44100 }
for (let [L, R] of stream) { moogLadder(L, p); moogLadder(R, p) }

// Right — one object per channel
let pL = { fc: 1000, fs: 44100 }, pR = { fc: 1000, fs: 44100 }
for (let [L, R] of stream) { moogLadder(L, pL); moogLadder(R, pR) }

Filtering the same buffer twice for multi-band — second band sees pre-filtered input

// Wrong
filter(buffer, { coefs: bands[0] })
filter(buffer, { coefs: bands[1] })   // input already filtered!

// Right — copy per band
let bufs = bands.map(b => { let c = Float64Array.from(buffer); filter(c, { coefs: b.coefs }); return c })

Omitting fs — silently uses 44100 Hz math on 48000 Hz audio

// Wrong — wrong cutoffs at 48 kHz
moogLadder(buffer, { fc: 1000 })

// Right
moogLadder(buffer, { fc: 1000, fs: 48000 })

See also

  • audio-effect — audio effects: phaser, flanger, chorus, wah, compressor, reverb, delay, and more
  • digital-filter — general-purpose filter design: Butterworth, Chebyshev, Bessel, Elliptic, FIR, and more
  • audio-decode — decode audio files to PCM buffers
  • audio-speaker — output PCM audio to system speakers
  • Web Audio API — browser built-in audio; basic biquad shapes only, requires AudioContext

Footnotes

  1. IEC 61672-1:2013, Electroacoustics — Sound level meters — Part 1: Specifications. Supersedes IEC 651:1979. 2

  2. ITU-R BS.1770-4:2015, Algorithms to measure audio programme loudness and true-peak audio level. Adopted by EBU R128.

  3. ITU-R BS.468-4:1986, Measurement of audio-frequency noise voltage level in sound broadcasting. Originally CCIR 468, 1968.

  4. RIAA standard (1954); IEC 60098:1987, Analogue audio disk records and reproducing equipment.

  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.

  6. IEC 61260-1:2014, Electroacoustics — Octave-band and fractional-octave-band filters — Part 1: Specifications. ANSI S1.11:2004.

  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.

  8. Zwicker, E. (1961). "Subdivision of the audible frequency range into critical bands." JASA 33(2), p. 248.

  9. O'Shaughnessy, D. (2000). Speech Communications: Human and Machine, 2nd ed. IEEE Press.

  10. Zavalishin, V. (2012). The Art of VA Filter Design. Native Instruments. 2 3 4

  11. Moog, R.A. (1965). Voltage controlled electronic music modules. Patent US3475623.

  12. Pirkle, W.C. (2019). Designing Audio Effect Plugins in C++, 2nd ed. Routledge.

  13. Stilson, T. & Smith, J.O. (1996). "Analyzing the Moog VCF with considerations for digital implementation." Proc. ICMC.

  14. Dudley, H. (1939). "The vocoder." Bell Laboratories Record 17, pp. 122–126. Patent US2151091.

  15. Atal, B.S. & Hanauer, S.L. (1971). "Speech Analysis and Synthesis by Linear Prediction of the Speech Wave." JASA 50(2B), pp. 637–655.

  16. Zölzer, U. (2011). DAFX: Digital Audio Effects, 2nd ed. Wiley.

  17. Linkwitz, S. & Riley, R. (1976). "Active Crossover Networks for Non-Coincident Drivers." JAES 24(1), pp. 2–8.

  18. Bauer, B.B. (1961). "Stereophonic Earphones and Binaural Loudspeakers." JAES 9(2), pp. 148–151.

  19. Baxandall, P.J. (1952). "Transistor Tone-Control Design." Wireless World 58(10), pp. 402–405.

About

Canonical audio filters implementations

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors