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.
- {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/PKG-INFO +126 -29
- {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/README.md +121 -24
- {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/pyproject.toml +8 -5
- {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/src/PyOctaveBand.egg-info/PKG-INFO +126 -29
- {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/src/PyOctaveBand.egg-info/SOURCES.txt +5 -2
- pyoctaveband-1.2.2/src/PyOctaveBand.egg-info/requires.txt +4 -0
- {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/src/pyoctaveband/__init__.py +57 -11
- pyoctaveband-1.2.2/src/pyoctaveband/_version.py +1 -0
- {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/src/pyoctaveband/core.py +57 -5
- {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/src/pyoctaveband/frequencies.py +52 -7
- {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/src/pyoctaveband/parametric_filters.py +57 -5
- {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/src/pyoctaveband/utils.py +1 -1
- {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/tests/test_basic.py +5 -0
- {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/tests/test_errors_and_edge_cases.py +46 -0
- pyoctaveband-1.2.2/tests/test_filter_design.py +71 -0
- pyoctaveband-1.2.2/tests/test_nominal_frequencies.py +139 -0
- {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/tests/test_parametric_filters.py +158 -1
- {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/tests/test_stateful_weighting_filter.py +13 -0
- pyoctaveband-1.2.2/tests/test_utils.py +31 -0
- pyoctaveband-1.1.5/src/PyOctaveBand.egg-info/requires.txt +0 -4
- pyoctaveband-1.1.5/tests/test_coverage_fix.py +0 -158
- {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/LICENSE +0 -0
- {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/setup.cfg +0 -0
- {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/src/PyOctaveBand.egg-info/dependency_links.txt +0 -0
- {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/src/PyOctaveBand.egg-info/top_level.txt +0 -0
- {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/src/pyoctaveband/calibration.py +0 -0
- {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/src/pyoctaveband/filter_design.py +0 -0
- {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/tests/test_advanced.py +0 -0
- {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/tests/test_audio_processing.py +0 -0
- {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/tests/test_multichannel.py +0 -0
- {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/tests/test_parametric.py +0 -0
- {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/tests/test_performance.py +0 -0
- {pyoctaveband-1.1.5 → pyoctaveband-1.2.2}/tests/test_signal_theory_limits.py +0 -0
- {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.
|
|
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
|
[](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
|
-
|
|
35
|
+
- [PyOctaveBand](#pyoctaveband)
|
|
36
|
+
- [📑 Table of Contents](#-table-of-contents)
|
|
37
|
+
- [🚀 Getting Started](#-getting-started)
|
|
36
38
|
- [Installation](#installation)
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
- [
|
|
58
|
-
- [
|
|
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
|
-
|
|
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`:
|
|
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
|
-
|
|
16
|
+
- [PyOctaveBand](#pyoctaveband)
|
|
17
|
+
- [📑 Table of Contents](#-table-of-contents)
|
|
18
|
+
- [🚀 Getting Started](#-getting-started)
|
|
17
19
|
- [Installation](#installation)
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
- [
|
|
39
|
-
- [
|
|
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
|
-
|
|
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`:
|
|
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
|
-
|
|
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
|