Digital filter design and processing.
|
IIR FIR Smooth |
Adaptive Multirate Core |
npm install digital-filter
import { butterworth, filter, onePole } from 'digital-filter'
// import butterworth from 'digital-filter/iir/butterworth.js'
onePole(data, { fc: 100, fs: 44100 }) // smooth in-place
let sos = butterworth(4, 1000, 44100) // design a 4th-order lowpass
filter(data, { coefs: sos }) // apply in-place
let params = { coefs: sos }
filter(block1, params) // state persists between blocks
filter(block2, params)For audio-domain filters (weighting, EQ, synth, measurement) see audio-filter.
Filter. Takes an array of samples, outputs an array of samples. output[i] = (input[i] + input[i-1] + input[i-2]) / 3 smooths out fast changes – that's a lowpass.
Frequency response. Every filter passes some frequencies and cuts others. The plots show how much each frequency is kept (magnitude, in dB) and how much it's delayed (phase). 0 dB = unchanged, –3 dB = half power.
IIR vs FIR.
| IIR | FIR | |
|---|---|---|
| How | Feedback (output depends on previous output) | No feedback |
| Efficiency | 5–20 multiplies for sharp lowpass | 100–1000+ multiplies |
| Phase | Nonlinear (always) | Linear (symmetric coefficients) |
| Stability | Can blow up | Always stable |
| Latency | Low (few samples) | High (N/2 samples) |
| Adaptive | Hard to adapt coefficients in real time | Easy – coefficients are the taps (LMS, NLMS) |
| Use for | Real-time, low latency | Offline, linear phase, adaptive |
Adaptive. Means the filter adjusts its own coefficients in real time to minimize error against a reference signal — used for echo cancellation, noise cancellation, system identification.
SOS. Second-Order Sections – an IIR filter split into a chain of biquads (2nd-order, 5 coefficients each). A 4th-order Butterworth = 2 biquads. All design functions return SOS arrays to avoid float64 precision loss.
Plots. Four panels. Top-left: magnitude (dB vs Hz). Top-right: phase (degrees vs Hz). Bottom-left: group delay (samples vs Hz), flat = no distortion. Bottom-right: impulse response. Dashed line =
Formulas.
IIR filters use feedback – efficient (5–20 multiplies for a sharp lowpass), low latency, nonlinear phase. Designed from analog prototypes via the bilinear transform (or matched-z transform), implemented as cascaded second-order sections (SOS).1
Nine second-order filter types – the building block for everything else. Every parametric EQ, every crossover, every Butterworth cascade is made of these.2
biquad.lowpass(fc, Q, fs)·highpass·bandpass·bandpass2·notch·allpassbiquad.peaking(fc, Q, fs, dBgain)·lowshelf·highshelf
Q controls peak width – 0.707 is Butterworth-flat, higher = sharper resonance.
let lp = biquad.lowpass(1000, 0.707, 44100)
filter(data, { coefs: lp })Use when: single-band EQ, notch, shelf, simple 2nd-order filter.
Not for: steeper than –12 dB/oct (use butterworth/chebyshev which cascade biquads).
scipy: scipy.signal.iirfilter(1, ...). MATLAB: various Audio Toolbox functions.
State variable filter – same transfer function as a biquad, but trapezoidal integration allows zero-delay feedback. Safe for per-sample parameter modulation. Six simultaneous outputs. Simper/Cytomic (2011). Params: fc, Q, fs, type.
svf(data, { fc: 1000, Q: 2, fs: 44100, type: 'lowpass' })Use when: real-time synthesis with parameter modulation (LFO, envelope, touch).
Not for: need SOS coefficients for analysis (use biquad), higher than 2nd order.
Maximally flat magnitude – no ripple anywhere. The safe default for anti-aliasing, crossovers, general-purpose filtering. Butterworth (1930).3
Poles at
–3 dB at fc · –6N dB/oct slope · 10.9% overshoot at order 4 · 73 samples settling
let sos = butterworth(4, 1000, 44100)
filter(data, { coefs: sos })Use when: general-purpose filtering, anti-aliasing, crossovers.
Not for: sharpest transition (use chebyshev/elliptic), waveform preservation (use bessel).
scipy: scipy.signal.butter. MATLAB: butter.
Steeper cutoff than Butterworth for the same order – at the cost of passband ripple.
Default 1 dB ripple · –34 dB at 2× fc · 8.7% overshoot · 256 samples settling
let sos = chebyshev(4, 1000, 44100, 1) // 1 dB rippleUse when: sharper cutoff than Butterworth, passband ripple tolerable.
Not for: passband flatness (use butterworth/legendre), waveform shape (use bessel).
scipy: scipy.signal.cheby1. MATLAB: cheby1.
Flat passband, equiripple stopband. The ripple goes into the rejection region instead.
Flat passband · –40 dB stopband floor · –40 dB at 2× fc
let sos = chebyshev2(4, 2000, 44100, 40) // 40 dB rejectionUse when: flat passband needed with sharper rolloff than Butterworth.
Not for: deep stopband at high frequencies (Butterworth keeps falling; Cheby II bounces).
scipy: scipy.signal.cheby2. MATLAB: cheby2.
Sharpest transition for a given order – ripple in both passband and stopband. A 4th-order elliptic matches a 7th-order Butterworth. Cauer (1958).4
Default 1 dB ripple, 40 dB attenuation · –40 dB at 2× fc · 10.6% overshoot
let sos = elliptic(4, 1000, 44100, 1, 40)Use when: minimum order / sharpest transition is critical.
Not for: passband flatness or waveform shape (worst phase response of all families).
scipy: scipy.signal.ellip. MATLAB: ellip.
Maximally flat group delay – preserves waveform shape with near-zero overshoot. For biomedical signals (ECG, EEG), control systems, anywhere ringing distorts the measurement. Thomson (1949).5
–3 dB at fc · –14 dB at 2× fc (gentlest rolloff) · 0.9% overshoot · 28 samples settling
let sos = bessel(4, 1000, 44100)Use when: waveform preservation (ECG, transients, control systems).
Not for: sharp frequency cutoff (gentlest rolloff of all families).
scipy: scipy.signal.bessel. MATLAB: besself (analog only).
Steepest monotonic (ripple-free) rolloff. Between Butterworth and Chebyshev. Papoulis (1958), Bond (2004).6
–3 dB at fc · –31 dB at 2× fc · no ripple · 11.3% overshoot
let sos = legendre(4, 1000, 44100)Use when: sharpest cutoff without any ripple.
Not for: ripple tolerable (chebyshev is steeper), waveform shape (use bessel).
Crossover: LP + HP sum to perfectly flat magnitude. Two cascaded Butterworth filters. Linkwitz & Riley (1976).7 Returns { low, high }. Order must be even (2, 4, 6, 8).
–6 dB at fc (both bands) · bands sum to 0 dB at all frequencies
let { low, high } = linkwitzRiley(4, 2000, 44100)Use when: crossover networks, multiband processing.
Not for: single LP or HP (use butterworth directly), more than 2 bands (use crossover from audio-filter).
MATLAB: fdesign.crossover (Audio Toolbox).
Give it your specs and it picks the best family and minimum order automatically. Returns { sos, order, type }.
let { sos, order, type } = iirdesign(1000, 1500, 1, 40, 44100)Use when: you know specs (passband, stopband, ripple, attenuation) but not the family.
Not for: specific family needed (use butterworth/chebyshev/etc directly).
scipy: scipy.signal.iirdesign. MATLAB: designfilt.
Estimate minimum filter order needed to meet specs. Returns { order, Wn }.
let { order, Wn } = buttord(1000, 1500, 1, 40, 44100) // → order ≈ 13
let { order } = ellipord(1000, 1500, 1, 40, 44100) // → order ≈ 5 (much lower)scipy: scipy.signal.buttord, cheb1ord, cheb2ord, ellipord.
MATLAB: buttord, cheb1ord, cheb2ord, ellipord.
All at order 4,
| Butterworth | Chebyshev I | Chebyshev II | Elliptic | Bessel | Legendre | |
|---|---|---|---|---|---|---|
| Passband | Flat | 1 dB ripple | Flat | 1 dB ripple | Flat (soft) | Flat |
| @2 kHz | –24 dB | –34 dB | –40 dB | –40 dB | –14 dB | –31 dB |
| Overshoot | 10.9% | 8.7% | 13.0% | 10.6% | 0.9% | 11.3% |
| Best for | General | Sharp cutoff | Flat pass | Min order | No ringing | Sharp, no ripple |
Finite impulse response – no feedback, always stable. Symmetric coefficients give perfect linear phase. More taps = sharper cutoff = more latency. All design functions return Float64Array.
Window method FIR – truncated sinc multiplied by a window function. Supports lowpass, highpass, bandpass, bandstop. Default window: Hamming.
Linear phase · delay = (N–1)/2 samples · –43 dB sidelobes (Hamming)
let h = firwin(63, 1000, 44100)Use when: 80% of FIR tasks – quick, predictable LP/HP/BP/BS.
Not for: tight specs (use remez), arbitrary shapes (use firwin2).
scipy: scipy.signal.firwin. MATLAB: fir1.
Least-squares optimal – minimizes total squared error. Smoother transitions than remez.
Linear phase · smooth transition · no equiripple
let h = firls(63, [0, 0.3, 0.4, 1], [1, 1, 0, 0])Use when: average error matters more than worst-case, audio interpolation.
Not for: tight stopband specs (use remez).
scipy: scipy.signal.firls. MATLAB: firls.
Parks-McClellan equiripple – narrowest transition band for given taps. Parks & McClellan (1972).8
Linear phase · equiripple · sharpest cutoff per tap
let h = remez(63, [0, 0.3, 0.4, 1], [1, 1, 0, 0])Use when: tight specs, guaranteed worst-case rejection.
Not for: sidelobes must decay (use firwin with window), average error (use firls).
scipy: scipy.signal.remez. MATLAB: firpm.
Arbitrary magnitude response via frequency sampling. Specify gain at any frequency points.
Linear phase · any shape
let h = firwin2(201, [0, 0.1, 0.2, 0.4, 0.5, 1], [0, 0, 1, 1, 0, 0])Use when: custom EQ curves, matching measured responses.
scipy: scipy.signal.firwin2. MATLAB: fir2.
90° phase shift at unity magnitude. For analytic signal, envelope extraction, SSB modulation, pitch detection.
Linear phase · zero at DC and Nyquist
let h = hilbert(65)Use when: analytic signal, envelope, instantaneous frequency.
Not for: wideband to DC (Hilbert is zero at DC/Nyquist).
scipy: scipy.signal.hilbert (different – applies to signal, not design).
MATLAB: hilbert (same caveat).
FIR derivative with better noise immunity than a first difference.
Antisymmetric · linear phase
let h = differentiator(31)Use when: rate-of-change, velocity from position, edge detection.
Not for: noisy data needing simultaneous smoothing (use savitzkyGolay with derivative:1).
Pulse shaping for digital communications (QAM, PSK, OFDM). Zero ISI at symbol centers. root: true for matched TX/RX pair.
let h = raisedCosine(101, 0.35, 8, { root: true })Use when: QAM/PSK pulse shaping, SDR baseband.
MATLAB: rcosdesign.
Gaussian pulse shaping – the standard for GMSK (GSM) and Bluetooth. More spectrally compact than raised cosine at the cost of some ISI.
let h = gaussianFir(33, 0.3, 4)Use when: GMSK/Bluetooth modulation.
Not for: ISI-free pulses (use raisedCosine).
MATLAB: gaussdesign.
Optimal detector for a known waveform in white noise – time-reversed, energy-normalized template. Maximizes SNR at detection point.
let h = matchedFilter(template)
let corr = convolution(received, h)Use when: radar, sonar, preamble/sync detection.
Not for: colored noise (pre-whiten first), unknown template (use adaptive).
Convert linear-phase FIR to minimum-phase via cepstral method. Same magnitude, ~half the delay.
let hMin = minimumPhase(firwin(101, 4000, 44100))Use when: FIR latency too high, linear phase not required.
scipy: scipy.signal.minimum_phase.
IIR approximation of an arbitrary magnitude response via Yule-Walker method. Returns { b, a }.
let { b, a } = yulewalk(8, [0, 0.2, 0.3, 0.5, 1], [1, 1, 0, 0, 0])Use when: IIR match to target curve. MATLAB: yulewalk.
Newton-Cotes quadrature coefficients. Rules: rectangular [1], trapezoidal [0.5, 0.5], simpson [1/6, 4/6, 1/6], simpson38 [1/8, 3/8, 3/8, 1/8].
Simpson:
let h = integrator('simpson')Use when: numerical integration of sampled data.
Lattice/ladder IIR using reflection coefficients (PARCOR). Alternative topology to direct form – each stage is independently stable when k (reflection coefficients), v (ladder/feedforward, optional).
// Use reflection coefficients from levinson LPC analysis
let { k } = levinson(autocorrelation, 12)
lattice(data, { k }) // apply as synthesis filterUse when: LPC synthesis, speech coding, high-precision filtering where numerical stability matters.
Not for: general filtering (use filter with SOS).
MATLAB: latcfilt.
Frequency-warped FIR – replaces unit delays with allpass delays. Concentrates frequency resolution at low frequencies, matching how the ear perceives pitch. Params: coefs, lambda (warping factor).
// lambda ≈ 0.7 for 44.1 kHz maps to Bark-like resolution
warpedFir(data, { coefs: new Float64Array([0.5, 0.3, 0.15, 0.05]), lambda: 0.7 })Use when: perceptual audio coding, low-order EQ that sounds better than uniform-resolution FIR.
Not for: linear phase (warped FIR is not linear phase).
Estimates how many FIR taps you need and what Kaiser window firwin.
// "I need 60 dB rejection with 10% of Nyquist transition width"
let { numtaps, beta } = kaiserord(0.1, 60)
// numtaps ≈ 55, beta ≈ 5.65
let h = firwin(numtaps, 4000, 44100, { window: kaiser(numtaps, beta) })Use when: estimating FIR order before designing with firwin.
scipy: scipy.signal.kaiserord. MATLAB: kaiserord.
| Method | Optimality | Best for | Weakness |
|---|---|---|---|
firwin |
Good enough | Quick prototyping, 80% of tasks | Not optimal for tight specs |
firls |
Least-squares | Smooth specs, audio | Wider transition than remez |
remez |
Minimax | Sharpest transitions, tight specs | Convergence issues at high order |
firwin2 |
Frequency sampling | Arbitrary shapes, EQ curves | Not formally optimal |
Smoothing and denoising. All operate in-place: fn(data, params) → data.
Exponential moving average – the simplest IIR smoother. One multiply, no overshoot. Params: fc, fs.
–6 dB/oct · 1 multiply · IIR
onePole(data, { fc: 100, fs: 44100 })Use when: smoothing control signals, sensor data, parameter changes.
Not for: sharp cutoff (use butterworth), preserving peaks (use savitzkyGolay).
scipy: scipy.signal.lfilter([1-a], [1, -a]). MATLAB: filter(1-a, [1 -a], x).
Boxcar average of last N samples. Params: memory.
Linear phase · FIR · nulls at multiples of fs/N
movingAverage(data, { memory: 8 })Use when: periodic noise removal, simple averaging.
Not for: preserving peaks (use savitzkyGolay).
MATLAB: movmean.
Exponential decay accumulator. Same as onePole but parameterized by decay factor. Params: lambda (0–1).
1 multiply · IIR
leakyIntegrator(data, { lambda: 0.95 })Use when: running average, DC estimation.
Polynomial fit to sliding window – preserves peak height, width, and shape. Also computes smooth derivatives. Savitzky & Golay (1964).9 Params: windowSize, degree, derivative.
Fits polynomial of degree
FIR · linear phase · preserves moments up to degree
savitzkyGolay(data, { windowSize: 11, degree: 3 })Use when: spectroscopy, chromatography, peak-sensitive measurement.
Not for: frequency-selective filtering, causal/online processing.
scipy: scipy.signal.savgol_filter. MATLAB: sgolayfilt.
Recursive Gaussian (Young-van Vliet). O(1) cost regardless of sigma. Forward-backward for zero phase. Params: sigma.
Approximates
IIR · zero phase (offline) · O(1) per sample
gaussianIir(data, { sigma: 10 })Use when: large-kernel Gaussian smoothing.
Not for: exact Gaussian (approximation), causal filtering.
Self-adjusting SVF – cutoff adapts to signal speed. For smoothing parameter changes without zipper noise. Params: minFc, maxFc, sensitivity, fs.
Adaptive · 2nd-order SVF
dynamicSmoothing(data, { minFc: 1, maxFc: 5000, sensitivity: 1, fs: 44100 })Use when: audio parameter smoothing at audio rate.
Not for: non-audio rate (use oneEuro).
Nonlinear – replaces each sample with neighborhood median. Removes impulse noise while preserving edges. Params: size.
Nonlinear · preserves edges · O(N log N) per sample
median(data, { size: 5 })Use when: clicks, pops, sensor outliers, edge-preserving denoising.
Not for: frequency-selective filtering (no defined frequency response).
scipy: scipy.signal.medfilt. MATLAB: medfilt1.
Adaptive lowpass – cutoff increases with signal speed. Smooth at rest, responsive when moving. Casiez et al. (2012).10 Params: minCutoff, beta, dCutoff, fs.
Adaptive · 1st-order IIR with time-varying cutoff
oneEuro(data, { minCutoff: 1, beta: 0.007, fs: 60 })Use when: mouse/touch/gaze input, sensor fusion, UI jitter.
Not for: audio-rate processing (use dynamicSmoothing).
| Filter | Type | Phase | Edge preservation | Adaptive | Best for |
|---|---|---|---|---|---|
onePole |
IIR | Nonlinear | No | No | Simplest smoothing |
movingAverage |
FIR | Linear | No | No | Periodic noise, simple |
leakyIntegrator |
IIR | Nonlinear | No | No | Decay-factor control |
median |
Nonlinear | — | Yes | No | Impulse noise, outliers |
savitzkyGolay |
FIR | Linear | Yes (peaks) | No | Peak-preserving measurement |
gaussianIir |
IIR | Zero (offline) | No | No | Large-kernel smoothing |
oneEuro |
IIR | Nonlinear | No | Yes | UI jitter, sensor fusion |
dynamicSmoothing |
IIR | Nonlinear | No | Yes | Audio parameter smoothing |
Filters that learn from a reference signal. Returns filtered output; params.error contains error; params.w updated in place.
Least Mean Squares – stochastic gradient descent. Widrow & Hoff (1960).11 Params: order, mu.
O(N)/sample · slow convergence · very robust
lms(input, desired, { order: 128, mu: 0.01 })Use when: educational, simple implementation.
Not for: practice – almost always use nlms instead.
MATLAB: dsp.LMSFilter.
Normalized LMS – step size adapts to input power. The practical default. Params: order, mu (0–2), eps.
O(N)/sample · medium convergence · robust
nlms(farEnd, microphone, { order: 512, mu: 0.5 })
// params.error = cleaned signalUse when: echo cancellation, noise cancellation, system identification. Start here.
Not for: fastest convergence (use rls).
MATLAB: dsp.LMSFilter (normalized mode).
Recursive Least Squares – fastest convergence (~2N samples) via inverse correlation matrix. Params: order, lambda, delta.
O(N²)/sample · fast convergence · fragile if lambda wrong · use for N ≤ 64
rls(input, desired, { order: 32, lambda: 0.99, delta: 100 })Use when: fast-changing systems, short filters.
Not for: N > 128 (O(N²) too expensive), robustness matters (use nlms).
MATLAB: dsp.RLSFilter.
Levinson-Durbin recursion – takes autocorrelation values, returns LPC prediction coefficients. The standard way to compute linear prediction for speech, spectral estimation, and lattice filter coefficients. Returns { a, error, k } (prediction coefficients, prediction error power, reflection coefficients).
Solves
O(N²)/block · batch (not real-time)
// Compute autocorrelation of a speech frame, then solve for LPC coefficients
let R = new Float64Array(13)
for (let lag = 0; lag < 13; lag++)
for (let i = 0; i < frame.length - lag; i++)
R[lag] += frame[i] * frame[i + lag]
let { a, error, k } = levinson(R, 12)
// a = prediction coefficients, k = reflection coefficients for latticeUse when: LPC analysis, speech coding (CELP, LPC-10), AR spectral estimation.
Not for: real-time sample-by-sample adaptation (use nlms/rls).
MATLAB: levinson.
| Algorithm | Complexity | Convergence | Tracking | Stability | Best for |
|---|---|---|---|---|---|
lms |
O(N)/sample | Slow | Slow | Very robust | Educational |
nlms |
O(N)/sample | Medium | Medium | Robust | Real-world default |
rls |
O(N²)/sample | Fast (~2N) | Fast | Fragile | Short filters, fast-changing |
levinson |
O(N²)/block | Instant (batch) | N/A | Stable | LPC, speech coding |
Change sample rates without aliasing. Anti-alias before decimating, anti-image after interpolating.
Anti-alias FIR lowpass + downsample by factor M. Returns shorter Float64Array.
let down = decimate(data, 4)Use when: reducing sample rate, downsampling for analysis.
scipy: scipy.signal.decimate. MATLAB: decimate.
Upsample by factor L + anti-image FIR lowpass. Returns longer Float64Array.
let up = interpolate(data, 4)Use when: increasing sample rate, upsampling before nonlinear processing.
scipy: scipy.signal.resample_poly. MATLAB: interp.
Half-band FIR – nearly half the coefficients are zero, halving multiply count. The building block for efficient 2× rate changes.
let h = halfBand(31)Use when: efficient 2× decimation/interpolation. Cascade for 4×, 8×.
Not for: non-2x rate changes.
Cascaded Integrator-Comb – multiplier-free decimation. Only additions and subtractions.
Multiplier-free · sinc-shaped passband droop
let down = cic(data, 8, 3)Use when: high decimation ratios (10×–1000×), hardware/FPGA, SDR.
Not for: precision (sinc-shaped passband droop needs compensation).
MATLAB: dsp.CICDecimator.
Decompose FIR into M polyphase components. Compute only the output samples you keep. Returns Array<Float64Array>.
let phases = polyphase(firCoefs, 4)Use when: efficient multirate filtering.
Fractional delay via polynomial interpolation. Delay can change every sample. Params: delay, order.
farrow(data, { delay: 3.7, order: 3 })Use when: pitch shifting, variable-rate resampling, time-stretching.
Not for: fixed delay (use thiran – allpass, preserves all frequencies).
Allpass fractional delay – unity magnitude, maximally flat group delay. Returns { b, a }.
let { b, a } = thiran(3.7)Use when: physical modeling synthesis (waveguide strings, tubes).
Not for: variable delay per sample (use farrow).
Multi-stage upsampling with anti-alias FIR. Oversample before nonlinear processing, then decimate back.
Cascade of interpolation stages with anti-alias FIR at each.
let up = oversample(data, 4)Use when: oversampling before distortion/waveshaping/saturation.
Not for: integer rate changes (use decimate/interpolate).
Apply coefficients, analyze responses, convert formats.
Applies SOS coefficients to data in-place. Direct Form II Transposed. State persists between calls. Params: coefs.
let sos = butterworth(4, 1000, 44100)
let params = { coefs: sos }
filter(block1, params) // state preserved
filter(block2, params) // seamlessArbitrary-order IIR processing. Direct Form II Transposed with feedforward/feedback coefficient arrays (up to any order). Params: b (numerator), a (denominator), state.
iir(data, { b: [0.1, 0.2, 0.1], a: [1, -0.8, 0.2] })Use when: need direct transfer function form (b/a arrays), higher than 2nd order without SOS, Web Audio IIRFilterNode compatibility.
Zero-phase forward-backward filtering. Doubles effective order, eliminates phase distortion. Offline only. Params: coefs.
filtfilt(data, { coefs: butterworth(4, 1000, 44100) })Remove DC offset or linear trend from data in-place. Types: 'linear' (default), 'constant'/'dc'.
detrend(data) // remove linear trend
detrend(data, 'constant') // remove DC offset onlyscipy: scipy.signal.detrend. MATLAB: detrend.
Direct convolution. Returns Float64Array of length N + M – 1.
let out = convolution(signal, firCoefs)Frequency response of SOS filter. Returns { frequencies, magnitude, phase }. Second argument can be a number (evenly spaced points) or an array of Hz values.
let resp = freqz(sos, 512, 44100) // 512 evenly spaced points
let resp = freqz(sos, [100, 1000, 10000], 44100) // at specific frequencies
let dB = mag2db(resp.magnitude) // 20·log10(mag){ frequencies, delay }.
let { frequencies, delay } = groupDelay(sos, 512, 44100)Time-domain analysis. Returns Float64Array.
let ir = impulseResponse(sos, 256)
let step = stepResponse(sos, 256)Filter property tests.
isStable(sos) // all poles inside unit circle?
isMinPhase(sos) // all zeros inside unit circle?
isFir(sos) // a1=a2=0 (no feedback)?
isLinPhase(h) // symmetric/antisymmetric coefficients?Format conversion between SOS, transfer function (b/a), and zeros/poles/gain.
let { zeros, poles, gain } = sos2zpk(sos)
let { b, a } = sos2tf(sos)
let zpk = tf2zpk(b, a)
let sos2 = zpk2sos(zpk)
let sos3 = tf2sos(b, a) // shortcut: tf → zpk → sos
let { b: b2, a: a2 } = zpk2tf(zpk)Compute initial conditions for SOS filter to start in steady state (no transient when input starts at a non-zero value).
let sos = butterworth(4, 1000, 44100)
let zi = sosfilt_zi(sos)
filter(data, { coefs: sos, state: zi }) // no startup transientAnalog prototype → digital SOS pipeline. Used internally by IIR design functions.
transform.polesSos(poles, fc, fs, 'lowpass')
transform.poleZerosSos(poles, zeros, fc, fs, 'bandpass')
transform.prewarp(fc, fs) // bilinear frequency prewarpingWhat does the dB scale mean?
When does phase matter? When waveform shape must be preserved: crossovers (drivers must sum correctly), biomedical (ECG/EEG morphology), communications (intersymbol interference). For EQ, phase is usually inaudible.
What is the bilinear transform? Maps analog prototypes to digital:
When can a filter become unstable? When poles move outside the unit circle. Causes: coefficient quantization (use SOS, not direct form), Q approaching 0, feedback gain too high. Check with isStable(sos). FIR is always stable.
What is aliasing? Frequencies above decimate and interpolate handle anti-aliasing automatically.
| I need to… | Use | Notes |
|---|---|---|
| Remove frequencies above/below a cutoff | butterworth(N, fc, fs) |
Default, flat passband |
| Sharpest possible cutoff | elliptic(N, fc, fs, rp, rs) |
Minimum order for specs |
| Sharp, passband ripple OK | chebyshev(N, fc, fs, ripple) |
Steeper than Butterworth |
| Sharp, no ripple anywhere | legendre(N, fc, fs) |
Between Butterworth and Chebyshev |
| Auto-select family + order | iirdesign(fpass, fstop, rp, rs, fs) |
From specs |
| Notch out one frequency | biquad.notch(fc, Q, fs) |
Q=30 for narrow null |
| Boost/cut a band | biquad.peaking(fc, Q, fs, dB) |
Parametric EQ |
| Split into bands | linkwitzRiley(4, fc, fs) |
LP+HP sum to flat |
| No ringing/overshoot | bessel(N, fc, fs) |
Flat group delay |
| No phase distortion | filtfilt(data, {coefs}) |
Zero-phase, offline |
| Smooth a signal | onePole(data, {fc, fs}) |
Simplest |
| Smooth, preserve peaks | savitzkyGolay(data, {windowSize, degree}) |
Polynomial fit |
| Reduce sensor jitter | oneEuro(data, params) |
Adaptive |
| Cancel echo/noise | nlms(input, desired, params) |
Start here |
| Quick FIR | firwin(N, fc, fs, {type}) |
Window method |
| Sharp FIR | remez(N, bands, desired) |
Parks-McClellan |
| Downsample | decimate(data, factor) |
Anti-alias included |
| Upsample | interpolate(data, factor) |
Anti-image included |
Linear phase needed?
├── Yes → FIR or filtfilt (offline)
└── No
Waveform must be preserved?
├── Yes → bessel
└── No
Passband ripple OK?
├── Yes
│ ├── Stopband ripple also OK? → elliptic
│ └── Stopband monotonic → chebyshev
└── No (passband must be flat)
├── Stopband ripple OK? → chebyshev2
└── No ripple anywhere?
├── Steepest monotonic? → legendre
└── Default → butterworth
for (let f of [60, 120, 180]) filter(data, { coefs: biquad.notch(f, 30, 44100) })let params = { order: 512, mu: 0.5 }
nlms(farEnd, microphone, params)
// params.error = cleaned signalfilter(data, { coefs: butterworth(2, 0.5, 500, 'highpass') }) // baseline wander
filter(data, { coefs: butterworth(4, 40, 500) }) // noise
filter(data, { coefs: biquad.notch(50, 35, 500) }) // powerlinelet htx = raisedCosine(101, 0.35, 8, { root: true })
let shaped = convolution(symbols, htx)let fs = 256
let delta = butterworth(4, [0.5, 4], fs, 'bandpass') // deep sleep
let theta = butterworth(4, [4, 8], fs, 'bandpass') // drowsiness
let alpha = butterworth(4, [8, 13], fs, 'bandpass') // relaxed, eyes closed
let beta = butterworth(4, [13, 30], fs, 'bandpass') // active thinking
let signal = Float64Array.from(data)
filter(signal, { coefs: alpha })
// Band power:
let power = 0
for (let i = 0; i < signal.length; i++) power += signal[i] ** 2
power /= signal.length// Autocorrelation → LPC coefficients → lattice synthesis
let order = 12, R = new Float64Array(order + 1)
for (let k = 0; k <= order; k++)
for (let i = 0; i < frame.length - k; i++)
R[k] += frame[i] * frame[i + k]
let { a, k } = levinson(R, order)
// k = reflection coefficients, use with lattice() for synthesis
// a = prediction coefficients, use with iir() for inverse filteringimport { comb, onePole } from 'audio-filter'
let delay = Math.round(44100 / 440) // A4
let data = new Float64Array(44100)
for (let i = 0; i < delay; i++) data[i] = Math.random() * 2 - 1
comb(data, { delay, gain: 0.996, type: 'feedback' })
onePole(data, { fc: 4000, fs: 44100 })
// delay sets pitch, gain ≈ 1 = long sustain, lower fc = duller (nylon)- FIR when IIR suffices – Butterworth order 4: 10 multiplies. Equivalent FIR: 100+.
- High order when elliptic works – elliptic 4 ≈ Butterworth 12. Use
iirdesign. - Forgetting SOS – never use high-order direct form. This library returns SOS by default.
- filtfilt in real-time – needs entire signal for backward pass.
- LP+HP for crossover – doesn't sum flat. Use
linkwitzRiley. - Q too high – Q > 10 creates tall resonance. For EQ: 0.5–8.
- In-place –
filter()modifies data. Copy first:Float64Array.from(data). - Stale state – state persists in params. New signal → new params.
Generate 4-panel SVG plots (magnitude, phase, group delay, impulse response) for any filter. Used internally for this readme's illustrations; available for downstream packages like audio-filter.
import { plotFilter, plotFir, plotCompare, theme } from 'digital-filter/plot'
import { writeFileSync } from 'node:fs'
// SOS filter → SVG string
writeFileSync('my-filter.svg', plotFilter(sos, 'Butterworth order 4, fc=1kHz'))
// Impulse response → SVG string
writeFileSync('my-fir.svg', plotFir(h, 'firwin lowpass, 63 taps'))
// Compare multiple filters overlaid
writeFileSync('comparison.svg', plotCompare([
['Butterworth', butterworth(4, 1000, 44100)],
['Chebyshev', chebyshev(4, 1000, 44100, 1)],
], 'IIR comparison, fc=1kHz'))plotFilter(sos, title?, opts?) – SOS (biquad cascade) → 4-panel SVG.
plotFir(h, title?, opts?) – impulse response array → 4-panel SVG.
plotCompare(filters, title?, opts?) – multiple SOS overlaid → 4-panel SVG. filters: array of [name, sos] or [name, sos, color].
Options: { fs, bins, color, fill }. Defaults from theme.
theme – mutable object controlling defaults:
theme.colors = ['#4a90d9', '#e74c3c', '#2ecc71', ...] // per-panel or per-series
theme.fill = true // fill under curves
theme.fs = 44100
theme.bins = 2048 // FFT bins
theme.grid = '#e5e7eb' // grid line color
theme.axis = '#d1d5db' // axis color
theme.text = '#6b7280' // label colorRegenerate all plots: npm run plot
Textbooks
- Oppenheim & Schafer, Discrete-Time Signal Processing, 3rd ed, 2009 – the canonical DSP textbook
- J.O. Smith III, Introduction to Digital Filters – free, audio-focused
- Zolzer, DAFX: Digital Audio Effects, 2nd ed, 2011 – audio effects
- Haykin, Adaptive Filter Theory, 5th ed, 2014 – LMS, RLS, Kalman
- Zavalishin, The Art of VA Filter Design – virtual analog, SVF, ZDF
Papers & standards
- RBJ Audio EQ Cookbook (W3C Note, 1998) – biquad coefficient formulas2
- Butterworth, "On the Theory of Filter Amplifiers," 19303
- Thomson, "Delay Networks Having Maximally Flat Frequency Characteristics," 19495
- Papoulis, "Optimum Filters with Monotonic Response," 19586
- Cauer, Synthesis of Linear Communication Networks, 19584
- Parks & McClellan, "Chebyshev Approximation for Nonrecursive Digital Filters," 19728
- Linkwitz, "Active Crossover Networks for Noncoincident Drivers," 19767
- Widrow & Hoff, "Adaptive Switching Circuits," 196011
- Savitzky & Golay, "Smoothing and Differentiation of Data," 19649
- Casiez et al., "1€ Filter," CHI, 201210
- Simper, "Linear Trapezoidal Integrated SVF," Cytomic, 2011
Online
- ccrma.stanford.edu/~jos – J.O. Smith's 4 DSP books (free)
- earlevel.com – biquad tutorials (Nigel Redmon)
- musicdsp.org – audio DSP snippet archive
- dsprelated.com – DSP community and free books
- audio-filter – weighting, EQ, synthesis, measurement, effects
- window-function – collection of window functions
Footnotes
-
Direct form above order ~6 loses precision with float64. Cascaded biquads don't. ↩
-
Robert Bristow-Johnson, Audio EQ Cookbook, 1998. ↩ ↩2
-
S. Butterworth, "On the Theory of Filter Amplifiers," Wireless Engineer, 1930. ↩ ↩2
-
W. Cauer, Synthesis of Linear Communication Networks, 1958. ↩ ↩2
-
W.E. Thomson, "Delay Networks Having Maximally Flat Frequency Characteristics," Proc. IEE, 1949. ↩ ↩2
-
A. Papoulis, "Optimum Filters with Monotonic Response," Proc. IRE, 1958. ↩ ↩2
-
S.H. Linkwitz, "Active Crossover Networks for Noncoincident Drivers," JAES, 1976. ↩ ↩2
-
T.W. Parks, J.H. McClellan, "Chebyshev Approximation for Nonrecursive Digital Filters," IEEE Trans., 1972. ↩ ↩2
-
A. Savitzky, M.J.E. Golay, "Smoothing and Differentiation of Data," Analytical Chemistry, 1964. ↩ ↩2
-
B. Widrow, M.E. Hoff, "Adaptive Switching Circuits," IRE WESCON, 1960. ↩ ↩2