Skip to content

Filter Class Verification (IEC 61260-1)

Standards: IEC 61260ANSI S1.11

A filter bank becomes a measuring instrument only once its bands have been proved against a specification. IEC 61260-1:2014 writes that specification as an acceptance mask: a corridor of relative attenuation around each mid frequency, narrow in the passband, opening into a minimum-attenuation requirement far outside the band, with one corridor per performance class. A bank “is class 1” when every band of it stays inside the class 1 corridor at every normalized frequency, and the margin in decibels says by how much.

This page is the verification half of the octave-filtering topic: the 2014 mask and the per-band verdict, the stricter class 0 kept alive by the withdrawn IEC 61260:1995 and ANSI S1.11-2004 masks, a reading of what a class actually buys in a measurement (passband error, stopband leakage, uncertainty budget), and the one-page accredited fiche that turns the verdict into a document. The design half, the band mathematics and the parameter reference, is Filter Banks, and the five architectures with their compared responses are Filter Architecture Gallery; the same machinery applied to the frequency weightings is section 6 of Frequency Weighting.

1. Verifying the class against IEC 61260-1:2014

Section titled “1. Verifying the class against IEC 61260-1:2014”

verify_filter_class checks every band of a bank against the acceptance limits of IEC 61260-1:2014 (Table 1, with the fractional-octave breakpoint mapping and log-frequency interpolation from the standard) and reports the performance class per band with its margin in dB:

from phonometry import filters
bank = filters.OctaveFilterBank(fs=48000, fraction=3, order=6)
result = filters.verify_filter_class(bank)
print(result["overall_class"]) # 1
print(result["range_limited"]) # True for a decimated bank
print(result["bands"][0])
# {'freq': 12.589254117941678, 'class': 1, 'checked_to_omega': 3.8127755266765493, 'margin_class1_db': 0.3999999999999595, 'margin_class2_db': 0.5999999999999595}

How far up the mask the verdict actually reaches. checked_to_omega is the highest normalized frequency at which that band was evaluated, and on a multirate bank it is the band’s own decimated Nyquist rather than the end of the Table 1 mask. On the bank above it runs from 3.81 down to 1.20, so the far-stopband requirement — at least 70 dB for class 1 beyond roughly — is not demonstrated on the band filter at all. It is taken as satisfied by the anti-alias low-pass that precedes the decimation, which removed that energy before the band ever saw it. range_limited is the flag that this argument was used, and it is True here.

Say that plainly in a report: the verdict attests the mask up to checked_to_omega, and the rest is an argument about the decimation chain. When a document requires the full mask on the band filter itself, design the same bank with design=filters.FilterDesign(resample=False); its low bands are then evaluated to of order instead of 4. range_limited stays True even then, because the top band’s upper edge still approaches Nyquist — that band is the one the flag is warning about once decimation is out of the picture.

What the margin measures. It is the minimum, over every normalized frequency the band was evaluated at, of the distance to the nearest limit of that class: positive when the response stays inside the corridor everywhere, and negative by exactly the worst violation when it does not. The reported class is the strictest class whose margin is non-negative.

Which constraint binds is worth knowing before you try to improve the number. A maximally-flat Butterworth is flat at mid-band, where the class 1 corridor is ±0.4 dB, so its margin saturates at +0.400 dB the moment the stopband stops being the limiting factor: measured on a 48 kHz one-third-octave bank, order 2 fails outright (class None, worst class 1 margin −27.03 dB) and orders 4, 6, 8 and 10 all report class 1 with exactly +0.400 dB. Raising the order therefore helps only while the margin is negative for a stopband reason; once a design passes, the margin is capped by the passband half-corridor and no order will move it. A margin of +0.400 dB is not a mediocre result to be improved — it is the best a compliant design can report against this mask.

The Table 1 acceptance mask itself is public too: class_limits(fraction, filter_class, omega) returns the minimum/maximum relative-attenuation limits at normalized frequencies , the same limits the verifier and the figure below use.

Butterworth band response threading between the forbidden regions of the IEC 61260-1 class 1 acceptance maskButterworth band response threading between the forbidden regions of the IEC 61260-1 class 1 acceptance mask

The order-6 Butterworth response (blue) threads between the forbidden regions: it must attenuate at least the red mask outside the band and no more than the purple mask inside it.

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
bank = filters.OctaveFilterBank(fs, fraction=1, order=6, limits=[800, 1200])
idx = int(np.argmin(np.abs(np.array(bank.freq) - 1000)))
fm, fsd = bank.freq[idx], fs / bank.factor[idx]
w, h = sosfreqz(bank.sos[idx], worN=2**15, fs=fsd)
att = -20 * np.log10(np.abs(h) + 1e-12)
delta_a = att - np.interp(fm, w, att) # relative attenuation
grid = np.logspace(np.log10(0.05), np.log10(8), 2000)
lo1, hi1 = filters.class_limits(1.0, 1, grid) # class 1 min/max attenuation
fig, ax = plt.subplots(figsize=(9, 5.5))
ax.fill_between(grid, -10, lo1, alpha=0.15, color="tab:red",
label="Forbidden: too little attenuation")
finite = np.isfinite(hi1)
ax.fill_between(grid[finite], hi1[finite], 90, alpha=0.15, color="tab:purple",
label="Forbidden: too much attenuation")
ax.plot(w / fm, delta_a, label="Butterworth order 6")
ax.set(xscale="log", xlim=(0.08, 8), ylim=(-6, 90),
xlabel="Normalized frequency f / fm",
ylabel="Relative attenuation [dB]")
ax.legend()
plt.show()

With default parameters (order 6), Butterworth meets class 1, and so does Chebyshev II: its attenuation default is now 72 dB, clearing the 70 dB far-stopband class 1 limit (scipy pins the cheby2 equiripple floor at exactly attenuation, so any value qualifies; the 72 dB default keeps the same +0.400 dB passband margin as Butterworth). Chebyshev I, Elliptic and Bessel do not meet class limits at order 6: passband ripple (cheby1/ellip) and slow roll-off (bessel) violate the mask. Class 1 is the strictest verdict this edition can return; the stricter class 0 the default bank also clears belongs to the withdrawn 1995 mask and is section 2.

2. Class 0 (IEC 61260:1995 / ANSI S1.11-2004)

Section titled “2. Class 0 (IEC 61260:1995 / ANSI S1.11-2004)”

The tightest performance class, class 0, was defined by the earlier IEC 61260:1995 and its US twin ANSI S1.11-2004 (both withdrawn/superseded but still referenced for laboratory-grade instruments); IEC 61260-1:2014 dropped it. Its class 1/2 masks differ slightly from the 2014 edition, so it lives behind an edition switch rather than being mixed into the 2014 mask:

from phonometry import filters
fs = 48000
bank = filters.OctaveFilterBank(fs, fraction=1, order=6, limits=[800, 1200])
result = filters.verify_filter_class(bank, edition="1995") # classes 0, 1, 2
print(result["overall_class"]) # 0 (the default Butterworth clears it)
print(result["bands"][0]["margin_class0_db"])
Nested pass-band acceptance corridors for class 0, 1 and 2 of IEC 61260:1995 with the order-6 Butterworth response sitting inside the tightest class 0 corridorNested pass-band acceptance corridors for class 0, 1 and 2 of IEC 61260:1995 with the order-6 Butterworth response sitting inside the tightest class 0 corridor

The class 0 corridor (±0.15 dB at mid-band) is the tightest; class 1 (±0.3 dB) and class 2 (±0.5 dB) are progressively wider. The order-6 Butterworth threads inside class 0 across the whole pass-band.

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
bank = filters.OctaveFilterBank(fs, fraction=1, order=6, limits=[800, 1200])
idx = int(np.argmin(np.abs(np.array(bank.freq) - 1000)))
fm, fsd = bank.freq[idx], fs / bank.factor[idx]
w, h = sosfreqz(bank.sos[idx], worN=2**15, fs=fsd)
att = -20 * np.log10(np.abs(h) + 1e-12)
delta_a = att - np.interp(fm, w, att)
# Pass-band only: outside the band edges the maximum limit is +inf.
g = 10 ** (3 / 10)
grid = np.linspace(g ** -0.5, g ** 0.5, 1500)
pb = (w / fm >= g ** -0.5) & (w / fm <= g ** 0.5)
fig, ax = plt.subplots(figsize=(9, 5.5))
for cls in (2, 1, 0): # nested corridors, class 0 tightest
lo, hi = filters.class_limits(1.0, cls, grid, edition="1995")
ax.plot(grid, hi, label=f"Class {cls} corridor")
ax.plot(grid, lo, color=ax.lines[-1].get_color())
ax.plot(w[pb] / fm, delta_a[pb], "k", lw=2, label="Butterworth order 6")
ax.set(xscale="log", xlim=(g ** -0.5, g ** 0.5), ylim=(-0.7, 6),
xlabel="Normalized frequency f / fm",
ylabel="Relative attenuation [dB]")
ax.legend()
plt.show()

The masks are worst-case error bounds on a measurement, not abstract grades:

  • In the passband the corridor bounds how much the band can mis-read in-band content: a class 1 bank reads a mid-band tone within ±0.4 dB of its true level and a class 2 bank within ±0.6 dB (IEC 61260-1:2014 Table 1; the stricter IEC 61260:1995 Table 1 masks allowed ±0.3 dB for class 1, ±0.5 dB for class 2 and ±0.15 dB for class 0). Toward the band edges the corridor widens, which is the honest admission that a tone sitting exactly on an edge is genuinely ambiguous between two bands (both read it about 3 dB down).

  • In the stopband the minimum-attenuation mask bounds leakage from the rest of the spectrum: far from the band, class 1 demands at least 70 dB of relative attenuation (the reason the cheby2 default is 72 dB). In energy terms, an out-of-band tone must be roughly 70 dB stronger than the band’s own content before it doubles the band’s energy reading (+3 dB). The practical consequence: measuring bands far below a dominant tone, the reading floors out at the leakage skirt about 70 dB down, and a steeper architecture (or higher order) is the only way to push that floor lower.

    One-third-octave band levels of a 1 kHz tone at 100 dB SPL sitting on a pink-noise floor whose own band levels are about 22 dB, analysed with order 6 and order 10 banks. The 1 kHz band reads 100 dB, the neighbouring bands fall away steeply, and below about 500 Hz the measured levels flatten onto a skirt some 65 to 70 dB below the tone, well above the true noise floor drawn dotted; above about 8 kHz the skirt has fallen below the noise and the measured levels join it. The order 10 bank pulls the skirt visibly lower on the high side of the toneOne-third-octave band levels of a 1 kHz tone at 100 dB SPL sitting on a pink-noise floor whose own band levels are about 22 dB, analysed with order 6 and order 10 banks. The 1 kHz band reads 100 dB, the neighbouring bands fall away steeply, and below about 500 Hz the measured levels flatten onto a skirt some 65 to 70 dB below the tone, well above the true noise floor drawn dotted; above about 8 kHz the skirt has fallen below the noise and the measured levels join it. The order 10 bank pulls the skirt visibly lower on the high side of the tone

    What the last sentence looks like as a measurement. Every band whose level sits on the skirt is reporting the filter’s rejection of the 1 kHz tone, not the sound present in that band — the dotted line is what is really there. The test is not subtle once you look for it: raise the order and the bands that are measuring the filter move, while the bands that are measuring the sound do not.

  • For the uncertainty budget, the class is the filter’s contribution to the measurement uncertainty: a class 1 bank adds up to a few tenths of a dB to a band level, comparable to a class 1 sound level meter’s other tolerance terms, which is why instrument-grade chains specify the class of every stage rather than a single overall figure.

Which architecture reaches which class? Under the 2014 edition, whose only classes are 1 and 2, the library’s default Butterworth order-6 bank meets class 1 with a +0.400 dB binding margin — that is the ceiling, and section 1 explains why. Against the stricter 1995 / ANSI S1.11-2004 mask (edition="1995") the same default reaches class 0; the configuration the conformance suite verifies at that class is the octave-band bank at 48 kHz, so re-run verify_filter_class(bank, edition="1995") yourself before writing class 0 into a document for any other fraction or sample rate. Writing “class 0 per IEC 61260-1:2014” is a claim against a class that edition does not define.

The table reports the best class each architecture reaches at order 6, fs 48 kHz, under the 1995 mask; the architectures other than Butterworth fall short because they trade the IEC mask for a different property by construction:

ArchitectureBest class (order 6, fs 48 kHz, edition="1995")Why
butter (default)0Maximally-flat pass-band, monotone roll-off; fits the mask
cheby21Flat pass-band but the mask relationship binds at class 1
cheby1Pass-band ripple violates the flatness limit
ellipPass- and stop-band ripple
besselFlat group delay bought with a slow roll-off

Under the 2014 edition the same ranking reads 1 / 1 / — / — / —: cheby2 joins Butterworth at the top because class 0 no longer exists to separate them.

Four panels, each the 1 kHz octave band of an order-6 bank drawn against the IEC 61260-1 acceptance corridor with its out-of-tolerance samples marked in red. Butterworth threads the class 1 corridor cleanly and reports overall_class 1. Chebyshev I leaves the corridor in two short bursts just inside each band edge, where its passband ripple sits, and reports None. Elliptic does the same at both edges, and reports None. Bessel drifts out of the corridor along the whole lower skirt because it rolls off too slowly, and reports NoneFour panels, each the 1 kHz octave band of an order-6 bank drawn against the IEC 61260-1 acceptance corridor with its out-of-tolerance samples marked in red. Butterworth threads the class 1 corridor cleanly and reports overall_class 1. Chebyshev I leaves the corridor in two short bursts just inside each band edge, where its passband ripple sits, and reports None. Elliptic does the same at both edges, and reports None. Bessel drifts out of the corridor along the whole lower skirt because it rolls off too slowly, and reports None

The verdicts of the table, drawn. Chebyshev I and Elliptic poke through the limit just inside the band edges, where their ripple lives; Bessel leaves it along the skirt, because it never falls fast enough. The red samples are the ones verify_filter_class counted against the design, and this is exactly what filter_class_compliance(bank).plot() draws for a bank of your own — note that the plot shades the corridor of the class each design came closest to, so the three failing panels show the class 2 corridor.

Show the code for this figure
import matplotlib.pyplot as plt
# `filters` is the import of the snippets above.
fs = 48000
fig, axes = plt.subplots(2, 2, figsize=(12, 8))
for ax, ftype in zip(axes.ravel(), ("butter", "cheby1", "ellip", "bessel")):
bank = filters.OctaveFilterBank(
fs, fraction=1, order=6, limits=[800, 1200],
design=filters.FilterDesign(filter_type=ftype))
result = filters.filter_class_compliance(bank)
result.plot(ax=ax)
ax.set_title(f"{ftype}: overall_class = {result.overall_class}")
plt.tight_layout()
plt.show()

So the sensible default is the common one (Butterworth order 6), while the alternative architectures are deliberate opt-ins whose purpose (steeper roll-off, linear phase) works against the class mask. Away from these settings (very high fraction or near-Nyquist bands), always re-run verify_filter_class to confirm the class you need.

verify_filter_class answers a question about a design: does this transfer function fit the mask. A laboratory answers a different question about a device on a date, and the two verdicts are not interchangeable.

The full Table 1 walk belongs to pattern evaluation (IEC 61260-2:2016), which a filter-set model passes once. What a working instrument actually receives is the periodic test of IEC 61260-3:2016, and it is narrower on purpose: the relative attenuation is measured at the exact midband frequency of every filter in the set, through the electrical input on the reference level range, together with the effective-bandwidth deviation from a swept-frequency test, the linear operating range with its level-range control and overload behaviour, and the lower limit of that range — all under stated environmental conditions and with traceable test equipment. That is why a certificate carries a date, a temperature and a set of serial numbers, and why it says nothing about the parts of the mask it did not walk.

What carries over is worth stating in a report: a class verdict from this page is inherited by every measurement made with the library’s filters, and a hardware chain adds a verdict of its own. Name both. The equivalent regime for sound level meters — IEC 61672-3 periodic tests, and IEC 60942 for the calibrator — is in Calibration and dBFS.

filter_class_compliance(bank) wraps the same verification as a result object that exposes .plot() and .report(), so a type-test verdict can be rendered as a one-page accredited fiche. The fiche lists every band’s achieved class and its binding margin, overlays the worst-margin band’s measured relative attenuation on the class corridor, and boxes the overall class-compliance result. Pass a required_class on the ReportMetadata to add a PASS/FAIL verdict row (a bank “meets class N” when its achieved class is at least as strict, i.e. a class index of N or lower). The fiche renders in English by default; pass language="es" for a Spanish fiche (translated fixed strings and a comma decimal separator), e.g. result.report("iec61260_es.pdf", language="es").

from phonometry import (
OctaveFilterBank,
ReportMetadata,
filter_class_compliance,
)
bank = OctaveFilterBank(fs=48000, fraction=1, order=6, limits=[125, 4000])
result = filter_class_compliance(bank) # overall_class == 1
result.plot() # the worst-margin band on its class corridor
result.report(
"iec61260.pdf",
metadata=ReportMetadata(
specimen="1/1-octave filter bank",
measurement_standard="IEC 61260-1:2014",
required_class=1, # class 1 (or stricter) required
),
) # -> Class 1 - COMPLIES, PASS

The example fiche is regenerated with make reports and kept rendered in the repository; click the preview to open the PDF.

IEC 61260-1 filter class compliance example report (PDF)

One-page filter-class-compliance fiche: a metadata header, a per-band classification table listing each octave band's achieved class and binding margin, the worst-margin band's measured relative attenuation overlaid on the green class-1 acceptance corridor, the boxed Class 1 - COMPLIES (margin +0.40 dB) result and a PASS verdict against the required class 1.

Download the report (PDF)

Filter class compliance fiche (FilterComplianceResult.report), achieved class with the binding margin in dB and the measured relative attenuation over the Table 1 corridor.

Passing edition="1995" verifies against the older IEC 61260:1995 / ANSI S1.11-2004 mask, which keeps the stricter class 0 that the 2014 edition dropped, so a modest order-6 bank can be certified to class 0:

bank = OctaveFilterBank(fs=48000, fraction=1, order=6, limits=[250, 4000])
result = filter_class_compliance(bank, edition="1995") # overall_class == 0
result.plot() # the class-0 corridor of the 1995 edition
result.report("iec61260_1995.pdf",
metadata=ReportMetadata(measurement_standard="IEC 61260:1995",
required_class=0)) # -> Class 0 - COMPLIES
IEC 61260:1995 class-0 filter compliance example report (PDF)

One-page filter-class-compliance fiche under the 1995 edition: a per-band classification table showing every octave band achieving class 0, the measured relative attenuation overlaid on the green class-0 acceptance corridor, the boxed Class 0 - COMPLIES (margin +0.15 dB) result and a PASS verdict against the required class 0.

Download the report (PDF)

Class 0 is retained by the IEC 61260:1995 / ANSI S1.11-2004 edition (edition='1995'); the 2014 edition keeps only classes 1 and 2.
  • Covered

    The IEC 61260-1:2014 Table 1 class 1 / class 2 acceptance limits (with the fractional-octave breakpoint mapping and the log-frequency interpolation of the standard), checked band by band by verify_filter_class and published as a mask by class_limits; the withdrawn IEC 61260:1995 / ANSI S1.11-2004 class 0 mask, reachable with edition="1995"; and the accredited one-page fiche of filter_class_compliance().report(), with its optional required_class PASS/FAIL verdict, in English and Spanish.

  • Not covered

    IEC 61260-1’s conformance tests for the physical filter itself (overload recovery, filter linearity, environmental influences) apply to hardware analog and digital filters and are not implemented: verify_filter_class checks the designed digital response against Table 1, not an instrument. Those tests belong to IEC 61260-2:2016 (pattern evaluation) and IEC 61260-3:2016 (periodic tests), which section 3b summarizes and this page does not run. Near Nyquist, the bilinear transform warps the frequency axis and the bank has no correction for it, so the stopband mask beyond the processing Nyquist is reported as range_limited rather than verified: keep the top band edge comfortably below Nyquist or raise fs.

Which filter architecture meets IEC 61260-1 class 1 with default settings?

Section titled “Which filter architecture meets IEC 61260-1 class 1 with default settings?”

With the default order 6, Butterworth meets class 1 of the IEC 61260-1:2014 Table 1 acceptance limits, and so does Chebyshev II: its default attenuation of 72 dB clears the 70 dB far-stopband class 1 limit. Chebyshev I, Elliptic and Bessel do not: passband ripple (cheby1, ellip) and slow roll-off (bessel) violate the mask. verify_filter_class reports the achieved class per band.

What is class 0 and which standard defines it?

Section titled “What is class 0 and which standard defines it?”

Class 0 is the tightest filter performance class, defined by IEC 61260:1995 and its US twin ANSI S1.11-2004 and dropped by IEC 61260-1:2014. Its passband corridor allows only ±0.15 dB at mid-band, against ±0.3 dB for class 1 in the 1995 masks. It stays available through edition="1995", and the default order-6 Butterworth bank meets class 0 in the configuration the conformance report verifies, the octave-band bank at 48 kHz. Under IEC 61260-1:2014 that same bank is class 1: the 2014 edition defines no class 0, so a class 0 claim must cite the 1995 / ANSI S1.11-2004 mask it was measured against.