Skip to content

Commit bc50b21

Browse files
authored
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.
1 parent 6fd5eb1 commit bc50b21

1 file changed

Lines changed: 100 additions & 23 deletions

File tree

README.md

Lines changed: 100 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,34 +13,43 @@ Now available on [PyPI](https://pypi.org/project/PyOctaveBand/).
1313
---
1414

1515
## 📑 Table of Contents
16-
1. [🚀 Getting Started](#-getting-started)
16+
- [PyOctaveBand](#pyoctaveband)
17+
- [📑 Table of Contents](#-table-of-contents)
18+
- [🚀 Getting Started](#-getting-started)
1719
- [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)
2025
- [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)
3439
- [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)
4048
- [Time Integration](#time-integration)
41-
10. [🧪 Testing and Quality](#-development-and-verification)
49+
- [🧪 Development and Verification](#-development-and-verification)
4250
- [Test Categories](#test-categories)
4351
- [Commands](#commands)
52+
- [Author](#author)
4453

4554
---
4655

@@ -84,7 +93,7 @@ All core functionality can be imported directly from the `pyoctaveband` package.
8493
| `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) |
8594
| `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 |
8695
| `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 |
8897
| `normalizedfreq` | `function` | **Standard IEC Frequencies.**<br>• `fraction`: 1 or 3 | `freqs = normalizedfreq(fraction=3)`<br><br>• `freqs`: List of standard center frequencies [Hz] |
8998

9099
---
@@ -422,6 +431,74 @@ $$
422431
f_1 = f_m \cdot G^{-1/2b}, \quad f_2 = f_m \cdot G^{1/2b}
423432
$$
424433

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.
501+
425502
### Magnitude Responses |H(jw)|
426503
The library implements standard classical filter prototypes:
427504

0 commit comments

Comments
 (0)