phonometry 3.0.0__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,370 @@
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
+ import math
10
+ from typing import List, Tuple, cast
11
+
12
+ import numpy as np
13
+ from scipy import signal
14
+
15
+ from .utils import _typesignal
16
+
17
+ try:
18
+ from numba import jit as _numba_jit
19
+ except ImportError: # pragma: no cover - depends on install extras
20
+ # unused-ignore: with numba absent its import is Any and the ignore is
21
+ # unnecessary; with numba installed the assignment needs it.
22
+ _numba_jit = None # type: ignore[assignment, unused-ignore]
23
+
24
+
25
+ class WeightingFilter:
26
+ """
27
+ Class-based frequency weighting filter (A, C, Z).
28
+ Allows pre-calculating and reusing filter coefficients.
29
+ """
30
+
31
+ def __init__(self, fs: int, curve: str = "A",
32
+ stateful: bool = False, steady_ic: bool = False,
33
+ high_accuracy: bool | None = None) -> None:
34
+ """
35
+ Initialize the weighting filter.
36
+
37
+ :param fs: Sample rate in Hz.
38
+ :param curve: 'A', 'C' or 'Z'.
39
+ :param stateful: If True, the weighting filter is stateful. Useful for block processing.
40
+ :param steady_ic: If True, calculate steady state initial conditions for filter.
41
+ :param high_accuracy: If True, design and run the filter at an internal
42
+ oversampled rate (>= 96 kHz) so the response stays within
43
+ IEC 61672-1 class 1 tolerances up to 16 kHz. The plain bilinear
44
+ design exceeds class 1 limits at 12.5 kHz for fs <= 48 kHz.
45
+ Defaults to True except in stateful mode (the internal FIR
46
+ resampling is incompatible with block processing).
47
+ """
48
+ if fs <= 0:
49
+ raise ValueError("Sample rate 'fs' must be positive.")
50
+ if high_accuracy is None:
51
+ high_accuracy = not stateful
52
+ if high_accuracy and stateful:
53
+ raise ValueError("high_accuracy is not compatible with stateful processing.")
54
+
55
+ self.fs = fs
56
+ self.curve = curve.upper()
57
+ self.stateful = stateful
58
+ self.high_accuracy = high_accuracy
59
+ self._oversample = min(8, max(1, math.ceil(96000 / fs))) if high_accuracy else 1
60
+
61
+ if self.curve == "Z":
62
+ self.sos = np.array([])
63
+ if self.stateful:
64
+ self.zi = np.array([])
65
+ return
66
+
67
+ if self.curve not in ["A", "C"]:
68
+ raise ValueError("Weighting curve must be 'A', 'C' or 'Z'")
69
+
70
+ # Analog ZPK for A and C weighting
71
+ # f1, f2, f3, f4 constants as per IEC 61672-1
72
+ f1 = 20.598997
73
+ f4 = 12194.217
74
+
75
+ if self.curve == "A":
76
+ f2 = 107.65265
77
+ f3 = 737.86223
78
+ # Zeros at 0 Hz
79
+ z = np.array([0, 0, 0, 0])
80
+ # Poles
81
+ p = np.array(
82
+ [
83
+ -2 * np.pi * f1,
84
+ -2 * np.pi * f1,
85
+ -2 * np.pi * f4,
86
+ -2 * np.pi * f4,
87
+ -2 * np.pi * f2,
88
+ -2 * np.pi * f3,
89
+ ]
90
+ )
91
+ # k chosen to give 0 dB at 1000 Hz
92
+ k = 3.5174303309e13
93
+
94
+ else: # C weighting
95
+ z = np.array([0, 0])
96
+ p = np.array([-2 * np.pi * f1, -2 * np.pi * f1, -2 * np.pi * f4, -2 * np.pi * f4])
97
+ k = 5.91797e8
98
+
99
+ # Recalculate k to ensure 0dB at 1kHz
100
+ w = 2 * np.pi * 1000
101
+ h = k * np.prod(1j * w - z) / np.prod(1j * w - p)
102
+ k = k / np.abs(h)
103
+
104
+ design_fs = self.fs * self._oversample
105
+ zd, pd, kd = signal.bilinear_zpk(z, p, k, design_fs)
106
+ self.sos = signal.zpk2sos(zd, pd, kd)
107
+
108
+ # Initialize filter state for stateful block-wise processing.
109
+ # Uses lazy allocation: zi is sized on first filter() call so that
110
+ # the channel dimension matches the actual input shape.
111
+ if self.stateful:
112
+ self.zi = np.array([])
113
+ self._steady_ic = steady_ic
114
+
115
+ def _init_filter_state(self, x_proc: np.ndarray) -> None:
116
+ """Allocate or reallocate ``zi`` to match the input shape."""
117
+ n_sections = self.sos.shape[0]
118
+ if x_proc.ndim == 1:
119
+ if self._steady_ic:
120
+ self.zi = signal.sosfilt_zi(self.sos)
121
+ else:
122
+ self.zi = np.zeros((n_sections, 2))
123
+ else:
124
+ n_channels = x_proc.shape[0]
125
+ if self._steady_ic:
126
+ zi_base = signal.sosfilt_zi(self.sos)
127
+ self.zi = np.tile(zi_base[:, np.newaxis, :], (1, n_channels, 1))
128
+ else:
129
+ self.zi = np.zeros((n_sections, n_channels, 2))
130
+
131
+ def _needs_zi_reinit(self, x_proc: np.ndarray) -> bool:
132
+ """Check whether ``zi`` must be (re)allocated for *x_proc*."""
133
+ if self.zi.size == 0:
134
+ return True
135
+ if x_proc.ndim == 1:
136
+ return self.zi.ndim != 2
137
+ return self.zi.ndim != 3 or self.zi.shape[1] != x_proc.shape[0]
138
+
139
+ def filter(self, x: List[float] | np.ndarray) -> np.ndarray:
140
+ """
141
+ Apply the weighting filter to a signal.
142
+
143
+ :param x: Input signal (1D or 2D [channels, samples]).
144
+ :return: Weighted signal.
145
+ """
146
+ x_proc = _typesignal(x)
147
+ if self.curve == "Z":
148
+ return x_proc
149
+
150
+ if self.stateful:
151
+ if self._needs_zi_reinit(x_proc):
152
+ self._init_filter_state(x_proc)
153
+ y, self.zi = signal.sosfilt(self.sos, x_proc, axis=-1, zi=self.zi)
154
+ elif self._oversample > 1:
155
+ if x_proc.shape[-1] == 0:
156
+ return x_proc # resample_poly rejects empty input
157
+ up = signal.resample_poly(x_proc, self._oversample, 1, axis=-1)
158
+ y_up = signal.sosfilt(self.sos, up, axis=-1)
159
+ y = signal.resample_poly(y_up, 1, self._oversample, axis=-1)
160
+ else:
161
+ y = signal.sosfilt(self.sos, x_proc, axis=-1)
162
+
163
+ return cast(np.ndarray, y)
164
+
165
+
166
+ def weighting_filter(
167
+ x: List[float] | np.ndarray, fs: int, curve: str = "A", high_accuracy: bool = True
168
+ ) -> np.ndarray:
169
+ """
170
+ Apply frequency weighting (A or C) to a signal.
171
+
172
+ :param x: Input signal.
173
+ :param fs: Sample rate.
174
+ :param curve: 'A', 'C' or 'Z' (Z is zero weighting/bypass).
175
+ :param high_accuracy: Use internal oversampling for IEC 61672-1 class 1
176
+ accuracy at high frequencies (default True).
177
+ :return: Weighted signal.
178
+ """
179
+ wf = WeightingFilter(fs, curve, high_accuracy=high_accuracy)
180
+ return wf.filter(x)
181
+
182
+
183
+ def _prepare_time_weighting_initial_state(
184
+ x_sq: np.ndarray,
185
+ initial_state: str | float | np.ndarray | None,
186
+ ) -> np.ndarray:
187
+ """Return the previous output state ``y[-1]`` for time weighting."""
188
+ invalid_initial_state_message = "initial_state must be None, 'zero', 'first', a scalar, or an array"
189
+ state_shape = x_sq.shape[:-1]
190
+
191
+ if initial_state is None:
192
+ return np.zeros(state_shape, dtype=x_sq.dtype)
193
+
194
+ if isinstance(initial_state, str):
195
+ state_name = initial_state.lower()
196
+ if state_name == "zero":
197
+ return np.zeros(state_shape, dtype=x_sq.dtype)
198
+ if state_name == "first":
199
+ if x_sq.shape[-1] == 0:
200
+ raise ValueError(invalid_initial_state_message)
201
+ return np.asarray(np.take(x_sq, 0, axis=-1), dtype=x_sq.dtype).copy()
202
+ raise ValueError(invalid_initial_state_message)
203
+
204
+ state = np.asarray(initial_state, dtype=x_sq.dtype)
205
+ if state.shape == ():
206
+ return np.full(state_shape, state.item(), dtype=x_sq.dtype)
207
+
208
+ try:
209
+ return np.broadcast_to(state, state_shape).astype(x_sq.dtype, copy=True)
210
+ except ValueError as exc:
211
+ raise ValueError(
212
+ "initial_state must be scalar or broadcastable to the input shape without the time axis"
213
+ ) from exc
214
+
215
+
216
+ def _impulse_kernel_py(
217
+ x_t: np.ndarray,
218
+ alpha_rise: float,
219
+ alpha_fall: float,
220
+ initial_state: np.ndarray,
221
+ ) -> np.ndarray:
222
+ """Asymmetric time-weighting kernel (pure Python; jitted when numba is present)."""
223
+ y_t = np.zeros_like(x_t)
224
+ curr_y = initial_state.copy()
225
+
226
+ for i in range(x_t.shape[0]):
227
+ val = x_t[i]
228
+ rising = val > curr_y
229
+
230
+ diff = val - curr_y
231
+ factor = np.where(rising, alpha_rise, alpha_fall)
232
+ curr_y += factor * diff
233
+ y_t[i] = curr_y
234
+
235
+ return y_t
236
+
237
+
238
+ if _numba_jit is not None:
239
+ _apply_impulse_kernel = _numba_jit(nopython=True, cache=True)(_impulse_kernel_py)
240
+ else: # pragma: no cover - exercised only without numba installed
241
+ _apply_impulse_kernel = _impulse_kernel_py
242
+
243
+ def time_weighting(
244
+ x: List[float] | np.ndarray,
245
+ fs: int,
246
+ mode: str = "fast",
247
+ initial_state: str | float | np.ndarray | None = None,
248
+ ) -> np.ndarray:
249
+ """
250
+ Apply time weighting to a signal (Exponential averaging).
251
+
252
+ :param x: Input signal (raw pressure/voltage). The function squares it internally.
253
+ :param fs: Sample rate.
254
+ :param mode: 'fast' (125ms), 'slow' (1000ms), 'impulse' (35ms rise, 1500ms fall).
255
+ :param initial_state: Previous mean-square output state ``y[-1]``. Use None/'zero' for
256
+ zero initialization (default), 'first' to initialize from the first input energy,
257
+ or a scalar/array broadcastable to the input shape without the time axis.
258
+ :return: Time-weighted squared signal (sound pressure level envelope).
259
+ """
260
+ x_proc = _typesignal(x)
261
+ if fs <= 0:
262
+ raise ValueError("Sample rate 'fs' must be positive.")
263
+ x_sq = x_proc**2
264
+ initial = _prepare_time_weighting_initial_state(x_sq, initial_state)
265
+
266
+ mode_lower = mode.lower()
267
+
268
+ if mode_lower in ["fast", "slow"]:
269
+ tau = 0.125 if mode_lower == "fast" else 1.0
270
+ alpha = 1 - np.exp(-1 / (fs * tau))
271
+ b = [alpha]
272
+ a = [1, -(1 - alpha)]
273
+ # We apply the weighting to the squared signal to get the Mean Square value
274
+ zi = np.expand_dims((1 - alpha) * initial, axis=-1)
275
+ y, _ = signal.lfilter(b, a, x_sq, axis=-1, zi=zi)
276
+ return cast(np.ndarray, y)
277
+
278
+ elif mode_lower == "impulse":
279
+ # IEC 61672-1: 35ms for rising, 1500ms for falling
280
+ tau_rise = 0.035
281
+ tau_fall = 1.5
282
+
283
+ alpha_rise = 1 - np.exp(-1 / (fs * tau_rise))
284
+ alpha_fall = 1 - np.exp(-1 / (fs * tau_fall))
285
+
286
+ # Move time axis to front for iteration
287
+ x_t = np.moveaxis(x_sq, -1, 0)
288
+
289
+ # Ensure contiguous array for Numba
290
+ x_t = np.ascontiguousarray(x_t)
291
+ initial_kernel = initial if initial.ndim == 0 else np.ascontiguousarray(initial)
292
+ y_t = _apply_impulse_kernel(x_t, alpha_rise, alpha_fall, initial_kernel)
293
+
294
+ # Move time axis back
295
+ return np.moveaxis(y_t, 0, -1)
296
+
297
+ else:
298
+ raise ValueError("Invalid time weighting mode. Use ['fast', 'slow', 'impulse']")
299
+
300
+
301
+ class TimeWeighting:
302
+ """
303
+ Stateful time weighting for block processing.
304
+
305
+ Wraps :func:`time_weighting` carrying the exponential integrator state
306
+ across blocks, so concatenated block outputs equal a single continuous call.
307
+ """
308
+
309
+ def __init__(self, fs: int, mode: str = "fast") -> None:
310
+ """
311
+ :param fs: Sample rate in Hz.
312
+ :param mode: 'fast' (125 ms), 'slow' (1000 ms) or 'impulse' (35 ms / 1.5 s).
313
+ """
314
+ if fs <= 0:
315
+ raise ValueError("Sample rate 'fs' must be positive.")
316
+ if mode.lower() not in ("fast", "slow", "impulse"):
317
+ raise ValueError("Invalid time weighting mode. Use ['fast', 'slow', 'impulse']")
318
+ self.fs = fs
319
+ self.mode = mode.lower()
320
+ self._state: np.ndarray | None = None
321
+
322
+ def process(self, x: List[float] | np.ndarray) -> np.ndarray:
323
+ """Apply time weighting to a block, continuing from the previous block."""
324
+ x_proc = _typesignal(x)
325
+ if x_proc.shape[-1] == 0:
326
+ return x_proc # nothing to process; keep the carried state
327
+ env = time_weighting(x_proc, self.fs, mode=self.mode, initial_state=self._state)
328
+ self._state = np.asarray(env[..., -1]).copy()
329
+ return env
330
+
331
+ def reset(self) -> None:
332
+ """Forget the carried state (the next block starts from rest)."""
333
+ self._state = None
334
+
335
+
336
+ def linkwitz_riley(
337
+ x: List[float] | np.ndarray,
338
+ fs: int,
339
+ freq: float,
340
+ order: int = 4
341
+ ) -> Tuple[np.ndarray, np.ndarray]:
342
+ """
343
+ Linkwitz-Riley crossover filter (Butterworth squared).
344
+ Splits signal into low and high bands with flat sum response.
345
+
346
+ :param x: Input signal.
347
+ :param fs: Sample rate.
348
+ :param freq: Crossover frequency.
349
+ :param order: Total order (must be even, typically 2 or 4).
350
+ :return: (low_pass_signal, high_pass_signal)
351
+ """
352
+ x_proc = _typesignal(x)
353
+ if order % 2 != 0:
354
+ raise ValueError("Linkwitz-Riley order must be even (typically 2 or 4).")
355
+
356
+ # A Linkwitz-Riley filter of order N is two Butterworth filters of order N/2 in series
357
+ half_order = order // 2
358
+ wn = freq / (fs / 2)
359
+
360
+ sos_lp = signal.butter(half_order, wn, btype='low', output='sos')
361
+ sos_hp = signal.butter(half_order, wn, btype='high', output='sos')
362
+
363
+ # Pass twice
364
+ lp = signal.sosfilt(sos_lp, x_proc)
365
+ lp = signal.sosfilt(sos_lp, lp)
366
+
367
+ hp = signal.sosfilt(sos_hp, x_proc)
368
+ hp = signal.sosfilt(sos_hp, hp)
369
+
370
+ return lp, hp
phonometry/py.typed ADDED
File without changes
phonometry/utils.py ADDED
@@ -0,0 +1,74 @@
1
+ # Copyright (c) 2026. Jose M. Requena-Plens
2
+ """
3
+ Signal processing utilities for phonometry.
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 float64 numpy array.
17
+
18
+ Integer inputs (e.g. int16 audio from ``scipy.io.wavfile.read``) are
19
+ converted to float64 to prevent silent overflow when the signal is
20
+ squared internally. Float64 arrays are passed through without copying.
21
+
22
+ :param x: Input signal.
23
+ :return: Numpy float64 array.
24
+ """
25
+ return np.atleast_1d(np.asarray(x, dtype=np.float64))
26
+
27
+
28
+ def _resample_to_length(y: np.ndarray, factor: int, target_length: int) -> np.ndarray:
29
+ """
30
+ Resample signal and ensure the output matches target_length exactly.
31
+ Handles both 1D and 2D (channels, samples) arrays.
32
+
33
+ :param y: Input signal.
34
+ :param factor: Resampling factor.
35
+ :param target_length: Target length.
36
+ :return: Resampled signal.
37
+ """
38
+ if factor == 1:
39
+ # Nothing to resample: fall through to the slice/pad logic only.
40
+ y_resampled = y
41
+ else:
42
+ y_resampled = cast(np.ndarray, signal.resample_poly(y, factor, 1, axis=-1))
43
+ current_length = y_resampled.shape[-1]
44
+
45
+ if current_length > target_length:
46
+ # Slice along the last axis (works for both 1D and 2D)
47
+ y_resampled = y_resampled[..., :target_length]
48
+
49
+ elif current_length < target_length:
50
+ diff = target_length - current_length
51
+ # Pad only the last axis. This works for both 1D and 2D arrays.
52
+ # For 1D, pad_width becomes `[(0, diff)]`.
53
+ # For 2D, pad_width becomes `[(0, 0), (0, diff)]`.
54
+ pad_width: List[Tuple[int, int]] = [(0, 0)] * (y_resampled.ndim - 1) + [(0, diff)]
55
+
56
+ y_resampled = np.pad(y_resampled, pad_width, mode='constant')
57
+
58
+ return y_resampled
59
+
60
+
61
+ def _downsamplingfactor(freq: List[float], fs: int, headroom: float = 1.25) -> np.ndarray:
62
+ """
63
+ Compute optimal downsampling factors for filter stability.
64
+
65
+ :param freq: Band upper-edge frequencies.
66
+ :param fs: Sample rate.
67
+ :param headroom: Required ratio between the decimated Nyquist and the
68
+ band's upper edge. 1.25 reproduces the classic ``fs / (2 + 0.5)``
69
+ guard; filter types whose design extends above the upper edge
70
+ (cheby2 stopband) need more.
71
+ :return: Array of factors.
72
+ """
73
+ factor = (np.floor((fs / 2) / (headroom * np.array(freq)))).astype("int")
74
+ return cast(np.ndarray, np.clip(factor, 1, 500))
@@ -0,0 +1,126 @@
1
+ Metadata-Version: 2.4
2
+ Name: phonometry
3
+ Version: 3.0.0
4
+ Summary: Acoustic measurement toolkit: fractional octave-band filters, A/C/Z and time weighting, and sound level metrology per IEC 61260-1 and IEC 61672-1.
5
+ Author-email: Jose Manuel Requena Plens <jmrplens@gmail.com>
6
+ Project-URL: Homepage, https://github.com/jmrplens/phonometry
7
+ Project-URL: Bug Tracker, https://github.com/jmrplens/phonometry/issues
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.13
10
+ Classifier: Programming Language :: Python :: 3.14
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Topic :: Multimedia :: Sound/Audio :: Analysis
15
+ Classifier: Topic :: Scientific/Engineering :: Physics
16
+ Classifier: Typing :: Typed
17
+ Requires-Python: >=3.13
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: numpy>=2.4.4
21
+ Requires-Dist: scipy>=1.17.1
22
+ Provides-Extra: perf
23
+ Requires-Dist: numba>=0.65.1; extra == "perf"
24
+ Provides-Extra: plot
25
+ Requires-Dist: matplotlib>=3.10.9; extra == "plot"
26
+ Provides-Extra: full
27
+ Requires-Dist: numba>=0.65.1; extra == "full"
28
+ Requires-Dist: matplotlib>=3.10.9; extra == "full"
29
+ Dynamic: license-file
30
+
31
+ <!-- Package -->
32
+ [![PyPI version](https://img.shields.io/pypi/v/phonometry?logo=pypi&logoColor=white)](https://pypi.org/project/phonometry/)
33
+ [![Python versions](https://img.shields.io/pypi/pyversions/phonometry?logo=python&logoColor=white)](https://pypi.org/project/phonometry/)
34
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/jmrplens/phonometry/blob/main/LICENSE)
35
+
36
+ <!-- Quality -->
37
+ [![CI](https://github.com/jmrplens/phonometry/actions/workflows/python-app.yml/badge.svg)](https://github.com/jmrplens/phonometry/actions/workflows/python-app.yml)
38
+ [![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=jmrplens_PyOctaveBand&metric=alert_status)](https://sonarcloud.io/summary/overall?id=jmrplens_PyOctaveBand)
39
+ [![codecov](https://codecov.io/gh/jmrplens/phonometry/branch/main/graph/badge.svg)](https://codecov.io/gh/jmrplens/phonometry)
40
+
41
+ <!-- Citation & support -->
42
+ [![DOI](https://img.shields.io/badge/DOI-10.5281%2Fzenodo.21215280-blue?logo=doi&logoColor=white)](https://doi.org/10.5281/zenodo.21215280)
43
+
44
+ # phonometry
45
+
46
+ > *phonometry* β€” the measurement of sound. Formerly published as **PyOctaveBand**.
47
+
48
+ Acoustic measurement toolkit for Python: fractional octave-band filter banks, frequency and time weighting, and sound level metrology β€” conformance-tested against **IEC 61260-1:2014 / ANSI S1.11-2004** (filters) and **IEC 61672-1:2013** (weighting and levels) class 1 tolerance limits.
49
+
50
+ <img src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/filter_type_comparison.png" alt="Magnitude response comparison of the five filter architectures for the 1 kHz octave band, with a zoom at the -3 dB crossover" width="80%">
51
+
52
+ ## ✨ Highlights
53
+
54
+ - πŸŽ›οΈ 1/1, 1/3 and arbitrary fractional octave filter banks (stable SOS + multirate decimation)
55
+ - πŸ—οΈ Five architectures: Butterworth, Chebyshev I/II, Elliptic, Bessel β€” all with βˆ’3 dB points on the ANSI band edges
56
+ - πŸ”Š A/C/Z frequency weighting within IEC 61672-1 class 1 tolerances
57
+ - ⏱️ Fast/Slow/Impulse time ballistics, `Leq`, `LAeq` and `L10/L50/L90` statistical levels
58
+ - πŸ—ΊοΈ Octave spectrogram (band levels over time) and zero-phase offline filtering
59
+ - πŸ“ Physical SPL calibration and dBFS modes
60
+ - ⚑ Vectorized multichannel processing and stateful block (real-time) workflows
61
+
62
+ ## πŸš€ Installation
63
+
64
+ ```bash
65
+ pip install phonometry
66
+ ```
67
+
68
+ Optional extras: `phonometry[plot]` (matplotlib for response plots), `phonometry[perf]` (numba for faster impulse ballistics), `phonometry[full]` (both).
69
+
70
+ ## πŸ“š Documentation
71
+
72
+ **Full documentation website: https://jmrplens.github.io/phonometry/** (English / EspaΓ±ol)
73
+
74
+ Or browse the Markdown docs on GitHub:
75
+
76
+ | Page | Contents |
77
+ | :--- | :--- |
78
+ | [Getting Started](https://github.com/jmrplens/phonometry/blob/main/docs/getting-started.md) | Installation, first analysis, WAV files |
79
+ | [Filter Banks](https://github.com/jmrplens/phonometry/blob/main/docs/filter-banks.md) | Architectures, response gallery, band decomposition, zero-phase |
80
+ | [Frequency Weighting](https://github.com/jmrplens/phonometry/blob/main/docs/weighting.md) | A/C/Z curves, class 1 high-accuracy mode |
81
+ | [Time Weighting](https://github.com/jmrplens/phonometry/blob/main/docs/time-weighting.md) | Fast/Slow/Impulse ballistics, initial state |
82
+ | [Levels](https://github.com/jmrplens/phonometry/blob/main/docs/levels.md) | Leq, LAeq, L10/L50/L90, octave spectrogram |
83
+ | [Calibration and dBFS](https://github.com/jmrplens/phonometry/blob/main/docs/calibration.md) | Physical SPL, digital full-scale, RMS vs peak |
84
+ | [Block Processing](https://github.com/jmrplens/phonometry/blob/main/docs/block-processing.md) | Stateful streaming workflows |
85
+ | [Multichannel](https://github.com/jmrplens/phonometry/blob/main/docs/multichannel.md) | Vectorized multichannel analysis, performance |
86
+ | [API Reference](https://github.com/jmrplens/phonometry/blob/main/docs/api-reference.md) | Every public function and class |
87
+ | [Theory](https://github.com/jmrplens/phonometry/blob/main/docs/theory.md) | Standards, math, design decisions |
88
+ | [Why phonometry](https://github.com/jmrplens/phonometry/blob/main/docs/why-phonometry.md) | IEC compliance verification vs other libraries |
89
+
90
+ ## ⚑ Quick start
91
+
92
+ ```python
93
+ import numpy as np
94
+ from phonometry import octavefilter
95
+
96
+ fs = 48000
97
+ t = np.linspace(0, 1, fs, endpoint=False)
98
+ # Composite signal: 100Hz + 1000Hz
99
+ signal = np.sin(2 * np.pi * 100 * t) + np.sin(2 * np.pi * 1000 * t)
100
+
101
+ # Apply 1/3 octave filter bank
102
+ spl, freq = octavefilter(signal, fs=fs, fraction=3)
103
+
104
+ print(f"Bands: {freq}")
105
+ print(f"SPL [dB]: {spl}")
106
+ ```
107
+
108
+ <img src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/signal_response_fraction_3.png" alt="One-third-octave spectrum analysis of a multi-tone signal with the raw PSD in the background" width="80%">
109
+
110
+ *1/3 Octave Band spectrum analysis of a complex signal. More examples in the
111
+ [documentation](https://jmrplens.github.io/phonometry/).*
112
+
113
+ ## πŸ§ͺ Development
114
+
115
+ ```bash
116
+ make install # dependencies + editable install
117
+ make check # ruff + mypy + bandit + tests
118
+ make graphs # regenerate documentation images
119
+ ```
120
+
121
+ See https://github.com/jmrplens/phonometry/blob/main/CONTRIBUTING.md and the
122
+ https://github.com/jmrplens/phonometry/blob/main/CHANGELOG.md
123
+
124
+ ## πŸ“„ License
125
+
126
+ [MIT](https://github.com/jmrplens/phonometry/blob/main/LICENSE)
@@ -0,0 +1,16 @@
1
+ phonometry/__init__.py,sha256=jFIpT1TzUSkVztO-LiFNpkA0Nqmiwh1SdgKTI-0qoTE,8160
2
+ phonometry/_version.py,sha256=GbetSl2f2RooDvKeN_2ANFqlVg6x_TSQZme_To4_N3M,626
3
+ phonometry/calibration.py,sha256=wxDu4jGMqEo9nWigsAlWlpc_r7KzCreecq_cU2LMxvE,4079
4
+ phonometry/compliance.py,sha256=VuAOWkJAnkAI7YKX9-fQK7AG9U4D3hmDEfrUXhRMFjo,8161
5
+ phonometry/core.py,sha256=1TDW7KWsE9v1S5-CJcw6kjfjeLgmq0dzZa1v4Zcs_9I,18684
6
+ phonometry/filter_design.py,sha256=T7mYPCCVaIW62eO3cVkVNOlnwX3_rAwH1co73fHri-M,7587
7
+ phonometry/frequencies.py,sha256=9Wk7NILiN8VTu68qc5NdKKOqOMuGaH8JzvQ1bxkPlNo,5871
8
+ phonometry/levels.py,sha256=RlNlZGg3Cd1NMtEdus1ulgY75JxPfu5je1h8xT-pKGM,9808
9
+ phonometry/parametric_filters.py,sha256=WJ863nmO1Rye3RFDPSLsfoIbA2rVncL_UlsKbq_YxGI,13393
10
+ phonometry/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ phonometry/utils.py,sha256=ZWdWSjCSmYHCl749cH0MuGYhKZ4A5GOf8aYrboJAwoo,2639
12
+ phonometry-3.0.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
13
+ phonometry-3.0.0.dist-info/METADATA,sha256=GQRPDiJj_fuUuGpPCWcXCf2_hkJRr96o4u18WiZQrgs,6652
14
+ phonometry-3.0.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
15
+ phonometry-3.0.0.dist-info/top_level.txt,sha256=ditjBTAq4gCoEFT8KPKY0PMNts0TlNu3uuzpcjQQtiM,11
16
+ phonometry-3.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+