Skip to content

Filter Architecture Gallery

Standards: IEC 61260ANSI S1.11

Choosing a filter architecture is a trade-off: selectivity, passband ripple and phase behaviour cannot all be optimal at once, and each of the five architectures phonometry offers resolves the trade-off differently. Three of them — Butterworth, Chebyshev II and Bessel — place their −3 dB points on the ANSI S1.11 band edges, so for those three the choice changes how a band rejects its neighbours and how it treats transients, not where the band sits. The two equiripple designs are the exception, and section 3 gives the size of it: cheby1 and ellip treat the band edges as their ripple edge, so their bands are effectively wider and every band level reads a few tenths of a decibel high. This page puts the architectures side by side: the comparison at the −3 dB crossover, the full 1/1 and 1/3 octave response gallery, usage examples per architecture, and the Linkwitz-Riley crossover for when the goal is splitting a signal rather than measuring bands.

The design mathematics behind these banks (band edges, poles and zeros, multirate decimation) and the parameter reference live in Filter Banks; proving that a designed bank meets a performance class of IEC 61260-1 is Filter Class Verification.

1. The five architectures at the band edges

Section titled “1. The five architectures at the band edges”

All five designs are compared on the same 1 kHz octave band at 48 kHz. Read the figure twice: the wide view shows how fast each skirt falls away from the band, and the inset around the −3 dB line shows where they cross. What changes between those two views is that Chebyshev I ripples inside the band, Chebyshev II ripples outside it, Elliptic does both and falls fastest, and Bessel is the smoothest and the slowest.

Magnitude response comparison of the five filter architectures for the 1 kHz octave band, with a zoom at the -3 dB crossoverMagnitude response comparison of the five filter architectures for the 1 kHz octave band, with a zoom at the -3 dB crossover
Show the code for this figure
import matplotlib.pyplot as plt
import numpy as np
from scipy.signal import sosfreqz
from phonometry import filters
fs = 48000
fig, ax = plt.subplots(figsize=(9, 5))
for ftype in ("butter", "cheby1", "cheby2", "ellip", "bessel"):
# limits picks out the single 1 kHz octave band
bank = filters.OctaveFilterBank(fs, fraction=1, order=6, limits=[800, 1200],
design=filters.FilterDesign(filter_type=ftype))
idx = int(np.argmin(np.abs(np.array(bank.freq) - 1000)))
fsd = fs / bank.factor[idx] # rate the band actually runs at
w, h = sosfreqz(bank.sos[idx], worN=16384, fs=fsd)
ax.semilogx(w, 20 * np.log10(np.abs(h) + 1e-9), label=ftype)
ax.axhline(-3, color="gray", linestyle=":", label="-3 dB")
# The inset the section is named after: the same curves around the crossover.
from mpl_toolkits.axes_grid1.inset_locator import inset_axes
axins = inset_axes(ax, width="35%", height="45%", loc="upper left", borderpad=3)
for line in ax.get_lines()[:-1]:
axins.semilogx(line.get_xdata(), line.get_ydata(), label=line.get_label())
axins.axhline(-3, color="gray", linestyle=":")
axins.set(xscale="log", xlim=(650, 1500), ylim=(-4, 0.5),
title="Zoom at -3 dB (log scale)")
ax.set(xlim=(100, 8000), ylim=(-80, 5),
xlabel="Frequency [Hz]", ylabel="Magnitude [dB]")
ax.grid(True, which="both", alpha=0.3)
ax.legend()
plt.show()
TypeNameUsage ExampleBest For
butterButterworthoctave_filter(x, fs, design=FilterDesign(filter_type='butter'))General acoustic measurement.
cheby1Chebyshev Ioctave_filter(x, fs, design=FilterDesign(filter_type='cheby1', ripple=0.1))Sharper roll-off at the cost of ripple.
cheby2Chebyshev IIoctave_filter(x, fs, design=FilterDesign(filter_type='cheby2'))Flat passband with stopband zeros.
ellipEllipticoctave_filter(x, fs, design=FilterDesign(filter_type='ellip', ripple=0.1))Maximum selectivity.
besselBesseloctave_filter(x, fs, design=FilterDesign(filter_type='bessel'))Preserving transient waveform shapes (see the group-delay figure of Filter Banks).
Two panels for the 1 kHz octave band at 48 kHz, order 6. Left, grouped bars of the relative attenuation one and two octaves above the band mid frequency for each of the five architectures: Bessel is the shallowest at minus 16 and minus 60 decibels, Butterworth minus 40 and minus 88, Chebyshev I minus 50 and minus 102, Chebyshev II minus 55 and minus 73, Elliptic the steepest at minus 72 and minus 81. Right, the group delay at the band mid frequency in milliseconds: Bessel 1.22, Chebyshev II 1.58, Butterworth 1.74, Elliptic 1.75 and Chebyshev I 1.97Two panels for the 1 kHz octave band at 48 kHz, order 6. Left, grouped bars of the relative attenuation one and two octaves above the band mid frequency for each of the five architectures: Bessel is the shallowest at minus 16 and minus 60 decibels, Butterworth minus 40 and minus 88, Chebyshev I minus 50 and minus 102, Chebyshev II minus 55 and minus 73, Elliptic the steepest at minus 72 and minus 81. Right, the group delay at the band mid frequency in milliseconds: Bessel 1.22, Chebyshev II 1.58, Butterworth 1.74, Elliptic 1.75 and Chebyshev I 1.97

The choice, quantified. Selectivity and group delay move in opposite directions, and the class column of the table below says which of the five you may actually use for a standards-compliant band measurement.

ArchitectureRipple across the band centreAt At Group delay at IEC 61260-1 class (order 6, 48 kHz)
butter0.00 dB−39.6 dB−88.2 dB1.74 ms1
cheby10.10 dB−50.2 dB−101.6 dB1.97 msnone
cheby20.00 dB−55.1 dB−73.0 dB1.58 ms1
ellip0.10 dB−72.1 dB−80.8 dB1.75 msnone
bessel0.21 dB−16.4 dB−60.2 dB1.22 msnone

Read the last column first: three of the five cannot be used for a standards-compliant band measurement at all with the default parameters — the equiripple pair because their band edges are not their −3 dB points (see Filter Banks), Bessel because it rolls off too slowly for the mask. That is a hard constraint, not a preference, and it comes before any of the other columns. Among the two that pass, the choice is a real trade: cheby2 buys 15 dB more rejection one octave out and pays for it with a 15 dB shallower far stopband, because its equiripple floor is pinned at attenuation.

One more thing the table cannot show: rejection only matters when a strong neighbour must not contaminate a weak band, and there the ceiling is the attenuation parameter (72 dB by default), not the architecture.

Show the code for this figure
import numpy as np
from scipy.signal import sosfreqz
# `filters` is the import of the snippet above.
fs = 48000
for ftype in ("butter", "cheby1", "cheby2", "ellip", "bessel"):
# resample=False so the response exists above the decimated Nyquist
bank = filters.OctaveFilterBank(
fs, fraction=1, order=6, limits=[800, 1200],
design=filters.FilterDesign(filter_type=ftype, resample=False))
idx = int(np.argmin(np.abs(np.array(bank.freq) - 1000)))
f_m = float(bank.freq[idx])
w, h = sosfreqz(bank.sos[idx], worN=1 << 17, fs=fs)
mag = 20 * np.log10(np.abs(h) + 1e-15)
mag -= mag.max()
grid = np.linspace(f_m * 0.95, f_m * 1.05, 4001)
_, h_c = sosfreqz(bank.sos[idx], worN=2 * np.pi * grid / fs)
delay = -np.gradient(np.unwrap(np.angle(h_c)), 2 * np.pi * grid)
banked = filters.OctaveFilterBank(
fs, fraction=1, order=6, limits=[800, 1200],
design=filters.FilterDesign(filter_type=ftype))
print(f"{ftype:8s} {mag[np.argmin(abs(w - 2 * f_m))]:7.1f} dB "
f"{mag[np.argmin(abs(w - 4 * f_m))]:7.1f} dB "
f"{delay[len(delay) // 2] * 1e3:5.2f} ms "
f"class {filters.verify_filter_class(banked)['overall_class']}")

Full spectral view of the filter banks for Octave (1/1) and 1/3-Octave fractions.

ArchitectureOctave (fraction=1)One-third octave (fraction=3)
ButterworthButterworth octave-band filter bank frequency responseButterworth octave-band filter bank frequency responseButterworth one-third-octave filter bank frequency responseButterworth one-third-octave filter bank frequency response
Chebyshev IChebyshev I octave-band filter bank frequency responseChebyshev I octave-band filter bank frequency responseChebyshev I one-third-octave filter bank frequency responseChebyshev I one-third-octave filter bank frequency response
Chebyshev IIChebyshev II octave-band filter bank frequency responseChebyshev II octave-band filter bank frequency responseChebyshev II one-third-octave filter bank frequency responseChebyshev II one-third-octave filter bank frequency response
EllipticElliptic octave-band filter bank frequency responseElliptic octave-band filter bank frequency responseElliptic one-third-octave filter bank frequency responseElliptic one-third-octave filter bank frequency response
BesselBessel octave-band filter bank frequency responseBessel octave-band filter bank frequency responseBessel one-third-octave filter bank frequency responseBessel one-third-octave filter bank frequency response

Compare the rows on two things only, and the differences stop being subtle. Flat tops: Chebyshev I puts its ripple inside each band, on the flat top; Chebyshev II puts it outside, as the notch train between bands; Elliptic does both. Valley depth: how far the response falls between two adjacent bands is the leakage budget of a band level, and Bessel’s skirts overlap far more than any of the others — that overlap is its slow roll-off, drawn.

Show the code for this figure
from phonometry import filters
# One figure per architecture and fraction: the whole response gallery
fs = 48000
for ftype in ("butter", "cheby1", "cheby2", "ellip", "bessel"):
for fraction in (1, 3):
# ResponsePlot(show=True) draws the bank's frequency response
filters.OctaveFilterBank(fs=fs, fraction=fraction, order=6,
limits=[12, 20000],
design=filters.FilterDesign(filter_type=ftype),
response_plot=filters.ResponsePlot(show=True))

The Butterworth filter is known for its maximally flat passband. It is the standard choice for acoustic measurements where no ripple is allowed within the frequency bands.

import numpy as np
from phonometry import filters
# A calibrated signal in Pa so the guide runs standalone
fs = 48000
x = 0.2 * np.sin(2 * np.pi * 1000 * np.arange(fs) / fs)
# Standard one-third-octave measurement
spl, freq = filters.octave_filter(x, fs, fraction=3,
design=filters.FilterDesign(filter_type='butter'))
Butterworth one-third-octave filter bank frequency responseButterworth one-third-octave filter bank frequency response
Show the code for this figure
from phonometry import filters
# Draw this bank's response (1/3 octave, order 6, Butterworth)
filters.OctaveFilterBank(fs=48000, fraction=3, order=6, limits=[12, 20000],
design=filters.FilterDesign(filter_type='butter'),
response_plot=filters.ResponsePlot(show=True))

Chebyshev Type I filters provide a steeper roll-off than Butterworth at the expense of ripples in the passband. Useful when high selectivity is needed near the cut-off frequencies.

import numpy as np
from phonometry import filters
# A calibrated signal in Pa so the guide runs standalone
fs = 48000
x = 0.2 * np.sin(2 * np.pi * 1000 * np.arange(fs) / fs)
# Selectivity with 0.1 dB passband ripple
spl, freq = filters.octave_filter(
x, fs, fraction=3,
design=filters.FilterDesign(filter_type='cheby1', ripple=0.1))
Chebyshev I one-third-octave filter bank frequency responseChebyshev I one-third-octave filter bank frequency response
Show the code for this figure
from phonometry import filters
# Draw this bank's response (1/3 octave, order 6, Chebyshev I)
filters.OctaveFilterBank(fs=48000, fraction=3, order=6, limits=[12, 20000],
design=filters.FilterDesign(filter_type='cheby1', ripple=0.1),
response_plot=filters.ResponsePlot(show=True))

Also known as Inverse Chebyshev, it has a flat passband and ripples in the stopband. It provides faster roll-off than Butterworth without affecting the signal in the passband. The stopband edges are placed automatically so that the −3 dB points land on the band edges (attenuation must be for a −3 dB point to exist at all; below that the design raises ValueError). Note that the default of 72 dB is set by conformance and not by realizability: SciPy pins the equiripple floor at exactly attenuation, and IEC 61260-1 class 1 demands 70 dB far from the band, so an attenuation of, say, 6 dB is arithmetically legal and silently loses the class — see Filter Class Verification.

import numpy as np
from phonometry import filters
# A calibrated signal in Pa so the guide runs standalone
fs = 48000
x = 0.2 * np.sin(2 * np.pi * 1000 * np.arange(fs) / fs)
# Flat passband, class-1 default 72 dB stopband attenuation
spl, freq = filters.octave_filter(x, fs, fraction=3,
design=filters.FilterDesign(filter_type='cheby2'))
Chebyshev II one-third-octave filter bank frequency responseChebyshev II one-third-octave filter bank frequency response
Show the code for this figure
from phonometry import filters
# Draw this bank's response (1/3 octave, order 6, Chebyshev II)
filters.OctaveFilterBank(fs=48000, fraction=3, order=6, limits=[12, 20000],
design=filters.FilterDesign(filter_type='cheby2'),
response_plot=filters.ResponsePlot(show=True))

Elliptic (Cauer) filters have the shortest transition width (steepest roll-off) for a given order. They feature ripples in both the passband and stopband.

import numpy as np
from phonometry import filters
# A calibrated signal in Pa so the guide runs standalone
fs = 48000
x = 0.2 * np.sin(2 * np.pi * 1000 * np.arange(fs) / fs)
# Maximum selectivity for extreme band isolation
spl, freq = filters.octave_filter(
x, fs, fraction=3,
design=filters.FilterDesign(filter_type='ellip', ripple=0.1))
Elliptic one-third-octave filter bank frequency responseElliptic one-third-octave filter bank frequency response
Show the code for this figure
from phonometry import filters
# Draw this bank's response (1/3 octave, order 6, Elliptic)
filters.OctaveFilterBank(fs=48000, fraction=3, order=6, limits=[12, 20000],
design=filters.FilterDesign(filter_type='ellip', ripple=0.1),
response_plot=filters.ResponsePlot(show=True))

Bessel filters are optimized for linear phase response and minimal group delay. They preserve the shape of filtered waveforms (transients) better than any other type, but have the slowest roll-off. That claim is temporal, so the evidence for it is temporal too, and it lives on the sibling page: the group-delay figure of Filter Banks shows Bessel staying nearly flat across the passband while Chebyshev I and Elliptic peak at the band edges, and the band-decomposition figure of the same page shows what a longer ring does to a transient.

import numpy as np
from phonometry import filters
# A calibrated signal in Pa so the guide runs standalone
fs = 48000
x = 0.2 * np.sin(2 * np.pi * 1000 * np.arange(fs) / fs)
# Best for pulse analysis and transient preservation
spl, freq = filters.octave_filter(x, fs, fraction=3,
design=filters.FilterDesign(filter_type='bessel'))
Bessel one-third-octave filter bank frequency responseBessel one-third-octave filter bank frequency response
Show the code for this figure
from phonometry import filters
# Draw this bank's response (1/3 octave, order 6, Bessel)
filters.OctaveFilterBank(fs=48000, fraction=3, order=6, limits=[12, 20000],
design=filters.FilterDesign(filter_type='bessel'),
response_plot=filters.ResponsePlot(show=True))

4. Linkwitz-Riley crossover (linkwitz_riley)

Section titled “4. Linkwitz-Riley crossover (linkwitz_riley)”

Linkwitz-Riley is not a sixth architecture: it does not build an IEC band, and it is not a FilterDesign(filter_type=…) value. It is called directly, and it does the opposite job — it splits one signal into two branches that recombine into a flat magnitude response, which is what a loudspeaker crossover needs and what a measurement band must never do. Each branch is a Butterworth of order order/2 applied twice, so each is exactly −6 dB at the crossover frequency (not −3 dB as in the bands above): the branches add to unity in amplitude, not in power.

Which recombination is flat depends on the parity of order/2, and the naive sum is wrong for half the even orders:

orderorder/2Branch phase difference at Flat recombination
21 (odd)180°low - high
42 (even)low + high
63 (odd)180°low - high
84 (even)low + high

At orders 2 and 6 the branches are in antiphase at and low + high collapses into a deep notch there instead of summing flat. Even in the flat cases the sum is all-pass, not phase-free: its magnitude is flat to better than 0.001 dB from 20 Hz to 20 kHz while its phase turns a full 360° through the crossover.

import numpy as np
from phonometry import filters
# recording: a calibrated capture in Pa so the guide runs standalone
fs = 48000
recording = 0.2 * np.sin(2 * np.pi * 1000 * np.arange(fs) / fs)
# Split the recording into Low and High bands at 1000 Hz
order = 4
low, high = filters.linkwitz_riley(recording, fs, freq=1000, order=order)
# Flat recombination: sum for order/2 even, difference for order/2 odd
recombined = low + high if (order // 2) % 2 == 0 else low - high
Linkwitz-Riley 4th-order crossover: low-pass, high-pass and their flat sumLinkwitz-Riley 4th-order crossover: low-pass, high-pass and their flat sum
Show the code for this figure
import matplotlib.pyplot as plt
import numpy as np
from scipy.signal import freqz
from phonometry import filters
# Measure both branches: split a unit impulse and take the spectra.
fs = 48000
impulse = np.zeros(fs)
impulse[0] = 1.0
low, high = filters.linkwitz_riley(impulse, fs, freq=1000, order=4)
w, h_lp = freqz(low, worN=8192, fs=fs)
_, h_hp = freqz(high, worN=8192, fs=fs)
fig, ax = plt.subplots(figsize=(9, 5))
ax.semilogx(w, 20 * np.log10(np.abs(h_lp) + 1e-9), label="Low-pass (LR4)")
ax.semilogx(w, 20 * np.log10(np.abs(h_hp) + 1e-9), label="High-pass (LR4)")
ax.semilogx(w, 20 * np.log10(np.abs(h_lp + h_hp) + 1e-9), "--",
label="Sum (flat)")
ax.set(xlim=(20, 20000), ylim=(-60, 5),
xlabel="Frequency [Hz]", ylabel="Magnitude [dB]")
ax.grid(True, which="both", alpha=0.3)
ax.legend()
plt.show()

In a real crossover the two branches sum acoustically, not electrically, and the flatness above only survives if the two acoustic paths are time-aligned. A woofer/tweeter acoustic-centre offset of a few centimetres is enough phase rotation at 1 kHz to notch the summation, and the remedy is a delay on the forward driver, not a different filter order. Verify by measuring the summed on-axis response with the swept-sine method of System measurement.

  • Covered

    The five architectures every IEC-band bank can be built on (Butterworth, Chebyshev I/II, Elliptic, Bessel): their magnitude responses compared at the −3 dB crossover and in the full 1/1 and 1/3 octave gallery, the band-edge convention (exact for Butterworth, Chebyshev II and Bessel; the ripple edge for the two equiripple families), and a usage example per architecture, plus the Linkwitz-Riley crossover, whose two branches recombine flat — by sum or by difference, depending on the parity of order/2.

  • Not covered

    The band mathematics, the pole-zero, stability and multirate design and the octave_filter() / OctaveFilterBank parameter reference stay in Filter Banks. The IEC 61260-1 Table 1 class acceptance masks, which decide the class each of these architectures actually reaches, are Filter Class Verification.

Which filter architecture should I choose?

Section titled “Which filter architecture should I choose?”

Butterworth is the standard choice for general acoustic measurement: a maximally flat passband with no ripple. Chebyshev I gives a sharper roll-off at the cost of passband ripple; Chebyshev II keeps the passband flat and puts the ripple in the stopband instead; Elliptic offers maximum selectivity, with ripple in both bands; and Bessel preserves transient waveform shapes thanks to its linear phase response, but has the slowest roll-off. Note that Chebyshev I and Elliptic take the band edges as their ripple edge rather than their −3 dB point, so their band levels sit a few tenths of a decibel above the other three and must not be mixed with them in one spectrum. Linkwitz-Riley is not one of the five: it is a crossover, and its two branches recombine flat by low + high when order/2 is even and by low - high when it is odd.