What filters do to gain and timing.
On this page
A filter keeps some frequencies of a signal and turns others down. Each frequency gets a gain (how much it is scaled) and a phase shift (how much it is delayed). FIR filters build each output from recent inputs only; IIR filters also feed back their past outputs. Both act once per sample, and designing one means choosing its weights so the gain is right where it matters, paid for in delay and computation.
The filters here are LTI systems, linear and time-invariant (double the input and the output doubles; the filter never changes), so a sine goes in and the same sine comes out, scaled by the magnitude response |H(f)| and shifted by the phase response ∠H(f).
A finite impulse response (FIR) filter makes each output a weighted sum of the newest input and a few past inputs; the weights are called taps.
Figure 2. 1The newest four samples, each times ¼. 2Their sum is y[n].
This sliding weighted sum is a convolution, the same operation a convolution layer computes (CNN Basics: Convolution, Stride and Pooling).
An infinite impulse response (IIR) filter also feeds its own output back in, like an echo that adds weaker and weaker copies of the sound. Each copy is the last one times a feedback weight a between 0 and 1.
A level meter’s smoother keeps half of its last output (a = 0.5) and adds half of the new sample (b = 0.5). It turns one click into 0.5, 0.25, 0.125 and on, halving every sample.
The one-pole low-pass from the card, run on noise with lfilter.
import numpy as np
from scipy import signal
FS = 8000
a = 0.8 # a in the card
x = np.random.default_rng(0).standard_normal(FS)
y = signal.lfilter([1 - a], [1, -a], x)
A low-pass passes slow changes and cuts fast ones. Its cutoff is where the gain bends down, by convention the −3 dB point, where the power has halved and the amplitude is about 0.71 of the input’s.
Figure 4. 1Each echo is a times the last. 2The −3 dB point falls as a grows.
A 2-tap average, b = [0.5, 0.5], passes a steady level whole and removes the fastest wiggle, at the Nyquist limit, where its two taps cancel.
The frequency response of a 31-tap FIR low-pass with freqz.
import numpy as np
from scipy import signal
FS, N_TAPS, CUTOFF_HZ = 16000, 31, 1000
b = signal.firwin(N_TAPS, CUTOFF_HZ, fs=FS) # taps b_i
f, H = signal.freqz(b, worN=1024, fs=FS)
gain_db = 20 * np.log10(np.abs(H))
phase = np.unwrap(np.angle(H))
The smoother of Section 3, a = b = 0.5, has gain 1 (0 dB) on a steady level and 1/3 (−9.5 dB) at the Nyquist limit.
freqz evaluates this ratio for either kind.The IIR filter used from here on is a Butterworth low-pass, the IIR design with the flattest possible passband before the gain falls away smoothly. Its order is its number of poles, the past outputs it feeds back: the filter of Section 3 has one, and order 4 has four, which cuts more steeply. SciPy builds it from second-order sections, a chain of small two-pole filters, which keeps the arithmetic stable. Figure 5 runs a 4th-order Butterworth with its cutoff at 1 kHz, on audio sampled at 16 kHz.
Figure 5. 1The dot reads the gain. The output lags, and shrinks above 1 kHz.
A click is many sines whose peaks meet at one instant. In Figure 6 the thin lines are three such sines, ticks mark their peaks and the thick line is their sum. Delay them all equally and the click survives; delay them unequally and it smears.
Symmetric FIR taps delay every frequency equally, so the phase is a straight line in frequency, called linear phase. A 5-tap symmetric filter delays everything by 2 samples, 125 µs at 16 kHz; a 101-tap one by 50 samples, 3.125 ms.
The Butterworth of Figure 5 cannot do this; its delay changes with frequency, most near the cutoff. A straight phase line delays every frequency equally, so the ringing at a pulse’s edges stays even; a bent one delays some frequencies longer and makes it lopsided (Figure 7).
Figure 7. 1Ringing at each edge. 2Straight phase, even; bent phase, lopsided.
Offline, the whole recording is there, future included, so the filter can run forward and then backward over the result, which needs the signal reversed in time (Anushiravani 2016). A 4th-order Butterworth low-pass at 1 kHz shows what the second pass changes.
| Passes | Gain at 1 kHz | Where the gain is −3 dB |
|---|---|---|
| Forward only | −3 dB | 1 kHz |
| Forward, then backward | −6 dB | about 898 Hz |
| FIR | IIR | |
|---|---|---|
| Taps for a sharp edge | Many | Few |
| Phase | Can be exactly linear | Not linear |
| Stability | Always stable | Needs the feedback to fade (|a| < 1 here) |
| Delay | M/2 samples, the same for all frequencies | Small, but changes with frequency |
| Pick it when | The waveform’s shape matters | Compute is tight and timing does not matter |
Filters run per sample on a live stream, such as a hum notch or the low-pass before decimation (keeping only every second or fourth sample), or over a whole file offline.
| Step | How often, on what data | State it keeps | Cost |
|---|---|---|---|
Design (firwin, butter) | once at design time, on the specification | none | one-off |
| FIR filter | once per sample, on the newest inputs | one input per tap | one multiply per tap |
| IIR filter | once per sample, on the input and past outputs | past outputs, the feedback state | a few multiplies per section |
Zero phase (filtfilt) | once per recording, offline, on the whole recording | none between recordings | two passes of the filter |
Rates, state and cost come from the sources in References.
One noise signal through a 4th-order Butterworth low-pass, live with sosfilt and offline with sosfiltfilt.
import numpy as np
from scipy import signal
FS, ORDER, CUTOFF_HZ = 16000, 4, 1000
x = np.random.default_rng(0).standard_normal(FS)
sos = signal.butter(ORDER, CUTOFF_HZ, fs=FS, output='sos')
y_live = signal.sosfilt(sos, x)
y_off = signal.sosfiltfilt(sos, x)
| Setting | Default | Source |
|---|---|---|
butter output | 'ba' by default; 'sos' recommended | scipy.signal.butter |
firwin window | 'hamming'; the cutoff is the −6 dB point | scipy.signal.firwin |
sosfilt start | zi=None, starting from silence | scipy.signal.sosfilt |
| Knob, where it enters | Raise it | Lower it | Default |
|---|---|---|---|
| Order M, the index of the last tap in the FIR card | Sharper edge, more delay and cost per sample | Gentler edge; M = 0 is a plain gain | No default; numtaps = M + 1 is required (scipy.signal.firwin) |
| Cutoff fc, where the gain bends down | More of the band passes; at the Nyquist frequency nothing is cut | More is cut; near 0 only the slow trend survives | No default; Wn is required. butter puts it at the −3 dB point, firwin at −6 dB (scipy.signal.butter) |
| Feedback a in the one-pole card | Longer echo, lower −3 dB point; a = 1 never fades | Shorter echo; a = 0 passes the input unchanged | None; lfilter takes the given weights (scipy.signal.lfilter) |
SciPy documents butter, firwin and sosfilt.
output='sos'.firwin with an even tap count cannot pass Nyquist and raises an error for a high-pass.zf back as zi, or each block restarts from silence.| What you see | Likely cause | What to do |
|---|---|---|
| Ripples near sharp edges in the output | A short FIR with a sharp edge rings (ripples on both sides of each edge) | More taps, or a gentler edge |
| Events look shifted in time after filtering | The filter's delay | Shift back by M/2 for an FIR, or use sosfiltfilt offline |
| The cutoff lands far from where you set it | The cutoff was read in the wrong units | Pass fs= with the cutoff in hertz, or give the cutoff as a fraction of the Nyquist frequency |
Spectral Subtraction for Denoising and The Wiener Filter set a gain per frequency from the noise; the anti-alias filter on Aliasing and Anti-Aliasing Filters is this page’s Butterworth.
How does the spectrum change over time? That is The STFT and Spectrograms.