PyOctaveBand 1.1.5__tar.gz → 1.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/PKG-INFO +126 -29
  2. {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/README.md +121 -24
  3. {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/pyproject.toml +8 -5
  4. {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/src/PyOctaveBand.egg-info/PKG-INFO +126 -29
  5. {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/src/PyOctaveBand.egg-info/SOURCES.txt +5 -2
  6. pyoctaveband-1.2.2/src/PyOctaveBand.egg-info/requires.txt +4 -0
  7. {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/src/pyoctaveband/__init__.py +57 -11
  8. pyoctaveband-1.2.2/src/pyoctaveband/_version.py +1 -0
  9. {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/src/pyoctaveband/core.py +57 -5
  10. {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/src/pyoctaveband/frequencies.py +52 -7
  11. {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/src/pyoctaveband/parametric_filters.py +57 -5
  12. {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/src/pyoctaveband/utils.py +1 -1
  13. {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/tests/test_basic.py +5 -0
  14. {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/tests/test_errors_and_edge_cases.py +46 -0
  15. pyoctaveband-1.2.2/tests/test_filter_design.py +71 -0
  16. pyoctaveband-1.2.2/tests/test_nominal_frequencies.py +139 -0
  17. {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/tests/test_parametric_filters.py +158 -1
  18. {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/tests/test_stateful_weighting_filter.py +13 -0
  19. pyoctaveband-1.2.2/tests/test_utils.py +31 -0
  20. pyoctaveband-1.1.5/src/PyOctaveBand.egg-info/requires.txt +0 -4
  21. pyoctaveband-1.1.5/tests/test_coverage_fix.py +0 -158
  22. {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/LICENSE +0 -0
  23. {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/setup.cfg +0 -0
  24. {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/src/PyOctaveBand.egg-info/dependency_links.txt +0 -0
  25. {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/src/PyOctaveBand.egg-info/top_level.txt +0 -0
  26. {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/src/pyoctaveband/calibration.py +0 -0
  27. {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/src/pyoctaveband/filter_design.py +0 -0
  28. {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/tests/test_advanced.py +0 -0
  29. {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/tests/test_audio_processing.py +0 -0
  30. {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/tests/test_multichannel.py +0 -0
  31. {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/tests/test_parametric.py +0 -0
  32. {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/tests/test_performance.py +0 -0
  33. {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/tests/test_signal_theory_limits.py +0 -0
  34. {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/tests/test_stateful_octave_filter_bank.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyOctaveBand
3
- Version: 1.1.5
3
+ Version: 1.2.2
4
4
  Summary: Octave-Band and Fractional Octave-Band filter for signals in time domain.
5
5
  Author-email: Jose Manuel Requena Plens <jmrplens@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/jmrplens/PyOctaveBand
@@ -11,10 +11,10 @@ Classifier: Operating System :: OS Independent
11
11
  Requires-Python: >=3.11
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
- Requires-Dist: numpy
15
- Requires-Dist: scipy
16
- Requires-Dist: matplotlib
17
- Requires-Dist: numba
14
+ Requires-Dist: numpy>=2.4.4
15
+ Requires-Dist: scipy>=1.17.1
16
+ Requires-Dist: matplotlib>=3.10.9
17
+ Requires-Dist: numba>=0.65.1
18
18
  Dynamic: license-file
19
19
 
20
20
  [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/donate?hosted_button_id=BLP3R6VGYJB4Q)
@@ -32,34 +32,43 @@ Now available on [PyPI](https://pypi.org/project/PyOctaveBand/).
32
32
  ---
33
33
 
34
34
  ## 📑 Table of Contents
35
- 1. [🚀 Getting Started](#-getting-started)
35
+ - [PyOctaveBand](#pyoctaveband)
36
+ - [📑 Table of Contents](#-table-of-contents)
37
+ - [🚀 Getting Started](#-getting-started)
36
38
  - [Installation](#installation)
37
- - [Basic Usage](#basic-usage-13-octave-analysis)
38
- 2. [🛠️ Filter Architectures](#️-filter-architectures)
39
+ - [📖 Quick API Reference](#-quick-api-reference)
40
+ - [Basic Usage: 1/3 Octave Analysis](#basic-usage-13-octave-analysis)
41
+ - [Multichannel Support](#multichannel-support)
42
+ - [Block processing](#block-processing)
43
+ - [🛠️ Filter Architectures](#️-filter-architectures)
39
44
  - [Filter Comparison and Zoom](#filter-comparison-and-zoom)
40
- - [Gallery of Responses](#gallery-of-filter-bank-responses)
41
- 3. [🔊 Acoustic Weighting (A, C, Z)](#-acoustic-weighting-a-c-z)
42
- 4. [⏱️ Time Weighting and Integration](#️-time-weighting-and-integration)
43
- 5. [⚡ Performance: Multichannel & Vectorization](#-performance-multichannel--vectorization)
44
- 6. [🔍 Filter Usage and Examples](#-filter-usage-and-examples)
45
- - [1. Butterworth](#1-butterworth-butter)
46
- - [2. Chebyshev I](#2-chebyshev-i-cheby1)
47
- - [3. Chebyshev II](#3-chebyshev-ii-cheby2)
48
- - [4. Elliptic](#4-elliptic-ellip)
49
- - [5. Bessel](#5-bessel-bessel)
50
- - [6. Linkwitz-Riley](#6-linkwitz-riley-lr)
51
- 7. [📏 Calibration and dBFS](#-calibration-and-dbfs)
52
- - [Physical Calibration](#physical-calibration-sound-level-meter)
45
+ - [Gallery of Filter Bank Responses](#gallery-of-filter-bank-responses)
46
+ - [🔊 Acoustic Weighting (A, C, Z)](#-acoustic-weighting-a-c-z)
47
+ - [⏱️ Time Weighting and Integration](#️-time-weighting-and-integration)
48
+ - [⚡ Performance: Multichannel \& Vectorization](#-performance-multichannel-vectorization)
49
+ - [🔍 Filter Usage and Examples](#-filter-usage-and-examples)
50
+ - [1. Butterworth (`butter`)](#1-butterworth-butter)
51
+ - [2. Chebyshev I (`cheby1`)](#2-chebyshev-i-cheby1)
52
+ - [3. Chebyshev II (`cheby2`)](#3-chebyshev-ii-cheby2)
53
+ - [4. Elliptic (`ellip`)](#4-elliptic-ellip)
54
+ - [5. Bessel (`bessel`)](#5-bessel-bessel)
55
+ - [6. Linkwitz-Riley (`lr`)](#6-linkwitz-riley-lr)
56
+ - [📏 Calibration and dBFS](#-calibration-and-dbfs)
57
+ - [Physical Calibration (Sound Level Meter)](#physical-calibration-sound-level-meter)
53
58
  - [Digital Analysis (dBFS)](#digital-analysis-dbfs)
54
- 8. [📊 Signal Decomposition](#-signal-decomposition-and-stability)
55
- 9. [📖 Theory and Equations](#-theoretical-background)
56
- - [Octave Band Frequencies](#octave-band-frequencies-ansi-s111--iec-61260)
57
- - [Magnitude Responses](#magnitude-responses-hjw)
58
- - [Weighting Curves](#weighting-curves-iec-61672-1)
59
+ - [RMS vs Peak Levels](#rms-vs-peak-levels)
60
+ - [📊 Signal Decomposition and Stability](#-signal-decomposition-and-stability)
61
+ - [📖 Theoretical Background](#-theoretical-background)
62
+ - [Octave Band Frequencies (ANSI S1.11 / IEC 61260)](#octave-band-frequencies-ansi-s111-iec-61260)
63
+ - [Frequency Resolution vs FFT Bin Spacing](#frequency-resolution-vs-fft-bin-spacing)
64
+ - [Magnitude Responses |H(jw)|](#magnitude-responses-hjw)
65
+ - [Filter Bank Design \& Numerical Stability](#filter-bank-design-numerical-stability)
66
+ - [Weighting Curves (IEC 61672-1)](#weighting-curves-iec-61672-1)
59
67
  - [Time Integration](#time-integration)
60
- 10. [🧪 Testing and Quality](#-development-and-verification)
68
+ - [🧪 Development and Verification](#-development-and-verification)
61
69
  - [Test Categories](#test-categories)
62
70
  - [Commands](#commands)
71
+ - [Author](#author)
63
72
 
64
73
  ---
65
74
 
@@ -100,10 +109,10 @@ All core functionality can be imported directly from the `pyoctaveband` package.
100
109
  | `octavefilter` | `function` | **High-level analysis.**<br>• `x`: Signal array<br>• `fs`: Sample rate [Hz]<br>• `fraction`: 1, 3, etc. (Default: 1)<br>• `order`: Filter order (Default: 6)<br>• `limits`: [f_min, f_max] (Default: [12, 20000])<br>• `filter_type`: 'butter', 'cheby1', 'cheby2', 'ellip', 'bessel' (Default: 'butter')<br>• `sigbands`: Return time signals (Default: False)<br>• `detrend`: Remove DC offset (Default: True)<br>• `calibration_factor`: Sensitivity multiplier (Default: 1.0)<br>• `dbfs`: Output in dBFS instead of dB SPL (Default: False)<br>• `mode`: 'rms' or 'peak' (Default: 'rms')<br>• `show`: Plot response (Default: False)<br>• `plot_file`: Path to save plot (Default: None)<br>• `ripple`: Passband ripple [dB] (for cheby1/ellip)<br>• `attenuation`: Stopband atten. [dB] (for cheby2/ellip) | `spl, freq = octavefilter(x, fs, ...)`<br>• `spl`: levels [dB]<br>• `freq`: frequencies [Hz]<br><br>**With `sigbands=True`:**<br>`spl, freq, xb = octavefilter(x, fs, sigbands=True)`<br>• `xb`: List of filtered signals (one per band)<br><br>**Calibrated usage:**<br>`spl, f = octavefilter(x, fs, calibration_factor=0.05)` |
101
110
  | `OctaveFilterBank` | `class` | **Efficient bank implementation.**<br>• `fs`: Sample rate [Hz]<br>• `fraction`: 1, 3, etc.<br>• `order`: Filter order<br>• `limits`: [f_min, f_max] (Default: [12, 20000])<br>• `filter_type`: Architecture name<br>• `show`: Plot response (Default: False)<br>• `plot_file`: Path to save plot (Default: None)<br>• `calibration_factor`: Sensitivity multiplier<br>• `dbfs`: Use dBFS (Default: False)<br>• `ripple`: Passband ripple [dB]<br>• `attenuation`: Stopband attenuation [dB] | `bank = OctaveFilterBank(fs=48000, fraction=3, order=6, filter_type='butter', show=True)`<br>`spl, f = bank.filter(x, sigbands=False, mode='rms', detrend=True)`<br><br>• `bank`: Instance of the filter bank |
102
111
  | `weighting_filter` | `function` | **Acoustic weighting.**<br>• `x`: Signal array<br>• `fs`: Sample rate [Hz]<br>• `curve`: 'A', 'C', or 'Z' (Default: 'A') | `y = weighting_filter(x, fs, curve='A')`<br><br>• `y`: 1D array of weighted signal |
103
- | `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) |
112
+ | `time_weighting` | `function` | **Energy capture.**<br>• `x`: Raw signal array (squared internally; time is the last axis)<br>• `fs`: Sample rate [Hz]<br>• `mode`: 'fast', 'slow', or 'impulse'<br>• `initial_state`: None, 'zero', 'first', scalar, or array (Default: None)<br>• `None`: use the default rest state (`y[-1] = 0`)<br>• `'zero'`: explicitly initialize `y[-1]` to zero<br>• scalar: broadcast to every channel<br>• array: must match/broadcast to `x.shape[:-1]`; for `x.shape == (n_channels, n_samples)`, use shape `(n_channels,)` | `env = time_weighting(x, fs, mode='fast', initial_state=None)`<br><br>• `env`: energy envelope (Mean Square), same shape as `x` |
104
113
  | `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 |
105
114
  | `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) |
106
- | `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] |
115
+ | `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 |
107
116
  | `normalizedfreq` | `function` | **Standard IEC Frequencies.**<br>• `fraction`: 1 or 3 | `freqs = normalizedfreq(fraction=3)`<br><br>• `freqs`: List of standard center frequencies [Hz] |
108
117
 
109
118
  ---
@@ -254,6 +263,24 @@ energy_envelope = time_weighting(signal, fs, mode='fast')
254
263
  spl_t = 10 * np.log10(energy_envelope / (2e-5)**2)
255
264
  ```
256
265
 
266
+ By default, the exponential integrator starts from rest (`y[-1] = 0`). Passing `initial_state=None` leaves this default unspecified, while `initial_state='zero'` requests the same zero state explicitly. If the recorded segment begins after a steady signal is already present, you can start from the first sample energy instead:
267
+
268
+ ```python
269
+ energy_envelope = time_weighting(signal, fs, mode='fast', initial_state='first')
270
+ ```
271
+
272
+ For block processing, pass the last output value from the previous block as the next block's `initial_state` instead of resetting each block:
273
+
274
+ ```python
275
+ state = None
276
+
277
+ for block in audio_blocks:
278
+ energy_envelope = time_weighting(block, fs, mode='fast', initial_state=state)
279
+ state = energy_envelope[-1]
280
+ ```
281
+
282
+ For multichannel blocks with time on the last axis, carry one state per channel: use `state = energy_envelope[..., -1]`. A scalar `initial_state` is applied to every channel, while an array must match or broadcast to the non-time shape, such as `(n_channels,)` for input shaped `(n_channels, n_samples)`.
283
+
257
284
  ---
258
285
 
259
286
  ## ⚡ Performance: Multichannel & Vectorization
@@ -441,6 +468,74 @@ $$
441
468
  f_1 = f_m \cdot G^{-1/2b}, \quad f_2 = f_m \cdot G^{1/2b}
442
469
  $$
443
470
 
471
+ ### Frequency Resolution vs FFT Bin Spacing
472
+
473
+ `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.
474
+
475
+ 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.
476
+
477
+ For example, with `fraction=3` and `limits=[12, 20000]`, the exact third-octave band around 1 kHz is approximately:
478
+
479
+ | Nominal band | Lower edge | Center | Upper edge | Bandwidth |
480
+ | :--- | ---: | ---: | ---: | ---: |
481
+ | 1 kHz | 891.25 Hz | 1000.00 Hz | 1122.02 Hz | 230.77 Hz |
482
+
483
+ You can inspect the exact bands with:
484
+
485
+ ```python
486
+ from pyoctaveband import getansifrequencies
487
+
488
+ fc, fl, fu, labels = getansifrequencies(fraction=3, limits=[12, 20000])
489
+ for label, center, lower, upper in zip(labels, fc, fl, fu):
490
+ print(label, center, lower, upper, upper - lower)
491
+ ```
492
+
493
+ If you need narrowband FFT bins for tonal inspection, run Welch/FFT on the original signal and use the PyOctaveBand band edges as masks:
494
+
495
+ ```python
496
+ import numpy as np
497
+ from scipy import signal
498
+ from pyoctaveband import octavefilter, getansifrequencies
499
+
500
+ fs = 100_000
501
+ x = pressure_signal_pa # 1D pressure signal in Pa
502
+
503
+ # Standardized third-octave levels from PyOctaveBand.
504
+ levels, centers = octavefilter(
505
+ x,
506
+ fs=fs,
507
+ fraction=3,
508
+ limits=[12, 20_000],
509
+ )
510
+
511
+ # Same standardized band definitions, including lower/upper edges.
512
+ fc, fl, fu, labels = getansifrequencies(fraction=3, limits=[12, 20_000])
513
+
514
+ # Narrowband Welch estimate on the original signal.
515
+ nperseg = min(2**15, len(x))
516
+ freq_bins, psd = signal.welch(
517
+ x,
518
+ fs=fs,
519
+ window="hann",
520
+ nperseg=nperseg,
521
+ noverlap=nperseg // 2,
522
+ scaling="density",
523
+ )
524
+
525
+ # Example: list the Welch bins inside the third-octave band closest to 1 kHz.
526
+ band_index = int(np.argmin(np.abs(np.asarray(fc) - 1000.0)))
527
+ in_band = (freq_bins >= fl[band_index]) & (freq_bins <= fu[band_index])
528
+
529
+ print("Selected third-octave band:", labels[band_index])
530
+ print("Welch bin spacing:", freq_bins[1] - freq_bins[0], "Hz")
531
+ for f, pxx in zip(freq_bins[in_band], psd[in_band]):
532
+ print(f, pxx)
533
+ ```
534
+
535
+ 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.
536
+
537
+ 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.
538
+
444
539
  ### Magnitude Responses |H(jw)|
445
540
  The library implements standard classical filter prototypes:
446
541
 
@@ -506,6 +601,8 @@ $$
506
601
 
507
602
  Where `tau` is the time constant (e.g., 125ms for Fast).
508
603
 
604
+ The default initial condition is `y[-1] = 0`. Use `initial_state='first'` to start from the first input energy, or pass a scalar/array with the previous mean-square output state.
605
+
509
606
  ---
510
607
 
511
608
  ## 🧪 Development and Verification
@@ -13,34 +13,43 @@ Now available on [PyPI](https://pypi.org/project/PyOctaveBand/).
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
 
@@ -81,10 +90,10 @@ All core functionality can be imported directly from the `pyoctaveband` package.
81
90
  | `octavefilter` | `function` | **High-level analysis.**<br>• `x`: Signal array<br>• `fs`: Sample rate [Hz]<br>• `fraction`: 1, 3, etc. (Default: 1)<br>• `order`: Filter order (Default: 6)<br>• `limits`: [f_min, f_max] (Default: [12, 20000])<br>• `filter_type`: 'butter', 'cheby1', 'cheby2', 'ellip', 'bessel' (Default: 'butter')<br>• `sigbands`: Return time signals (Default: False)<br>• `detrend`: Remove DC offset (Default: True)<br>• `calibration_factor`: Sensitivity multiplier (Default: 1.0)<br>• `dbfs`: Output in dBFS instead of dB SPL (Default: False)<br>• `mode`: 'rms' or 'peak' (Default: 'rms')<br>• `show`: Plot response (Default: False)<br>• `plot_file`: Path to save plot (Default: None)<br>• `ripple`: Passband ripple [dB] (for cheby1/ellip)<br>• `attenuation`: Stopband atten. [dB] (for cheby2/ellip) | `spl, freq = octavefilter(x, fs, ...)`<br>• `spl`: levels [dB]<br>• `freq`: frequencies [Hz]<br><br>**With `sigbands=True`:**<br>`spl, freq, xb = octavefilter(x, fs, sigbands=True)`<br>• `xb`: List of filtered signals (one per band)<br><br>**Calibrated usage:**<br>`spl, f = octavefilter(x, fs, calibration_factor=0.05)` |
82
91
  | `OctaveFilterBank` | `class` | **Efficient bank implementation.**<br>• `fs`: Sample rate [Hz]<br>• `fraction`: 1, 3, etc.<br>• `order`: Filter order<br>• `limits`: [f_min, f_max] (Default: [12, 20000])<br>• `filter_type`: Architecture name<br>• `show`: Plot response (Default: False)<br>• `plot_file`: Path to save plot (Default: None)<br>• `calibration_factor`: Sensitivity multiplier<br>• `dbfs`: Use dBFS (Default: False)<br>• `ripple`: Passband ripple [dB]<br>• `attenuation`: Stopband attenuation [dB] | `bank = OctaveFilterBank(fs=48000, fraction=3, order=6, filter_type='butter', show=True)`<br>`spl, f = bank.filter(x, sigbands=False, mode='rms', detrend=True)`<br><br>• `bank`: Instance of the filter bank |
83
92
  | `weighting_filter` | `function` | **Acoustic weighting.**<br>• `x`: Signal array<br>• `fs`: Sample rate [Hz]<br>• `curve`: 'A', 'C', or 'Z' (Default: 'A') | `y = weighting_filter(x, fs, curve='A')`<br><br>• `y`: 1D array of weighted signal |
84
- | `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) |
93
+ | `time_weighting` | `function` | **Energy capture.**<br>• `x`: Raw signal array (squared internally; time is the last axis)<br>• `fs`: Sample rate [Hz]<br>• `mode`: 'fast', 'slow', or 'impulse'<br>• `initial_state`: None, 'zero', 'first', scalar, or array (Default: None)<br>• `None`: use the default rest state (`y[-1] = 0`)<br>• `'zero'`: explicitly initialize `y[-1]` to zero<br>• scalar: broadcast to every channel<br>• array: must match/broadcast to `x.shape[:-1]`; for `x.shape == (n_channels, n_samples)`, use shape `(n_channels,)` | `env = time_weighting(x, fs, mode='fast', initial_state=None)`<br><br>• `env`: energy envelope (Mean Square), same shape as `x` |
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
  ---
@@ -235,6 +244,24 @@ energy_envelope = time_weighting(signal, fs, mode='fast')
235
244
  spl_t = 10 * np.log10(energy_envelope / (2e-5)**2)
236
245
  ```
237
246
 
247
+ By default, the exponential integrator starts from rest (`y[-1] = 0`). Passing `initial_state=None` leaves this default unspecified, while `initial_state='zero'` requests the same zero state explicitly. If the recorded segment begins after a steady signal is already present, you can start from the first sample energy instead:
248
+
249
+ ```python
250
+ energy_envelope = time_weighting(signal, fs, mode='fast', initial_state='first')
251
+ ```
252
+
253
+ For block processing, pass the last output value from the previous block as the next block's `initial_state` instead of resetting each block:
254
+
255
+ ```python
256
+ state = None
257
+
258
+ for block in audio_blocks:
259
+ energy_envelope = time_weighting(block, fs, mode='fast', initial_state=state)
260
+ state = energy_envelope[-1]
261
+ ```
262
+
263
+ For multichannel blocks with time on the last axis, carry one state per channel: use `state = energy_envelope[..., -1]`. A scalar `initial_state` is applied to every channel, while an array must match or broadcast to the non-time shape, such as `(n_channels,)` for input shaped `(n_channels, n_samples)`.
264
+
238
265
  ---
239
266
 
240
267
  ## ⚡ Performance: Multichannel & Vectorization
@@ -422,6 +449,74 @@ $$
422
449
  f_1 = f_m \cdot G^{-1/2b}, \quad f_2 = f_m \cdot G^{1/2b}
423
450
  $$
424
451
 
452
+ ### Frequency Resolution vs FFT Bin Spacing
453
+
454
+ `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.
455
+
456
+ 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.
457
+
458
+ For example, with `fraction=3` and `limits=[12, 20000]`, the exact third-octave band around 1 kHz is approximately:
459
+
460
+ | Nominal band | Lower edge | Center | Upper edge | Bandwidth |
461
+ | :--- | ---: | ---: | ---: | ---: |
462
+ | 1 kHz | 891.25 Hz | 1000.00 Hz | 1122.02 Hz | 230.77 Hz |
463
+
464
+ You can inspect the exact bands with:
465
+
466
+ ```python
467
+ from pyoctaveband import getansifrequencies
468
+
469
+ fc, fl, fu, labels = getansifrequencies(fraction=3, limits=[12, 20000])
470
+ for label, center, lower, upper in zip(labels, fc, fl, fu):
471
+ print(label, center, lower, upper, upper - lower)
472
+ ```
473
+
474
+ If you need narrowband FFT bins for tonal inspection, run Welch/FFT on the original signal and use the PyOctaveBand band edges as masks:
475
+
476
+ ```python
477
+ import numpy as np
478
+ from scipy import signal
479
+ from pyoctaveband import octavefilter, getansifrequencies
480
+
481
+ fs = 100_000
482
+ x = pressure_signal_pa # 1D pressure signal in Pa
483
+
484
+ # Standardized third-octave levels from PyOctaveBand.
485
+ levels, centers = octavefilter(
486
+ x,
487
+ fs=fs,
488
+ fraction=3,
489
+ limits=[12, 20_000],
490
+ )
491
+
492
+ # Same standardized band definitions, including lower/upper edges.
493
+ fc, fl, fu, labels = getansifrequencies(fraction=3, limits=[12, 20_000])
494
+
495
+ # Narrowband Welch estimate on the original signal.
496
+ nperseg = min(2**15, len(x))
497
+ freq_bins, psd = signal.welch(
498
+ x,
499
+ fs=fs,
500
+ window="hann",
501
+ nperseg=nperseg,
502
+ noverlap=nperseg // 2,
503
+ scaling="density",
504
+ )
505
+
506
+ # Example: list the Welch bins inside the third-octave band closest to 1 kHz.
507
+ band_index = int(np.argmin(np.abs(np.asarray(fc) - 1000.0)))
508
+ in_band = (freq_bins >= fl[band_index]) & (freq_bins <= fu[band_index])
509
+
510
+ print("Selected third-octave band:", labels[band_index])
511
+ print("Welch bin spacing:", freq_bins[1] - freq_bins[0], "Hz")
512
+ for f, pxx in zip(freq_bins[in_band], psd[in_band]):
513
+ print(f, pxx)
514
+ ```
515
+
516
+ 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.
517
+
518
+ 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.
519
+
425
520
  ### Magnitude Responses |H(jw)|
426
521
  The library implements standard classical filter prototypes:
427
522
 
@@ -487,6 +582,8 @@ $$
487
582
 
488
583
  Where `tau` is the time constant (e.g., 125ms for Fast).
489
584
 
585
+ The default initial condition is `y[-1] = 0`. Use `initial_state='first'` to start from the first input energy, or pass a scalar/array with the previous mean-square output state.
586
+
490
587
  ---
491
588
 
492
589
  ## 🧪 Development and Verification
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "PyOctaveBand"
7
- version = "1.1.5"
7
+ dynamic = ["version"]
8
8
  authors = [
9
9
  { name="Jose Manuel Requena Plens", email="jmrplens@gmail.com" },
10
10
  ]
@@ -17,10 +17,10 @@ classifiers = [
17
17
  "Operating System :: OS Independent",
18
18
  ]
19
19
  dependencies = [
20
- "numpy",
21
- "scipy",
22
- "matplotlib",
23
- "numba",
20
+ "numpy>=2.4.4",
21
+ "scipy>=1.17.1",
22
+ "matplotlib>=3.10.9",
23
+ "numba>=0.65.1",
24
24
  ]
25
25
 
26
26
  [project.urls]
@@ -30,6 +30,9 @@ dependencies = [
30
30
  [tool.setuptools.packages.find]
31
31
  where = ["src"]
32
32
 
33
+ [tool.setuptools.dynamic]
34
+ version = {attr = "pyoctaveband._version.__version__"}
35
+
33
36
  [tool.mypy]
34
37
  python_version = "3.11"
35
38
  strict = true