PyOctaveBand 1.0.1__py3-none-any.whl
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/__init__.py +96 -0
- pyoctaveband/calibration.py +32 -0
- pyoctaveband/core.py +191 -0
- pyoctaveband/filter_design.py +118 -0
- pyoctaveband/frequencies.py +142 -0
- pyoctaveband/parametric_filters.py +139 -0
- pyoctaveband/utils.py +53 -0
- pyoctaveband-1.0.1.dist-info/METADATA +361 -0
- pyoctaveband-1.0.1.dist-info/RECORD +12 -0
- pyoctaveband-1.0.1.dist-info/WHEEL +5 -0
- pyoctaveband-1.0.1.dist-info/licenses/LICENSE +674 -0
- pyoctaveband-1.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Copyright (c) 2026. Jose M. Requena-Plens
|
|
2
|
+
"""
|
|
3
|
+
Weighting filters (A, C, Z) and time weighting utilities for audio analysis.
|
|
4
|
+
Implementation according to IEC 61672-1:2013.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import List, Tuple, cast
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
from scipy import signal
|
|
13
|
+
|
|
14
|
+
from .utils import _typesignal
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def weighting_filter(x: List[float] | np.ndarray, fs: int, curve: str = "A") -> np.ndarray:
|
|
18
|
+
"""
|
|
19
|
+
Apply frequency weighting (A or C) to a signal.
|
|
20
|
+
|
|
21
|
+
:param x: Input signal.
|
|
22
|
+
:param fs: Sample rate.
|
|
23
|
+
:param curve: 'A', 'C' or 'Z' (Z is zero weighting/bypass).
|
|
24
|
+
:return: Weighted signal.
|
|
25
|
+
"""
|
|
26
|
+
x_proc = _typesignal(x)
|
|
27
|
+
curve = curve.upper()
|
|
28
|
+
|
|
29
|
+
if curve == "Z":
|
|
30
|
+
return x_proc
|
|
31
|
+
|
|
32
|
+
if curve not in ["A", "C"]:
|
|
33
|
+
raise ValueError("Weighting curve must be 'A', 'C' or 'Z'")
|
|
34
|
+
|
|
35
|
+
# Analog ZPK for A and C weighting
|
|
36
|
+
# f1, f2, f3, f4 constants as per IEC 61672-1
|
|
37
|
+
f1 = 20.598997
|
|
38
|
+
f4 = 12194.217
|
|
39
|
+
|
|
40
|
+
if curve == "A":
|
|
41
|
+
f2 = 107.65265
|
|
42
|
+
f3 = 737.86223
|
|
43
|
+
# Zeros at 0 Hz
|
|
44
|
+
z = np.array([0, 0, 0, 0])
|
|
45
|
+
# Poles
|
|
46
|
+
p = np.array([-2*np.pi*f1, -2*np.pi*f1, -2*np.pi*f4, -2*np.pi*f4,
|
|
47
|
+
-2*np.pi*f2, -2*np.pi*f3])
|
|
48
|
+
# k chosen to give 0 dB at 1000 Hz
|
|
49
|
+
# Reference gain at 1000Hz for A weighting: 10^(A1000/20) = 1.0 (0 dB)
|
|
50
|
+
k = 3.5174303309e13
|
|
51
|
+
|
|
52
|
+
# Recalculate k to ensure 0dB at 1kHz
|
|
53
|
+
w = 2 * np.pi * 1000
|
|
54
|
+
h = k * np.prod(1j * w - z) / np.prod(1j * w - p)
|
|
55
|
+
k = k / np.abs(h)
|
|
56
|
+
|
|
57
|
+
else: # C weighting
|
|
58
|
+
z = np.array([0, 0])
|
|
59
|
+
p = np.array([-2*np.pi*f1, -2*np.pi*f1, -2*np.pi*f4, -2*np.pi*f4])
|
|
60
|
+
k = 5.91797e8
|
|
61
|
+
|
|
62
|
+
# Recalculate k to ensure 0dB at 1kHz
|
|
63
|
+
w = 2 * np.pi * 1000
|
|
64
|
+
h = k * np.prod(1j * w - z) / np.prod(1j * w - p)
|
|
65
|
+
k = k / np.abs(h)
|
|
66
|
+
|
|
67
|
+
zd, pd, kd = signal.bilinear_zpk(z, p, k, fs)
|
|
68
|
+
sos = signal.zpk2sos(zd, pd, kd)
|
|
69
|
+
|
|
70
|
+
return cast(np.ndarray, signal.sosfilt(sos, x_proc))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def time_weighting(x: List[float] | np.ndarray, fs: int, mode: str = "fast") -> np.ndarray:
|
|
74
|
+
"""
|
|
75
|
+
Apply time weighting to a signal (Exponential averaging).
|
|
76
|
+
|
|
77
|
+
:param x: Input signal (usually the squared signal x^2).
|
|
78
|
+
:param fs: Sample rate.
|
|
79
|
+
:param mode: 'fast' (125ms), 'slow' (1000ms), 'impulse' (35ms rise).
|
|
80
|
+
:return: Time-weighted squared signal (sound pressure level envelope).
|
|
81
|
+
"""
|
|
82
|
+
x_proc = _typesignal(x)
|
|
83
|
+
|
|
84
|
+
modes = {
|
|
85
|
+
"fast": 0.125,
|
|
86
|
+
"slow": 1.0,
|
|
87
|
+
"impulse": 0.035
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if mode.lower() not in modes:
|
|
91
|
+
raise ValueError(f"Invalid time weighting mode. Use {list(modes.keys())}")
|
|
92
|
+
|
|
93
|
+
tau = modes[mode.lower()]
|
|
94
|
+
|
|
95
|
+
# RC filter implementation: y[n] = y[n-1] + (x[n] - y[n-1]) * dt / tau
|
|
96
|
+
# This is a first order IIR filter
|
|
97
|
+
alpha = 1 - np.exp(-1 / (fs * tau))
|
|
98
|
+
b = [alpha]
|
|
99
|
+
a = [1, -(1 - alpha)]
|
|
100
|
+
|
|
101
|
+
# We apply the weighting to the squared signal to get the Mean Square value
|
|
102
|
+
return cast(np.ndarray, signal.lfilter(b, a, x_proc**2))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def linkwitz_riley(
|
|
106
|
+
x: List[float] | np.ndarray,
|
|
107
|
+
fs: int,
|
|
108
|
+
freq: float,
|
|
109
|
+
order: int = 4
|
|
110
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
111
|
+
"""
|
|
112
|
+
Linkwitz-Riley crossover filter (Butterworth squared).
|
|
113
|
+
Splits signal into low and high bands with flat sum response.
|
|
114
|
+
|
|
115
|
+
:param x: Input signal.
|
|
116
|
+
:param fs: Sample rate.
|
|
117
|
+
:param freq: Crossover frequency.
|
|
118
|
+
:param order: Total order (must be even, typically 2 or 4).
|
|
119
|
+
:return: (low_pass_signal, high_pass_signal)
|
|
120
|
+
"""
|
|
121
|
+
x_proc = _typesignal(x)
|
|
122
|
+
if order % 2 != 0:
|
|
123
|
+
raise ValueError("Linkwitz-Riley order must be even (typically 2 or 4).")
|
|
124
|
+
|
|
125
|
+
# A Linkwitz-Riley filter of order N is two Butterworth filters of order N/2 in series
|
|
126
|
+
half_order = order // 2
|
|
127
|
+
wn = freq / (fs / 2)
|
|
128
|
+
|
|
129
|
+
sos_lp = signal.butter(half_order, wn, btype='low', output='sos')
|
|
130
|
+
sos_hp = signal.butter(half_order, wn, btype='high', output='sos')
|
|
131
|
+
|
|
132
|
+
# Pass twice
|
|
133
|
+
lp = signal.sosfilt(sos_lp, x_proc)
|
|
134
|
+
lp = signal.sosfilt(sos_lp, lp)
|
|
135
|
+
|
|
136
|
+
hp = signal.sosfilt(sos_hp, x_proc)
|
|
137
|
+
hp = signal.sosfilt(sos_hp, hp)
|
|
138
|
+
|
|
139
|
+
return lp, hp
|
pyoctaveband/utils.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Copyright (c) 2026. Jose M. Requena-Plens
|
|
2
|
+
"""
|
|
3
|
+
Signal processing utilities for pyoctaveband.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import List, Tuple, cast
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
from scipy import signal
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _typesignal(x: List[float] | np.ndarray | Tuple[float, ...]) -> np.ndarray:
|
|
15
|
+
"""
|
|
16
|
+
Ensure signal is a numpy array.
|
|
17
|
+
|
|
18
|
+
:param x: Input signal.
|
|
19
|
+
:return: Numpy array.
|
|
20
|
+
"""
|
|
21
|
+
if isinstance(x, np.ndarray):
|
|
22
|
+
return x
|
|
23
|
+
return cast(np.ndarray, np.atleast_1d(np.array(x)))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _resample_to_length(y: np.ndarray, factor: int, target_length: int) -> np.ndarray:
|
|
27
|
+
"""
|
|
28
|
+
Resample signal and ensure the output matches target_length exactly.
|
|
29
|
+
|
|
30
|
+
:param y: Input signal.
|
|
31
|
+
:param factor: Resampling factor.
|
|
32
|
+
:param target_length: Target length.
|
|
33
|
+
:return: Resampled signal.
|
|
34
|
+
"""
|
|
35
|
+
y_resampled = signal.resample_poly(y, factor, 1)
|
|
36
|
+
if len(y_resampled) > target_length:
|
|
37
|
+
y_resampled = y_resampled[:target_length]
|
|
38
|
+
elif len(y_resampled) < target_length:
|
|
39
|
+
y_resampled = np.pad(y_resampled, (0, target_length - len(y_resampled)))
|
|
40
|
+
return cast(np.ndarray, y_resampled)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _downsamplingfactor(freq: List[float], fs: int) -> np.ndarray:
|
|
44
|
+
"""
|
|
45
|
+
Compute optimal downsampling factors for filter stability.
|
|
46
|
+
|
|
47
|
+
:param freq: Frequencies.
|
|
48
|
+
:param fs: Sample rate.
|
|
49
|
+
:return: Array of factors.
|
|
50
|
+
"""
|
|
51
|
+
guard = 0.50
|
|
52
|
+
factor = (np.floor((fs / (2 + guard)) / np.array(freq))).astype("int")
|
|
53
|
+
return cast(np.ndarray, np.clip(factor, 1, 500))
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: PyOctaveBand
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: Octave-Band and Fractional Octave-Band filter for signals in time domain.
|
|
5
|
+
Author-email: Jose Manuel Requena Plens <jmrplens@gmail.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/jmrplens/PyOctaveBand
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/jmrplens/PyOctaveBand/issues
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: numpy
|
|
15
|
+
Requires-Dist: scipy
|
|
16
|
+
Requires-Dist: matplotlib
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
[](https://www.paypal.com/donate?hosted_button_id=BLP3R6VGYJB4Q)
|
|
20
|
+
[](https://ko-fi.com/jmrplens)
|
|
21
|
+
[](https://github.com/jmrplens/PyOctaveBand/actions/workflows/python-app.yml)
|
|
22
|
+
|
|
23
|
+
# PyOctaveBand
|
|
24
|
+
Advanced Octave-Band and Fractional Octave-Band filter bank for signals in the time domain. Fully compliant with **ANSI s1.11-2004** and **IEC 61260-1-2014**.
|
|
25
|
+
|
|
26
|
+
This library provides professional-grade tools for acoustic analysis, including frequency weighting (A, C, Z), time ballistics (Fast, Slow, Impulse), and multiple filter architectures.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## ๐ Table of Contents
|
|
31
|
+
1. [๐ Getting Started](#-getting-started)
|
|
32
|
+
- [Installation](#installation)
|
|
33
|
+
- [Basic Usage](#basic-usage-13-octave-analysis)
|
|
34
|
+
2. [๐ ๏ธ Filter Architectures](#๏ธ-filter-architectures)
|
|
35
|
+
- [Filter Comparison and Zoom](#filter-comparison-and-zoom)
|
|
36
|
+
- [Gallery of Responses](#gallery-of-filter-bank-responses)
|
|
37
|
+
3. [๐ Acoustic Weighting (A, C, Z)](#-acoustic-weighting-a-c-z)
|
|
38
|
+
4. [โฑ๏ธ Time Weighting and Integration](#๏ธ-time-weighting-and-integration)
|
|
39
|
+
5. [โก Performance: OctaveFilterBank](#-performance-octavefilterbank-class)
|
|
40
|
+
6. [๐ Filter Usage and Examples](#-filter-usage-and-examples)
|
|
41
|
+
- [1. Butterworth](#1-butterworth-butter)
|
|
42
|
+
- [2. Chebyshev I](#2-chebyshev-i-cheby1)
|
|
43
|
+
- [3. Chebyshev II](#3-chebyshev-ii-cheby2)
|
|
44
|
+
- [4. Elliptic](#4-elliptic-ellip)
|
|
45
|
+
- [5. Bessel](#5-bessel-bessel)
|
|
46
|
+
- [6. Linkwitz-Riley](#6-linkwitz-riley-lr)
|
|
47
|
+
7. [๐ Calibration and dBFS](#-calibration-and-dbfs)
|
|
48
|
+
- [Physical Calibration](#physical-calibration-sonรณmetro)
|
|
49
|
+
- [Digital Analysis (dBFS)](#digital-analysis-dbfs)
|
|
50
|
+
8. [๐ Signal Decomposition](#-signal-decomposition-and-stability)
|
|
51
|
+
9. [๐ Theory and Equations](#-theoretical-background)
|
|
52
|
+
- [Octave Band Frequencies](#octave-band-frequencies-ansi-s111--iec-61260)
|
|
53
|
+
- [Magnitude Responses](#magnitude-responses-hjw)
|
|
54
|
+
- [Weighting Curves](#weighting-curves-iec-61672-1)
|
|
55
|
+
- [Time Integration](#time-integration)
|
|
56
|
+
10. [๐งช Testing and Quality](#-development-and-verification)
|
|
57
|
+
- [Test Categories](#test-categories)
|
|
58
|
+
- [Commands](#commands)
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## ๐ Getting Started
|
|
63
|
+
|
|
64
|
+
### Installation
|
|
65
|
+
|
|
66
|
+
You can install `PyOctaveBand` by cloning the repository or adding it as a git submodule to your project.
|
|
67
|
+
|
|
68
|
+
**Option 1: Cloning and Installing**
|
|
69
|
+
```bash
|
|
70
|
+
git clone https://github.com/jmrplens/PyOctaveBand.git
|
|
71
|
+
cd PyOctaveBand
|
|
72
|
+
pip install .
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Option 2: Git Submodule (Recommended for projects)**
|
|
76
|
+
Add PyOctaveBand as a dependency within your own git repository:
|
|
77
|
+
```bash
|
|
78
|
+
git submodule add https://github.com/jmrplens/PyOctaveBand.git
|
|
79
|
+
# Then install in editable mode to use it from your project
|
|
80
|
+
pip install -e ./PyOctaveBand
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Basic Usage: 1/3 Octave Analysis
|
|
84
|
+
Analyze a signal and get the Sound Pressure Level (SPL) per frequency band.
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
import numpy as np
|
|
88
|
+
from pyoctaveband import octavefilter
|
|
89
|
+
|
|
90
|
+
fs = 48000
|
|
91
|
+
t = np.linspace(0, 1, fs)
|
|
92
|
+
# Composite signal: 100Hz + 1000Hz
|
|
93
|
+
signal = np.sin(2 * np.pi * 100 * t) + np.sin(2 * np.pi * 1000 * t)
|
|
94
|
+
|
|
95
|
+
# Apply 1/3 octave filter bank
|
|
96
|
+
spl, freq = octavefilter(signal, fs=fs, fraction=3)
|
|
97
|
+
|
|
98
|
+
print(f"Bands: {freq}")
|
|
99
|
+
print(f"SPL [dB]: {spl}")
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## ๐ ๏ธ Filter Architectures
|
|
105
|
+
|
|
106
|
+
PyOctaveBand supports several filter types, each with its own transfer function characteristic.
|
|
107
|
+
|
|
108
|
+
### Filter Comparison and Zoom
|
|
109
|
+
We use Second-Order Sections (SOS) for all filters to ensure numerical stability. The following plot compares the architectures focusing on the -3 dB crossover point.
|
|
110
|
+
|
|
111
|
+
<img src=".github/images/filter_type_comparison.png" width="80%"></img>
|
|
112
|
+
|
|
113
|
+
| Type | Name | Usage Example | Best For |
|
|
114
|
+
| :--- | :--- | :--- | :--- |
|
|
115
|
+
| `butter` | **Butterworth** | `octavefilter(x, fs, filter_type='butter')` | General acoustic measurement. |
|
|
116
|
+
| `cheby1` | **Chebyshev I** | `octavefilter(x, fs, filter_type='cheby1', ripple=0.1)` | Sharper roll-off at the cost of ripple. |
|
|
117
|
+
| `cheby2` | **Chebyshev II** | `octavefilter(x, fs, filter_type='cheby2', attenuation=60)` | Flat passband with stopband zeros. |
|
|
118
|
+
| `ellip` | **Elliptic** | `octavefilter(x, fs, filter_type='ellip', ripple=0.1, attenuation=60)` | Maximum selectivity. |
|
|
119
|
+
| `bessel` | **Bessel** | `octavefilter(x, fs, filter_type='bessel')` | Preserving transient waveform shapes. |
|
|
120
|
+
|
|
121
|
+
### Gallery of Filter Bank Responses
|
|
122
|
+
Full spectral view of the filter banks for Octave (1/1) and 1/3-Octave fractions.
|
|
123
|
+
|
|
124
|
+
| Architecture | 1/1 Octave (Fraction=1) | 1/3 Octave (Fraction=3) |
|
|
125
|
+
| :--- | :--- | :--- |
|
|
126
|
+
| **Butterworth** | <img src=".github/images/filter_butter_fraction_1_order_6.png" width="100%"> | <img src=".github/images/filter_butter_fraction_3_order_6.png" width="100%"> |
|
|
127
|
+
| **Chebyshev I** | <img src=".github/images/filter_cheby1_fraction_1_order_6.png" width="100%"> | <img src=".github/images/filter_cheby1_fraction_3_order_6.png" width="100%"> |
|
|
128
|
+
| **Chebyshev II** | <img src=".github/images/filter_cheby2_fraction_1_order_6.png" width="100%"> | <img src=".github/images/filter_cheby2_fraction_3_order_6.png" width="100%"> |
|
|
129
|
+
| **Elliptic** | <img src=".github/images/filter_ellip_fraction_1_order_6.png" width="100%"> | <img src=".github/images/filter_ellip_fraction_3_order_6.png" width="100%"> |
|
|
130
|
+
| **Bessel** | <img src=".github/images/filter_bessel_fraction_1_order_6.png" width="100%"> | <img src=".github/images/filter_bessel_fraction_3_order_6.png" width="100%"> |
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## ๐ Acoustic Weighting (A, C, Z)
|
|
135
|
+
|
|
136
|
+
Frequency weighting curves simulate the human ear\'s sensitivity.
|
|
137
|
+
|
|
138
|
+
<img src=".github/images/weighting_responses.png" width="80%"></img>
|
|
139
|
+
|
|
140
|
+
* **A-Weighting (`A`):** Standard for environmental noise (IEC 61672-1).
|
|
141
|
+
* **C-Weighting (`C`):** Used for peak sound pressure and high-level noise.
|
|
142
|
+
* **Z-Weighting (`Z`):** Zero weighting, completely flat response.
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
from pyoctaveband import weighting_filter
|
|
146
|
+
|
|
147
|
+
# Apply A-weighting to the raw signal
|
|
148
|
+
weighted_signal = weighting_filter(signal, fs, curve='A')
|
|
149
|
+
|
|
150
|
+
# Apply C-weighting for peak analysis
|
|
151
|
+
c_weighted_signal = weighting_filter(signal, fs, curve='C')
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## โฑ๏ธ Time Weighting and Integration
|
|
157
|
+
|
|
158
|
+
Accurate SPL measurement requires capturing energy over specific time windows.
|
|
159
|
+
|
|
160
|
+
<img src=".github/images/time_weighting_analysis.png" width="80%"></img>
|
|
161
|
+
|
|
162
|
+
* **Fast (`fast`):** $\tau = 125$ ms. Standard for noise fluctuations.
|
|
163
|
+
* **Slow (`slow`):** $\tau = 1000$ ms. Standard for steady noise.
|
|
164
|
+
* **Impulse (`impulse`):** 35 ms rise time. For explosive sounds.
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
from pyoctaveband import time_weighting
|
|
168
|
+
|
|
169
|
+
# Calculate energy envelope (Mean Square)
|
|
170
|
+
energy_envelope = time_weighting(signal, fs, mode='fast')
|
|
171
|
+
# dB SPL relative to 20ฮผPa
|
|
172
|
+
spl_t = 10 * np.log10(energy_envelope / (2e-5)**2)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## โก Performance: OctaveFilterBank Class
|
|
178
|
+
|
|
179
|
+
Pre-calculating coefficients saves significant CPU time when processing multiple frames.
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
from pyoctaveband import OctaveFilterBank
|
|
183
|
+
|
|
184
|
+
bank = OctaveFilterBank(fs=48000, fraction=3, filter_type='butter')
|
|
185
|
+
|
|
186
|
+
# Process multiple signals efficiently
|
|
187
|
+
for frame in stream:
|
|
188
|
+
spl, freq = bank.filter(frame)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## ๐ Filter Usage and Examples
|
|
194
|
+
|
|
195
|
+
This section provides detailed examples and characteristics for each supported filter architecture.
|
|
196
|
+
|
|
197
|
+
### 1. Butterworth (`butter`)
|
|
198
|
+
The Butterworth filter is known for its **maximally flat passband**. It is the standard choice for acoustic measurements where no ripple is allowed within the frequency bands.
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
from pyoctaveband import octavefilter
|
|
202
|
+
# Default standard measurement
|
|
203
|
+
spl, freq = octavefilter(x, fs, filter_type='butter')
|
|
204
|
+
```
|
|
205
|
+
<img src=".github/images/filter_butter_fraction_3_order_6.png" width="60%"></img>
|
|
206
|
+
|
|
207
|
+
### 2. Chebyshev I (`cheby1`)
|
|
208
|
+
Chebyshev Type I filters provide a **steeper roll-off** than Butterworth at the expense of ripples in the passband. Useful when high selectivity is needed near the cut-off frequencies.
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
# Selectivity with 0.1 dB passband ripple
|
|
212
|
+
spl, freq = octavefilter(x, fs, filter_type='cheby1', ripple=0.1)
|
|
213
|
+
```
|
|
214
|
+
<img src=".github/images/filter_cheby1_fraction_3_order_6.png" width="60%"></img>
|
|
215
|
+
|
|
216
|
+
### 3. Chebyshev II (`cheby2`)
|
|
217
|
+
Also known as Inverse Chebyshev, it has a **flat passband** and ripples in the stopband. It provides faster roll-off than Butterworth without affecting the signal in the passband.
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
# Flat passband with 60 dB stopband attenuation
|
|
221
|
+
spl, freq = octavefilter(x, fs, filter_type='cheby2', attenuation=60)
|
|
222
|
+
```
|
|
223
|
+
<img src=".github/images/filter_cheby2_fraction_3_order_6.png" width="60%"></img>
|
|
224
|
+
|
|
225
|
+
### 4. Elliptic (`ellip`)
|
|
226
|
+
Elliptic (Cauer) filters have the **shortest transition width** (steepest roll-off) for a given order. They feature ripples in both the passband and stopband.
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
# Maximum selectivity for extreme band isolation
|
|
230
|
+
spl, freq = octavefilter(x, fs, filter_type='ellip', ripple=0.1, attenuation=60)
|
|
231
|
+
```
|
|
232
|
+
<img src=".github/images/filter_ellip_fraction_3_order_6.png" width="60%"></img>
|
|
233
|
+
|
|
234
|
+
### 5. Bessel (`bessel`)
|
|
235
|
+
Bessel filters are optimized for **linear phase response** and minimal group delay. They preserve the shape of filtered waveforms (transients) better than any other type, but have the slowest roll-off.
|
|
236
|
+
|
|
237
|
+
```python
|
|
238
|
+
# Best for pulse analysis and transient preservation
|
|
239
|
+
spl, freq = octavefilter(x, fs, filter_type='bessel')
|
|
240
|
+
```
|
|
241
|
+
<img src=".github/images/filter_bessel_fraction_3_order_6.png" width="60%"></img>
|
|
242
|
+
|
|
243
|
+
### 6. Linkwitz-Riley (`lr`)
|
|
244
|
+
Specifically designed for **audio crossovers**. Linkwitz-Riley filters (typically 4th order) allow splitting a signal into bands that, when summed, result in a perfectly flat magnitude response and zero phase difference between bands at the crossover.
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
from pyoctaveband import linkwitz_riley
|
|
248
|
+
# Split signal into Low and High bands at 1000 Hz
|
|
249
|
+
low, high = linkwitz_riley(signal, fs, freq=1000, order=4)
|
|
250
|
+
# Reconstruction: low + high == signal (flat response)
|
|
251
|
+
```
|
|
252
|
+
<img src=".github/images/crossover_lr4.png" width="60%"></img>
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## ๐ Calibration and dBFS
|
|
257
|
+
|
|
258
|
+
PyOctaveBand can return results in physical **Sound Pressure Level (dB SPL)** or digital **decibels relative to Full Scale (dBFS)**.
|
|
259
|
+
|
|
260
|
+
### Physical Calibration (Sound Level Meter)
|
|
261
|
+
To get accurate SPL measurements from a digital recording, you must first calculate the sensitivity of your measurement chain using a reference tone (e.g., 94 dB @ 1kHz).
|
|
262
|
+
|
|
263
|
+
```python
|
|
264
|
+
from pyoctaveband import octavefilter, calculate_sensitivity
|
|
265
|
+
|
|
266
|
+
# 1. Record your 94dB calibrator signal
|
|
267
|
+
# ref_signal = ... (your recording)
|
|
268
|
+
|
|
269
|
+
# 2. Calculate sensitivity factor
|
|
270
|
+
sensitivity = calculate_sensitivity(ref_signal, target_spl=94.0)
|
|
271
|
+
|
|
272
|
+
# 3. Apply calibration to your measurements
|
|
273
|
+
spl, freq = octavefilter(signal, fs, calibration_factor=sensitivity)
|
|
274
|
+
# Now 'spl' values are in real-world dB SPL!
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Digital Analysis (dBFS)
|
|
278
|
+
...
|
|
279
|
+
### RMS vs Peak Levels
|
|
280
|
+
PyOctaveBand supports two measurement modes to align with professional software like BK:
|
|
281
|
+
- **RMS (`mode='rms'`)**: Energy-based level (standard).
|
|
282
|
+
- **Peak (`mode='peak'`)**: Absolute maximum value reached in the frame (Peak-holding).
|
|
283
|
+
|
|
284
|
+
```python
|
|
285
|
+
# Measure peak-holding levels for impact analysis
|
|
286
|
+
spl_peak, freq = octavefilter(signal, fs, mode='peak')
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## ๐ Signal Decomposition and Stability
|
|
292
|
+
|
|
293
|
+
By setting `sigbands=True`, you can retrieve the time-domain components of each band. This is useful for advanced analysis or signal reconstruction.
|
|
294
|
+
|
|
295
|
+
```python
|
|
296
|
+
import numpy as np
|
|
297
|
+
from pyoctaveband import octavefilter
|
|
298
|
+
|
|
299
|
+
# 1. Generate a signal (Sum of 250Hz and 1000Hz)
|
|
300
|
+
fs = 8000
|
|
301
|
+
t = np.linspace(0, 0.5, fs // 2, endpoint=False)
|
|
302
|
+
y = np.sin(2 * np.pi * 250 * t) + np.sin(2 * np.pi * 1000 * t)
|
|
303
|
+
|
|
304
|
+
# 2. Filter into octave bands and get time-domain signals (sigbands=True)
|
|
305
|
+
spl, freq, xb = octavefilter(y, fs=fs, fraction=1, sigbands=True)
|
|
306
|
+
|
|
307
|
+
# 'xb' is a list of arrays, where xb[i] is the signal filtered in band freq[i]
|
|
308
|
+
# Each band in 'xb' has the same length as the original input 'y'.
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
<img src=".github/images/signal_decomposition.png" width="80%"></img>
|
|
312
|
+
|
|
313
|
+
*The bottom plot shows the **Impulse Response** of a band, demonstrating the stability and decay characteristics of the filter.*
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## ๐ Theoretical Background
|
|
318
|
+
|
|
319
|
+
### Octave Band Frequencies (ANSI S1.11 / IEC 61260)
|
|
320
|
+
The mid-band frequencies ($f_m$) and edges ($f_1, f_2$) use a base-10 ratio $G = 10^{0.3}$:
|
|
321
|
+
- Mid-band: $f_m = 1000 \cdot G^{x/b}$ (for odd $b$)
|
|
322
|
+
- Band edges: $f_1 = f_m \cdot G^{-1/2b}$, $f_2 = f_m \cdot G^{1/2b}$
|
|
323
|
+
|
|
324
|
+
### Magnitude Responses $|H(j\omega)|$
|
|
325
|
+
1. **Butterworth:** $|H(j\omega)| = \frac{1}{\sqrt{1 + (\omega/\omega_c)^{2n}}}$ (Maximally flat)
|
|
326
|
+
2. **Chebyshev I:** $|H(j\omega)| = \frac{1}{\sqrt{1 + \epsilon^2 T_n^2(\omega/\omega_c)}}$ ($T_n$ is Chebyshev polynomial)
|
|
327
|
+
3. **Elliptic:** $|H(j\omega)| = \frac{1}{\sqrt{1 + \epsilon^2 R_n^2(\omega/\omega_c, L)}}$ ($R_n$ is Jacobian elliptic function)
|
|
328
|
+
|
|
329
|
+
### Weighting Curves (IEC 61672-1)
|
|
330
|
+
The A-weighting transfer function:
|
|
331
|
+
$$R_A(f) = \frac{12194^2 \cdot f^4}{(f^2 + 20.6^2)\sqrt{(f^2 + 107.7^2)(f^2 + 737.9^2)}(f^2 + 12194^2)}$$
|
|
332
|
+
$$A(f) = 20 \log_{10}(R_A(f)) + 2.00$$
|
|
333
|
+
|
|
334
|
+
### Time Integration
|
|
335
|
+
Implemented as a first-order IIR exponential integrator:
|
|
336
|
+
$$y[n] = \alpha \cdot x^2[n] + (1 - \alpha) \cdot y[n-1]$$
|
|
337
|
+
$$\alpha = 1 - e^{-1 / (f_s \cdot \tau)}$$
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## ๐งช Development and Verification
|
|
342
|
+
|
|
343
|
+
We maintain 100% stability and compliance through a rigorous test suite.
|
|
344
|
+
|
|
345
|
+
### Test Categories
|
|
346
|
+
1. **Isolation Tests:** Verifies that a pure 1kHz tone is attenuated by >20dB in the 250Hz and 4kHz bands.
|
|
347
|
+
2. **Weighting Response:** Checks gains at 100Hz (-19.1dB for A) and 1kHz (0dB).
|
|
348
|
+
3. **Stability (IR Tail):** Analyzes the Impulse Response of every filter. Energy in the last 100ms must be $< 10^{-6}$ to pass.
|
|
349
|
+
4. **Crossover Flatness:** Verifies that the sum of Linkwitz-Riley bands has $< 0.1$ dB deviation.
|
|
350
|
+
|
|
351
|
+
### Commands
|
|
352
|
+
```bash
|
|
353
|
+
# Run full suite
|
|
354
|
+
pytest tests/
|
|
355
|
+
|
|
356
|
+
# Generate technical report
|
|
357
|
+
python scripts/benchmark_filters.py
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
# Author
|
|
361
|
+
Jose M. Requena Plens, 2020 - 2026.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
pyoctaveband/__init__.py,sha256=sOQTYrlXZR9WXxkSAhzOYoHPLLmOFICqiMWpTu8983Q,3707
|
|
2
|
+
pyoctaveband/calibration.py,sha256=34ChOWw8ECmzsAlo4aqmyvLsDWARGngRqjpwcVvebbI,1007
|
|
3
|
+
pyoctaveband/core.py,sha256=kcwdX7wLLGXXfe0Rf3VFb5enIEiI9wlX5AdYjx-Kad8,7303
|
|
4
|
+
pyoctaveband/filter_design.py,sha256=ELVvb2xuF8ihKEjDR6M4m3vtrESBDTwfAQGvTxyeF9g,3911
|
|
5
|
+
pyoctaveband/frequencies.py,sha256=I9sEujrgLFu6HHTN-H-aViqcKBldDerBA9RBefQG52k,4136
|
|
6
|
+
pyoctaveband/parametric_filters.py,sha256=HXqkwbawPjcyvO3j41PrEGew6u0gX5TAy45EnpEFPlg,4219
|
|
7
|
+
pyoctaveband/utils.py,sha256=pTL6QDp-c5O8x3iIn4GLQMfP37xugP6uArg814zzwoM,1523
|
|
8
|
+
pyoctaveband-1.0.1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
9
|
+
pyoctaveband-1.0.1.dist-info/METADATA,sha256=SxTB3-JxFADCNBOA__EADsCu-a93tbW9DjKGxkA2IsU,14687
|
|
10
|
+
pyoctaveband-1.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
11
|
+
pyoctaveband-1.0.1.dist-info/top_level.txt,sha256=1pNs6wTDZMfo46rmnw61uK6xR9CIulvQM7nqydpp2u0,13
|
|
12
|
+
pyoctaveband-1.0.1.dist-info/RECORD,,
|