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.
@@ -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
+ [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/donate?hosted_button_id=BLP3R6VGYJB4Q)
20
+ [![Donate](https://img.shields.io/badge/Donate-Ko--fi-brightgreen?color=ff5f5f)](https://ko-fi.com/jmrplens)
21
+ [![Python application](https://github.com/jmrplens/PyOctaveBand/actions/workflows/python-app.yml/badge.svg)](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,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+