[READ-ONLY] Mirror of https://github.com/jmrplens/PyOctaveBand. [Python3] Octave-Band and Fractional Octave-Band filter. For signal in time domain. jmrplens.github.io/PyOctaveBand/
acoustics audio filter frequency frequency-analysis frequency-domain octave python3 signal time-domain
0

Configure Feed

Select the types of activity you want to include in your feed.

docs: clarify octave band frequency resolution

Document that octavefilter uses time-domain fractional-octave bands rather than FFT bins, and add a Welch example for mapping narrowband bins to band edges.

authored by

José M. Requena Plens and committed by
GitHub
(May 9, 2026, 1:33 PM +0200) bc50b214 6fd5eb15

+100 -23
+100 -23
README.md
··· 13 13 --- 14 14 15 15 ## 📑 Table of Contents 16 - 1. [🚀 Getting Started](#-getting-started) 16 + - [PyOctaveBand](#pyoctaveband) 17 + - [📑 Table of Contents](#-table-of-contents) 18 + - [🚀 Getting Started](#-getting-started) 17 19 - [Installation](#installation) 18 - - [Basic Usage](#basic-usage-13-octave-analysis) 19 - 2. [🛠️ Filter Architectures](#️-filter-architectures) 20 + - [📖 Quick API Reference](#-quick-api-reference) 21 + - [Basic Usage: 1/3 Octave Analysis](#basic-usage-13-octave-analysis) 22 + - [Multichannel Support](#multichannel-support) 23 + - [Block processing](#block-processing) 24 + - [🛠️ Filter Architectures](#️-filter-architectures) 20 25 - [Filter Comparison and Zoom](#filter-comparison-and-zoom) 21 - - [Gallery of Responses](#gallery-of-filter-bank-responses) 22 - 3. [🔊 Acoustic Weighting (A, C, Z)](#-acoustic-weighting-a-c-z) 23 - 4. [⏱️ Time Weighting and Integration](#️-time-weighting-and-integration) 24 - 5. [⚡ Performance: Multichannel & Vectorization](#-performance-multichannel--vectorization) 25 - 6. [🔍 Filter Usage and Examples](#-filter-usage-and-examples) 26 - - [1. Butterworth](#1-butterworth-butter) 27 - - [2. Chebyshev I](#2-chebyshev-i-cheby1) 28 - - [3. Chebyshev II](#3-chebyshev-ii-cheby2) 29 - - [4. Elliptic](#4-elliptic-ellip) 30 - - [5. Bessel](#5-bessel-bessel) 31 - - [6. Linkwitz-Riley](#6-linkwitz-riley-lr) 32 - 7. [📏 Calibration and dBFS](#-calibration-and-dbfs) 33 - - [Physical Calibration](#physical-calibration-sound-level-meter) 26 + - [Gallery of Filter Bank Responses](#gallery-of-filter-bank-responses) 27 + - [🔊 Acoustic Weighting (A, C, Z)](#-acoustic-weighting-a-c-z) 28 + - [⏱️ Time Weighting and Integration](#️-time-weighting-and-integration) 29 + - [⚡ Performance: Multichannel \& Vectorization](#-performance-multichannel-vectorization) 30 + - [🔍 Filter Usage and Examples](#-filter-usage-and-examples) 31 + - [1. Butterworth (`butter`)](#1-butterworth-butter) 32 + - [2. Chebyshev I (`cheby1`)](#2-chebyshev-i-cheby1) 33 + - [3. Chebyshev II (`cheby2`)](#3-chebyshev-ii-cheby2) 34 + - [4. Elliptic (`ellip`)](#4-elliptic-ellip) 35 + - [5. Bessel (`bessel`)](#5-bessel-bessel) 36 + - [6. Linkwitz-Riley (`lr`)](#6-linkwitz-riley-lr) 37 + - [📏 Calibration and dBFS](#-calibration-and-dbfs) 38 + - [Physical Calibration (Sound Level Meter)](#physical-calibration-sound-level-meter) 34 39 - [Digital Analysis (dBFS)](#digital-analysis-dbfs) 35 - 8. [📊 Signal Decomposition](#-signal-decomposition-and-stability) 36 - 9. [📖 Theory and Equations](#-theoretical-background) 37 - - [Octave Band Frequencies](#octave-band-frequencies-ansi-s111--iec-61260) 38 - - [Magnitude Responses](#magnitude-responses-hjw) 39 - - [Weighting Curves](#weighting-curves-iec-61672-1) 40 + - [RMS vs Peak Levels](#rms-vs-peak-levels) 41 + - [📊 Signal Decomposition and Stability](#-signal-decomposition-and-stability) 42 + - [📖 Theoretical Background](#-theoretical-background) 43 + - [Octave Band Frequencies (ANSI S1.11 / IEC 61260)](#octave-band-frequencies-ansi-s111-iec-61260) 44 + - [Frequency Resolution vs FFT Bin Spacing](#frequency-resolution-vs-fft-bin-spacing) 45 + - [Magnitude Responses |H(jw)|](#magnitude-responses-hjw) 46 + - [Filter Bank Design \& Numerical Stability](#filter-bank-design-numerical-stability) 47 + - [Weighting Curves (IEC 61672-1)](#weighting-curves-iec-61672-1) 40 48 - [Time Integration](#time-integration) 41 - 10. [🧪 Testing and Quality](#-development-and-verification) 49 + - [🧪 Development and Verification](#-development-and-verification) 42 50 - [Test Categories](#test-categories) 43 51 - [Commands](#commands) 52 + - [Author](#author) 44 53 45 54 --- 46 55 ··· 84 93 | `time_weighting` | `function` | **Energy capture.**<br>• `x`: Raw signal array (squared internally)<br>• `fs`: Sample rate [Hz]<br>• `mode`: 'fast', 'slow', or 'impulse' | `env = time_weighting(x, fs, mode='fast')`<br><br>• `env`: 1D array of energy envelope (Mean Square) | 85 94 | `linkwitz_riley` | `function` | **Audio crossover.**<br>• `x`: Signal array<br>• `fs`: Sample rate [Hz]<br>• `freq`: Crossover frequency [Hz]<br>• `order`: Any even number (Default: 4) | `lo, hi = linkwitz_riley(x, fs, freq=1000, order=4)`<br><br>• `lo`: Low-pass filtered signal<br>• `hi`: High-pass filtered signal | 86 95 | `calculate_sensitivity` | `function`| **SPL Calibration.**<br>• `ref_signal`: Calibration signal<br>• `target_spl`: Level of calibrator (Default: 94.0)<br>• `ref_pressure`: Reference pressure (Default: 20e-6) | `s = calculate_sensitivity(ref_signal, target_spl=94.0)`<br><br>• `s`: Float (multiplier for pressure) | 87 - | `getansifrequencies` | `function` | **ANSI Frequency generator.**<br>• `fraction`: 1, 3, etc. (Required)<br>• `limits`: [f_min, f_max] (Default: [12, 20000]) | `f_cen, f_low, f_high = getansifrequencies(fraction=3)`<br><br>• `f_cen`: List of center frequencies [Hz]<br>• `f_low`: List of lower edges [Hz]<br>• `f_high`: List of upper edges [Hz] | 96 + | `getansifrequencies` | `function` | **ANSI Frequency generator.**<br>• `fraction`: 1, 3, etc. (Required)<br>• `limits`: [f_min, f_max] (Default: [12, 20000]) | `f_cen, f_low, f_high, labels = getansifrequencies(fraction=3)`<br><br>• `f_cen`: List of center frequencies [Hz]<br>• `f_low`: List of lower edges [Hz]<br>• `f_high`: List of upper edges [Hz]<br>• `labels`: IEC nominal frequency labels | 88 97 | `normalizedfreq` | `function` | **Standard IEC Frequencies.**<br>• `fraction`: 1 or 3 | `freqs = normalizedfreq(fraction=3)`<br><br>• `freqs`: List of standard center frequencies [Hz] | 89 98 90 99 --- ··· 421 430 $$ 422 431 f_1 = f_m \cdot G^{-1/2b}, \quad f_2 = f_m \cdot G^{1/2b} 423 432 $$ 433 + 434 + ### Frequency Resolution vs FFT Bin Spacing 435 + 436 + `octavefilter` is a **time-domain fractional-octave filter bank**, not an FFT or Welch spectrum estimator. Therefore, its result does not have a frequency resolution in the `fs / nfft` sense. 437 + 438 + For `fraction=3`, the output contains one scalar level per third-octave band. The relevant frequency granularity is the standardized band definition: center frequency, lower edge, and upper edge. Because fractional-octave bands are logarithmically spaced, their absolute bandwidth in Hz grows with frequency while their relative bandwidth remains approximately constant. 439 + 440 + For example, with `fraction=3` and `limits=[12, 20000]`, the exact third-octave band around 1 kHz is approximately: 441 + 442 + | Nominal band | Lower edge | Center | Upper edge | Bandwidth | 443 + | :--- | ---: | ---: | ---: | ---: | 444 + | 1 kHz | 891.25 Hz | 1000.00 Hz | 1122.02 Hz | 230.77 Hz | 445 + 446 + You can inspect the exact bands with: 447 + 448 + ```python 449 + from pyoctaveband import getansifrequencies 450 + 451 + fc, fl, fu, labels = getansifrequencies(fraction=3, limits=[12, 20000]) 452 + for label, center, lower, upper in zip(labels, fc, fl, fu): 453 + print(label, center, lower, upper, upper - lower) 454 + ``` 455 + 456 + If you need narrowband FFT bins for tonal inspection, run Welch/FFT on the original signal and use the PyOctaveBand band edges as masks: 457 + 458 + ```python 459 + import numpy as np 460 + from scipy import signal 461 + from pyoctaveband import octavefilter, getansifrequencies 462 + 463 + fs = 100_000 464 + x = pressure_signal_pa # 1D pressure signal in Pa 465 + 466 + # Standardized third-octave levels from PyOctaveBand. 467 + levels, centers = octavefilter( 468 + x, 469 + fs=fs, 470 + fraction=3, 471 + limits=[12, 20_000], 472 + ) 473 + 474 + # Same standardized band definitions, including lower/upper edges. 475 + fc, fl, fu, labels = getansifrequencies(fraction=3, limits=[12, 20_000]) 476 + 477 + # Narrowband Welch estimate on the original signal. 478 + nperseg = min(2**15, len(x)) 479 + freq_bins, psd = signal.welch( 480 + x, 481 + fs=fs, 482 + window="hann", 483 + nperseg=nperseg, 484 + noverlap=nperseg // 2, 485 + scaling="density", 486 + ) 487 + 488 + # Example: list the Welch bins inside the third-octave band closest to 1 kHz. 489 + band_index = int(np.argmin(np.abs(np.asarray(fc) - 1000.0))) 490 + in_band = (freq_bins >= fl[band_index]) & (freq_bins <= fu[band_index]) 491 + 492 + print("Selected third-octave band:", labels[band_index]) 493 + print("Welch bin spacing:", freq_bins[1] - freq_bins[0], "Hz") 494 + for f, pxx in zip(freq_bins[in_band], psd[in_band]): 495 + print(f, pxx) 496 + ``` 497 + 498 + This keeps the two concepts separate: PyOctaveBand gives standardized fractional-octave levels, while Welch gives narrowband FFT bins. With `fs=100000` and `nperseg=2**15`, the Welch bin spacing is about `3.05 Hz`. Window choice and overlap affect leakage and averaging variance, but they do not change the bin spacing of each FFT segment. 499 + 500 + When `sigbands=True`, `octavefilter` can also return the time-domain waveform filtered by each band. Applying Welch/FFT to one selected filtered waveform can be useful as a diagnostic view of the content inside that filtered band, but it does not recover FFT bins from the scalar band levels. 424 501 425 502 ### Magnitude Responses |H(jw)| 426 503 The library implements standard classical filter prototypes: