[READ-ONLY] Mirror of https://github.com/jmrplens/PyOctaveBand. [Python3] Octave-Band and Fractional Octave-Band filter. For signal in time domain. jmrplens.github.io/PyOctaveBand/
acoustics audio filter frequency frequency-analysis frequency-domain octave python3 signal time-domain
0

Configure Feed

Select the types of activity you want to include in your feed.

docs: retrofit the house conventions onto the legacy guides

Audit batch 8 - the pre-convention guides catch up with the style the
newer ones follow, on all three surfaces (repo, site EN, site ES):

- Standards footers for the ten guides that cited clauses inline but
never closed with the normative scope (calibration, filter-banks,
intensity, levels, time-weighting, weighting, outdoor-propagation,
sound-power, psychoacoustics, plus materials synced from the site
style and a new room-acoustics footer); block-processing,
multichannel and getting-started verified as having no normative
citations to footer
- every touched snippet is now self-contained or carries an explicit
reuse annotation, and ~20 prints gained expected outputs EXECUTED
against the library (levels LN values re-verified, materials
# 222956, human-vibration VDV/MTVV/crest, getting-started ~91 dB
bands, Zwicker sone/phon values, enclosed-space RT spectrum)
- 12 figure-reproduction <details> blocks added (materials x3,
outdoor-propagation x2, time-weighting x2, weighting x3,
speech-intelligibility, getting-started - with an honest note where
the committed figure overlays extra context), all verified runnable
- structural reorders, content-preserving: the exponential detector now
precedes the F/S/I modes; the filter parameter table follows the
comparison it references; weighting opens with 'Where the curves come
from'; block-processing gains a motivating opening and drops its
duplicated constraints list; the raw LaTeX in human-vibration code
spans becomes proper math; F2/F3/F4 defined in prose in intensity
- sound-power SS1-3 gain result figures: three new deterministic
generators (12 PNG variants, ES translations, visually verified)
embedded with captions and <details> using each result's .plot()

José M. Requena Plens (Jul 10, 2026, 9:03 AM +0200) 3aeceed6 c0210728

+2358 -372
+11 -14
docs/block-processing.md
··· 2 2 3 3 # Block Processing 4 4 5 + Some measurements never fit in memory: an hour-long environmental recording, 6 + a live monitor that must report levels while the microphone is still 7 + capturing, or an embedded logger that only ever sees one buffer at a time. In 8 + all of these the signal has to be processed block by block — and the filters 9 + must behave exactly as if they had seen the whole signal at once. 10 + 5 11 The `OctaveFilterBank`, `WeightingFilter` (for A, C, or Z-weighting) and 6 12 `TimeWeighting` classes support block (streaming) processing: the internal 7 13 filter state is carried between calls, so concatenated block outputs match a ··· 15 21 16 22 Create a stateful filter bank with `stateful=True`. The internal state is 17 23 zero-initialized by default but may be initialized for step-response 18 - steady-state (like `scipy.signal.sosfilt_zi`) with `steady_ic=True`. 19 - 20 - Notes when using a stateful `OctaveFilterBank`: 21 - 22 - - Detrending should be disabled during block processing (`detrend=False`), as it 23 - can introduce discontinuities between blocks. 24 - - Resampling is not supported for block processing, so you need to set 25 - `resample=False`. 26 - - `zero_phase=True` is incompatible with stateful mode (forward-backward 27 - filtering needs the whole signal). 28 - - Stateful `WeightingFilter` uses the legacy bilinear design 29 - (`high_accuracy=False`); see [Frequency Weighting](weighting.md). 24 + steady-state (like `scipy.signal.sosfilt_zi`) with `steady_ic=True`. The 25 + options that must be disabled in stateful mode are summarized in the 26 + [constraints table](#stateful-mode-constraints) at the end of this guide. 30 27 31 28 ## Example 32 29 ··· 69 66 ``` 70 67 71 68 Or manage the state yourself with the functional API — see 72 - [Time Weighting](time-weighting.md#block-processing). 69 + [Time Weighting](time-weighting.md#6-block-processing). 73 70 74 71 ## Multichannel state 75 72 ··· 103 100 | `detrend` | must be `False` | Per-block detrending creates boundary discontinuities | 104 101 | `resample` | must be `False` | The resampler is not stateful | 105 102 | `zero_phase` | unsupported | Forward-backward filtering needs the whole signal | 106 - | `high_accuracy` (weighting) | resolves to `False` by default; explicitly passing `True` raises `ValueError` | The polyphase resampling inside is block-incompatible | 103 + | `high_accuracy` (weighting) | resolves to `False` by default — the legacy bilinear design, see [Frequency Weighting](weighting.md); explicitly passing `True` raises `ValueError` | The polyphase resampling inside is block-incompatible | 107 104 | `steady_ic` | optional | Starts the filters in step-response steady state |
+8
docs/calibration.md
··· 162 162 Integer signals (e.g. int16 from `scipy.io.wavfile.read`) are converted to 163 163 float64 internally before any squaring, so calibration and level results are 164 164 identical whether you pass the raw integer array or a float conversion. 165 + 166 + --- 167 + 168 + **Standards.** IEC 60942:2017, *Electroacoustics — Sound calibrators* — the 169 + calibrator level and class assumptions behind `sensitivity()` (the 94 dB 170 + principal level and the Table 1 class tolerances) and the short-term 171 + level-fluctuation stability check of the reference recording (5.3.3, Table 2 172 + class 1 limits per nominal frequency).
+1
docs/enclosed-space-absorption.md
··· 86 86 volume=60.0, air_condition="20C_50-70", 87 87 ) 88 88 print(result.reverberation_time.round(2)) 89 + # [2.13 1.03 0.62 0.48 0.43 0.42 0.4 ] 89 90 ``` 90 91 91 92 <picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/enclosed_space_absorption_dark.png"><img src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/enclosed_space_absorption.png" alt="Two panels for a 60 cubic metre office with a bare versus an acoustically-treated ceiling. Left: the equivalent absorption area per octave band, much higher across mid and high frequencies with the acoustic ceiling. Right: the reverberation time per octave band, falling from around five seconds at low frequency for the bare room to under one second with the acoustic ceiling" width="96%"></picture>
+43 -25
docs/filter-banks.md
··· 6 6 characteristic. All banks place their **−3 dB points on the ANSI S1.11 band 7 7 edges**, so band levels are comparable across architectures. 8 8 9 - ## Fractional octave bands: the math 9 + ## 1. Fractional octave bands: the math 10 10 11 11 IEC 61260-1:2014 builds every band from the base-10 octave ratio 12 12 $G = 10^{3/10} \approx 1.99526$ (so "one octave" is *not* exactly 2). For ··· 32 32 33 33 <picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/diagram_multirate_dark.svg"><img src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/diagram_multirate.svg" alt="Multirate decimation: high bands filtered at the input rate, low bands after anti-alias low-pass and decimation so the SOS sections stay numerically healthy" width="92%"></picture> 34 34 35 - ### `octave_filter()` / `OctaveFilterBank` parameters 35 + ## 2. Filter Comparison and Zoom 36 + 37 + We use Second-Order Sections (SOS) for all filters to ensure numerical stability. 38 + The following plot compares the architectures focusing on the -3 dB crossover point. 39 + 40 + <picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/filter_type_comparison_dark.png"><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%"></picture> 41 + 42 + | Type | Name | Usage Example | Best For | 43 + | :--- | :--- | :--- | :--- | 44 + | `butter` | **Butterworth** | `octave_filter(x, fs, filter_type='butter')` | General acoustic measurement. | 45 + | `cheby1` | **Chebyshev I** | `octave_filter(x, fs, filter_type='cheby1', ripple=0.1)` | Sharper roll-off at the cost of ripple. | 46 + | `cheby2` | **Chebyshev II** | `octave_filter(x, fs, filter_type='cheby2')` | Flat passband with stopband zeros. | 47 + | `ellip` | **Elliptic** | `octave_filter(x, fs, filter_type='ellip', ripple=0.1)` | Maximum selectivity. | 48 + | `bessel` | **Bessel** | `octave_filter(x, fs, filter_type='bessel')` | Preserving transient waveform shapes. | 49 + 50 + ## 3. `octave_filter()` / `OctaveFilterBank` parameters 36 51 37 52 | Parameter | Type | Units | Range / default | Notes | 38 53 | :--- | :--- | :--- | :--- | :--- | ··· 58 73 Table 1 acceptance limits and reports the class (`1`, `2` or `None` if outside both) with per-band 59 74 margins. 60 75 61 - ## Filter Comparison and Zoom 62 - 63 - We use Second-Order Sections (SOS) for all filters to ensure numerical stability. 64 - The following plot compares the architectures focusing on the -3 dB crossover point. 65 - 66 - <picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/filter_type_comparison_dark.png"><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%"></picture> 67 - 68 - | Type | Name | Usage Example | Best For | 69 - | :--- | :--- | :--- | :--- | 70 - | `butter` | **Butterworth** | `octave_filter(x, fs, filter_type='butter')` | General acoustic measurement. | 71 - | `cheby1` | **Chebyshev I** | `octave_filter(x, fs, filter_type='cheby1', ripple=0.1)` | Sharper roll-off at the cost of ripple. | 72 - | `cheby2` | **Chebyshev II** | `octave_filter(x, fs, filter_type='cheby2')` | Flat passband with stopband zeros. | 73 - | `ellip` | **Elliptic** | `octave_filter(x, fs, filter_type='ellip', ripple=0.1)` | Maximum selectivity. | 74 - | `bessel` | **Bessel** | `octave_filter(x, fs, filter_type='bessel')` | Preserving transient waveform shapes. | 75 - 76 - ## Gallery of Filter Bank Responses 76 + ## 4. Gallery of Filter Bank Responses 77 77 78 78 Full spectral view of the filter banks for Octave (1/1) and 1/3-Octave fractions. 79 79 ··· 85 85 | **Elliptic** | <picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/filter_ellip_fraction_1_order_6_dark.png"><img src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/filter_ellip_fraction_1_order_6.png" alt="Elliptic octave-band filter bank frequency response" width="100%"></picture> | <picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/filter_ellip_fraction_3_order_6_dark.png"><img src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/filter_ellip_fraction_3_order_6.png" alt="Elliptic one-third-octave filter bank frequency response" width="100%"></picture> | 86 86 | **Bessel** | <picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/filter_bessel_fraction_1_order_6_dark.png"><img src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/filter_bessel_fraction_1_order_6.png" alt="Bessel octave-band filter bank frequency response" width="100%"></picture> | <picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/filter_bessel_fraction_3_order_6_dark.png"><img src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/filter_bessel_fraction_3_order_6.png" alt="Bessel one-third-octave filter bank frequency response" width="100%"></picture> | 87 87 88 - ## Filter Usage and Examples 88 + ## 5. Filter Usage and Examples 89 89 90 90 ### 1. Butterworth (`butter`) 91 91 ··· 114 114 the cut-off frequencies. 115 115 116 116 ```python 117 + # Uses `x` and `fs` from the snippet above. 117 118 # Selectivity with 0.1 dB passband ripple 118 119 spl, freq = octave_filter(x, fs, filter_type='cheby1', ripple=0.1) 119 120 ``` ··· 128 129 −3 dB points land on the band edges (`attenuation` must be > 3.01 dB). 129 130 130 131 ```python 132 + # Uses `x` and `fs` from the snippet above. 131 133 # Flat passband, class-1 default 72 dB stopband attenuation 132 134 spl, freq = octave_filter(x, fs, filter_type='cheby2') 133 135 ``` ··· 140 142 roll-off) for a given order. They feature ripples in both the passband and stopband. 141 143 142 144 ```python 145 + # Uses `x` and `fs` from the snippet above. 143 146 # Maximum selectivity for extreme band isolation 144 147 spl, freq = octave_filter(x, fs, filter_type='ellip', ripple=0.1) 145 148 ``` ··· 153 156 any other type, but have the slowest roll-off. 154 157 155 158 ```python 159 + # Uses `x` and `fs` from the snippet above. 156 160 # Best for pulse analysis and transient preservation 157 161 spl, freq = octave_filter(x, fs, filter_type='bessel') 158 162 ``` ··· 169 173 ```python 170 174 from phonometry import linkwitz_riley 171 175 172 - signal = x # reuse the calibrated signal from the top of the page 176 + # Uses `x` and `fs` from the snippet above. 177 + signal = x 173 178 # Split signal into Low and High bands at 1000 Hz 174 179 low, high = linkwitz_riley(signal, fs, freq=1000, order=4) 175 180 # Reconstruction: low + high == signal (flat response) ··· 177 182 178 183 <picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/crossover_lr4_dark.png"><img src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/crossover_lr4.png" alt="Linkwitz-Riley 4th-order crossover: low-pass, high-pass and their flat sum" width="60%"></picture> 179 184 180 - ## Verifying the IEC 61260-1 class 185 + ## 6. Verifying the IEC 61260-1 class 181 186 182 187 `verify_filter_class` checks every band of a bank against the acceptance 183 188 limits of **IEC 61260-1:2014** (Table 1, with the fractional-octave breakpoint ··· 189 194 190 195 bank = OctaveFilterBank(fs=48000, fraction=3, order=6) 191 196 result = verify_filter_class(bank) 192 - print(result["overall_class"]) # 1, 2 or None 193 - print(result["bands"][0]) # {'freq': ..., 'class': 1, 'margin_class1_db': ...} 197 + print(result["overall_class"]) # 1 198 + print(result["bands"][0]) 199 + # {'freq': 12.589254117941678, 'class': 1, 'margin_class1_db': 0.39999999999997266, 'margin_class2_db': 0.5999999999999727} 194 200 ``` 195 201 196 202 <picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/class_mask_overlay_dark.png"><img src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/class_mask_overlay.png" alt="Butterworth band response threading between the forbidden regions of the IEC 61260-1 class 1 acceptance mask" width="80%"></picture> ··· 207 213 not meet class limits at order 6: passband ripple (cheby1/ellip) and slow 208 214 roll-off (bessel) violate the mask. 209 215 210 - ## Signal Decomposition and Stability 216 + ## 7. Signal Decomposition and Stability 211 217 212 218 By setting `sigbands=True`, you can retrieve the time-domain components of each 213 219 band. This allows for advanced analysis or comparing how different architectures ··· 253 259 254 260 <picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/group_delay_comparison_dark.png"><img src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/group_delay_comparison.png" alt="Group delay of the 1 kHz octave band for the five architectures: Bessel nearly flat, Chebyshev and Elliptic peaking at the band edges" width="80%"></picture> 255 261 256 - ## Zero-phase filtering 262 + ## 8. Zero-phase filtering 257 263 258 264 For offline analysis you can eliminate group delay entirely: `zero_phase=True` 259 265 filters each band forward-backward (`scipy.signal.sosfiltfilt`), keeping band ··· 267 273 ```python 268 274 from phonometry import OctaveFilterBank 269 275 276 + # Uses `y` from the snippet above. 270 277 bank = OctaveFilterBank(fs=48000, fraction=3) 271 278 spl, freq, xb = bank.filter(y, sigbands=True, zero_phase=True) 272 279 ``` ··· 275 282 276 283 *Causal filtering delays the burst by the filter's group delay; zero-phase 277 284 filtering keeps it aligned with the input.* 285 + 286 + --- 287 + 288 + **Standards.** IEC 61260-1:2014, *Electroacoustics — Octave-band and 289 + fractional-octave-band filters — Part 1: Specifications* — the base-10 mid 290 + frequencies and band edges of §1 (5.2-5.5), the nominal band labels, and the 291 + Table 1 class 1 / class 2 acceptance limits (with the fractional-octave 292 + breakpoint mapping and log-frequency interpolation) verified in §6. 293 + ANSI S1.11-2004, *Octave-Band and Fractional-Octave-Band Analog and Digital 294 + Filters* — the band-edge convention on which every bank places its −3 dB 295 + points.
+19
docs/getting-started.md
··· 64 64 spl, freq = octave_filter(signal, fs=fs, fraction=3) 65 65 66 66 print(f"Bands: {freq}") 67 + # Bands: [12.589254117941678, 15.848931924611138, ..., 19952.623149688785] (33 bands) 67 68 print(f"SPL [dB]: {spl}") 69 + # SPL [dB]: [46.88395351 47.96774897 49.04991279 ...] — ~90.7 dB at 100 Hz and ~90.9 dB at 1 kHz 68 70 ``` 69 71 70 72 <picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/signal_response_fraction_3_dark.png"><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%"></picture> 71 73 72 74 *Example of a 1/3 Octave Band spectrum analysis of a complex signal.* 75 + 76 + <details> 77 + <summary>Show the code for this figure</summary> 78 + 79 + ```python 80 + import matplotlib.pyplot as plt 81 + 82 + # Uses `spl` and `freq` from the snippet above. 83 + # The committed figure additionally overlays the raw-signal PSD in the background. 84 + fig, ax = plt.subplots() 85 + ax.semilogx(freq, spl, marker="o", markerfacecolor="white") 86 + ax.set_xlabel("Frequency [Hz]") 87 + ax.set_ylabel("SPL [dB]") 88 + plt.show() 89 + ``` 90 + 91 + </details> 73 92 74 93 ## Analyzing an audio file 75 94
+23 -22
docs/human-vibration.md
··· 21 21 ## 1. Frequency weightings (ISO 8041-1) 22 22 23 23 Every human-vibration weighting is the product of four analog stages evaluated 24 - at `s = j·2πf` (ISO 8041-1 Formulae (1)–(5)): a second-order Butterworth 24 + at $s = j\,2\pi f$ (ISO 8041-1 Formulae (1)–(5)): a second-order Butterworth 25 25 **high-pass** and **low-pass** band limiting, an **acceleration–velocity 26 - transition** carrying the overall gain `K`, and an **upward step**: 26 + transition** carrying the overall gain $K$, and an **upward step**: 27 27 28 28 $$ 29 29 H(s) = H_h(s)\,H_l(s)\,H_t(s)\,H_s(s). 30 30 $$ 31 31 32 - A single Table 3 parameter set `(f_1,\,Q_1,\,f_2,\,Q_2,\,f_3,\,f_4,\,Q_4,\, 33 - f_5,\,Q_5,\,f_6,\,Q_6,\,K)` realises all nine weightings — `Wb, Wc, Wd, We, Wf, 34 - Wh, Wj, Wk, Wm` — a corner set to infinity collapsing its stage to unity. The 35 - principal whole-body weighting is `Wk` (vertical, seat surface); `Wd` is the 36 - horizontal weighting, and `Wh` the hand-arm weighting. 32 + A single Table 3 parameter set $(f_1, Q_1, f_2, Q_2, f_3, f_4, Q_4, f_5, Q_5, 33 + f_6, Q_6, K)$ realises all nine weightings — `Wb, Wc, Wd, We, Wf, Wh, Wj, Wk, 34 + Wm` — a corner set to infinity collapsing its stage to unity. The principal 35 + whole-body weighting is `Wk` (vertical, seat surface); `Wd` is the horizontal 36 + weighting, and `Wh` the hand-arm weighting. 37 37 38 38 ```python 39 39 import phonometry as ph ··· 83 83 84 84 The basic evaluation is the **weighted r.m.s. acceleration**. From a 85 85 one-third-octave spectrum it is (ISO 2631-1 Eq. (9); the identical construction 86 - gives the hand-arm `a_hw` of ISO 5349-1 Eq. (A.1)): 86 + gives the hand-arm $a_{hw}$ of ISO 5349-1 Eq. (A.1)): 87 87 88 88 $$ 89 89 a_w = \sqrt{\sum_i \left(W_i\,a_i\right)^2}, 90 90 $$ 91 91 92 - with `W_i` the weighting factor at band centre `i` and `a_i` the measured band 92 + with $W_i$ the weighting factor at band centre $i$ and $a_i$ the measured band 93 93 acceleration. 94 94 95 95 ```python ··· 147 147 ISO 2631-1 adds dose measures computed on the weighted time signal: the 148 148 **running r.m.s.** and its maximum, the **maximum transient vibration value** 149 149 `MTVV` (Eq. (4), a 1 s running r.m.s.); the fourth-power **vibration dose 150 - value** `VDV = (∫ a_w^4\,dt)^{1/4}` (Eq. (5)); the **motion sickness dose value** 151 - `MSDV = (∫ a_w^2\,dt)^{1/2}`; and the **crest factor** (peak / r.m.s.), whose 152 - value above 9 signals that the basic method is inadequate. 150 + value** $\text{VDV} = \left(\int a_w^4\,dt\right)^{1/4}$ (Eq. (5)); the **motion 151 + sickness dose value** $\text{MSDV} = \left(\int a_w^2\,dt\right)^{1/2}$; and the 152 + **crest factor** (peak / r.m.s.), whose value above 9 signals that the basic 153 + method is inadequate. 153 154 154 155 ```python 155 156 import numpy as np ··· 159 160 raw = np.random.default_rng(0).standard_normal(int(60 * fs)) # 60 s record 160 161 a_w = ph.apply_weighting(raw, fs, "Wk") # weighted signal 161 162 162 - print(round(ph.vibration_dose_value(a_w, fs), 3)) # VDV [m/s^1.75] 163 - print(round(ph.mtvv(a_w, fs), 3)) # MTVV [m/s^2] 164 - print(round(ph.crest_factor(a_w), 2)) # crest factor 163 + print(round(ph.vibration_dose_value(a_w, fs), 3)) # 0.744 VDV [m/s^1.75] 164 + print(round(ph.mtvv(a_w, fs), 3)) # 0.265 MTVV [m/s^2] 165 + print(round(ph.crest_factor(a_w), 2)) # 3.74 crest factor 165 166 ``` 166 167 167 168 ## 3. Vibration total value and daily exposure `A(8)` 168 169 169 170 Across the three axes the **vibration total value** combines the axis-weighted 170 - r.m.s. accelerations with the posture multiplying factors `k_j` (ISO 2631-1 171 - Eq. (10); for hand-arm, ISO 5349-1 Eq. (1) with every `k = 1`): 171 + r.m.s. accelerations with the posture multiplying factors $k_j$ (ISO 2631-1 172 + Eq. (10); for hand-arm, ISO 5349-1 Eq. (1) with every $k = 1$): 172 173 173 174 $$ 174 175 a_v = \sqrt{\sum_j k_j^2\,a_{wj}^2}. ··· 183 184 ``` 184 185 185 186 The **daily exposure** normalises the total value to a reference 8-hour day 186 - (`T_0 = 28 800 s`). For a single operation `A(8) = a_v·\sqrt{T/T_0}`; several 187 - operations combine through their partial exposures `A_i(8) = a_{vi}·\sqrt{T_i/T_0}` 188 - as `A(8) = \sqrt{\sum_i A_i(8)^2}` (ISO 5349-1 Eqs. (2)/(3); ISO 5349-2 187 + ($T_0 = 28\,800$ s). For a single operation $A(8) = a_v\,\sqrt{T/T_0}$; several 188 + operations combine through their partial exposures $A_i(8) = a_{vi}\,\sqrt{T_i/T_0}$ 189 + as $A(8) = \sqrt{\sum_i A_i(8)^2}$ (ISO 5349-1 Eqs. (2)/(3); ISO 5349-2 189 190 Eqs. (1)–(3)). 190 191 191 192 `daily_vibration_exposure` builds the partial exposures, combines them and ··· 248 249 ## 4. Exposure–response guidance 249 250 250 251 For hand-transmitted vibration, ISO 5349-1 Annex C relates the daily exposure to 251 - the group-mean lifetime `D_y` (in years) that produces vibration-white-finger in 252 - 10 % of an exposed group, `D_y = 31.8\,A(8)^{-1.06}` (Eq. (C.1)): 252 + the group-mean lifetime $D_y$ (in years) that produces vibration-white-finger in 253 + 10 % of an exposed group, $D_y = 31.8\,A(8)^{-1.06}$ (Eq. (C.1)): 253 254 254 255 ```python 255 256 import phonometry as ph
+23 -3
docs/intensity.md
··· 101 101 practical ceiling. Larger spacers reach lower frequencies, smaller ones 102 102 higher. 103 103 - **Reactive fields**: when `pressure_intensity_index` (F2 in ISO 9614-1) 104 - approaches the probe's residual index δ_pI0, phase errors dominate. The 105 - ISO 9614-1 Annex A field indicators and the dynamic-capability criterion 106 - are available directly: 104 + approaches the probe's residual index δ_pI0, phase errors dominate. 105 + 106 + Over a measurement surface, the ISO 9614-1 Annex A field indicators grade the 107 + scan itself. **F2**, the surface pressure-intensity indicator, is the surface 108 + pressure level minus the level of the mean *magnitude* of the normal 109 + intensity — the larger it is, the closer the measurement sits to the probe's 110 + phase-error floor. **F3**, the negative partial power indicator, is the same 111 + difference taken with the *signed* mean intensity — F3 − F2 > 0 reveals power 112 + flowing inward through parts of the surface. **F4**, the field non-uniformity 113 + indicator, is the normalised spread of the per-position intensities — the 114 + larger it is, the more measurement positions the surface needs. Together with 115 + the dynamic-capability criterion they are available directly: 107 116 108 117 ```python 109 118 import numpy as np ··· 134 143 135 144 See [Theory](theory.md) for the derivations and [Calibration](calibration.md) 136 145 for absolute scaling of the two channels. 146 + 147 + --- 148 + 149 + **Standards.** IEC 61043:1994, *Electroacoustics — Instruments for the 150 + measurement of sound intensity — Measurements with pairs of pressure sensing 151 + microphones* — the two-microphone cross-spectral intensity estimator, the 152 + finite-difference bias correction and the usable-bandwidth bound (clause 7.3, 153 + Table 3). ISO 9614-1:1993, *Acoustics — Determination of sound power levels 154 + of noise sources using sound intensity — Part 1: Measurement at discrete 155 + points* — the pressure-intensity index, the Annex A field indicators F2, F3 156 + and F4, and the dynamic-capability criterion (Annex B).
+20
docs/levels.md
··· 60 60 **L90** the background level. 61 61 62 62 ```python 63 + import numpy as np 63 64 from phonometry import ln_levels 64 65 65 66 # A steady tone gives L10 = L50 = L90; percentiles only tell a story for a 66 67 # *fluctuating* level. Synthesize 3 s alternating between a quiet and a 67 68 # ~10 dB louder half-second so the statistics separate. 69 + fs = 48000 68 70 rng = np.random.default_rng(0) 69 71 segment = fs // 2 # 0.5 s per level 70 72 quiet = 0.02 * rng.standard_normal(segment) # background ··· 108 110 109 111 ```python 110 112 from phonometry import lc_peak, sel, sound_exposure, lex_8h 113 + 114 + # Uses `recording`, `fs` and `sensitivity` from the Leq snippet above. 111 115 112 116 # C-weighted peak (IEC 61672-1 §5.13) - occupational action limits use this 113 117 peak = lc_peak(recording, fs, calibration_factor=sensitivity) ··· 226 230 ```python 227 231 from phonometry import OctaveFilterBank 228 232 233 + # Uses `recording` from the Leq snippet above. 229 234 bank = OctaveFilterBank(fs=48000, fraction=3) 230 235 levels, freq, times = bank.spectrogram(recording, window_time=0.125, overlap=0.5) 231 236 # levels: (bands, frames) — ready for pcolormesh(times, freq, levels) ··· 257 262 ```python 258 263 import matplotlib.pyplot as plt 259 264 265 + # Uses `levels`, `freq` and `times` from the spectrogram snippet above. 260 266 fig, ax = plt.subplots() 261 267 mesh = ax.pcolormesh(times, freq, levels, shading="auto") 262 268 ax.set_yscale("log") ··· 272 278 tonal-prominence verdicts in [Prominent Discrete Tones](tone-prominence.md), 273 279 and the ISO 226 equal-loudness contours live with the perception metrics in 274 280 [Psychoacoustics](psychoacoustics.md). 281 + 282 + --- 283 + 284 + **Standards.** IEC 61672-1:2013, *Electroacoustics — Sound level meters — 285 + Part 1: Specifications* — the Fast/Slow/Impulse envelope ballistics behind 286 + `ln_levels`, the C-weighted peak of §5.13 (verified against the Table 5 tone 287 + bursts) and the sound exposure level verified against the Table 4 LAE column. 288 + IEC 61252, *Electroacoustics — Specifications for personal sound exposure 289 + meters* — the sound exposure E in Pa²h and the normalized 8 h level LEX,8h 290 + (≡ LEP,d), anchored at 3.2 Pa²h ⇔ exactly 90 dB. ISO 1996-1:2016, *Acoustics — 291 + Description, measurement and assessment of environmental noise — Part 1: 292 + Basic quantities and assessment procedures* — Lden (3.6.4), Ldn (3.6.5) and 293 + the composite whole-day rating level of clause 6.5 (Formulae 5-6, Table A.1 294 + adjustments).
+77 -4
docs/materials.md
··· 54 54 500 Hz peak overshoots the shifted curve by ≥ 0.25, adding the `M` indicator, so 55 55 the rating is $0.60(\text{M})$, class C.* 56 56 57 + <details> 58 + <summary>Show the code for this figure</summary> 59 + 60 + ```python 61 + import matplotlib.pyplot as plt 62 + from phonometry import weighted_absorption 63 + 64 + # ISO 11654 Annex A.2 practical coefficients at 250/500/1000/2000/4000 Hz 65 + result = weighted_absorption([0.35, 1.00, 0.65, 0.60, 0.55]) 66 + result.plot() # practical curve vs shifted reference, deviations shaded 67 + plt.show() 68 + ``` 69 + 70 + </details> 71 + 57 72 ```python 58 73 from phonometry import weighted_absorption, absorption_class 59 74 ··· 108 123 *The slightly super-linear pressure drop is fitted through the origin; the 109 124 specific airflow resistance is the fit read at 0.5 mm/s.* 110 125 126 + <details> 127 + <summary>Show the code for this figure</summary> 128 + 129 + ```python 130 + import matplotlib.pyplot as plt 131 + import numpy as np 132 + from phonometry import static_airflow_resistance 133 + 134 + area = np.pi * 0.05**2 # 100 mm diameter cell [m^2] 135 + u = np.array([0.5, 1, 2, 4, 8, 12]) * 1e-3 # linear velocity [m/s] 136 + dp = 1.6e4 * u + 4.0e5 * u**2 # measured pressure drop [Pa] 137 + r = static_airflow_resistance(u, dp, area=area, thickness=0.05) 138 + 139 + u_fit = np.linspace(0.0, 13e-3, 200) 140 + dp_fit = r.linear_coefficient * u_fit + r.quadratic_coefficient * u_fit**2 141 + fig, ax = plt.subplots() 142 + ax.plot(u_fit * 1e3, dp_fit, label="Through-origin fit dp = a u + b u^2") 143 + ax.plot(u * 1e3, dp, "o", label="Measured pressure drop") 144 + ax.plot(r.evaluation_velocity * 1e3, r.pressure_drop, "D", 145 + label="Evaluation at 0.5 mm/s") 146 + ax.set_xlabel("Linear airflow velocity u [mm/s]") 147 + ax.set_ylabel("Pressure drop dp [Pa]") 148 + ax.set_title(f"R_s = {r.specific_resistance:.0f} Pa·s/m") 149 + ax.legend() 150 + plt.show() 151 + ``` 152 + 153 + </details> 154 + 111 155 ```python 112 156 import numpy as np 113 157 from phonometry import static_airflow_resistance ··· 159 203 piston_stroke_specimen=14e-3, piston_stroke_termination=1.4e-3, 160 204 frequency=2.0, cavity_volume=7.854e-4, kappa_prime=kp, 161 205 ) 162 - print(round(R)) # airflow resistance R [Pa*s/m^3] 206 + print(round(R)) # 222956 airflow resistance R [Pa*s/m^3] 163 207 ``` 164 208 165 209 Pass the `effective_kappa` result to `alternating_airflow_resistance` for an ··· 189 233 190 234 *A small level difference means a near-perfect absorber; a level difference of 191 235 9.54 dB gives $s = 3$, $|r| = 0.5$ and $\alpha = 0.75$.* 236 + 237 + <details> 238 + <summary>Show the code for this figure</summary> 239 + 240 + ```python 241 + import matplotlib.pyplot as plt 242 + import numpy as np 243 + from phonometry import ( 244 + standing_wave_absorption, standing_wave_ratio_from_level, 245 + standing_wave_reflection_magnitude, 246 + ) 247 + 248 + level_diff = np.linspace(0.5, 40.0, 300) # L_max - L_min [dB] 249 + swr = standing_wave_ratio_from_level(level_diff) 250 + fig, ax = plt.subplots() 251 + ax.plot(level_diff, standing_wave_absorption(swr), 252 + label="Absorption coefficient alpha") 253 + ax.plot(level_diff, standing_wave_reflection_magnitude(swr), "--", 254 + label="Reflection factor magnitude |r|") 255 + ax.set_xlabel("Standing-wave level difference L_max - L_min [dB]") 256 + ax.set_ylabel("alpha, |r|") 257 + ax.legend() 258 + plt.show() 259 + ``` 260 + 261 + </details> 192 262 193 263 ```python 194 264 from phonometry import standing_wave_absorption, standing_wave_ratio_from_level ··· 230 300 r = reflection_factor(h12, spacing=spacing, x1=x1, wavenumber=k0) 231 301 print(np.round(absorption_from_reflection(r), 3)) # [0.75 0.75 0.75] 232 302 print(np.round(normalized_surface_impedance(r), 2)) # Z / rho c0 303 + # [1.15-1.23j 1.15-1.23j 1.15-1.23j] 233 304 ``` 234 305 235 306 The high-level `two_microphone_impedance` wraps this chain and returns an ··· 272 343 273 344 --- 274 345 275 - *Standards: ISO 11654:1997, ISO 9053-1:2018, ISO 9053-2:2020, ISO 10534-1:1996, 276 - ISO 10534-2:1998 and ASTM E2611-19. Every equation is derived from the standard 346 + **Standards.** ISO 11654:1997 (weighted sound-absorption rating); ISO 9053-1:2018 347 + (static airflow resistance); ISO 9053-2:2020 (alternating airflow resistance); 348 + ISO 10534-1:1996 and ISO 10534-2:1998 (impedance tube); ASTM E2611-19 349 + (four-microphone transmission loss). Every equation is derived from the standard 277 350 text; the [conformance report](CONFORMANCE.md) validates the library against the 278 351 standards' own worked examples (ISO 11654 Annex A, ISO 9053-2 Annex A.3) and 279 - closed-form identities.* 352 + closed-form identities.
+74
docs/outdoor-propagation.md
··· 43 43 *The dry 20 °C / 10 % curve absorbs most at mid frequencies, but the humid 44 44 curves overtake it below ~200 Hz — the relaxation signature.* 45 45 46 + <details> 47 + <summary>Show the code for this figure</summary> 48 + 49 + ```python 50 + import matplotlib.pyplot as plt 51 + import numpy as np 52 + from phonometry import air_attenuation 53 + 54 + freqs = np.geomspace(50.0, 10000.0, 400) 55 + fig, ax = plt.subplots() 56 + for temp, rh in [(20.0, 50.0), (20.0, 10.0), (0.0, 70.0), (30.0, 80.0)]: 57 + ax.loglog(freqs, air_attenuation(freqs, temp, rh) * 1000.0, 58 + label=f"{temp:g} °C, {rh:g} % RH") 59 + ax.set_xlabel("Frequency [Hz]") 60 + ax.set_ylabel("Attenuation coefficient alpha [dB/km]") 61 + ax.legend() 62 + plt.show() 63 + ``` 64 + 65 + </details> 66 + 46 67 ```python 47 68 import numpy as np 48 69 from phonometry import air_attenuation, air_attenuation_m ··· 147 168 148 169 <picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/outdoor_attenuation_breakdown_dark.png"><img src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/outdoor_attenuation_breakdown.png" alt="ISO 9613-2 per-octave-band attenuation breakdown as a stacked bar of Adiv, Aatm, Agr and Abar with the total A overlaid, for a 200 m path over porous ground with a 4 m barrier" width="80%"></picture> 149 170 171 + <details> 172 + <summary>Show the code for this figure</summary> 173 + 174 + ```python 175 + import matplotlib.pyplot as plt 176 + import numpy as np 177 + from phonometry import Barrier, outdoor_propagation_attenuation 178 + 179 + bands = np.array([63.0, 125.0, 250.0, 500.0, 1000.0, 2000.0, 4000.0, 8000.0]) 180 + barrier = Barrier(source_to_edge=101.0, edge_to_receiver=101.0) 181 + att = outdoor_propagation_attenuation( 182 + 200.0, 1.5, 1.5, bands, ground_source=1.0, ground_middle=1.0, 183 + ground_receiver=1.0, barrier=barrier, temperature=15.0, 184 + relative_humidity=70.0, 185 + ) 186 + 187 + x = np.arange(len(bands)) 188 + fig, ax = plt.subplots() 189 + bottom = np.zeros(len(bands)) 190 + for term, label in [(att.a_div, "Adiv — divergence"), 191 + (att.a_atm, "Aatm — atmospheric"), 192 + (att.a_gr, "Agr — ground"), 193 + (att.a_bar, "Abar — barrier")]: 194 + ax.bar(x, term, bottom=bottom, label=label) 195 + bottom = bottom + term 196 + ax.plot(x, att.a_total, "D-", color="black", label="A — total") 197 + ax.set_xticks(x) 198 + ax.set_xticklabels([f"{b:g}" for b in bands]) 199 + ax.set_xlabel("Octave-band centre frequency [Hz]") 200 + ax.set_ylabel("Attenuation A [dB]") 201 + ax.legend() 202 + plt.show() 203 + ``` 204 + 205 + </details> 206 + 150 207 ```python 151 208 import numpy as np 152 209 from phonometry import ( ··· 250 307 [Room Acoustics guide](room-acoustics.md) for how $\alpha$ feeds 251 308 ISO 354, and the [Occupational Noise Exposure guide](occupational-exposure.md) for the ISO 9612 occupational 252 309 exposure that consumes A-weighted levels. 310 + 311 + --- 312 + 313 + **Standards.** ISO 9613-1:1993, *Acoustics — Attenuation of sound during 314 + propagation outdoors — Part 1: Calculation of the absorption of sound by the 315 + atmosphere* — the pure-tone attenuation coefficient $\alpha$ (Eq. (5)) with the 316 + oxygen and nitrogen relaxation frequencies (Eq. (3)/(4)), the Annex B humidity 317 + conversion and the exact Table 1 midbands (Eq. (6), Note 5). ISO 9613-2:1996, 318 + *Acoustics — Attenuation of sound during propagation outdoors — Part 2: General 319 + method of calculation* — the downwind receiver level (Eq. (3)/(4)) assembled 320 + from geometrical divergence (Eq. (7)), atmospheric absorption (Eq. (8)), the 321 + ground effect (Eq. (9), Table 3) with its A-weighted alternative 322 + (Eq. (10)/(11)), barrier screening (Eqs. (12)–(17)) and the meteorological 323 + correction (Eq. (21)/(22)). ISO 354:2003, *Acoustics — Measurement of sound 324 + absorption in a reverberation room* — only the clause 8.1.2.1 conversion 325 + $m = \alpha/(10 \lg e)$ behind `air_attenuation_m`; the reverberation-room 326 + method itself is covered in the [Room Acoustics guide](room-acoustics.md).
+23 -3
docs/psychoacoustics.md
··· 42 42 43 43 # From a raw recording: calibration_factor scales digital units to Pa 44 44 res = loudness_zwicker(x, fs, field="free", calibration_factor=sens) 45 - print(f"N = {res.loudness:.1f} sone ({res.loudness_level:.0f} phon)") 45 + print(f"N = {res.loudness:.1f} sone ({res.loudness_level:.0f} phon)") # 13.1 sone (77 phon) 46 46 47 47 # Time-varying signals: percentile loudness N5 is the reporting standard 48 48 res = loudness_zwicker(x, fs) # stationary=False (default) 49 - print(res.n5, res.n10, res.loudness) # N5, N10, Nmax 49 + print(f"{res.n5:.1f} {res.n10:.1f} {res.loudness:.1f}") # 13.1 13.1 13.1 — N5, N10, Nmax 50 50 51 51 # From 28 one-third-octave band levels (25 Hz .. 12.5 kHz) 52 52 res = loudness_zwicker_from_spectrum(levels_28, field="diffuse") ··· 124 124 ```python 125 125 from phonometry import sharpness_din 126 126 127 + # Uses `x`, `fs` and `sens` from the snippet above. 127 128 s = sharpness_din(x, fs, calibration_factor=sens) # acum 128 129 s_aures = sharpness_din(x, fs, method="aures") # Annex B variant 129 130 ``` ··· 268 269 269 270 res = loudness_moore_glasberg_time(x, fs, field="free") 270 271 print(f"N_max = {res.n_max:.3f} sone ({res.loudness_level_max:.0f} phon)") # 1.000 sone (40 phon) 271 - print(f"long-term loudness exceeded 5% of the time: {res.percentiles[5.0]:.3f} sone") 272 + print(f"long-term loudness exceeded 5% of the time: {res.percentiles[5.0]:.3f} sone") # 0.999 sone 272 273 273 274 res.plot() # short-term S'(t) and long-term S''(t) loudness vs time 274 275 ``` ··· 492 493 See [Prominent Discrete Tones](tone-prominence.md) for the ECMA-418-1 TNR/PR 493 494 prominence verdicts, [Speech Transmission Index](speech-transmission.md) for 494 495 STI/STIPA, and [Theory](theory.md) for the underlying math. 496 + 497 + --- 498 + 499 + **Standards.** ISO 532-1:2017, *Acoustics — Methods for calculating 500 + loudness — Part 1: Zwicker method* — stationary and time-varying loudness in 501 + sones from the normative Annex A.4 reference program, with the N5/N10 502 + percentile loudness, validated against the Annex B set. ISO 532-2:2017, 503 + *... Part 2: Moore-Glasberg method* — stationary loudness from roex excitation 504 + patterns on the ERB-number scale, with explicit binaural summation. 505 + ISO 532-3:2023, *... Part 3: Moore-Glasberg-Schlittenlacher method* — 506 + time-varying short-term and long-term loudness and the peak N_max. 507 + DIN 45692:2009, *Messtechnische Simulation der Hörempfindung Schärfe* — 508 + sharpness in acum (clause 6 weighting, Annex B von Bismarck and Aures 509 + variants, Table A.2 targets). ISO 226:2023, *Acoustics — Normal 510 + equal-loudness-level contours* — the contours (Formula 1), the loudness level 511 + of pure tones (Formula 2) and the hearing threshold. ECMA-418-2:2025, 512 + *Psychoacoustic metrics for ITT equipment — Part 2 (methods for describing 513 + human perception based on the Sottek Hearing Model)* — the Sottek Hearing 514 + Model loudness (sone_HMS), tonality (tu_HMS) and roughness (asper).
+10
docs/room-acoustics.md
··· 475 475 source/receiving-room levels. 476 476 - [Theory](theory.md) — Schroeder integration, regression windows and the 477 477 reference-curve derivation. 478 + 479 + --- 480 + 481 + **Standards.** ISO 18233:2006 (application of new measurement methods — the 482 + swept-sine and MLS acquisition of impulse responses); ISO 3382-1:2009 and 483 + ISO 3382-2:2008 (reverberation time and room parameters from the Schroeder 484 + decay); ISO 3382-3:2022 (open-plan office speech metrics); ISO 354:2003 485 + (sound absorption in a reverberation room). Validated against closed-form 486 + decays and the standards' own parameter definitions in the 487 + [conformance report](CONFORMANCE.md).
+123
docs/sound-power.md
··· 105 105 res.plot() # sound power level bars per band, LWA in the title (needs matplotlib) 106 106 ``` 107 107 108 + <picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/sound_power_pressure_result_dark.png"><img src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/sound_power_pressure_result.png" alt="The enveloping-surface sound power level spectrum of the ISO 3744 hemisphere example, one bar per octave band from 63 Hz to 8 kHz peaking near 500 Hz, with the A-weighted total of 92.4 dB(A) in the title" width="88%"></picture> 109 + 110 + *One bar per band: the energy-averaged surface pressure minus the background 111 + (`K1`) and environmental (`K2`) corrections plus the surface term 112 + `10 lg(S/S0)` gives `LW(f)`, and the A-weighted energy sum across bands gives 113 + the single-number `LWA` in the title.* 114 + 115 + <details> 116 + <summary>Show the code for this figure</summary> 117 + 118 + ```python 119 + import matplotlib.pyplot as plt 120 + import numpy as np 121 + 122 + # res is the SoundPowerResult computed above. One line: 123 + res.plot() 124 + plt.show() 125 + 126 + # By hand: a bar spectrum of LW with the A-weighted total in the title. 127 + positions = np.arange(freqs.size) 128 + fig, ax = plt.subplots() 129 + ax.bar(positions, res.sound_power_level, width=0.7, color="#1f77b4") 130 + ax.set_xticks(positions) 131 + ax.set_xticklabels([f"{f:g}" for f in freqs], rotation=45, ha="right") 132 + ax.set_xlabel("Frequency [Hz]") 133 + ax.set_ylabel("Sound power level LW [dB]") 134 + ax.set_title( 135 + f"Enveloping-surface sound power (ISO 3744) " 136 + f"LWA = {res.sound_power_level_a:.1f} dB(A)") 137 + plt.show() 138 + ``` 139 + 140 + </details> 141 + 108 142 The A-weighted total `LWA` is combined from the band powers with the ISO 3744 109 143 Annex E A-weighting corrections, so it needs `frequencies`. Passing the room 110 144 data (`reverberation_time` + `volume`, or `absorption_area`, or ··· 200 234 201 235 rev.plot() # reverberation-room LW spectrum, LWA in the title (needs matplotlib) 202 236 ``` 237 + 238 + <picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/sound_power_reverberation_result_dark.png"><img src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/sound_power_reverberation_result.png" alt="The reverberation-room sound power level spectrum of the ISO 3741 example, one bar per one-third-octave band from 100 Hz to 10 kHz falling gently with frequency, with the A-weighted total of 92.1 dB(A) in the title" width="88%"></picture> 239 + 240 + *The mean room level carried through the absorption-area, Waterhouse and 241 + meteorological terms of Eq. 20 gives the one-third-octave `LW(f)`, and the 242 + A-weighted energy sum across the 21 bands gives the `LWA` in the title.* 243 + 244 + <details> 245 + <summary>Show the code for this figure</summary> 246 + 247 + ```python 248 + import matplotlib.pyplot as plt 249 + import numpy as np 250 + 251 + # rev is the ReverberationSoundPowerResult computed above. One line: 252 + rev.plot() 253 + plt.show() 254 + 255 + # By hand: a bar spectrum of LW with the A-weighted total in the title. 256 + positions = np.arange(freqs.size) 257 + fig, ax = plt.subplots() 258 + ax.bar(positions, rev.sound_power_level, width=0.7, color="#1f77b4") 259 + ax.set_xticks(positions) 260 + ax.set_xticklabels([f"{f:g}" for f in freqs], rotation=45, ha="right") 261 + ax.set_xlabel("Frequency [Hz]") 262 + ax.set_ylabel("Sound power level LW [dB]") 263 + ax.set_title( 264 + f"Reverberation-room sound power (ISO 3741) " 265 + f"LWA = {rev.sound_power_level_a:.1f} dB(A)") 266 + plt.show() 267 + ``` 268 + 269 + </details> 203 270 204 271 `levels` may be a 1D mean spectrum or a 2D `(NM, NB)` array averaged over 205 272 positions. When the room volume, its reverberation time or the microphone ··· 303 370 304 371 res.plot() # LW spectrum; non-positive (undeterminable) bands hatched (needs matplotlib) 305 372 ``` 373 + 374 + <picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/sound_power_intensity_result_dark.png"><img src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/sound_power_intensity_result.png" alt="The intensity-scanning sound power level spectrum of the ISO 9614-2 example, one bar per octave band from 125 Hz to 4 kHz all near 85 dB, with the A-weighted total of 90.9 dB(A) in the title" width="88%"></picture> 375 + 376 + *The partial powers `<In,i>·Si` of the six segments sum to each band's `LW`; 377 + every band here nets positive power and passes the field-indicator criteria at 378 + engineering grade, so all six bars stand, and the A-weighted total of 379 + 90.9 dB(A) heads the title.* 380 + 381 + <details> 382 + <summary>Show the code for this figure</summary> 383 + 384 + ```python 385 + import matplotlib.pyplot as plt 386 + import numpy as np 387 + 388 + # res is the SoundPowerIntensityResult computed above. One line: 389 + res.plot() 390 + plt.show() 391 + 392 + # By hand: a bar spectrum of LW with the A-weighted total in the title. 393 + positions = np.arange(freqs.size) 394 + fig, ax = plt.subplots() 395 + ax.bar(positions, res.sound_power_level, width=0.7, color="#1f77b4") 396 + ax.set_xticks(positions) 397 + ax.set_xticklabels([f"{f:g}" for f in freqs], rotation=45, ha="right") 398 + ax.set_xlabel("Frequency [Hz]") 399 + ax.set_ylabel("Sound power level LW [dB]") 400 + ax.set_title( 401 + f"Intensity-scanning sound power (ISO 9614-2) " 402 + f"LWA = {res.sound_power_level_a:.1f} dB(A)") 403 + plt.show() 404 + ``` 405 + 406 + </details> 306 407 307 408 Supplying `normal_intensity_2` (the second sweep) averages the two for the 308 409 partial powers and evaluates criterion 3; `pressure_levels` enables `FpI`; ··· 552 653 absorption area; impact and airborne insulation. 553 654 - [Levels](levels.md) — energy averaging and the A-weighting behind `LWA`. 554 655 - [Theory](theory.md) — the Waterhouse, K1/K2 and C1/C2 derivations. 656 + 657 + --- 658 + 659 + **Standards.** ISO 3744:2010, *Acoustics — Determination of sound power levels 660 + and sound energy levels of noise sources using sound pressure — Engineering 661 + methods for an essentially free field over a reflecting plane* — the 662 + enveloping-surface method: hemisphere and box surface areas, the `K1`/`K2` 663 + corrections, the Annex B microphone positions and the Annex E A-weighting. 664 + ISO 3746:2010, *… Survey method using an enveloping measurement surface over a 665 + reflecting plane* — the survey grade sharing the same formulae with coarser 666 + criteria. ISO 3741:2010, *… Precision methods for reverberation test rooms* — 667 + the direct (Eq. 20) and comparison methods with the Waterhouse and 668 + meteorological corrections and the Table 1 qualification criteria. 669 + ISO 3745:2012, *… Precision methods for anechoic rooms and hemi-anechoic 670 + rooms* — the Clause 8 power level, the per-position background correction 671 + (Eq. 11), the meteorological corrections and the standardized microphone 672 + arrays. ISO 9614-2:1996, *Acoustics — Determination of sound power levels of 673 + noise sources using sound intensity — Part 2: Measurement by scanning* — the 674 + partial powers, the `FpI` and `F+/-` field indicators and the grade criteria. 675 + ISO 9614-3:2002, *… Part 3: Precision method for measurement by scanning* — 676 + the grade-1 scanning method, its field indicators and the clause 9.2 677 + not-applicable flagging.
+41
docs/speech-intelligibility.md
··· 156 156 157 157 <picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/sii_vocal_efforts_dark.png"><img src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/sii_vocal_efforts.png" alt="Two panels. Left: the four ANSI S3.5-1997 standard speech spectra — normal, raised, loud and shout — over the 18 one-third-octave bands from 160 Hz to 8000 Hz, each higher vocal effort lifting the whole spectrum. Right: the resulting Speech Intelligibility Index in a fixed broadband noise, rising from 0.12 (normal) through 0.36 and 0.59 to 0.79 (shout)" width="96%"></picture> 158 158 159 + <details> 160 + <summary>Show the code for this figure</summary> 161 + 162 + ```python 163 + import numpy as np 164 + import matplotlib.pyplot as plt 165 + import phonometry as ph 166 + 167 + # The four ANSI S3.5-1997 Table 3 spectra and the fixed broadband noise above. 168 + noise = np.array([48.0, 47.0, 46.0, 44.0, 42.0, 40.0, 38.0, 36.0, 34.0, 169 + 32.0, 30.0, 28.0, 26.0, 24.0, 22.0, 20.0, 18.0, 16.0]) 170 + efforts = ph.sii.VOCAL_EFFORTS # ("normal", "raised", "loud", "shout") 171 + freqs = ph.sii.BAND_CENTERS # the 18 one-third-octave band centres 172 + 173 + fig, (ax_s, ax_i) = plt.subplots(1, 2, figsize=(12, 5)) 174 + 175 + # Left: each higher vocal effort lifts the whole speech spectrum. 176 + for effort in efforts: 177 + ax_s.plot(freqs, ph.standard_speech_spectrum(effort), "o-", 178 + label=effort.capitalize()) 179 + ax_s.set_xscale("log") 180 + ax_s.set_xticks(list(freqs)) 181 + ax_s.set_xticklabels([f"{f:g}" for f in freqs], rotation=45, ha="right") 182 + ax_s.xaxis.set_minor_formatter(plt.NullFormatter()) 183 + ax_s.set_xlabel("One-third-octave band [Hz]") 184 + ax_s.set_ylabel("Speech spectrum level [dB SPL]") 185 + ax_s.legend() 186 + 187 + # Right: the SII each spectrum reaches in the fixed noise. 188 + sii = [ph.speech_intelligibility_index(e, noise).sii for e in efforts] 189 + pos = np.arange(len(efforts)) 190 + ax_i.bar(pos, sii) 191 + ax_i.set_xticks(pos) 192 + ax_i.set_xticklabels([e.capitalize() for e in efforts]) 193 + ax_i.set_ylim(0.0, 1.0) 194 + ax_i.set_ylabel("Speech Intelligibility Index") 195 + plt.show() 196 + ``` 197 + 198 + </details> 199 + 159 200 The vocal-effort names work anywhere a speech spectrum is expected, including as 160 201 the first argument to `speech_intelligibility_index`. 161 202
+95 -22
docs/time-weighting.md
··· 5 5 Accurate SPL measurement requires capturing energy over specific time windows. 6 6 phonometry implements exact time constants per **IEC 61672-1:2013**. 7 7 8 + ## 1. The exponential detector 9 + 10 + A sound level meter's needle cannot follow the pressure waveform — it shows a 11 + running *mean square* with an exponential memory. Formally (IEC 61672-1, 3.8): 12 + 13 + $$ 14 + \tau\ \frac{dy}{dt} + y = x^2(t) 15 + \quad\Longleftrightarrow\quad 16 + y(t) = \frac{1}{\tau} \int_{-\infty}^{t} x^2(\xi)\ e^{-(t-\xi)/\tau}\ d\xi 17 + $$ 18 + 19 + a first-order low-pass on the squared signal. The time constant τ sets the 20 + trade-off: **Fast** (125 ms) follows speech-like fluctuations, **Slow** (1 s) 21 + steadies the readout for quasi-stationary noise. After a step onset the 22 + envelope reaches 63 % of its final value in one τ and ~99.8 % after 8τ — 23 + that is why level analyses discard the first instants of a recording. 24 + 25 + ## 2. The three time weightings 26 + 8 27 <picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/time_weighting_analysis_dark.png"><img src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/time_weighting_analysis.png" alt="Fast, Slow and Impulse time weighting responses to a noise burst" width="80%"></picture> 28 + 29 + <details> 30 + <summary>Show the code for this figure</summary> 31 + 32 + ```python 33 + import numpy as np 34 + import matplotlib.pyplot as plt 35 + from phonometry import time_weighting 36 + 37 + fs = 48000 38 + t = np.arange(int(fs * 4)) / fs 39 + burst = np.zeros_like(t) # 0.5 s noise burst (Pa) starting at t = 1 s 40 + rng = np.random.default_rng(42) 41 + burst[fs:int(1.5 * fs)] = 0.2 * rng.standard_normal(int(0.5 * fs)) 42 + 43 + p0 = 2e-5 44 + plt.figure() 45 + for mode in ('fast', 'slow', 'impulse'): 46 + envelope = time_weighting(burst, fs, mode=mode) 47 + plt.plot(t, 10 * np.log10(np.maximum(envelope, 1e-12) / p0**2), label=mode) 48 + plt.xlabel('Time [s]') 49 + plt.ylabel('Level [dB SPL]') 50 + plt.legend() 51 + plt.show() 52 + ``` 53 + 54 + </details> 9 55 10 56 * **Fast (`fast`):** τ = 125 ms. Standard for noise fluctuations. 11 57 * **Slow (`slow`):** τ = 1000 ms. Standard for steady noise. ··· 24 70 energy_envelope = time_weighting(recording, fs, mode='fast') 25 71 # dB SPL relative to 20 μPa 26 72 spl_t = 10 * np.log10(energy_envelope / (2e-5)**2) 73 + 74 + print(f"Steady-state Fast level: {spl_t[-1]:.1f} dB SPL") 75 + # Steady-state Fast level: 77.0 dB SPL 27 76 ``` 28 77 29 78 The asymmetric Impulse ballistics use two constants — a fast attack and a slow ··· 34 83 \alpha = \begin{cases}1 - e^{-1/(f_s \cdot 0.035)} & x^2[n] > y[n-1]\\[2pt] 1 - e^{-1/(f_s \cdot 1.5)} & \text{otherwise}\end{cases} 35 84 $$ 36 85 37 - ## The exponential detector 38 - 39 - A sound level meter's needle cannot follow the pressure waveform — it shows a 40 - running *mean square* with an exponential memory. Formally (IEC 61672-1, 3.8): 41 - 42 - $$ 43 - \tau\ \frac{dy}{dt} + y = x^2(t) 44 - \quad\Longleftrightarrow\quad 45 - y(t) = \frac{1}{\tau} \int_{-\infty}^{t} x^2(\xi)\ e^{-(t-\xi)/\tau}\ d\xi 46 - $$ 47 - 48 - a first-order low-pass on the squared signal. The time constant τ sets the 49 - trade-off: **Fast** (125 ms) follows speech-like fluctuations, **Slow** (1 s) 50 - steadies the readout for quasi-stationary noise. After a step onset the 51 - envelope reaches 63 % of its final value in one τ and ~99.8 % after 8τ — 52 - that is why level analyses discard the first instants of a recording. 53 - 54 - ### `time_weighting()` / `TimeWeighting` parameters 86 + ## 3. `time_weighting()` / `TimeWeighting` parameters 55 87 56 88 | Parameter | Type | Units | Range / default | Notes | 57 89 | :--- | :--- | :--- | :--- | :--- | ··· 63 95 The output has the units of $x^2$: take `10*log10(y / p0**2)` for SPL or use 64 96 the level functions, which do it for you. 65 97 66 - ## Verified ballistics (IEC 61672-1 Table 4) 98 + ## 4. Verified ballistics (IEC 61672-1 Table 4) 67 99 68 100 The Fast envelope's response to 4 kHz tonebursts lands exactly on the 69 101 standard's reference values — enforced in CI for burst durations from 1 s down ··· 71 103 72 104 <picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/tone_burst_iec_dark.png"><img src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/tone_burst_iec.png" alt="Fast envelope responses to 200, 50 and 10 ms tone bursts peaking exactly at the IEC 61672-1 Table 4 reference values" width="80%"></picture> 73 105 74 - ## Initial state 106 + <details> 107 + <summary>Show the code for this figure</summary> 108 + 109 + ```python 110 + import numpy as np 111 + import matplotlib.pyplot as plt 112 + from phonometry import time_weighting 113 + 114 + fs = 48000 115 + t = np.arange(int(fs * 2)) / fs 116 + tone = np.sin(2 * np.pi * 4000 * t) 117 + 118 + # Steady-state Fast reference of the continuous tone 119 + reference = time_weighting(tone, fs, mode='fast')[int(1.5 * fs):].mean() 120 + 121 + # 200 ms burst of the same tone (IEC 61672-1 Table 4 target: -1.0 dB) 122 + burst = np.zeros_like(t) 123 + burst[int(0.5 * fs):int(0.7 * fs)] = tone[int(0.5 * fs):int(0.7 * fs)] 124 + envelope = time_weighting(burst, fs, mode='fast') 125 + env_db = 10 * np.log10(np.maximum(envelope / reference, 1e-6)) 126 + 127 + plt.figure() 128 + plt.plot(t, env_db, label='Fast envelope') 129 + plt.axhline(-1.0, linestyle='--', label='IEC target −1.0 dB') 130 + plt.xlabel('Time [s]') 131 + plt.ylabel('Level re steady state [dB]') 132 + plt.legend() 133 + plt.show() 134 + ``` 135 + 136 + </details> 137 + 138 + ## 5. Initial state 75 139 76 140 By default, the exponential integrator starts from rest (`y[-1] = 0`). Passing 77 141 `initial_state=None` leaves this default unspecified, while `initial_state='zero'` ··· 79 143 steady signal is already present, you can start from the first sample energy instead: 80 144 81 145 ```python 146 + # Uses `recording` and `fs` from the snippet above. 82 147 energy_envelope = time_weighting(recording, fs, mode='fast', initial_state='first') 83 148 ``` 84 149 85 - ## Block processing 150 + ## 6. Block processing 86 151 87 152 For block processing, pass the last output value from the previous block as the 88 153 next block's `initial_state` instead of resetting each block: ··· 118 183 (verified for all three modes, mono and multichannel). Call `tw.reset()` to 119 184 start from rest again. 120 185 121 - ## Performance note 186 + ## 7. Performance note 122 187 123 188 The `impulse` mode uses an asymmetric kernel that is JIT-compiled when 124 189 [numba](https://numba.pydata.org/) is installed (`pip install phonometry[perf]`). ··· 127 192 See [Integrated & Statistical Levels](levels.md) for Leq/LN metrics built on 128 193 these envelopes, and [Why phonometry](why-phonometry.md) for the IEC 129 194 61672-1 tone-burst verification. 195 + 196 + --- 197 + 198 + **Standards.** IEC 61672-1:2013, *Electroacoustics — Sound level meters — 199 + Part 1: Specifications* — the exponential time-weighting detector (clause 3.8) 200 + with the F and S time constants (clause 5.7), and the 4 kHz toneburst reference 201 + responses of Table 4 (class 1 acceptance limits) used to verify the ballistics 202 + in CI.
+155 -41
docs/weighting.md
··· 8 8 9 9 <picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/weighting_responses_dark.png"><img src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/weighting_responses.png" alt="A, C and Z frequency weighting curves with a zoom showing the positive region of the A curve (+1.27 dB at 2.5 kHz)" width="80%"></picture> 10 10 11 + <details> 12 + <summary>Show the code for this figure</summary> 13 + 14 + ```python 15 + import matplotlib.pyplot as plt 16 + import numpy as np 17 + from phonometry import weighting_filter 18 + 19 + # Measure each curve's response: weight a centered unit impulse and take 20 + # its spectrum (1 s buffer -> 1 Hz frequency resolution). 21 + fs = 48000 22 + impulse = np.zeros(fs) 23 + impulse[fs // 2] = 1.0 24 + freqs = np.fft.rfftfreq(fs, 1 / fs) 25 + 26 + fig, ax = plt.subplots(figsize=(9, 5)) 27 + for curve in ("A", "C", "Z"): 28 + spectrum = np.fft.rfft(weighting_filter(impulse, fs, curve=curve)) 29 + ax.semilogx(freqs[1:], 20 * np.log10(np.abs(spectrum[1:])), label=curve) 30 + ax.set(xlim=(10, 20000), ylim=(-80, 10), 31 + xlabel="Frequency [Hz]", ylabel="Response [dB]") 32 + ax.grid(True, which="both", alpha=0.3) 33 + ax.legend() 34 + plt.show() 35 + ``` 36 + 37 + </details> 38 + 11 39 * **A-Weighting (`A`):** Standard for environmental noise (IEC 61672-1). 12 40 * **C-Weighting (`C`):** Used for peak sound pressure and high-level noise. 13 41 * **Z-Weighting (`Z`):** Zero weighting, completely flat response. 14 42 * **G-Weighting (`G`):** Infrasound weighting per ISO 7196 (see below). 15 43 16 - ```python 17 - import numpy as np 18 - from phonometry import weighting_filter 19 - 20 - # recording: a calibrated microphone capture (Pa) — recorded through your measurement chain. Synthesized here so the guide runs standalone. 21 - fs = 48000 22 - recording = 0.2 * np.sin(2 * np.pi * 1000 * np.arange(fs) / fs) 23 - 24 - # Apply A-weighting to the raw recording 25 - weighted_signal = weighting_filter(recording, fs, curve='A') 26 - 27 - # Apply C-weighting for peak analysis 28 - c_weighted_signal = weighting_filter(recording, fs, curve='C') 29 - ``` 30 - 31 - ## Infrasound: G-weighting (ISO 7196) 32 - 33 - The **G frequency weighting** (ISO 7196:1995) rates infrasound the way A-weighting 34 - rates audible noise. It is defined by a pole-zero configuration with 0 dB gain at 35 - 10 Hz, rises at 12 dB/octave from 1 Hz to 20 Hz (matching the steep growth of 36 - perception in that band) and falls off at 24 dB/octave outside it. Use it for 37 - sources with significant energy below 20 Hz (wind turbines, HVAC, blasting): 38 - 39 - ```python 40 - from phonometry import weighting_filter 41 - 42 - g_weighted = weighting_filter(recording, fs, curve='G') 43 - ``` 44 - 45 - <picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/g_weighting_response_dark.png"><img src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/g_weighting_response.png" alt="G-weighting frequency response from 0.1 Hz to 1 kHz with the ISO 7196 Table 2 nominal values overlaid" width="80%"></picture> 46 - 47 - The implementation follows the ISO 7196 Table 1 pole/zero values exactly and is 48 - verified in CI against every Table 2 nominal response value (0.25 Hz to 315 Hz). 49 - `WeightingFilter(fs, "G")` supports the same multichannel and stateful block 50 - processing as A/C. Levels measured with the G curve are reported as 51 - L<sub>pG</sub> (or L<sub>Geq</sub> for the equivalent level over time). 52 - 53 - ## Where the curves come from 44 + ## 1. Where the curves come from 54 45 55 46 The A and C curves are inverted equal-loudness contours, frozen into filters: 56 47 **A** approximates the inverse of the historic 40-phon contour (quiet levels, ··· 69 60 absence of weighting. The full pole/zero derivation is in the 70 61 [Theory](theory.md) page. 71 62 72 - ### `weighting_filter()` / `WeightingFilter` parameters 63 + ## 2. Basic usage 64 + 65 + ```python 66 + import numpy as np 67 + from phonometry import weighting_filter 68 + 69 + # recording: a calibrated microphone capture (Pa) — recorded through your measurement chain. Synthesized here so the guide runs standalone. 70 + fs = 48000 71 + recording = 0.2 * np.sin(2 * np.pi * 1000 * np.arange(fs) / fs) 72 + 73 + # Apply A-weighting to the raw recording 74 + weighted_signal = weighting_filter(recording, fs, curve='A') 75 + 76 + # Apply C-weighting for peak analysis 77 + c_weighted_signal = weighting_filter(recording, fs, curve='C') 78 + ``` 79 + 80 + ## 3. Infrasound: G-weighting (ISO 7196) 81 + 82 + The **G frequency weighting** (ISO 7196:1995) rates infrasound the way A-weighting 83 + rates audible noise. It is defined by a pole-zero configuration with 0 dB gain at 84 + 10 Hz, rises at 12 dB/octave from 1 Hz to 20 Hz (matching the steep growth of 85 + perception in that band) and falls off at 24 dB/octave outside it. Use it for 86 + sources with significant energy below 20 Hz (wind turbines, HVAC, blasting): 87 + 88 + ```python 89 + from phonometry import weighting_filter 90 + 91 + # Uses `recording` and `fs` from the snippet above. 92 + g_weighted = weighting_filter(recording, fs, curve='G') 93 + ``` 94 + 95 + <picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/g_weighting_response_dark.png"><img src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/g_weighting_response.png" alt="G-weighting frequency response from 0.1 Hz to 1 kHz with the ISO 7196 Table 2 nominal values overlaid" width="80%"></picture> 96 + 97 + <details> 98 + <summary>Show the code for this figure</summary> 99 + 100 + ```python 101 + import matplotlib.pyplot as plt 102 + import numpy as np 103 + from phonometry import weighting_filter 104 + 105 + # Measure the G response: weight a centered unit impulse and take its 106 + # spectrum. A long buffer gives the resolution the infrasound range 107 + # needs (20 s -> 0.05 Hz). 108 + fs = 4000 109 + impulse = np.zeros(20 * fs) 110 + impulse[impulse.size // 2] = 1.0 111 + freqs = np.fft.rfftfreq(impulse.size, 1 / fs) 112 + spectrum = np.fft.rfft(weighting_filter(impulse, fs, curve="G")) 113 + 114 + fig, ax = plt.subplots(figsize=(9, 5)) 115 + ax.semilogx(freqs[1:], 20 * np.log10(np.abs(spectrum[1:]))) 116 + ax.plot(10, 0, "o", color="tab:red", label="0 dB at 10 Hz") 117 + ax.set(xlim=(0.1, 1000), ylim=(-90, 15), 118 + xlabel="Frequency [Hz]", ylabel="G-weighting response [dB]") 119 + ax.grid(True, which="both", alpha=0.3) 120 + ax.legend() 121 + plt.show() 122 + ``` 123 + 124 + </details> 125 + 126 + The implementation follows the ISO 7196 Table 1 pole/zero values exactly and is 127 + verified in CI against every Table 2 nominal response value (0.25 Hz to 315 Hz). 128 + `WeightingFilter(fs, "G")` supports the same multichannel and stateful block 129 + processing as A/C. Levels measured with the G curve are reported as 130 + L<sub>pG</sub> (or L<sub>Geq</sub> for the equivalent level over time). 131 + 132 + ## 4. `weighting_filter()` / `WeightingFilter` parameters 73 133 74 134 | Parameter | Type | Units | Range / default | Notes | 75 135 | :--- | :--- | :--- | :--- | :--- | ··· 80 140 | `stateful` | bool (class only) | — | default `False` | Carries filter state across blocks (streaming) | 81 141 | `steady_ic` | bool (class only) | — | default `False` | Steady-state initial conditions (no onset transient) | 82 142 83 - ## Reusable filter object 143 + ## 5. Reusable filter object 84 144 85 145 If you weight many signals with the same parameters, design the filter once: 86 146 87 147 ```python 88 148 from phonometry import WeightingFilter 89 149 150 + # Uses `recording` and `fs` from the snippet above. 90 151 wf = WeightingFilter(fs, "A") 91 152 signals = [recording] # your batch of recordings 92 153 for recording in signals: 93 154 weighted = wf.filter(recording) 94 155 ``` 95 156 96 - ## High-frequency accuracy (`high_accuracy`) 157 + ## 6. High-frequency accuracy (`high_accuracy`) 97 158 98 159 A plain bilinear-transform design compresses the response near Nyquist: at 99 160 fs = 48 kHz the A-curve error at 12.5 kHz reaches −2.7 dB, outside the IEC ··· 109 170 *The plain bilinear design (red) crosses the class 1 tolerance near 12.5 kHz; 110 171 the oversampled design (blue) stays close to the analytic curve.* 111 172 173 + <details> 174 + <summary>Show the code for this figure</summary> 175 + 176 + ```python 177 + import matplotlib.pyplot as plt 178 + import numpy as np 179 + from phonometry import weighting_filter 180 + 181 + # Measured response of both designs at fs = 48 kHz: weight a centered 182 + # unit impulse and take its spectrum... 183 + fs = 48000 184 + impulse = np.zeros(fs) 185 + impulse[fs // 2] = 1.0 186 + freqs = np.fft.rfftfreq(fs, 1 / fs)[1:] 187 + 188 + # ...versus the analytic IEC 61672-1 A-curve built from the four corner 189 + # frequencies of section 1, normalized to 0 dB at 1 kHz. 190 + f1, f2, f3, f4 = 20.599, 107.653, 737.862, 12194.217 191 + gain = (f4**2 * freqs**4) / ((freqs**2 + f1**2) 192 + * np.sqrt((freqs**2 + f2**2) * (freqs**2 + f3**2)) 193 + * (freqs**2 + f4**2)) 194 + analytic = 20 * np.log10(gain / gain[np.argmin(np.abs(freqs - 1000))]) 195 + 196 + fig, ax = plt.subplots(figsize=(9, 5)) 197 + ax.semilogx(freqs, analytic, "k--", label="Analytic (IEC 61672-1)") 198 + for high_accuracy, label in ((False, "Plain bilinear"), 199 + (True, "Oversampled (default)")): 200 + weighted = weighting_filter(impulse, fs, curve="A", 201 + high_accuracy=high_accuracy) 202 + response = 20 * np.log10(np.abs(np.fft.rfft(weighted)))[1:] 203 + ax.semilogx(freqs, response, label=label) 204 + ax.set(xlim=(1000, 20000), ylim=(-12, 3), 205 + xlabel="Frequency [Hz]", ylabel="A-weighting response [dB]") 206 + ax.grid(True, which="both", alpha=0.3) 207 + ax.legend() 208 + plt.show() 209 + ``` 210 + 211 + </details> 212 + 112 213 - `high_accuracy=False` restores the legacy plain-bilinear behavior. 113 214 - **Stateful (block) processing** always uses the legacy design: the internal 114 215 FIR resampling is incompatible with block continuity. Passing 115 216 `high_accuracy=True` together with `stateful=True` raises a `ValueError`. 116 217 117 218 ```python 219 + from phonometry import WeightingFilter, weighting_filter 220 + 221 + # Uses `recording` and `fs` from the snippet above. 118 222 # Explicit legacy behavior 119 223 y = weighting_filter(recording, fs, curve="A", high_accuracy=False) 120 224 ··· 127 231 128 232 See [Block Processing](block-processing.md) for the streaming workflow and 129 233 [Theory](theory.md) for the analytic curve definitions. 234 + 235 + --- 236 + 237 + **Standards.** IEC 61672-1:2013, *Electroacoustics — Sound level meters — 238 + Part 1: Specifications* — the A, C and Z frequency-weighting curves (the 239 + Annex E analytic definition from four corner frequencies, normalized to 0 dB 240 + at 1 kHz) and the class 1 tolerances the `high_accuracy` design keeps up to 241 + 16 kHz. ISO 7196:1995, *Acoustics — Frequency-weighting characteristic for 242 + infrasound measurements* — the G-weighting pole/zero definition (Table 1), 243 + verified against every Table 2 nominal response value (0.25 Hz to 315 Hz).
+143
scripts/generate_graphs.py
··· 490 490 r"Potencia sonora de precisión (ISO 3745) LWA = \1 dB(A)"), 491 491 (r"^Precision intensity scanning \(ISO 9614-3\) LWA = (.+) dB\(A\)$", 492 492 r"Barrido de intensidad de precisión (ISO 9614-3) LWA = \1 dB(A)"), 493 + (r"^Enveloping-surface sound power \(ISO 3744\) LWA = (.+) dB\(A\)$", 494 + r"Potencia sonora por superficie envolvente (ISO 3744) LWA = \1 dB(A)"), 495 + (r"^Reverberation-room sound power \(ISO 3741\) LWA = (.+) dB\(A\)$", 496 + r"Potencia sonora en cámara reverberante (ISO 3741) LWA = \1 dB(A)"), 497 + (r"^Intensity-scanning sound power \(ISO 9614-2\) LWA = (.+) dB\(A\)$", 498 + r"Potencia sonora por barrido de intensidad (ISO 9614-2) LWA = \1 dB(A)"), 493 499 # Human-vibration dynamic titles (numeric a_w / A(8)) 494 500 (r"^Weighted seat acceleration \(ISO 2631-1\) (.+)$", 495 501 r"Aceleración ponderada del asiento (ISO 2631-1) \1"), ··· 3240 3246 plt.close() 3241 3247 3242 3248 3249 + def generate_sound_power_pressure_result(output_dir: str) -> None: 3250 + """ISO 3744: enveloping-surface LW spectrum from hemisphere pressure levels.""" 3251 + print("Generating sound_power_pressure_result.png...") 3252 + from phonometry import sound_power_pressure 3253 + 3254 + # The sound-power guide's section-1 example: octave-band SPL at the 10 3255 + # hemisphere positions of ISO 3744 (Annex B) around a machine on one 3256 + # reflecting plane, with a flat 55 dB background, corrected for background 3257 + # (K1) and for the test room (K2 from T = 0.6 s, V = 300 m^3). The library 3258 + # forms LW = Lp_bar - K1 - K2 + 10 lg(S/S0) per band and the A-weighted 3259 + # total LWA. 3260 + freqs = np.array([63, 125, 250, 500, 1000, 2000, 4000, 8000], dtype=float) 3261 + base = np.array([70.0, 74.0, 78.0, 80.0, 79.0, 76.0, 72.0, 66.0]) 3262 + rng = np.random.default_rng(0) 3263 + levels = base + rng.normal(0.0, 0.5, size=(10, 8)) 3264 + background = np.full((10, 8), 55.0) 3265 + result = sound_power_pressure( 3266 + levels, "hemisphere", radius=1.5, reflecting_planes=1, 3267 + background_levels=background, frequencies=freqs, 3268 + reverberation_time=0.6, volume=300.0, 3269 + ) 3270 + 3271 + lw = result.sound_power_level 3272 + lwa = result.sound_power_level_a 3273 + positions = np.arange(freqs.size, dtype=float) 3274 + fig, ax = plt.subplots(figsize=(10, 6.3)) 3275 + ax.bar(positions, lw, width=0.7, color=COLOR_PRIMARY, edgecolor=COLOR_FG, 3276 + linewidth=0.7, zorder=3) 3277 + ax.set_xticks(positions) 3278 + ax.set_xticklabels([f"{f:g}" for f in freqs], rotation=45, ha="right") 3279 + ax.set_title(f"Enveloping-surface sound power (ISO 3744) LWA = {lwa:.1f} dB(A)", 3280 + fontweight="bold", pad=12) 3281 + ax.set_xlabel(LABEL_FREQ_HZ) 3282 + ax.set_ylabel("Sound power level LW [dB]") 3283 + ax.set_ylim(0.0, float(np.nanmax(lw)) + 8.0) 3284 + ax.grid(axis="y", color=COLOR_GRID, linestyle="--", alpha=0.5, zorder=0) 3285 + ax.set_axisbelow(True) 3286 + plt.tight_layout() 3287 + plt.savefig(themed_path(output_dir, "sound_power_pressure_result.png")) 3288 + plt.close() 3289 + 3290 + 3291 + def generate_sound_power_reverberation_result(output_dir: str) -> None: 3292 + """ISO 3741: reverberation-room LW spectrum (direct method).""" 3293 + print("Generating sound_power_reverberation_result.png...") 3294 + from phonometry import sound_power_reverberation 3295 + 3296 + # The sound-power guide's section-2 example: one-third-octave mean room 3297 + # SPL from 100 Hz to 10 kHz in a qualified 200 m^3 reverberation room with 3298 + # T60 = 2 s, carried to LW through the Sabine absorption area, the 3299 + # Waterhouse correction and the meteorological corrections C1/C2 3300 + # (ISO 3741 Eq. 20). 3301 + freqs = np.array([100, 125, 160, 200, 250, 315, 400, 500, 630, 800, 1000, 3302 + 1250, 1600, 2000, 2500, 3150, 4000, 5000, 6300, 8000, 3303 + 10000], dtype=float) 3304 + lp = np.linspace(80.0, 70.0, freqs.size) 3305 + t60 = np.full(freqs.size, 2.0) 3306 + result = sound_power_reverberation( 3307 + lp, t60, volume=200.0, surface_area=220.0, frequencies=freqs, 3308 + temperature=20.0, static_pressure=101.0, 3309 + ) 3310 + 3311 + lw = result.sound_power_level 3312 + lwa = result.sound_power_level_a 3313 + positions = np.arange(freqs.size, dtype=float) 3314 + fig, ax = plt.subplots(figsize=(10, 6.3)) 3315 + ax.bar(positions, lw, width=0.7, color=COLOR_PRIMARY, edgecolor=COLOR_FG, 3316 + linewidth=0.7, zorder=3) 3317 + ax.set_xticks(positions) 3318 + ax.set_xticklabels([f"{f:g}" for f in freqs], rotation=45, ha="right") 3319 + ax.set_title( 3320 + f"Reverberation-room sound power (ISO 3741) LWA = {lwa:.1f} dB(A)", 3321 + fontweight="bold", pad=12) 3322 + ax.set_xlabel(LABEL_FREQ_HZ) 3323 + ax.set_ylabel("Sound power level LW [dB]") 3324 + ax.set_ylim(0.0, float(np.nanmax(lw)) + 8.0) 3325 + ax.grid(axis="y", color=COLOR_GRID, linestyle="--", alpha=0.5, zorder=0) 3326 + ax.set_axisbelow(True) 3327 + plt.tight_layout() 3328 + plt.savefig(themed_path(output_dir, "sound_power_reverberation_result.png")) 3329 + plt.close() 3330 + 3331 + 3332 + def generate_sound_power_intensity_result(output_dir: str) -> None: 3333 + """ISO 9614-2: intensity-scanning LW spectrum from segment sweeps.""" 3334 + print("Generating sound_power_intensity_result.png...") 3335 + import warnings 3336 + 3337 + from phonometry import sound_power_intensity 3338 + 3339 + # The sound-power guide's section-3 example: two repeated intensity sweeps 3340 + # over 6 surface segments and 6 octave bands, with the segment surface SPL 3341 + # and the probe's pressure-residual intensity index. The partial powers 3342 + # In_i * Si sum to the band LW; every band passes the field-indicator 3343 + # criteria at engineering grade here. 3344 + freqs = np.array([125, 250, 500, 1000, 2000, 4000], dtype=float) 3345 + areas = np.full(6, 0.5) 3346 + rng = np.random.default_rng(0) 3347 + scan1 = np.abs(rng.normal(1e-4, 2e-5, size=(6, 6))) 3348 + scan2 = scan1 * (1.0 + rng.normal(0.0, 0.02, size=(6, 6))) 3349 + pressure = np.full((6, 6), 80.0) 3350 + with warnings.catch_warnings(): 3351 + warnings.simplefilter("ignore") 3352 + result = sound_power_intensity( 3353 + scan1, areas, normal_intensity_2=scan2, pressure_levels=pressure, 3354 + pressure_residual_index=12.0, frequencies=freqs, 3355 + band_type="octave", grade="engineering", 3356 + ) 3357 + 3358 + lw = result.sound_power_level 3359 + lwa = result.sound_power_level_a 3360 + positions = np.arange(freqs.size, dtype=float) 3361 + fig, ax = plt.subplots(figsize=(10, 6.3)) 3362 + ax.bar(positions, np.nan_to_num(lw), width=0.7, color=COLOR_PRIMARY, 3363 + edgecolor=COLOR_FG, linewidth=0.7, zorder=3) 3364 + ax.set_xticks(positions) 3365 + ax.set_xticklabels([f"{f:g}" for f in freqs], rotation=45, ha="right") 3366 + ax.set_title( 3367 + f"Intensity-scanning sound power (ISO 9614-2) LWA = {lwa:.1f} dB(A)", 3368 + fontweight="bold", pad=12) 3369 + ax.set_xlabel(LABEL_FREQ_HZ) 3370 + ax.set_ylabel("Sound power level LW [dB]") 3371 + ax.set_ylim(0.0, float(np.nanmax(lw)) + 8.0) 3372 + ax.grid(axis="y", color=COLOR_GRID, linestyle="--", alpha=0.5, zorder=0) 3373 + ax.set_axisbelow(True) 3374 + plt.tight_layout() 3375 + plt.savefig(themed_path(output_dir, "sound_power_intensity_result.png")) 3376 + plt.close() 3377 + 3378 + 3243 3379 def generate_precision_anechoic_power(output_dir: str) -> None: 3244 3380 """ISO 3745: precision LW spectrum from a hemisphere pressure measurement.""" 3245 3381 print("Generating precision_anechoic_power.png...") ··· 4012 4148 generate_insitu_absorption(img_dir) 4013 4149 generate_precision_anechoic_power(img_dir) 4014 4150 generate_intensity_scan_power(img_dir) 4151 + 4152 + # Sound power result spectra for the three most-used routes 4153 + # (ISO 3744 enveloping surface, ISO 3741 reverberation room, 4154 + # ISO 9614-2 intensity scanning) 4155 + generate_sound_power_pressure_result(img_dir) 4156 + generate_sound_power_reverberation_result(img_dir) 4157 + generate_sound_power_intensity_result(img_dir) 4015 4158 4016 4159 # Human vibration (ISO 8041-1, ISO 2631-1/-2/-4, ISO 5349-1/-2, 4017 4160 # Directive 2002/44/EC): frequency weighting, weighted a_w, daily A(8)
.github/images/sound_power_intensity_result.png

This is a binary file and will not be displayed.

.github/images/sound_power_intensity_result_dark.png

This is a binary file and will not be displayed.

.github/images/sound_power_intensity_result_es.png

This is a binary file and will not be displayed.

.github/images/sound_power_intensity_result_es_dark.png

This is a binary file and will not be displayed.

.github/images/sound_power_pressure_result.png

This is a binary file and will not be displayed.

.github/images/sound_power_pressure_result_dark.png

This is a binary file and will not be displayed.

.github/images/sound_power_pressure_result_es.png

This is a binary file and will not be displayed.

.github/images/sound_power_pressure_result_es_dark.png

This is a binary file and will not be displayed.

.github/images/sound_power_reverberation_result.png

This is a binary file and will not be displayed.

.github/images/sound_power_reverberation_result_dark.png

This is a binary file and will not be displayed.

.github/images/sound_power_reverberation_result_es.png

This is a binary file and will not be displayed.

.github/images/sound_power_reverberation_result_es_dark.png

This is a binary file and will not be displayed.

+19
site/src/content/docs/getting-started.md
··· 65 65 spl, freq = octave_filter(signal, fs=fs, fraction=3) 66 66 67 67 print(f"Bands: {freq}") 68 + # Bands: [12.589254117941678, 15.848931924611138, ..., 19952.623149688785] (33 bands) 68 69 print(f"SPL [dB]: {spl}") 70 + # SPL [dB]: [46.88395351 47.96774897 49.04991279 ...] — ~90.7 dB at 100 Hz and ~90.9 dB at 1 kHz 69 71 ``` 70 72 71 73 <img class="light-only" 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" style="width:80%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/signal_response_fraction_3_dark.png" alt="One-third-octave spectrum analysis of a multi-tone signal with the raw PSD in the background" style="width:80%"> 72 74 73 75 *Example of a 1/3 Octave Band spectrum analysis of a complex signal.* 76 + 77 + <details> 78 + <summary>Show the code for this figure</summary> 79 + 80 + ```python 81 + import matplotlib.pyplot as plt 82 + 83 + # Uses `spl` and `freq` from the snippet above. 84 + # The committed figure additionally overlays the raw-signal PSD in the background. 85 + fig, ax = plt.subplots() 86 + ax.semilogx(freq, spl, marker="o", markerfacecolor="white") 87 + ax.set_xlabel("Frequency [Hz]") 88 + ax.set_ylabel("SPL [dB]") 89 + plt.show() 90 + ``` 91 + 92 + </details> 74 93 75 94 ## Analyzing an audio file 76 95
+19
site/src/content/docs/es/getting-started.md
··· 65 65 spl, freq = octave_filter(signal, fs=fs, fraction=3) 66 66 67 67 print(f"Bandas: {freq}") 68 + # Bandas: [12.589254117941678, 15.848931924611138, ..., 19952.623149688785] (33 bandas) 68 69 print(f"SPL [dB]: {spl}") 70 + # SPL [dB]: [46.88395351 47.96774897 49.04991279 ...] — ~90.7 dB en 100 Hz y ~90.9 dB en 1 kHz 69 71 ``` 70 72 71 73 <img class="light-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/signal_response_fraction_3_es.png" alt="Análisis en tercios de octava de una señal multitono con la PSD cruda de fondo" style="width:80%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/signal_response_fraction_3_es_dark.png" alt="Análisis en tercios de octava de una señal multitono con la PSD cruda de fondo" style="width:80%"> 72 74 73 75 *Ejemplo de análisis espectral en tercios de octava de una señal compleja.* 76 + 77 + <details> 78 + <summary>Mostrar el código de esta figura</summary> 79 + 80 + ```python 81 + import matplotlib.pyplot as plt 82 + 83 + # Usa `spl` y `freq` del snippet anterior. 84 + # La figura publicada superpone además la PSD de la señal cruda de fondo. 85 + fig, ax = plt.subplots() 86 + ax.semilogx(freq, spl, marker="o", markerfacecolor="white") 87 + ax.set_xlabel("Frecuencia [Hz]") 88 + ax.set_ylabel("SPL [dB]") 89 + plt.show() 90 + ``` 91 + 92 + </details> 74 93 75 94 ## Analizar un archivo de audio 76 95
+11 -14
site/src/content/docs/guides/block-processing.md
··· 3 3 description: "Stateful streaming workflows with carried filter state." 4 4 --- 5 5 6 + Some measurements never fit in memory: an hour-long environmental recording, 7 + a live monitor that must report levels while the microphone is still 8 + capturing, or an embedded logger that only ever sees one buffer at a time. In 9 + all of these the signal has to be processed block by block — and the filters 10 + must behave exactly as if they had seen the whole signal at once. 11 + 6 12 The `OctaveFilterBank`, `WeightingFilter` (for A, C, or Z-weighting) and 7 13 `TimeWeighting` classes support block (streaming) processing: the internal 8 14 filter state is carried between calls, so concatenated block outputs match a ··· 16 22 17 23 Create a stateful filter bank with `stateful=True`. The internal state is 18 24 zero-initialized by default but may be initialized for step-response 19 - steady-state (like `scipy.signal.sosfilt_zi`) with `steady_ic=True`. 20 - 21 - Notes when using a stateful `OctaveFilterBank`: 22 - 23 - - Detrending should be disabled during block processing (`detrend=False`), as it 24 - can introduce discontinuities between blocks. 25 - - Resampling is not supported for block processing, so you need to set 26 - `resample=False`. 27 - - `zero_phase=True` is incompatible with stateful mode (forward-backward 28 - filtering needs the whole signal). 29 - - Stateful `WeightingFilter` uses the legacy bilinear design 30 - (`high_accuracy=False`); see [Frequency Weighting](/phonometry/guides/weighting/). 25 + steady-state (like `scipy.signal.sosfilt_zi`) with `steady_ic=True`. The 26 + options that must be disabled in stateful mode are summarized in the 27 + [constraints table](#stateful-mode-constraints) at the end of this guide. 31 28 32 29 ## Example 33 30 ··· 70 67 ``` 71 68 72 69 Or manage the state yourself with the functional API — see 73 - [Time Weighting](/phonometry/guides/time-weighting/#block-processing). 70 + [Time Weighting](/phonometry/guides/time-weighting/#6-block-processing). 74 71 75 72 ## Multichannel state 76 73 ··· 104 101 | `detrend` | must be `False` | Per-block detrending creates boundary discontinuities | 105 102 | `resample` | must be `False` | The resampler is not stateful | 106 103 | `zero_phase` | unsupported | Forward-backward filtering needs the whole signal | 107 - | `high_accuracy` (weighting) | resolves to `False` by default; explicitly passing `True` raises `ValueError` | The polyphase resampling inside is block-incompatible | 104 + | `high_accuracy` (weighting) | resolves to `False` by default — the legacy bilinear design, see [Frequency Weighting](/phonometry/guides/weighting/); explicitly passing `True` raises `ValueError` | The polyphase resampling inside is block-incompatible | 108 105 | `steady_ic` | optional | Starts the filters in step-response steady state |
+8
site/src/content/docs/guides/calibration.md
··· 164 164 Integer signals (e.g. int16 from `scipy.io.wavfile.read`) are converted to 165 165 float64 internally before any squaring, so calibration and level results are 166 166 identical whether you pass the raw integer array or a float conversion. 167 + 168 + --- 169 + 170 + **Standards.** IEC 60942:2017, *Electroacoustics — Sound calibrators* — the 171 + calibrator level and class assumptions behind `sensitivity()` (the 94 dB 172 + principal level and the Table 1 class tolerances) and the short-term 173 + level-fluctuation stability check of the reference recording (5.3.3, Table 2 174 + class 1 limits per nominal frequency).
+1
site/src/content/docs/guides/enclosed-space-absorption.md
··· 87 87 volume=60.0, air_condition="20C_50-70", 88 88 ) 89 89 print(result.reverberation_time.round(2)) 90 + # [2.13 1.03 0.62 0.48 0.43 0.42 0.4 ] 90 91 ``` 91 92 92 93 <img class="light-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/enclosed_space_absorption.png" alt="Two panels for a 60 cubic metre office with a bare versus acoustically-treated ceiling: the equivalent absorption area per octave band, much higher with the acoustic ceiling, and the reverberation time falling from about five seconds at low frequency for the bare room to under one second with the acoustic ceiling" style="width:96%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/enclosed_space_absorption_dark.png" alt="Two panels for a 60 cubic metre office with a bare versus acoustically-treated ceiling: the equivalent absorption area per octave band, much higher with the acoustic ceiling, and the reverberation time falling from about five seconds at low frequency for the bare room to under one second with the acoustic ceiling" style="width:96%">
+43 -25
site/src/content/docs/guides/filter-banks.md
··· 7 7 characteristic. All banks place their **−3 dB points on the ANSI S1.11 band 8 8 edges**, so band levels are comparable across architectures. 9 9 10 - ## Fractional octave bands: the math 10 + ## 1. Fractional octave bands: the math 11 11 12 12 IEC 61260-1:2014 builds every band from the base-10 octave ratio 13 13 $G = 10^{3/10} \approx 1.99526$ (so "one octave" is *not* exactly 2). For ··· 33 33 34 34 <img class="light-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/diagram_multirate.svg" alt="Multirate decimation: high bands filtered at the input rate, low bands after anti-alias low-pass and decimation so the SOS sections stay numerically healthy" style="width:92%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/diagram_multirate_dark.svg" alt="Multirate decimation: high bands filtered at the input rate, low bands after anti-alias low-pass and decimation so the SOS sections stay numerically healthy" style="width:92%"> 35 35 36 - ### `octave_filter()` / `OctaveFilterBank` parameters 36 + ## 2. Filter Comparison and Zoom 37 + 38 + We use Second-Order Sections (SOS) for all filters to ensure numerical stability. 39 + The following plot compares the architectures focusing on the -3 dB crossover point. 40 + 41 + <img class="light-only" 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" style="width:80%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/filter_type_comparison_dark.png" alt="Magnitude response comparison of the five filter architectures for the 1 kHz octave band, with a zoom at the -3 dB crossover" style="width:80%"> 42 + 43 + | Type | Name | Usage Example | Best For | 44 + | :--- | :--- | :--- | :--- | 45 + | `butter` | **Butterworth** | `octave_filter(x, fs, filter_type='butter')` | General acoustic measurement. | 46 + | `cheby1` | **Chebyshev I** | `octave_filter(x, fs, filter_type='cheby1', ripple=0.1)` | Sharper roll-off at the cost of ripple. | 47 + | `cheby2` | **Chebyshev II** | `octave_filter(x, fs, filter_type='cheby2')` | Flat passband with stopband zeros. | 48 + | `ellip` | **Elliptic** | `octave_filter(x, fs, filter_type='ellip', ripple=0.1)` | Maximum selectivity. | 49 + | `bessel` | **Bessel** | `octave_filter(x, fs, filter_type='bessel')` | Preserving transient waveform shapes. | 50 + 51 + ## 3. `octave_filter()` / `OctaveFilterBank` parameters 37 52 38 53 | Parameter | Type | Units | Range / default | Notes | 39 54 | :--- | :--- | :--- | :--- | :--- | ··· 59 74 Table 1 acceptance limits and reports the class (`1`, `2` or `None` if outside both) with per-band 60 75 margins. 61 76 62 - ## Filter Comparison and Zoom 63 - 64 - We use Second-Order Sections (SOS) for all filters to ensure numerical stability. 65 - The following plot compares the architectures focusing on the -3 dB crossover point. 66 - 67 - <img class="light-only" 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" style="width:80%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/filter_type_comparison_dark.png" alt="Magnitude response comparison of the five filter architectures for the 1 kHz octave band, with a zoom at the -3 dB crossover" style="width:80%"> 68 - 69 - | Type | Name | Usage Example | Best For | 70 - | :--- | :--- | :--- | :--- | 71 - | `butter` | **Butterworth** | `octave_filter(x, fs, filter_type='butter')` | General acoustic measurement. | 72 - | `cheby1` | **Chebyshev I** | `octave_filter(x, fs, filter_type='cheby1', ripple=0.1)` | Sharper roll-off at the cost of ripple. | 73 - | `cheby2` | **Chebyshev II** | `octave_filter(x, fs, filter_type='cheby2')` | Flat passband with stopband zeros. | 74 - | `ellip` | **Elliptic** | `octave_filter(x, fs, filter_type='ellip', ripple=0.1)` | Maximum selectivity. | 75 - | `bessel` | **Bessel** | `octave_filter(x, fs, filter_type='bessel')` | Preserving transient waveform shapes. | 76 - 77 - ## Gallery of Filter Bank Responses 77 + ## 4. Gallery of Filter Bank Responses 78 78 79 79 Full spectral view of the filter banks for Octave (1/1) and 1/3-Octave fractions. 80 80 ··· 86 86 | **Elliptic** | <img class="light-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/filter_ellip_fraction_1_order_6.png" alt="Elliptic octave-band filter bank frequency response" style="width:100%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/filter_ellip_fraction_1_order_6_dark.png" alt="Elliptic octave-band filter bank frequency response" style="width:100%"> | <img class="light-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/filter_ellip_fraction_3_order_6.png" alt="Elliptic one-third-octave filter bank frequency response" style="width:100%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/filter_ellip_fraction_3_order_6_dark.png" alt="Elliptic one-third-octave filter bank frequency response" style="width:100%"> | 87 87 | **Bessel** | <img class="light-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/filter_bessel_fraction_1_order_6.png" alt="Bessel octave-band filter bank frequency response" style="width:100%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/filter_bessel_fraction_1_order_6_dark.png" alt="Bessel octave-band filter bank frequency response" style="width:100%"> | <img class="light-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/filter_bessel_fraction_3_order_6.png" alt="Bessel one-third-octave filter bank frequency response" style="width:100%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/filter_bessel_fraction_3_order_6_dark.png" alt="Bessel one-third-octave filter bank frequency response" style="width:100%"> | 88 88 89 - ## Filter Usage and Examples 89 + ## 5. Filter Usage and Examples 90 90 91 91 ### 1. Butterworth (`butter`) 92 92 ··· 115 115 the cut-off frequencies. 116 116 117 117 ```python 118 + # Uses `x` and `fs` from the snippet above. 118 119 # Selectivity with 0.1 dB passband ripple 119 120 spl, freq = octave_filter(x, fs, filter_type='cheby1', ripple=0.1) 120 121 ``` ··· 129 130 −3 dB points land on the band edges (`attenuation` must be > 3.01 dB). 130 131 131 132 ```python 133 + # Uses `x` and `fs` from the snippet above. 132 134 # Flat passband, class-1 default 72 dB stopband attenuation 133 135 spl, freq = octave_filter(x, fs, filter_type='cheby2') 134 136 ``` ··· 141 143 roll-off) for a given order. They feature ripples in both the passband and stopband. 142 144 143 145 ```python 146 + # Uses `x` and `fs` from the snippet above. 144 147 # Maximum selectivity for extreme band isolation 145 148 spl, freq = octave_filter(x, fs, filter_type='ellip', ripple=0.1) 146 149 ``` ··· 154 157 any other type, but have the slowest roll-off. 155 158 156 159 ```python 160 + # Uses `x` and `fs` from the snippet above. 157 161 # Best for pulse analysis and transient preservation 158 162 spl, freq = octave_filter(x, fs, filter_type='bessel') 159 163 ``` ··· 170 174 ```python 171 175 from phonometry import linkwitz_riley 172 176 173 - signal = x # reuse the calibrated signal from the top of the page 177 + # Uses `x` and `fs` from the snippet above. 178 + signal = x 174 179 # Split signal into Low and High bands at 1000 Hz 175 180 low, high = linkwitz_riley(signal, fs, freq=1000, order=4) 176 181 # Reconstruction: low + high == signal (flat response) ··· 178 183 179 184 <img class="light-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/crossover_lr4.png" alt="Linkwitz-Riley 4th-order crossover: low-pass, high-pass and their flat sum" style="width:60%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/crossover_lr4_dark.png" alt="Linkwitz-Riley 4th-order crossover: low-pass, high-pass and their flat sum" style="width:60%"> 180 185 181 - ## Verifying the IEC 61260-1 class 186 + ## 6. Verifying the IEC 61260-1 class 182 187 183 188 `verify_filter_class` checks every band of a bank against the acceptance 184 189 limits of **IEC 61260-1:2014** (Table 1, with the fractional-octave breakpoint ··· 190 195 191 196 bank = OctaveFilterBank(fs=48000, fraction=3, order=6) 192 197 result = verify_filter_class(bank) 193 - print(result["overall_class"]) # 1, 2 or None 194 - print(result["bands"][0]) # {'freq': ..., 'class': 1, 'margin_class1_db': ...} 198 + print(result["overall_class"]) # 1 199 + print(result["bands"][0]) 200 + # {'freq': 12.589254117941678, 'class': 1, 'margin_class1_db': 0.39999999999997266, 'margin_class2_db': 0.5999999999999727} 195 201 ``` 196 202 197 203 <img class="light-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/class_mask_overlay.png" alt="Butterworth band response threading between the forbidden regions of the IEC 61260-1 class 1 acceptance mask" style="width:80%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/class_mask_overlay_dark.png" alt="Butterworth band response threading between the forbidden regions of the IEC 61260-1 class 1 acceptance mask" style="width:80%"> ··· 208 214 not meet class limits at order 6: passband ripple (cheby1/ellip) and slow 209 215 roll-off (bessel) violate the mask. 210 216 211 - ## Signal Decomposition and Stability 217 + ## 7. Signal Decomposition and Stability 212 218 213 219 By setting `sigbands=True`, you can retrieve the time-domain components of each 214 220 band. This allows for advanced analysis or comparing how different architectures ··· 255 261 256 262 <img class="light-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/group_delay_comparison.png" alt="Group delay of the 1 kHz octave band for the five architectures: Bessel nearly flat, Chebyshev and Elliptic peaking at the band edges" style="width:80%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/group_delay_comparison_dark.png" alt="Group delay of the 1 kHz octave band for the five architectures: Bessel nearly flat, Chebyshev and Elliptic peaking at the band edges" style="width:80%"> 257 263 258 - ## Zero-phase filtering 264 + ## 8. Zero-phase filtering 259 265 260 266 For offline analysis you can eliminate group delay entirely: `zero_phase=True` 261 267 filters each band forward-backward (`scipy.signal.sosfiltfilt`), keeping band ··· 269 275 ```python 270 276 from phonometry import OctaveFilterBank 271 277 278 + # Uses `y` from the snippet above. 272 279 bank = OctaveFilterBank(fs=48000, fraction=3) 273 280 spl, freq, xb = bank.filter(y, sigbands=True, zero_phase=True) 274 281 ``` ··· 277 284 278 285 *Causal filtering delays the burst by the filter's group delay; zero-phase 279 286 filtering keeps it aligned with the input.* 287 + 288 + --- 289 + 290 + **Standards.** IEC 61260-1:2014, *Electroacoustics — Octave-band and 291 + fractional-octave-band filters — Part 1: Specifications* — the base-10 mid 292 + frequencies and band edges of §1 (5.2-5.5), the nominal band labels, and the 293 + Table 1 class 1 / class 2 acceptance limits (with the fractional-octave 294 + breakpoint mapping and log-frequency interpolation) verified in §6. 295 + ANSI S1.11-2004, *Octave-Band and Fractional-Octave-Band Analog and Digital 296 + Filters* — the band-edge convention on which every bank places its −3 dB 297 + points.
+3 -3
site/src/content/docs/guides/human-vibration.md
··· 161 161 raw = np.random.default_rng(0).standard_normal(int(60 * fs)) # 60 s record 162 162 a_w = ph.apply_weighting(raw, fs, "Wk") # weighted signal 163 163 164 - print(round(ph.vibration_dose_value(a_w, fs), 3)) # VDV [m/s^1.75] 165 - print(round(ph.mtvv(a_w, fs), 3)) # MTVV [m/s^2] 166 - print(round(ph.crest_factor(a_w), 2)) # crest factor 164 + print(round(ph.vibration_dose_value(a_w, fs), 3)) # 0.744 VDV [m/s^1.75] 165 + print(round(ph.mtvv(a_w, fs), 3)) # 0.265 MTVV [m/s^2] 166 + print(round(ph.crest_factor(a_w), 2)) # 3.74 crest factor 167 167 ``` 168 168 169 169 ## 3. Vibration total value and daily exposure `A(8)`
+23 -3
site/src/content/docs/guides/intensity.md
··· 102 102 practical ceiling. Larger spacers reach lower frequencies, smaller ones 103 103 higher. 104 104 - **Reactive fields**: when `pressure_intensity_index` (F2 in ISO 9614-1) 105 - approaches the probe's residual index δ_pI0, phase errors dominate. The 106 - ISO 9614-1 Annex A field indicators and the dynamic-capability criterion 107 - are available directly: 105 + approaches the probe's residual index δ_pI0, phase errors dominate. 106 + 107 + Over a measurement surface, the ISO 9614-1 Annex A field indicators grade the 108 + scan itself. **F2**, the surface pressure-intensity indicator, is the surface 109 + pressure level minus the level of the mean *magnitude* of the normal 110 + intensity — the larger it is, the closer the measurement sits to the probe's 111 + phase-error floor. **F3**, the negative partial power indicator, is the same 112 + difference taken with the *signed* mean intensity — F3 − F2 > 0 reveals power 113 + flowing inward through parts of the surface. **F4**, the field non-uniformity 114 + indicator, is the normalised spread of the per-position intensities — the 115 + larger it is, the more measurement positions the surface needs. Together with 116 + the dynamic-capability criterion they are available directly: 108 117 109 118 ```python 110 119 import numpy as np ··· 135 144 136 145 See [Theory](/phonometry/reference/theory/) for the derivations and [Calibration](/phonometry/guides/calibration/) 137 146 for absolute scaling of the two channels. 147 + 148 + --- 149 + 150 + **Standards.** IEC 61043:1994, *Electroacoustics — Instruments for the 151 + measurement of sound intensity — Measurements with pairs of pressure sensing 152 + microphones* — the two-microphone cross-spectral intensity estimator, the 153 + finite-difference bias correction and the usable-bandwidth bound (clause 7.3, 154 + Table 3). ISO 9614-1:1993, *Acoustics — Determination of sound power levels 155 + of noise sources using sound intensity — Part 1: Measurement at discrete 156 + points* — the pressure-intensity index, the Annex A field indicators F2, F3 157 + and F4, and the dynamic-capability criterion (Annex B).
+20
site/src/content/docs/guides/levels.md
··· 61 61 **L90** the background level. 62 62 63 63 ```python 64 + import numpy as np 64 65 from phonometry import ln_levels 65 66 66 67 # A steady tone gives L10 = L50 = L90; percentiles only tell a story for a 67 68 # *fluctuating* level. Synthesize 3 s alternating between a quiet and a 68 69 # ~10 dB louder half-second so the statistics separate. 70 + fs = 48000 69 71 rng = np.random.default_rng(0) 70 72 segment = fs // 2 # 0.5 s per level 71 73 quiet = 0.02 * rng.standard_normal(segment) # background ··· 109 111 110 112 ```python 111 113 from phonometry import lc_peak, sel, sound_exposure, lex_8h 114 + 115 + # Uses `recording`, `fs` and `sensitivity` from the Leq snippet above. 112 116 113 117 # C-weighted peak (IEC 61672-1 §5.13) - occupational action limits use this 114 118 peak = lc_peak(recording, fs, calibration_factor=sensitivity) ··· 227 231 ```python 228 232 from phonometry import OctaveFilterBank 229 233 234 + # Uses `recording` from the Leq snippet above. 230 235 bank = OctaveFilterBank(fs=48000, fraction=3) 231 236 levels, freq, times = bank.spectrogram(recording, window_time=0.125, overlap=0.5) 232 237 # levels: (bands, frames) — ready for pcolormesh(times, freq, levels) ··· 258 263 ```python 259 264 import matplotlib.pyplot as plt 260 265 266 + # Uses `levels`, `freq` and `times` from the spectrogram snippet above. 261 267 fig, ax = plt.subplots() 262 268 mesh = ax.pcolormesh(times, freq, levels, shading="auto") 263 269 ax.set_yscale("log") ··· 273 279 tonal-prominence verdicts in [Prominent Discrete Tones](/phonometry/guides/tone-prominence/), 274 280 and the ISO 226 equal-loudness contours live with the perception metrics in 275 281 [Psychoacoustics](/phonometry/guides/psychoacoustics/). 282 + 283 + --- 284 + 285 + **Standards.** IEC 61672-1:2013, *Electroacoustics — Sound level meters — 286 + Part 1: Specifications* — the Fast/Slow/Impulse envelope ballistics behind 287 + `ln_levels`, the C-weighted peak of §5.13 (verified against the Table 5 tone 288 + bursts) and the sound exposure level verified against the Table 4 LAE column. 289 + IEC 61252, *Electroacoustics — Specifications for personal sound exposure 290 + meters* — the sound exposure E in Pa²h and the normalized 8 h level LEX,8h 291 + (≡ LEP,d), anchored at 3.2 Pa²h ⇔ exactly 90 dB. ISO 1996-1:2016, *Acoustics — 292 + Description, measurement and assessment of environmental noise — Part 1: 293 + Basic quantities and assessment procedures* — Lden (3.6.4), Ldn (3.6.5) and 294 + the composite whole-day rating level of clause 6.5 (Formulae 5-6, Table A.1 295 + adjustments).
+72 -1
site/src/content/docs/guides/materials.md
··· 55 55 500 Hz peak overshoots the shifted curve by ≥ 0.25, adding the `M` indicator, so 56 56 the rating is $0.60(\text{M})$, class C.* 57 57 58 + <details> 59 + <summary>Show the code for this figure</summary> 60 + 61 + ```python 62 + import matplotlib.pyplot as plt 63 + from phonometry import weighted_absorption 64 + 65 + # ISO 11654 Annex A.2 practical coefficients at 250/500/1000/2000/4000 Hz 66 + result = weighted_absorption([0.35, 1.00, 0.65, 0.60, 0.55]) 67 + result.plot() # practical curve vs shifted reference, deviations shaded 68 + plt.show() 69 + ``` 70 + 71 + </details> 72 + 58 73 ```python 59 74 from phonometry import weighted_absorption, absorption_class 60 75 ··· 109 124 *The slightly super-linear pressure drop is fitted through the origin; the 110 125 specific airflow resistance is the fit read at 0.5 mm/s.* 111 126 127 + <details> 128 + <summary>Show the code for this figure</summary> 129 + 130 + ```python 131 + import matplotlib.pyplot as plt 132 + import numpy as np 133 + from phonometry import static_airflow_resistance 134 + 135 + area = np.pi * 0.05**2 # 100 mm diameter cell [m^2] 136 + u = np.array([0.5, 1, 2, 4, 8, 12]) * 1e-3 # linear velocity [m/s] 137 + dp = 1.6e4 * u + 4.0e5 * u**2 # measured pressure drop [Pa] 138 + r = static_airflow_resistance(u, dp, area=area, thickness=0.05) 139 + 140 + u_fit = np.linspace(0.0, 13e-3, 200) 141 + dp_fit = r.linear_coefficient * u_fit + r.quadratic_coefficient * u_fit**2 142 + fig, ax = plt.subplots() 143 + ax.plot(u_fit * 1e3, dp_fit, label="Through-origin fit dp = a u + b u^2") 144 + ax.plot(u * 1e3, dp, "o", label="Measured pressure drop") 145 + ax.plot(r.evaluation_velocity * 1e3, r.pressure_drop, "D", 146 + label="Evaluation at 0.5 mm/s") 147 + ax.set_xlabel("Linear airflow velocity u [mm/s]") 148 + ax.set_ylabel("Pressure drop dp [Pa]") 149 + ax.set_title(f"R_s = {r.specific_resistance:.0f} Pa·s/m") 150 + ax.legend() 151 + plt.show() 152 + ``` 153 + 154 + </details> 155 + 112 156 ```python 113 157 import numpy as np 114 158 from phonometry import static_airflow_resistance ··· 160 204 piston_stroke_specimen=14e-3, piston_stroke_termination=1.4e-3, 161 205 frequency=2.0, cavity_volume=7.854e-4, kappa_prime=kp, 162 206 ) 163 - print(round(R)) # airflow resistance R [Pa*s/m^3] 207 + print(round(R)) # 222956 airflow resistance R [Pa*s/m^3] 164 208 ``` 165 209 166 210 Pass the `effective_kappa` result to `alternating_airflow_resistance` for an ··· 190 234 191 235 *A small level difference means a near-perfect absorber; a level difference of 192 236 9.54 dB gives $s = 3$, $|r| = 0.5$ and $\alpha = 0.75$.* 237 + 238 + <details> 239 + <summary>Show the code for this figure</summary> 240 + 241 + ```python 242 + import matplotlib.pyplot as plt 243 + import numpy as np 244 + from phonometry import ( 245 + standing_wave_absorption, standing_wave_ratio_from_level, 246 + standing_wave_reflection_magnitude, 247 + ) 248 + 249 + level_diff = np.linspace(0.5, 40.0, 300) # L_max - L_min [dB] 250 + swr = standing_wave_ratio_from_level(level_diff) 251 + fig, ax = plt.subplots() 252 + ax.plot(level_diff, standing_wave_absorption(swr), 253 + label="Absorption coefficient alpha") 254 + ax.plot(level_diff, standing_wave_reflection_magnitude(swr), "--", 255 + label="Reflection factor magnitude |r|") 256 + ax.set_xlabel("Standing-wave level difference L_max - L_min [dB]") 257 + ax.set_ylabel("alpha, |r|") 258 + ax.legend() 259 + plt.show() 260 + ``` 261 + 262 + </details> 193 263 194 264 ```python 195 265 from phonometry import standing_wave_absorption, standing_wave_ratio_from_level ··· 231 301 r = reflection_factor(h12, spacing=spacing, x1=x1, wavenumber=k0) 232 302 print(np.round(absorption_from_reflection(r), 3)) # [0.75 0.75 0.75] 233 303 print(np.round(normalized_surface_impedance(r), 2)) # Z / rho c0 304 + # [1.15-1.23j 1.15-1.23j 1.15-1.23j] 234 305 ``` 235 306 236 307 The high-level `two_microphone_impedance` wraps this chain and returns an
+75
site/src/content/docs/guides/outdoor-propagation.md
··· 44 44 *The dry 20 °C / 10 % curve absorbs most at mid frequencies, but the humid 45 45 curves overtake it below ~200 Hz — the relaxation signature.* 46 46 47 + <details> 48 + <summary>Show the code for this figure</summary> 49 + 50 + ```python 51 + import matplotlib.pyplot as plt 52 + import numpy as np 53 + from phonometry import air_attenuation 54 + 55 + freqs = np.geomspace(50.0, 10000.0, 400) 56 + fig, ax = plt.subplots() 57 + for temp, rh in [(20.0, 50.0), (20.0, 10.0), (0.0, 70.0), (30.0, 80.0)]: 58 + ax.loglog(freqs, air_attenuation(freqs, temp, rh) * 1000.0, 59 + label=f"{temp:g} °C, {rh:g} % RH") 60 + ax.set_xlabel("Frequency [Hz]") 61 + ax.set_ylabel("Attenuation coefficient alpha [dB/km]") 62 + ax.legend() 63 + plt.show() 64 + ``` 65 + 66 + </details> 67 + 47 68 ```python 48 69 import numpy as np 49 70 from phonometry import air_attenuation, air_attenuation_m ··· 148 169 149 170 <img class="light-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/outdoor_attenuation_breakdown.png" alt="ISO 9613-2 per-octave-band attenuation breakdown as a stacked bar of Adiv, Aatm, Agr and Abar with the total A overlaid, for a 200 m path over porous ground with a 4 m barrier" style="width:80%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/outdoor_attenuation_breakdown_dark.png" alt="ISO 9613-2 per-octave-band attenuation breakdown as a stacked bar of Adiv, Aatm, Agr and Abar with the total A overlaid, for a 200 m path over porous ground with a 4 m barrier" style="width:80%"> 150 171 172 + <details> 173 + <summary>Show the code for this figure</summary> 174 + 175 + ```python 176 + import matplotlib.pyplot as plt 177 + import numpy as np 178 + from phonometry import Barrier, outdoor_propagation_attenuation 179 + 180 + bands = np.array([63.0, 125.0, 250.0, 500.0, 1000.0, 2000.0, 4000.0, 8000.0]) 181 + barrier = Barrier(source_to_edge=101.0, edge_to_receiver=101.0) 182 + att = outdoor_propagation_attenuation( 183 + 200.0, 1.5, 1.5, bands, ground_source=1.0, ground_middle=1.0, 184 + ground_receiver=1.0, barrier=barrier, temperature=15.0, 185 + relative_humidity=70.0, 186 + ) 187 + 188 + x = np.arange(len(bands)) 189 + fig, ax = plt.subplots() 190 + bottom = np.zeros(len(bands)) 191 + for term, label in [(att.a_div, "Adiv — divergence"), 192 + (att.a_atm, "Aatm — atmospheric"), 193 + (att.a_gr, "Agr — ground"), 194 + (att.a_bar, "Abar — barrier")]: 195 + ax.bar(x, term, bottom=bottom, label=label) 196 + bottom = bottom + term 197 + ax.plot(x, att.a_total, "D-", color="black", label="A — total") 198 + ax.set_xticks(x) 199 + ax.set_xticklabels([f"{b:g}" for b in bands]) 200 + ax.set_xlabel("Octave-band centre frequency [Hz]") 201 + ax.set_ylabel("Attenuation A [dB]") 202 + ax.legend() 203 + plt.show() 204 + ``` 205 + 206 + </details> 207 + 151 208 ```python 152 209 import numpy as np 153 210 from phonometry import ( ··· 251 308 [Room Acoustics guide](/phonometry/guides/room-acoustics/) for how $\alpha$ feeds 252 309 ISO 354, and the [Occupational Noise Exposure guide](/phonometry/guides/occupational-exposure/) for the ISO 9612 occupational 253 310 exposure that consumes A-weighted levels. 311 + 312 + --- 313 + 314 + **Standards.** ISO 9613-1:1993, *Acoustics — Attenuation of sound during 315 + propagation outdoors — Part 1: Calculation of the absorption of sound by the 316 + atmosphere* — the pure-tone attenuation coefficient $\alpha$ (Eq. (5)) with the 317 + oxygen and nitrogen relaxation frequencies (Eq. (3)/(4)), the Annex B humidity 318 + conversion and the exact Table 1 midbands (Eq. (6), Note 5). ISO 9613-2:1996, 319 + *Acoustics — Attenuation of sound during propagation outdoors — Part 2: General 320 + method of calculation* — the downwind receiver level (Eq. (3)/(4)) assembled 321 + from geometrical divergence (Eq. (7)), atmospheric absorption (Eq. (8)), the 322 + ground effect (Eq. (9), Table 3) with its A-weighted alternative 323 + (Eq. (10)/(11)), barrier screening (Eqs. (12)–(17)) and the meteorological 324 + correction (Eq. (21)/(22)). ISO 354:2003, *Acoustics — Measurement of sound 325 + absorption in a reverberation room* — only the clause 8.1.2.1 conversion 326 + $m = \alpha/(10 \lg e)$ behind `air_attenuation_m`; the reverberation-room 327 + method itself is covered in the 328 + [Room Acoustics guide](/phonometry/guides/room-acoustics/).
+23 -3
site/src/content/docs/guides/psychoacoustics.md
··· 43 43 44 44 # From a raw recording: calibration_factor scales digital units to Pa 45 45 res = loudness_zwicker(x, fs, field="free", calibration_factor=sens) 46 - print(f"N = {res.loudness:.1f} sone ({res.loudness_level:.0f} phon)") 46 + print(f"N = {res.loudness:.1f} sone ({res.loudness_level:.0f} phon)") # 13.1 sone (77 phon) 47 47 48 48 # Time-varying signals: percentile loudness N5 is the reporting standard 49 49 res = loudness_zwicker(x, fs) # stationary=False (default) 50 - print(res.n5, res.n10, res.loudness) # N5, N10, Nmax 50 + print(f"{res.n5:.1f} {res.n10:.1f} {res.loudness:.1f}") # 13.1 13.1 13.1 — N5, N10, Nmax 51 51 52 52 # From 28 one-third-octave band levels (25 Hz .. 12.5 kHz) 53 53 res = loudness_zwicker_from_spectrum(levels_28, field="diffuse") ··· 125 125 ```python 126 126 from phonometry import sharpness_din 127 127 128 + # Uses `x`, `fs` and `sens` from the snippet above. 128 129 s = sharpness_din(x, fs, calibration_factor=sens) # acum 129 130 s_aures = sharpness_din(x, fs, method="aures") # Annex B variant 130 131 ``` ··· 269 270 270 271 res = loudness_moore_glasberg_time(x, fs, field="free") 271 272 print(f"N_max = {res.n_max:.3f} sone ({res.loudness_level_max:.0f} phon)") # 1.000 sone (40 phon) 272 - print(f"long-term loudness exceeded 5% of the time: {res.percentiles[5.0]:.3f} sone") 273 + print(f"long-term loudness exceeded 5% of the time: {res.percentiles[5.0]:.3f} sone") # 0.999 sone 273 274 274 275 res.plot() # short-term S'(t) and long-term S''(t) loudness vs time 275 276 ``` ··· 494 495 ECMA-418-1 TNR/PR prominence verdicts, 495 496 [Speech Transmission Index](/phonometry/guides/speech-transmission/) for 496 497 STI/STIPA, and [Theory](/phonometry/reference/theory/) for the underlying math. 498 + 499 + --- 500 + 501 + **Standards.** ISO 532-1:2017, *Acoustics — Methods for calculating 502 + loudness — Part 1: Zwicker method* — stationary and time-varying loudness in 503 + sones from the normative Annex A.4 reference program, with the N5/N10 504 + percentile loudness, validated against the Annex B set. ISO 532-2:2017, 505 + *... Part 2: Moore-Glasberg method* — stationary loudness from roex excitation 506 + patterns on the ERB-number scale, with explicit binaural summation. 507 + ISO 532-3:2023, *... Part 3: Moore-Glasberg-Schlittenlacher method* — 508 + time-varying short-term and long-term loudness and the peak N_max. 509 + DIN 45692:2009, *Messtechnische Simulation der Hörempfindung Schärfe* — 510 + sharpness in acum (clause 6 weighting, Annex B von Bismarck and Aures 511 + variants, Table A.2 targets). ISO 226:2023, *Acoustics — Normal 512 + equal-loudness-level contours* — the contours (Formula 1), the loudness level 513 + of pure tones (Formula 2) and the hearing threshold. ECMA-418-2:2025, 514 + *Psychoacoustic metrics for ITT equipment — Part 2 (methods for describing 515 + human perception based on the Sottek Hearing Model)* — the Sottek Hearing 516 + Model loudness (sone_HMS), tonality (tu_HMS) and roughness (asper).
+10
site/src/content/docs/guides/room-acoustics.md
··· 476 476 source/receiving-room levels. 477 477 - [Theory](/phonometry/reference/theory/) — Schroeder integration, regression windows and the 478 478 reference-curve derivation. 479 + 480 + --- 481 + 482 + **Standards.** ISO 18233:2006 (application of new measurement methods — the 483 + swept-sine and MLS acquisition of impulse responses); ISO 3382-1:2009 and 484 + ISO 3382-2:2008 (reverberation time and room parameters from the Schroeder 485 + decay); ISO 3382-3:2022 (open-plan office speech metrics); ISO 354:2003 486 + (sound absorption in a reverberation room). Validated against closed-form 487 + decays and the standards' own parameter definitions in the 488 + [conformance report](https://github.com/jmrplens/phonometry/blob/main/docs/CONFORMANCE.md).
+123
site/src/content/docs/guides/sound-power.md
··· 106 106 res.plot() # sound power level bars per band, LWA in the title (needs matplotlib) 107 107 ``` 108 108 109 + <img class="light-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/sound_power_pressure_result.png" alt="The enveloping-surface sound power level spectrum of the ISO 3744 hemisphere example, one bar per octave band from 63 Hz to 8 kHz peaking near 500 Hz, with the A-weighted total of 92.4 dB(A) in the title" style="width:88%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/sound_power_pressure_result_dark.png" alt="The enveloping-surface sound power level spectrum of the ISO 3744 hemisphere example, one bar per octave band from 63 Hz to 8 kHz peaking near 500 Hz, with the A-weighted total of 92.4 dB(A) in the title" style="width:88%"> 110 + 111 + *One bar per band: the energy-averaged surface pressure minus the background 112 + (`K1`) and environmental (`K2`) corrections plus the surface term 113 + `10 lg(S/S0)` gives `LW(f)`, and the A-weighted energy sum across bands gives 114 + the single-number `LWA` in the title.* 115 + 116 + <details> 117 + <summary>Show the code for this figure</summary> 118 + 119 + ```python 120 + import matplotlib.pyplot as plt 121 + import numpy as np 122 + 123 + # res is the SoundPowerResult computed above. One line: 124 + res.plot() 125 + plt.show() 126 + 127 + # By hand: a bar spectrum of LW with the A-weighted total in the title. 128 + positions = np.arange(freqs.size) 129 + fig, ax = plt.subplots() 130 + ax.bar(positions, res.sound_power_level, width=0.7, color="#1f77b4") 131 + ax.set_xticks(positions) 132 + ax.set_xticklabels([f"{f:g}" for f in freqs], rotation=45, ha="right") 133 + ax.set_xlabel("Frequency [Hz]") 134 + ax.set_ylabel("Sound power level LW [dB]") 135 + ax.set_title( 136 + f"Enveloping-surface sound power (ISO 3744) " 137 + f"LWA = {res.sound_power_level_a:.1f} dB(A)") 138 + plt.show() 139 + ``` 140 + 141 + </details> 142 + 109 143 The A-weighted total `LWA` is combined from the band powers with the ISO 3744 110 144 Annex E A-weighting corrections, so it needs `frequencies`. Passing the room 111 145 data (`reverberation_time` + `volume`, or `absorption_area`, or ··· 201 235 202 236 rev.plot() # reverberation-room LW spectrum, LWA in the title (needs matplotlib) 203 237 ``` 238 + 239 + <img class="light-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/sound_power_reverberation_result.png" alt="The reverberation-room sound power level spectrum of the ISO 3741 example, one bar per one-third-octave band from 100 Hz to 10 kHz falling gently with frequency, with the A-weighted total of 92.1 dB(A) in the title" style="width:88%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/sound_power_reverberation_result_dark.png" alt="The reverberation-room sound power level spectrum of the ISO 3741 example, one bar per one-third-octave band from 100 Hz to 10 kHz falling gently with frequency, with the A-weighted total of 92.1 dB(A) in the title" style="width:88%"> 240 + 241 + *The mean room level carried through the absorption-area, Waterhouse and 242 + meteorological terms of Eq. 20 gives the one-third-octave `LW(f)`, and the 243 + A-weighted energy sum across the 21 bands gives the `LWA` in the title.* 244 + 245 + <details> 246 + <summary>Show the code for this figure</summary> 247 + 248 + ```python 249 + import matplotlib.pyplot as plt 250 + import numpy as np 251 + 252 + # rev is the ReverberationSoundPowerResult computed above. One line: 253 + rev.plot() 254 + plt.show() 255 + 256 + # By hand: a bar spectrum of LW with the A-weighted total in the title. 257 + positions = np.arange(freqs.size) 258 + fig, ax = plt.subplots() 259 + ax.bar(positions, rev.sound_power_level, width=0.7, color="#1f77b4") 260 + ax.set_xticks(positions) 261 + ax.set_xticklabels([f"{f:g}" for f in freqs], rotation=45, ha="right") 262 + ax.set_xlabel("Frequency [Hz]") 263 + ax.set_ylabel("Sound power level LW [dB]") 264 + ax.set_title( 265 + f"Reverberation-room sound power (ISO 3741) " 266 + f"LWA = {rev.sound_power_level_a:.1f} dB(A)") 267 + plt.show() 268 + ``` 269 + 270 + </details> 204 271 205 272 `levels` may be a 1D mean spectrum or a 2D `(NM, NB)` array averaged over 206 273 positions. When the room volume, its reverberation time or the microphone ··· 304 371 305 372 res.plot() # LW spectrum; non-positive (undeterminable) bands hatched (needs matplotlib) 306 373 ``` 374 + 375 + <img class="light-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/sound_power_intensity_result.png" alt="The intensity-scanning sound power level spectrum of the ISO 9614-2 example, one bar per octave band from 125 Hz to 4 kHz all near 85 dB, with the A-weighted total of 90.9 dB(A) in the title" style="width:88%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/sound_power_intensity_result_dark.png" alt="The intensity-scanning sound power level spectrum of the ISO 9614-2 example, one bar per octave band from 125 Hz to 4 kHz all near 85 dB, with the A-weighted total of 90.9 dB(A) in the title" style="width:88%"> 376 + 377 + *The partial powers `<In,i>·Si` of the six segments sum to each band's `LW`; 378 + every band here nets positive power and passes the field-indicator criteria at 379 + engineering grade, so all six bars stand, and the A-weighted total of 380 + 90.9 dB(A) heads the title.* 381 + 382 + <details> 383 + <summary>Show the code for this figure</summary> 384 + 385 + ```python 386 + import matplotlib.pyplot as plt 387 + import numpy as np 388 + 389 + # res is the SoundPowerIntensityResult computed above. One line: 390 + res.plot() 391 + plt.show() 392 + 393 + # By hand: a bar spectrum of LW with the A-weighted total in the title. 394 + positions = np.arange(freqs.size) 395 + fig, ax = plt.subplots() 396 + ax.bar(positions, res.sound_power_level, width=0.7, color="#1f77b4") 397 + ax.set_xticks(positions) 398 + ax.set_xticklabels([f"{f:g}" for f in freqs], rotation=45, ha="right") 399 + ax.set_xlabel("Frequency [Hz]") 400 + ax.set_ylabel("Sound power level LW [dB]") 401 + ax.set_title( 402 + f"Intensity-scanning sound power (ISO 9614-2) " 403 + f"LWA = {res.sound_power_level_a:.1f} dB(A)") 404 + plt.show() 405 + ``` 406 + 407 + </details> 307 408 308 409 Supplying `normal_intensity_2` (the second sweep) averages the two for the 309 410 partial powers and evaluates criterion 3; `pressure_levels` enables `FpI`; ··· 556 657 that consume a source `LW`. 557 658 - [Levels](/phonometry/guides/levels/) — energy averaging and the A-weighting behind `LWA`. 558 659 - [Theory](/phonometry/reference/theory/) — the Waterhouse, K1/K2 and C1/C2 derivations. 660 + 661 + --- 662 + 663 + **Standards.** ISO 3744:2010, *Acoustics — Determination of sound power levels 664 + and sound energy levels of noise sources using sound pressure — Engineering 665 + methods for an essentially free field over a reflecting plane* — the 666 + enveloping-surface method: hemisphere and box surface areas, the `K1`/`K2` 667 + corrections, the Annex B microphone positions and the Annex E A-weighting. 668 + ISO 3746:2010, *… Survey method using an enveloping measurement surface over a 669 + reflecting plane* — the survey grade sharing the same formulae with coarser 670 + criteria. ISO 3741:2010, *… Precision methods for reverberation test rooms* — 671 + the direct (Eq. 20) and comparison methods with the Waterhouse and 672 + meteorological corrections and the Table 1 qualification criteria. 673 + ISO 3745:2012, *… Precision methods for anechoic rooms and hemi-anechoic 674 + rooms* — the Clause 8 power level, the per-position background correction 675 + (Eq. 11), the meteorological corrections and the standardized microphone 676 + arrays. ISO 9614-2:1996, *Acoustics — Determination of sound power levels of 677 + noise sources using sound intensity — Part 2: Measurement by scanning* — the 678 + partial powers, the `FpI` and `F+/-` field indicators and the grade criteria. 679 + ISO 9614-3:2002, *… Part 3: Precision method for measurement by scanning* — 680 + the grade-1 scanning method, its field indicators and the clause 9.2 681 + not-applicable flagging.
+41
site/src/content/docs/guides/speech-intelligibility.md
··· 158 158 159 159 <img class="light-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/sii_vocal_efforts.png" alt="Left: the four ANSI S3.5-1997 standard speech spectra (normal, raised, loud, shout) over 160 Hz to 8000 Hz, each higher effort lifting the whole spectrum. Right: the resulting SII in a fixed broadband noise, rising from 0.12 (normal) to 0.79 (shout)" style="width:96%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/sii_vocal_efforts_dark.png" alt="Left: the four ANSI S3.5-1997 standard speech spectra (normal, raised, loud, shout) over 160 Hz to 8000 Hz, each higher effort lifting the whole spectrum. Right: the resulting SII in a fixed broadband noise, rising from 0.12 (normal) to 0.79 (shout)" style="width:96%"> 160 160 161 + <details> 162 + <summary>Show the code for this figure</summary> 163 + 164 + ```python 165 + import numpy as np 166 + import matplotlib.pyplot as plt 167 + import phonometry as ph 168 + 169 + # The four ANSI S3.5-1997 Table 3 spectra and the fixed broadband noise above. 170 + noise = np.array([48.0, 47.0, 46.0, 44.0, 42.0, 40.0, 38.0, 36.0, 34.0, 171 + 32.0, 30.0, 28.0, 26.0, 24.0, 22.0, 20.0, 18.0, 16.0]) 172 + efforts = ph.sii.VOCAL_EFFORTS # ("normal", "raised", "loud", "shout") 173 + freqs = ph.sii.BAND_CENTERS # the 18 one-third-octave band centres 174 + 175 + fig, (ax_s, ax_i) = plt.subplots(1, 2, figsize=(12, 5)) 176 + 177 + # Left: each higher vocal effort lifts the whole speech spectrum. 178 + for effort in efforts: 179 + ax_s.plot(freqs, ph.standard_speech_spectrum(effort), "o-", 180 + label=effort.capitalize()) 181 + ax_s.set_xscale("log") 182 + ax_s.set_xticks(list(freqs)) 183 + ax_s.set_xticklabels([f"{f:g}" for f in freqs], rotation=45, ha="right") 184 + ax_s.xaxis.set_minor_formatter(plt.NullFormatter()) 185 + ax_s.set_xlabel("One-third-octave band [Hz]") 186 + ax_s.set_ylabel("Speech spectrum level [dB SPL]") 187 + ax_s.legend() 188 + 189 + # Right: the SII each spectrum reaches in the fixed noise. 190 + sii = [ph.speech_intelligibility_index(e, noise).sii for e in efforts] 191 + pos = np.arange(len(efforts)) 192 + ax_i.bar(pos, sii) 193 + ax_i.set_xticks(pos) 194 + ax_i.set_xticklabels([e.capitalize() for e in efforts]) 195 + ax_i.set_ylim(0.0, 1.0) 196 + ax_i.set_ylabel("Speech Intelligibility Index") 197 + plt.show() 198 + ``` 199 + 200 + </details> 201 + 161 202 The vocal-effort names work anywhere a speech spectrum is expected, including as 162 203 the first argument to `speech_intelligibility_index`. 163 204
+95 -22
site/src/content/docs/guides/time-weighting.md
··· 6 6 Accurate SPL measurement requires capturing energy over specific time windows. 7 7 phonometry implements exact time constants per **IEC 61672-1:2013**. 8 8 9 + ## 1. The exponential detector 10 + 11 + A sound level meter's needle cannot follow the pressure waveform — it shows a 12 + running *mean square* with an exponential memory. Formally (IEC 61672-1, 3.8): 13 + 14 + $$ 15 + \tau\ \frac{dy}{dt} + y = x^2(t) 16 + \quad\Longleftrightarrow\quad 17 + y(t) = \frac{1}{\tau} \int_{-\infty}^{t} x^2(\xi)\ e^{-(t-\xi)/\tau}\ d\xi 18 + $$ 19 + 20 + a first-order low-pass on the squared signal. The time constant τ sets the 21 + trade-off: **Fast** (125 ms) follows speech-like fluctuations, **Slow** (1 s) 22 + steadies the readout for quasi-stationary noise. After a step onset the 23 + envelope reaches 63 % of its final value in one τ and ~99.8 % after 8τ — 24 + that is why level analyses discard the first instants of a recording. 25 + 26 + ## 2. The three time weightings 27 + 9 28 <img class="light-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/time_weighting_analysis.png" alt="Fast, Slow and Impulse time weighting responses to a noise burst" style="width:80%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/time_weighting_analysis_dark.png" alt="Fast, Slow and Impulse time weighting responses to a noise burst" style="width:80%"> 29 + 30 + <details> 31 + <summary>Show the code for this figure</summary> 32 + 33 + ```python 34 + import numpy as np 35 + import matplotlib.pyplot as plt 36 + from phonometry import time_weighting 37 + 38 + fs = 48000 39 + t = np.arange(int(fs * 4)) / fs 40 + burst = np.zeros_like(t) # 0.5 s noise burst (Pa) starting at t = 1 s 41 + rng = np.random.default_rng(42) 42 + burst[fs:int(1.5 * fs)] = 0.2 * rng.standard_normal(int(0.5 * fs)) 43 + 44 + p0 = 2e-5 45 + plt.figure() 46 + for mode in ('fast', 'slow', 'impulse'): 47 + envelope = time_weighting(burst, fs, mode=mode) 48 + plt.plot(t, 10 * np.log10(np.maximum(envelope, 1e-12) / p0**2), label=mode) 49 + plt.xlabel('Time [s]') 50 + plt.ylabel('Level [dB SPL]') 51 + plt.legend() 52 + plt.show() 53 + ``` 54 + 55 + </details> 10 56 11 57 * **Fast (`fast`):** τ = 125 ms. Standard for noise fluctuations. 12 58 * **Slow (`slow`):** τ = 1000 ms. Standard for steady noise. ··· 25 71 energy_envelope = time_weighting(recording, fs, mode='fast') 26 72 # dB SPL relative to 20 μPa 27 73 spl_t = 10 * np.log10(energy_envelope / (2e-5)**2) 74 + 75 + print(f"Steady-state Fast level: {spl_t[-1]:.1f} dB SPL") 76 + # Steady-state Fast level: 77.0 dB SPL 28 77 ``` 29 78 30 79 The asymmetric Impulse ballistics use two constants — a fast attack and a slow ··· 35 84 \alpha = \begin{cases}1 - e^{-1/(f_s \cdot 0.035)} & x^2[n] > y[n-1]\\[2pt] 1 - e^{-1/(f_s \cdot 1.5)} & \text{otherwise}\end{cases} 36 85 $$ 37 86 38 - ## The exponential detector 39 - 40 - A sound level meter's needle cannot follow the pressure waveform — it shows a 41 - running *mean square* with an exponential memory. Formally (IEC 61672-1, 3.8): 42 - 43 - $$ 44 - \tau\ \frac{dy}{dt} + y = x^2(t) 45 - \quad\Longleftrightarrow\quad 46 - y(t) = \frac{1}{\tau} \int_{-\infty}^{t} x^2(\xi)\ e^{-(t-\xi)/\tau}\ d\xi 47 - $$ 48 - 49 - a first-order low-pass on the squared signal. The time constant τ sets the 50 - trade-off: **Fast** (125 ms) follows speech-like fluctuations, **Slow** (1 s) 51 - steadies the readout for quasi-stationary noise. After a step onset the 52 - envelope reaches 63 % of its final value in one τ and ~99.8 % after 8τ — 53 - that is why level analyses discard the first instants of a recording. 54 - 55 - ### `time_weighting()` / `TimeWeighting` parameters 87 + ## 3. `time_weighting()` / `TimeWeighting` parameters 56 88 57 89 | Parameter | Type | Units | Range / default | Notes | 58 90 | :--- | :--- | :--- | :--- | :--- | ··· 64 96 The output has the units of $x^2$: take `10*log10(y / p0**2)` for SPL or use 65 97 the level functions, which do it for you. 66 98 67 - ## Verified ballistics (IEC 61672-1 Table 4) 99 + ## 4. Verified ballistics (IEC 61672-1 Table 4) 68 100 69 101 The Fast envelope's response to 4 kHz tonebursts lands exactly on the 70 102 standard's reference values — enforced in CI for burst durations from 1 s down ··· 72 104 73 105 <img class="light-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/tone_burst_iec.png" alt="Fast envelope responses to 200, 50 and 10 ms tone bursts peaking exactly at the IEC 61672-1 Table 4 reference values" style="width:80%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/tone_burst_iec_dark.png" alt="Fast envelope responses to 200, 50 and 10 ms tone bursts peaking exactly at the IEC 61672-1 Table 4 reference values" style="width:80%"> 74 106 75 - ## Initial state 107 + <details> 108 + <summary>Show the code for this figure</summary> 109 + 110 + ```python 111 + import numpy as np 112 + import matplotlib.pyplot as plt 113 + from phonometry import time_weighting 114 + 115 + fs = 48000 116 + t = np.arange(int(fs * 2)) / fs 117 + tone = np.sin(2 * np.pi * 4000 * t) 118 + 119 + # Steady-state Fast reference of the continuous tone 120 + reference = time_weighting(tone, fs, mode='fast')[int(1.5 * fs):].mean() 121 + 122 + # 200 ms burst of the same tone (IEC 61672-1 Table 4 target: -1.0 dB) 123 + burst = np.zeros_like(t) 124 + burst[int(0.5 * fs):int(0.7 * fs)] = tone[int(0.5 * fs):int(0.7 * fs)] 125 + envelope = time_weighting(burst, fs, mode='fast') 126 + env_db = 10 * np.log10(np.maximum(envelope / reference, 1e-6)) 127 + 128 + plt.figure() 129 + plt.plot(t, env_db, label='Fast envelope') 130 + plt.axhline(-1.0, linestyle='--', label='IEC target −1.0 dB') 131 + plt.xlabel('Time [s]') 132 + plt.ylabel('Level re steady state [dB]') 133 + plt.legend() 134 + plt.show() 135 + ``` 136 + 137 + </details> 138 + 139 + ## 5. Initial state 76 140 77 141 By default, the exponential integrator starts from rest (`y[-1] = 0`). Passing 78 142 `initial_state=None` leaves this default unspecified, while `initial_state='zero'` ··· 80 144 steady signal is already present, you can start from the first sample energy instead: 81 145 82 146 ```python 147 + # Uses `recording` and `fs` from the snippet above. 83 148 energy_envelope = time_weighting(recording, fs, mode='fast', initial_state='first') 84 149 ``` 85 150 86 - ## Block processing 151 + ## 6. Block processing 87 152 88 153 For block processing, pass the last output value from the previous block as the 89 154 next block's `initial_state` instead of resetting each block: ··· 119 184 (verified for all three modes, mono and multichannel). Call `tw.reset()` to 120 185 start from rest again. 121 186 122 - ## Performance note 187 + ## 7. Performance note 123 188 124 189 The `impulse` mode uses an asymmetric kernel that is JIT-compiled when 125 190 [numba](https://numba.pydata.org/) is installed (`pip install phonometry[perf]`). ··· 128 193 See [Integrated & Statistical Levels](/phonometry/guides/levels/) for Leq/LN metrics built on 129 194 these envelopes, and [Why phonometry](/phonometry/reference/why-phonometry/) for the IEC 130 195 61672-1 tone-burst verification. 196 + 197 + --- 198 + 199 + **Standards.** IEC 61672-1:2013, *Electroacoustics — Sound level meters — 200 + Part 1: Specifications* — the exponential time-weighting detector (clause 3.8) 201 + with the F and S time constants (clause 5.7), and the 4 kHz toneburst reference 202 + responses of Table 4 (class 1 acceptance limits) used to verify the ballistics 203 + in CI.
+155 -41
site/src/content/docs/guides/weighting.md
··· 9 9 10 10 <img class="light-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/weighting_responses.png" alt="A, C and Z frequency weighting curves with a zoom showing the positive region of the A curve (+1.27 dB at 2.5 kHz)" style="width:80%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/weighting_responses_dark.png" alt="A, C and Z frequency weighting curves with a zoom showing the positive region of the A curve (+1.27 dB at 2.5 kHz)" style="width:80%"> 11 11 12 + <details> 13 + <summary>Show the code for this figure</summary> 14 + 15 + ```python 16 + import matplotlib.pyplot as plt 17 + import numpy as np 18 + from phonometry import weighting_filter 19 + 20 + # Measure each curve's response: weight a centered unit impulse and take 21 + # its spectrum (1 s buffer -> 1 Hz frequency resolution). 22 + fs = 48000 23 + impulse = np.zeros(fs) 24 + impulse[fs // 2] = 1.0 25 + freqs = np.fft.rfftfreq(fs, 1 / fs) 26 + 27 + fig, ax = plt.subplots(figsize=(9, 5)) 28 + for curve in ("A", "C", "Z"): 29 + spectrum = np.fft.rfft(weighting_filter(impulse, fs, curve=curve)) 30 + ax.semilogx(freqs[1:], 20 * np.log10(np.abs(spectrum[1:])), label=curve) 31 + ax.set(xlim=(10, 20000), ylim=(-80, 10), 32 + xlabel="Frequency [Hz]", ylabel="Response [dB]") 33 + ax.grid(True, which="both", alpha=0.3) 34 + ax.legend() 35 + plt.show() 36 + ``` 37 + 38 + </details> 39 + 12 40 * **A-Weighting (`A`):** Standard for environmental noise (IEC 61672-1). 13 41 * **C-Weighting (`C`):** Used for peak sound pressure and high-level noise. 14 42 * **Z-Weighting (`Z`):** Zero weighting, completely flat response. 15 43 * **G-Weighting (`G`):** Infrasound weighting per ISO 7196 (see below). 16 44 17 - ```python 18 - import numpy as np 19 - from phonometry import weighting_filter 20 - 21 - # recording: a calibrated microphone capture (Pa) — recorded through your measurement chain. Synthesized here so the guide runs standalone. 22 - fs = 48000 23 - recording = 0.2 * np.sin(2 * np.pi * 1000 * np.arange(fs) / fs) 24 - 25 - # Apply A-weighting to the raw recording 26 - weighted_signal = weighting_filter(recording, fs, curve='A') 27 - 28 - # Apply C-weighting for peak analysis 29 - c_weighted_signal = weighting_filter(recording, fs, curve='C') 30 - ``` 31 - 32 - ## Infrasound: G-weighting (ISO 7196) 33 - 34 - The **G frequency weighting** (ISO 7196:1995) rates infrasound the way A-weighting 35 - rates audible noise. It is defined by a pole-zero configuration with 0 dB gain at 36 - 10 Hz, rises at 12 dB/octave from 1 Hz to 20 Hz (matching the steep growth of 37 - perception in that band) and falls off at 24 dB/octave outside it. Use it for 38 - sources with significant energy below 20 Hz (wind turbines, HVAC, blasting): 39 - 40 - ```python 41 - from phonometry import weighting_filter 42 - 43 - g_weighted = weighting_filter(recording, fs, curve='G') 44 - ``` 45 - 46 - <img class="light-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/g_weighting_response.png" alt="G-weighting frequency response from 0.1 Hz to 1 kHz with the ISO 7196 Table 2 nominal values overlaid" style="width:80%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/g_weighting_response_dark.png" alt="G-weighting frequency response from 0.1 Hz to 1 kHz with the ISO 7196 Table 2 nominal values overlaid" style="width:80%"> 47 - 48 - The implementation follows the ISO 7196 Table 1 pole/zero values exactly and is 49 - verified in CI against every Table 2 nominal response value (0.25 Hz to 315 Hz). 50 - `WeightingFilter(fs, "G")` supports the same multichannel and stateful block 51 - processing as A/C. Levels measured with the G curve are reported as 52 - L<sub>pG</sub> (or L<sub>Geq</sub> for the equivalent level over time). 53 - 54 - ## Where the curves come from 45 + ## 1. Where the curves come from 55 46 56 47 The A and C curves are inverted equal-loudness contours, frozen into filters: 57 48 **A** approximates the inverse of the historic 40-phon contour (quiet levels, ··· 70 61 absence of weighting. The full pole/zero derivation is in the 71 62 [Theory](/phonometry/reference/theory/) page. 72 63 73 - ### `weighting_filter()` / `WeightingFilter` parameters 64 + ## 2. Basic usage 65 + 66 + ```python 67 + import numpy as np 68 + from phonometry import weighting_filter 69 + 70 + # recording: a calibrated microphone capture (Pa) — recorded through your measurement chain. Synthesized here so the guide runs standalone. 71 + fs = 48000 72 + recording = 0.2 * np.sin(2 * np.pi * 1000 * np.arange(fs) / fs) 73 + 74 + # Apply A-weighting to the raw recording 75 + weighted_signal = weighting_filter(recording, fs, curve='A') 76 + 77 + # Apply C-weighting for peak analysis 78 + c_weighted_signal = weighting_filter(recording, fs, curve='C') 79 + ``` 80 + 81 + ## 3. Infrasound: G-weighting (ISO 7196) 82 + 83 + The **G frequency weighting** (ISO 7196:1995) rates infrasound the way A-weighting 84 + rates audible noise. It is defined by a pole-zero configuration with 0 dB gain at 85 + 10 Hz, rises at 12 dB/octave from 1 Hz to 20 Hz (matching the steep growth of 86 + perception in that band) and falls off at 24 dB/octave outside it. Use it for 87 + sources with significant energy below 20 Hz (wind turbines, HVAC, blasting): 88 + 89 + ```python 90 + from phonometry import weighting_filter 91 + 92 + # Uses `recording` and `fs` from the snippet above. 93 + g_weighted = weighting_filter(recording, fs, curve='G') 94 + ``` 95 + 96 + <img class="light-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/g_weighting_response.png" alt="G-weighting frequency response from 0.1 Hz to 1 kHz with the ISO 7196 Table 2 nominal values overlaid" style="width:80%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/g_weighting_response_dark.png" alt="G-weighting frequency response from 0.1 Hz to 1 kHz with the ISO 7196 Table 2 nominal values overlaid" style="width:80%"> 97 + 98 + <details> 99 + <summary>Show the code for this figure</summary> 100 + 101 + ```python 102 + import matplotlib.pyplot as plt 103 + import numpy as np 104 + from phonometry import weighting_filter 105 + 106 + # Measure the G response: weight a centered unit impulse and take its 107 + # spectrum. A long buffer gives the resolution the infrasound range 108 + # needs (20 s -> 0.05 Hz). 109 + fs = 4000 110 + impulse = np.zeros(20 * fs) 111 + impulse[impulse.size // 2] = 1.0 112 + freqs = np.fft.rfftfreq(impulse.size, 1 / fs) 113 + spectrum = np.fft.rfft(weighting_filter(impulse, fs, curve="G")) 114 + 115 + fig, ax = plt.subplots(figsize=(9, 5)) 116 + ax.semilogx(freqs[1:], 20 * np.log10(np.abs(spectrum[1:]))) 117 + ax.plot(10, 0, "o", color="tab:red", label="0 dB at 10 Hz") 118 + ax.set(xlim=(0.1, 1000), ylim=(-90, 15), 119 + xlabel="Frequency [Hz]", ylabel="G-weighting response [dB]") 120 + ax.grid(True, which="both", alpha=0.3) 121 + ax.legend() 122 + plt.show() 123 + ``` 124 + 125 + </details> 126 + 127 + The implementation follows the ISO 7196 Table 1 pole/zero values exactly and is 128 + verified in CI against every Table 2 nominal response value (0.25 Hz to 315 Hz). 129 + `WeightingFilter(fs, "G")` supports the same multichannel and stateful block 130 + processing as A/C. Levels measured with the G curve are reported as 131 + L<sub>pG</sub> (or L<sub>Geq</sub> for the equivalent level over time). 132 + 133 + ## 4. `weighting_filter()` / `WeightingFilter` parameters 74 134 75 135 | Parameter | Type | Units | Range / default | Notes | 76 136 | :--- | :--- | :--- | :--- | :--- | ··· 81 141 | `stateful` | bool (class only) | — | default `False` | Carries filter state across blocks (streaming) | 82 142 | `steady_ic` | bool (class only) | — | default `False` | Steady-state initial conditions (no onset transient) | 83 143 84 - ## Reusable filter object 144 + ## 5. Reusable filter object 85 145 86 146 If you weight many signals with the same parameters, design the filter once: 87 147 88 148 ```python 89 149 from phonometry import WeightingFilter 90 150 151 + # Uses `recording` and `fs` from the snippet above. 91 152 wf = WeightingFilter(fs, "A") 92 153 signals = [recording] # your batch of recordings 93 154 for recording in signals: 94 155 weighted = wf.filter(recording) 95 156 ``` 96 157 97 - ## High-frequency accuracy (`high_accuracy`) 158 + ## 6. High-frequency accuracy (`high_accuracy`) 98 159 99 160 A plain bilinear-transform design compresses the response near Nyquist: at 100 161 fs = 48 kHz the A-curve error at 12.5 kHz reaches −2.7 dB, outside the IEC ··· 110 171 *The plain bilinear design (red) crosses the class 1 tolerance near 12.5 kHz; 111 172 the oversampled design (blue) stays close to the analytic curve.* 112 173 174 + <details> 175 + <summary>Show the code for this figure</summary> 176 + 177 + ```python 178 + import matplotlib.pyplot as plt 179 + import numpy as np 180 + from phonometry import weighting_filter 181 + 182 + # Measured response of both designs at fs = 48 kHz: weight a centered 183 + # unit impulse and take its spectrum... 184 + fs = 48000 185 + impulse = np.zeros(fs) 186 + impulse[fs // 2] = 1.0 187 + freqs = np.fft.rfftfreq(fs, 1 / fs)[1:] 188 + 189 + # ...versus the analytic IEC 61672-1 A-curve built from the four corner 190 + # frequencies of section 1, normalized to 0 dB at 1 kHz. 191 + f1, f2, f3, f4 = 20.599, 107.653, 737.862, 12194.217 192 + gain = (f4**2 * freqs**4) / ((freqs**2 + f1**2) 193 + * np.sqrt((freqs**2 + f2**2) * (freqs**2 + f3**2)) 194 + * (freqs**2 + f4**2)) 195 + analytic = 20 * np.log10(gain / gain[np.argmin(np.abs(freqs - 1000))]) 196 + 197 + fig, ax = plt.subplots(figsize=(9, 5)) 198 + ax.semilogx(freqs, analytic, "k--", label="Analytic (IEC 61672-1)") 199 + for high_accuracy, label in ((False, "Plain bilinear"), 200 + (True, "Oversampled (default)")): 201 + weighted = weighting_filter(impulse, fs, curve="A", 202 + high_accuracy=high_accuracy) 203 + response = 20 * np.log10(np.abs(np.fft.rfft(weighted)))[1:] 204 + ax.semilogx(freqs, response, label=label) 205 + ax.set(xlim=(1000, 20000), ylim=(-12, 3), 206 + xlabel="Frequency [Hz]", ylabel="A-weighting response [dB]") 207 + ax.grid(True, which="both", alpha=0.3) 208 + ax.legend() 209 + plt.show() 210 + ``` 211 + 212 + </details> 213 + 113 214 - `high_accuracy=False` restores the legacy plain-bilinear behavior. 114 215 - **Stateful (block) processing** always uses the legacy design: the internal 115 216 FIR resampling is incompatible with block continuity. Passing 116 217 `high_accuracy=True` together with `stateful=True` raises a `ValueError`. 117 218 118 219 ```python 220 + from phonometry import WeightingFilter, weighting_filter 221 + 222 + # Uses `recording` and `fs` from the snippet above. 119 223 # Explicit legacy behavior 120 224 y = weighting_filter(recording, fs, curve="A", high_accuracy=False) 121 225 ··· 128 232 129 233 See [Block Processing](/phonometry/guides/block-processing/) for the streaming workflow and 130 234 [Theory](/phonometry/reference/theory/) for the analytic curve definitions. 235 + 236 + --- 237 + 238 + **Standards.** IEC 61672-1:2013, *Electroacoustics — Sound level meters — 239 + Part 1: Specifications* — the A, C and Z frequency-weighting curves (the 240 + Annex E analytic definition from four corner frequencies, normalized to 0 dB 241 + at 1 kHz) and the class 1 tolerances the `high_accuracy` design keeps up to 242 + 16 kHz. ISO 7196:1995, *Acoustics — Frequency-weighting characteristic for 243 + infrasound measurements* — the G-weighting pole/zero definition (Table 1), 244 + verified against every Table 2 nominal response value (0.25 Hz to 315 Hz).
+11 -13
site/src/content/docs/es/guides/block-processing.md
··· 3 3 description: "Flujos de streaming con estado de filtro conservado entre bloques." 4 4 --- 5 5 6 + Algunas mediciones nunca caben en memoria: una grabación ambiental de una hora, 7 + un monitor en vivo que debe informar niveles mientras el micrófono sigue 8 + capturando, o un registrador embebido que solo ve un búfer cada vez. En todos 9 + esos casos la señal tiene que procesarse bloque a bloque — y los filtros deben 10 + comportarse exactamente como si hubieran visto la señal completa de una vez. 11 + 6 12 Las clases `OctaveFilterBank`, `WeightingFilter` (para ponderación A, C o Z) y 7 13 `TimeWeighting` admiten procesado por bloques (streaming): el estado interno del 8 14 filtro se conserva entre llamadas, de modo que las salidas concatenadas por ··· 17 23 Crea un banco con estado usando `stateful=True`. El estado interno se inicializa 18 24 a cero por defecto, pero puede inicializarse al régimen permanente de la 19 25 respuesta al escalón (como `scipy.signal.sosfilt_zi`) con `steady_ic=True`. 20 - 21 - Notas al usar un `OctaveFilterBank` con estado: 22 - 23 - - El detrending debe desactivarse en procesado por bloques (`detrend=False`), ya 24 - que puede introducir discontinuidades entre bloques. 25 - - El remuestreo no está soportado en procesado por bloques: usa `resample=False`. 26 - - `zero_phase=True` es incompatible con el modo stateful (el filtrado 27 - bidireccional necesita la señal completa). 28 - - Un `WeightingFilter` con estado usa el diseño bilineal clásico 29 - (`high_accuracy=False`); consulta 30 - [Ponderación frecuencial](/phonometry/es/guides/weighting/). 26 + Las opciones que deben desactivarse en modo stateful se resumen en la 27 + [tabla de restricciones](#restricciones-del-modo-con-estado) al final de esta 28 + guía. 31 29 32 30 ## Ejemplo 33 31 ··· 70 68 ``` 71 69 72 70 O gestiona el estado tú mismo con la API funcional — consulta 73 - [Ponderación temporal](/phonometry/es/guides/time-weighting/#procesado-por-bloques). 71 + [Ponderación temporal](/phonometry/es/guides/time-weighting/#6-procesado-por-bloques). 74 72 75 73 ## Estado multicanal 76 74 ··· 105 103 | `detrend` | debe ser `False` | El detrending por bloque crea discontinuidades en las fronteras | 106 104 | `resample` | debe ser `False` | El remuestreador no conserva estado | 107 105 | `zero_phase` | no soportado | El filtrado bidireccional necesita la señal completa | 108 - | `high_accuracy` (ponderación) | por defecto se resuelve a `False`; pasar `True` explícito lanza `ValueError` | El remuestreo polifásico interno es incompatible con bloques | 106 + | `high_accuracy` (ponderación) | por defecto se resuelve a `False` — el diseño bilineal clásico, consulta [Ponderación frecuencial](/phonometry/es/guides/weighting/); pasar `True` explícito lanza `ValueError` | El remuestreo polifásico interno es incompatible con bloques | 109 107 | `steady_ic` | opcional | Arranca los filtros en el régimen permanente de la respuesta al escalón |
+9
site/src/content/docs/es/guides/calibration.md
··· 167 167 internamente a float64 antes de cualquier elevación al cuadrado, así que la 168 168 calibración y los niveles son idénticos tanto si pasas el array entero crudo 169 169 como una conversión a float. 170 + 171 + --- 172 + 173 + **Normas.** IEC 60942:2017, *Electroacoustics — Sound calibrators* — los 174 + supuestos de nivel y clase del calibrador en `sensitivity()` (el nivel 175 + principal de 94 dB y las tolerancias de clase de la Tabla 1) y la comprobación 176 + de estabilidad por fluctuación de nivel a corto plazo de la grabación de 177 + referencia (5.3.3, límites de clase 1 de la Tabla 2 según la frecuencia 178 + nominal).
+1
site/src/content/docs/es/guides/enclosed-space-absorption.md
··· 87 87 volume=60.0, air_condition="20C_50-70", 88 88 ) 89 89 print(result.reverberation_time.round(2)) 90 + # [2.13 1.03 0.62 0.48 0.43 0.42 0.4 ] 90 91 ``` 91 92 92 93 <img class="light-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/enclosed_space_absorption_es.png" alt="Dos paneles para una oficina de 60 metros cúbicos con techo desnudo frente a techo acústico: el área de absorción equivalente por banda de octava, mucho mayor con el techo acústico, y el tiempo de reverberación cayendo desde unos cinco segundos a baja frecuencia con el techo desnudo hasta menos de un segundo con el techo acústico" style="width:96%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/enclosed_space_absorption_es_dark.png" alt="Dos paneles para una oficina de 60 metros cúbicos con techo desnudo frente a techo acústico: el área de absorción equivalente por banda de octava, mucho mayor con el techo acústico, y el tiempo de reverberación cayendo desde unos cinco segundos a baja frecuencia con el techo desnudo hasta menos de un segundo con el techo acústico" style="width:96%">
+45 -27
site/src/content/docs/es/guides/filter-banks.md
··· 8 8 los bordes de banda de ANSI S1.11**, de modo que los niveles por banda son 9 9 comparables entre arquitecturas. 10 10 11 - ## Bandas de octava fraccionaria: las matemáticas 11 + ## 1. Bandas de octava fraccionaria: las matemáticas 12 12 13 13 IEC 61260-1:2014 construye cada banda a partir de la razón de octava en base 10 14 14 $G = 10^{3/10} \approx 1{,}99526$ (es decir, "una octava" *no* es exactamente 2). ··· 37 37 38 38 <img class="light-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/diagram_multirate_es.svg" alt="Diezmado multitasa: las bandas altas se filtran a la frecuencia de entrada y las bajas tras un paso-bajo antialiasing y diezmado, para que las secciones SOS se mantengan numéricamente sanas" style="width:92%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/diagram_multirate_es_dark.svg" alt="Diezmado multitasa: las bandas altas se filtran a la frecuencia de entrada y las bajas tras un paso-bajo antialiasing y diezmado, para que las secciones SOS se mantengan numéricamente sanas" style="width:92%"> 39 39 40 - ### Parámetros de `octave_filter()` / `OctaveFilterBank` 40 + ## 2. Comparación de filtros y zoom 41 + 42 + Usamos secciones de segundo orden (SOS) en todos los filtros para garantizar la 43 + estabilidad numérica. La siguiente gráfica compara las arquitecturas centrándose 44 + en el punto de cruce a −3 dB. 45 + 46 + <img class="light-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/filter_type_comparison_es.png" alt="Comparación de la respuesta en magnitud de las cinco arquitecturas para la banda de octava de 1 kHz, con zoom en el cruce a -3 dB" style="width:80%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/filter_type_comparison_es_dark.png" alt="Comparación de la respuesta en magnitud de las cinco arquitecturas para la banda de octava de 1 kHz, con zoom en el cruce a -3 dB" style="width:80%"> 47 + 48 + | Tipo | Nombre | Ejemplo de uso | Ideal para | 49 + | :--- | :--- | :--- | :--- | 50 + | `butter` | **Butterworth** | `octave_filter(x, fs, filter_type='butter')` | Medición acústica general. | 51 + | `cheby1` | **Chebyshev I** | `octave_filter(x, fs, filter_type='cheby1', ripple=0.1)` | Caída más abrupta a costa de rizado. | 52 + | `cheby2` | **Chebyshev II** | `octave_filter(x, fs, filter_type='cheby2')` | Banda de paso plana con ceros en la banda atenuada. | 53 + | `ellip` | **Elíptico** | `octave_filter(x, fs, filter_type='ellip', ripple=0.1)` | Máxima selectividad. | 54 + | `bessel` | **Bessel** | `octave_filter(x, fs, filter_type='bessel')` | Preservar la forma de los transitorios. | 55 + 56 + ## 3. Parámetros de `octave_filter()` / `OctaveFilterBank` 41 57 42 58 | Parámetro | Tipo | Unidades | Rango / por defecto | Notas | 43 59 | :--- | :--- | :--- | :--- | :--- | ··· 46 62 | `fraction` | int | — | por defecto `1`; habitual `3`; cualquier `b ≥ 1` | Bandas por octava = `b` | 47 63 | `order` | int | — | por defecto `6` | Orden SOS por banda | 48 64 | `limits` | lista `[lo, hi]` | Hz | por defecto `[12, 20000]` | Rango de análisis | 49 - | `filter_type` | str | — | `'butter'` (por defecto), `'cheby1'`, `'cheby2'`, `'ellip'`, `'bessel'` | Ver la comparación más abajo | 65 + | `filter_type` | str | — | `'butter'` (por defecto), `'cheby1'`, `'cheby2'`, `'ellip'`, `'bessel'` | Ver la comparación más arriba | 50 66 | `ripple` / `attenuation` | float | dB | `ripple` defecto `0.1`; `attenuation` defecto `72.0` | Rizado de banda de paso / atenuación en banda atenuada (cheby/ellip); `cheby2` necesita `attenuation ≥ 70` para clase 1, ya que scipy fija su suelo equirizado en exactamente este valor | 51 67 | `show` | bool | — | por defecto `False` | Dibuja la respuesta del banco (requiere matplotlib) | 52 68 | `sigbands` | bool | — | por defecto `False` | Devuelve también las señales temporales por banda | ··· 63 79 aceptación de la Tabla 1 de IEC 61260-1 e informa de la clase (`1`, `2` o `None` si queda fuera de ambas) con los 64 80 márgenes por banda. 65 81 66 - ## Comparación de filtros y zoom 67 - 68 - Usamos secciones de segundo orden (SOS) en todos los filtros para garantizar la 69 - estabilidad numérica. La siguiente gráfica compara las arquitecturas centrándose 70 - en el punto de cruce a −3 dB. 71 - 72 - <img class="light-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/filter_type_comparison_es.png" alt="Comparación de la respuesta en magnitud de las cinco arquitecturas para la banda de octava de 1 kHz, con zoom en el cruce a -3 dB" style="width:80%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/filter_type_comparison_es_dark.png" alt="Comparación de la respuesta en magnitud de las cinco arquitecturas para la banda de octava de 1 kHz, con zoom en el cruce a -3 dB" style="width:80%"> 73 - 74 - | Tipo | Nombre | Ejemplo de uso | Ideal para | 75 - | :--- | :--- | :--- | :--- | 76 - | `butter` | **Butterworth** | `octave_filter(x, fs, filter_type='butter')` | Medición acústica general. | 77 - | `cheby1` | **Chebyshev I** | `octave_filter(x, fs, filter_type='cheby1', ripple=0.1)` | Caída más abrupta a costa de rizado. | 78 - | `cheby2` | **Chebyshev II** | `octave_filter(x, fs, filter_type='cheby2')` | Banda de paso plana con ceros en la banda atenuada. | 79 - | `ellip` | **Elíptico** | `octave_filter(x, fs, filter_type='ellip', ripple=0.1)` | Máxima selectividad. | 80 - | `bessel` | **Bessel** | `octave_filter(x, fs, filter_type='bessel')` | Preservar la forma de los transitorios. | 81 - 82 - ## Galería de respuestas del banco 82 + ## 4. Galería de respuestas del banco 83 83 84 84 Vista espectral completa de los bancos para octava (1/1) y tercio de octava (1/3). 85 85 ··· 91 91 | **Elíptico** | <img class="light-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/filter_ellip_fraction_1_order_6_es.png" alt="Respuesta en frecuencia del banco elíptico de banda de octava" style="width:100%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/filter_ellip_fraction_1_order_6_es_dark.png" alt="Respuesta en frecuencia del banco elíptico de banda de octava" style="width:100%"> | <img class="light-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/filter_ellip_fraction_3_order_6_es.png" alt="Respuesta en frecuencia del banco elíptico de tercio de octava" style="width:100%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/filter_ellip_fraction_3_order_6_es_dark.png" alt="Respuesta en frecuencia del banco elíptico de tercio de octava" style="width:100%"> | 92 92 | **Bessel** | <img class="light-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/filter_bessel_fraction_1_order_6_es.png" alt="Respuesta en frecuencia del banco Bessel de banda de octava" style="width:100%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/filter_bessel_fraction_1_order_6_es_dark.png" alt="Respuesta en frecuencia del banco Bessel de banda de octava" style="width:100%"> | <img class="light-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/filter_bessel_fraction_3_order_6_es.png" alt="Respuesta en frecuencia del banco Bessel de tercio de octava" style="width:100%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/filter_bessel_fraction_3_order_6_es_dark.png" alt="Respuesta en frecuencia del banco Bessel de tercio de octava" style="width:100%"> | 93 93 94 - ## Uso y ejemplos por tipo de filtro 94 + ## 5. Uso y ejemplos por tipo de filtro 95 95 96 96 ### 1. Butterworth (`butter`) 97 97 ··· 120 120 cerca de las frecuencias de corte. 121 121 122 122 ```python 123 + # Usa `x` y `fs` del snippet anterior. 123 124 # Selectividad con 0.1 dB de rizado en la banda de paso 124 125 spl, freq = octave_filter(x, fs, filter_type='cheby1', ripple=0.1) 125 126 ``` ··· 135 136 (`attenuation` debe ser > 3,01 dB). 136 137 137 138 ```python 139 + # Usa `x` y `fs` del snippet anterior. 138 140 # Banda de paso plana, 72 dB de atenuación por defecto (clase 1) 139 141 spl, freq = octave_filter(x, fs, filter_type='cheby2') 140 142 ``` ··· 148 150 la atenuada. 149 151 150 152 ```python 153 + # Usa `x` y `fs` del snippet anterior. 151 154 # Máxima selectividad para aislamiento extremo entre bandas 152 155 spl, freq = octave_filter(x, fs, filter_type='ellip', ripple=0.1) 153 156 ``` ··· 161 164 mejor que ningún otro tipo, pero tienen la caída más lenta. 162 165 163 166 ```python 167 + # Usa `x` y `fs` del snippet anterior. 164 168 # Ideal para análisis de pulsos y preservación de transitorios 165 169 spl, freq = octave_filter(x, fs, filter_type='bessel') 166 170 ``` ··· 177 181 ```python 178 182 from phonometry import linkwitz_riley 179 183 180 - signal = x # reutiliza la señal calibrada del inicio de la página 184 + # Usa `x` y `fs` del snippet anterior. 185 + signal = x 181 186 # Dividir la señal en bandas grave y aguda a 1000 Hz 182 187 low, high = linkwitz_riley(signal, fs, freq=1000, order=4) 183 188 # Reconstrucción: low + high == signal (respuesta plana) ··· 185 190 186 191 <img class="light-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/crossover_lr4_es.png" alt="Crossover Linkwitz-Riley de 4.º orden: paso-bajo, paso-alto y su suma plana" style="width:60%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/crossover_lr4_es_dark.png" alt="Crossover Linkwitz-Riley de 4.º orden: paso-bajo, paso-alto y su suma plana" style="width:60%"> 187 192 188 - ## Verificar la clase IEC 61260-1 193 + ## 6. Verificar la clase IEC 61260-1 189 194 190 195 `verify_filter_class` comprueba cada banda de un banco contra los límites de 191 196 aceptación de **IEC 61260-1:2014** (Tabla 1, con el mapeo de breakpoints a ··· 197 202 198 203 bank = OctaveFilterBank(fs=48000, fraction=3, order=6) 199 204 result = verify_filter_class(bank) 200 - print(result["overall_class"]) # 1, 2 o None 201 - print(result["bands"][0]) # {'freq': ..., 'class': 1, 'margin_class1_db': ...} 205 + print(result["overall_class"]) # 1 206 + print(result["bands"][0]) 207 + # {'freq': 12.589254117941678, 'class': 1, 'margin_class1_db': 0.39999999999997266, 'margin_class2_db': 0.5999999999999727} 202 208 ``` 203 209 204 210 <img class="light-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/class_mask_overlay_es.png" alt="Respuesta Butterworth serpenteando entre las regiones prohibidas de la máscara de clase 1 de IEC 61260-1" style="width:80%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/class_mask_overlay_es_dark.png" alt="Respuesta Butterworth serpenteando entre las regiones prohibidas de la máscara de clase 1 de IEC 61260-1" style="width:80%"> ··· 216 222 los límites de clase con orden 6: el rizado de banda de paso (cheby1/ellip) y la 217 223 caída lenta (bessel) violan la máscara. 218 224 219 - ## Descomposición de la señal y estabilidad 225 + ## 7. Descomposición de la señal y estabilidad 220 226 221 227 Con `sigbands=True` puedes recuperar las componentes en el dominio del tiempo de 222 228 cada banda. Esto permite análisis avanzados o comparar cómo afectan distintas ··· 264 270 265 271 <img class="light-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/group_delay_comparison_es.png" alt="Retardo de grupo de la banda de 1 kHz para las cinco arquitecturas: Bessel casi plano, Chebyshev y elíptico con picos en los bordes" style="width:80%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/group_delay_comparison_es_dark.png" alt="Retardo de grupo de la banda de 1 kHz para las cinco arquitecturas: Bessel casi plano, Chebyshev y elíptico con picos en los bordes" style="width:80%"> 266 272 267 - ## Filtrado de fase cero 273 + ## 8. Filtrado de fase cero 268 274 269 275 Para análisis offline puedes eliminar por completo el retardo de grupo: 270 276 `zero_phase=True` filtra cada banda hacia delante y hacia atrás ··· 280 286 ```python 281 287 from phonometry import OctaveFilterBank 282 288 289 + # Usa `y` del snippet anterior. 283 290 bank = OctaveFilterBank(fs=48000, fraction=3) 284 291 spl, freq, xb = bank.filter(y, sigbands=True, zero_phase=True) 285 292 ``` ··· 288 295 289 296 *El filtrado causal retrasa la ráfaga según el retardo de grupo del filtro; el 290 297 filtrado de fase cero la mantiene alineada con la entrada.* 298 + 299 + --- 300 + 301 + **Normas.** IEC 61260-1:2014, *Electroacoustics — Octave-band and 302 + fractional-octave-band filters — Part 1: Specifications* — las frecuencias 303 + centrales y los bordes de banda en base 10 del §1 (5.2-5.5), las etiquetas 304 + nominales de banda y los límites de aceptación de clase 1 / clase 2 de la 305 + Tabla 1 (con el mapeo de breakpoints a fraccionales y la interpolación en 306 + frecuencia logarítmica) verificados en el §6. ANSI S1.11-2004, *Octave-Band 307 + and Fractional-Octave-Band Analog and Digital Filters* — el convenio de 308 + bordes de banda sobre el que cada banco sitúa sus puntos de −3 dB.
+3 -3
site/src/content/docs/es/guides/human-vibration.md
··· 163 163 raw = np.random.default_rng(0).standard_normal(int(60 * fs)) # registro de 60 s 164 164 a_w = ph.apply_weighting(raw, fs, "Wk") # señal ponderada 165 165 166 - print(round(ph.vibration_dose_value(a_w, fs), 3)) # VDV [m/s^1.75] 167 - print(round(ph.mtvv(a_w, fs), 3)) # MTVV [m/s^2] 168 - print(round(ph.crest_factor(a_w), 2)) # factor de cresta 166 + print(round(ph.vibration_dose_value(a_w, fs), 3)) # 0.744 VDV [m/s^1.75] 167 + print(round(ph.mtvv(a_w, fs), 3)) # 0.265 MTVV [m/s^2] 168 + print(round(ph.crest_factor(a_w), 2)) # 3.74 factor de cresta 169 169 ``` 170 170 171 171 ## 3. Valor total de vibración y exposición diaria `A(8)`
+24 -2
site/src/content/docs/es/guides/intensity.md
··· 104 104 menores, más altas. 105 105 - **Campos reactivos**: cuando `pressure_intensity_index` (F2 en ISO 9614-1) 106 106 se acerca al índice residual δ_pI0 de la sonda, dominan los errores de fase. 107 - Los indicadores de campo del Anexo A de ISO 9614-1 y el criterio de 108 - capacidad dinámica están disponibles directamente: 107 + 108 + Sobre una superficie de medición, los indicadores de campo del Anexo A de 109 + ISO 9614-1 califican el propio barrido. **F2**, el indicador 110 + presión-intensidad de superficie, es el nivel de presión de superficie menos 111 + el nivel de la *magnitud* media de la intensidad normal — cuanto mayor es, 112 + más cerca está la medición del suelo de error de fase de la sonda. **F3**, el 113 + indicador de potencia parcial negativa, es la misma diferencia tomada con la 114 + intensidad media *con signo* — F3 − F2 > 0 revela potencia que entra por 115 + partes de la superficie. **F4**, el indicador de no uniformidad del campo, es 116 + la dispersión normalizada de las intensidades por posición — cuanto mayor es, 117 + más posiciones de medición necesita la superficie. Junto con el criterio de 118 + capacidad dinámica están disponibles directamente: 109 119 110 120 ```python 111 121 import numpy as np ··· 137 147 Consulta [Teoría](/phonometry/es/reference/theory/) para las derivaciones y 138 148 [Calibración](/phonometry/es/guides/calibration/) para el escalado absoluto de 139 149 los dos canales. 150 + 151 + --- 152 + 153 + **Normas.** IEC 61043:1994, *Electroacoustics — Instruments for the 154 + measurement of sound intensity — Measurements with pairs of pressure sensing 155 + microphones* — el estimador de intensidad por espectro cruzado con dos 156 + micrófonos, la corrección del sesgo por diferencias finitas y el límite de 157 + ancho de banda utilizable (cláusula 7.3, Tabla 3). ISO 9614-1:1993, 158 + *Acoustics — Determination of sound power levels of noise sources using sound 159 + intensity — Part 1: Measurement at discrete points* — el índice 160 + presión-intensidad, los indicadores de campo F2, F3 y F4 del Anexo A y el 161 + criterio de capacidad dinámica (Anexo B).
+21
site/src/content/docs/es/guides/levels.md
··· 62 62 eventos), **L50** la mediana y **L90** el nivel de fondo. 63 63 64 64 ```python 65 + import numpy as np 65 66 from phonometry import ln_levels 66 67 67 68 # Un tono constante da L10 = L50 = L90; los percentiles solo cuentan algo con un 68 69 # nivel *fluctuante*. Sintetizamos 3 s alternando entre medio segundo tranquilo 69 70 # y otro ~10 dB más fuerte para que los estadísticos se separen. 71 + fs = 48000 70 72 rng = np.random.default_rng(0) 71 73 segment = fs // 2 # 0.5 s por nivel 72 74 quiet = 0.02 * rng.standard_normal(segment) # fondo ··· 111 113 112 114 ```python 113 115 from phonometry import lc_peak, sel, sound_exposure, lex_8h 116 + 117 + # Usa `recording`, `fs` y `sensitivity` del snippet de Leq anterior. 114 118 115 119 # Pico ponderado C (IEC 61672-1 §5.13): los límites de acción laborales usan esto 116 120 peak = lc_peak(recording, fs, calibration_factor=sensitivity) ··· 232 236 ```python 233 237 from phonometry import OctaveFilterBank 234 238 239 + # Usa `recording` del snippet de Leq anterior. 235 240 bank = OctaveFilterBank(fs=48000, fraction=3) 236 241 levels, freq, times = bank.spectrogram(recording, window_time=0.125, overlap=0.5) 237 242 # levels: (bandas, ventanas) — listo para pcolormesh(times, freq, levels) ··· 263 268 ```python 264 269 import matplotlib.pyplot as plt 265 270 271 + # Usa `levels`, `freq` y `times` del snippet del espectrograma anterior. 266 272 fig, ax = plt.subplots() 267 273 mesh = ax.pcolormesh(times, freq, levels, shading="auto") 268 274 ax.set_yscale("log") ··· 281 287 [Tonos discretos prominentes](/phonometry/es/guides/tone-prominence/), y las 282 288 curvas isofónicas de ISO 226 viven con las métricas de percepción en 283 289 [Psicoacústica](/phonometry/es/guides/psychoacoustics/). 290 + 291 + --- 292 + 293 + **Normas.** IEC 61672-1:2013, *Electroacoustics — Sound level meters — 294 + Part 1: Specifications* — la ponderación temporal Fast/Slow/Impulse de la 295 + envolvente de `ln_levels`, el pico ponderado C del §5.13 (verificado contra 296 + las ráfagas de tono de la Tabla 5) y el nivel de exposición sonora verificado 297 + contra la columna LAE de la Tabla 4. IEC 61252, *Electroacoustics — 298 + Specifications for personal sound exposure meters* — la exposición sonora E en 299 + Pa²h y el nivel normalizado a 8 h LEX,8h (≡ LEP,d), anclados en 300 + 3,2 Pa²h ⇔ exactamente 90 dB. ISO 1996-1:2016, *Acoustics — Description, 301 + measurement and assessment of environmental noise — Part 1: Basic quantities 302 + and assessment procedures* — Lden (3.6.4), Ldn (3.6.5) y el nivel de 303 + evaluación compuesto de jornada completa del apartado 6.5 (Fórmulas 5-6, 304 + ajustes de la Tabla A.1).
+72 -1
site/src/content/docs/es/guides/materials.md
··· 58 58 añadiendo el indicador `M`, de modo que la valoración es $0{,}60(\text{M})$, clase 59 59 C.* 60 60 61 + <details> 62 + <summary>Mostrar el código de esta figura</summary> 63 + 64 + ```python 65 + import matplotlib.pyplot as plt 66 + from phonometry import weighted_absorption 67 + 68 + # Coeficientes prácticos del Anexo A.2 de ISO 11654 a 250/500/1000/2000/4000 Hz 69 + result = weighted_absorption([0.35, 1.00, 0.65, 0.60, 0.55]) 70 + result.plot() # curva práctica frente a la referencia desplazada 71 + plt.show() 72 + ``` 73 + 74 + </details> 75 + 61 76 ```python 62 77 from phonometry import weighted_absorption, absorption_class 63 78 ··· 113 128 *La caída de presión, ligeramente superlineal, se ajusta pasando por el origen; 114 129 la resistencia específica al flujo de aire es el ajuste leído a 0,5 mm/s.* 115 130 131 + <details> 132 + <summary>Mostrar el código de esta figura</summary> 133 + 134 + ```python 135 + import matplotlib.pyplot as plt 136 + import numpy as np 137 + from phonometry import static_airflow_resistance 138 + 139 + area = np.pi * 0.05**2 # celda de 100 mm de diámetro [m^2] 140 + u = np.array([0.5, 1, 2, 4, 8, 12]) * 1e-3 # velocidad lineal [m/s] 141 + dp = 1.6e4 * u + 4.0e5 * u**2 # caída de presión medida [Pa] 142 + r = static_airflow_resistance(u, dp, area=area, thickness=0.05) 143 + 144 + u_fit = np.linspace(0.0, 13e-3, 200) 145 + dp_fit = r.linear_coefficient * u_fit + r.quadratic_coefficient * u_fit**2 146 + fig, ax = plt.subplots() 147 + ax.plot(u_fit * 1e3, dp_fit, label="Ajuste por el origen dp = a u + b u^2") 148 + ax.plot(u * 1e3, dp, "o", label="Caída de presión medida") 149 + ax.plot(r.evaluation_velocity * 1e3, r.pressure_drop, "D", 150 + label="Evaluación a 0.5 mm/s") 151 + ax.set_xlabel("Velocidad lineal del flujo u [mm/s]") 152 + ax.set_ylabel("Caída de presión dp [Pa]") 153 + ax.set_title(f"R_s = {r.specific_resistance:.0f} Pa·s/m") 154 + ax.legend() 155 + plt.show() 156 + ``` 157 + 158 + </details> 159 + 116 160 ```python 117 161 import numpy as np 118 162 from phonometry import static_airflow_resistance ··· 165 209 piston_stroke_specimen=14e-3, piston_stroke_termination=1.4e-3, 166 210 frequency=2.0, cavity_volume=7.854e-4, kappa_prime=kp, 167 211 ) 168 - print(round(R)) # resistencia al flujo de aire R [Pa*s/m^3] 212 + print(round(R)) # 222956 resistencia al flujo de aire R [Pa*s/m^3] 169 213 ``` 170 214 171 215 Pasa el resultado de `effective_kappa` a `alternating_airflow_resistance` para ··· 197 241 198 242 *Una diferencia de nivel pequeña significa un absorbente casi perfecto; una 199 243 diferencia de nivel de 9,54 dB da $s = 3$, $|r| = 0{,}5$ y $\alpha = 0{,}75$.* 244 + 245 + <details> 246 + <summary>Mostrar el código de esta figura</summary> 247 + 248 + ```python 249 + import matplotlib.pyplot as plt 250 + import numpy as np 251 + from phonometry import ( 252 + standing_wave_absorption, standing_wave_ratio_from_level, 253 + standing_wave_reflection_magnitude, 254 + ) 255 + 256 + level_diff = np.linspace(0.5, 40.0, 300) # L_max - L_min [dB] 257 + swr = standing_wave_ratio_from_level(level_diff) 258 + fig, ax = plt.subplots() 259 + ax.plot(level_diff, standing_wave_absorption(swr), 260 + label="Coeficiente de absorción alpha") 261 + ax.plot(level_diff, standing_wave_reflection_magnitude(swr), "--", 262 + label="Magnitud del factor de reflexión |r|") 263 + ax.set_xlabel("Diferencia de nivel de onda estacionaria L_max - L_min [dB]") 264 + ax.set_ylabel("alpha, |r|") 265 + ax.legend() 266 + plt.show() 267 + ``` 268 + 269 + </details> 200 270 201 271 ```python 202 272 from phonometry import standing_wave_absorption, standing_wave_ratio_from_level ··· 239 309 r = reflection_factor(h12, spacing=spacing, x1=x1, wavenumber=k0) 240 310 print(np.round(absorption_from_reflection(r), 3)) # [0.75 0.75 0.75] 241 311 print(np.round(normalized_surface_impedance(r), 2)) # Z / rho c0 312 + # [1.15-1.23j 1.15-1.23j 1.15-1.23j] 242 313 ``` 243 314 244 315 El `two_microphone_impedance` de alto nivel envuelve esta cadena y devuelve un
+75
site/src/content/docs/es/guides/outdoor-propagation.md
··· 47 47 *La curva seca de 20 °C / 10 % absorbe más a frecuencias medias, pero las curvas 48 48 húmedas la superan por debajo de ~200 Hz: la firma de la relajación.* 49 49 50 + <details> 51 + <summary>Mostrar el código de esta figura</summary> 52 + 53 + ```python 54 + import matplotlib.pyplot as plt 55 + import numpy as np 56 + from phonometry import air_attenuation 57 + 58 + freqs = np.geomspace(50.0, 10000.0, 400) 59 + fig, ax = plt.subplots() 60 + for temp, rh in [(20.0, 50.0), (20.0, 10.0), (0.0, 70.0), (30.0, 80.0)]: 61 + ax.loglog(freqs, air_attenuation(freqs, temp, rh) * 1000.0, 62 + label=f"{temp:g} °C, {rh:g} % HR") 63 + ax.set_xlabel("Frecuencia [Hz]") 64 + ax.set_ylabel("Coeficiente de atenuación alpha [dB/km]") 65 + ax.legend() 66 + plt.show() 67 + ``` 68 + 69 + </details> 70 + 50 71 ```python 51 72 import numpy as np 52 73 from phonometry import air_attenuation, air_attenuation_m ··· 156 177 157 178 <img class="light-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/outdoor_attenuation_breakdown_es.png" alt="Desglose por bandas de octava de la atenuación ISO 9613-2 como barra apilada de Adiv, Aatm, Agr y Abar con el total A superpuesto, para un recorrido de 200 m sobre suelo poroso con una barrera de 4 m" style="width:80%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/outdoor_attenuation_breakdown_es_dark.png" alt="Desglose por bandas de octava de la atenuación ISO 9613-2 como barra apilada de Adiv, Aatm, Agr y Abar con el total A superpuesto, para un recorrido de 200 m sobre suelo poroso con una barrera de 4 m" style="width:80%"> 158 179 180 + <details> 181 + <summary>Mostrar el código de esta figura</summary> 182 + 183 + ```python 184 + import matplotlib.pyplot as plt 185 + import numpy as np 186 + from phonometry import Barrier, outdoor_propagation_attenuation 187 + 188 + bands = np.array([63.0, 125.0, 250.0, 500.0, 1000.0, 2000.0, 4000.0, 8000.0]) 189 + barrier = Barrier(source_to_edge=101.0, edge_to_receiver=101.0) 190 + att = outdoor_propagation_attenuation( 191 + 200.0, 1.5, 1.5, bands, ground_source=1.0, ground_middle=1.0, 192 + ground_receiver=1.0, barrier=barrier, temperature=15.0, 193 + relative_humidity=70.0, 194 + ) 195 + 196 + x = np.arange(len(bands)) 197 + fig, ax = plt.subplots() 198 + bottom = np.zeros(len(bands)) 199 + for term, label in [(att.a_div, "Adiv — divergencia"), 200 + (att.a_atm, "Aatm — atmosférica"), 201 + (att.a_gr, "Agr — suelo"), 202 + (att.a_bar, "Abar — barrera")]: 203 + ax.bar(x, term, bottom=bottom, label=label) 204 + bottom = bottom + term 205 + ax.plot(x, att.a_total, "D-", color="black", label="A — total") 206 + ax.set_xticks(x) 207 + ax.set_xticklabels([f"{b:g}" for b in bands]) 208 + ax.set_xlabel("Frecuencia central de banda de octava [Hz]") 209 + ax.set_ylabel("Atenuación A [dB]") 210 + ax.legend() 211 + plt.show() 212 + ``` 213 + 214 + </details> 215 + 159 216 ```python 160 217 import numpy as np 161 218 from phonometry import ( ··· 264 321 [guía de exposición al ruido en el trabajo](/phonometry/es/guides/occupational-exposure/) 265 322 para la exposición ocupacional (ISO 9612) que consume niveles 266 323 ponderados A. 324 + 325 + --- 326 + 327 + **Normas.** ISO 9613-1:1993, *Acoustics — Attenuation of sound during 328 + propagation outdoors — Part 1: Calculation of the absorption of sound by the 329 + atmosphere* — el coeficiente de atenuación de tono puro $\alpha$ (Ec. (5)) con 330 + las frecuencias de relajación del oxígeno y el nitrógeno (Ec. (3)/(4)), la 331 + conversión de humedad del anexo B y las frecuencias centrales exactas de la 332 + Tabla 1 (Ec. (6), Nota 5). ISO 9613-2:1996, *Acoustics — Attenuation of sound 333 + during propagation outdoors — Part 2: General method of calculation* — el nivel 334 + en el receptor a favor del viento (Ec. (3)/(4)) compuesto por la divergencia 335 + geométrica (Ec. (7)), la absorción atmosférica (Ec. (8)), el efecto del suelo 336 + (Ec. (9), Tabla 3) con su alternativa ponderada A (Ec. (10)/(11)), el 337 + apantallamiento por barreras (Ecs. (12)–(17)) y la corrección meteorológica 338 + (Ec. (21)/(22)). ISO 354:2003, *Acoustics — Measurement of sound absorption in 339 + a reverberation room* — solo la conversión $m = \alpha/(10 \lg e)$ del apartado 340 + 8.1.2.1 tras `air_attenuation_m`; el método en sala reverberante se trata en la 341 + [guía de Acústica de salas](/phonometry/es/guides/room-acoustics/).
+24 -3
site/src/content/docs/es/guides/psychoacoustics.md
··· 45 45 46 46 # Desde una grabación sin calibrar: calibration_factor convierte unidades digitales en Pa 47 47 res = loudness_zwicker(x, fs, field="free", calibration_factor=sens) 48 - print(f"N = {res.loudness:.1f} sone ({res.loudness_level:.0f} phon)") 48 + print(f"N = {res.loudness:.1f} sone ({res.loudness_level:.0f} phon)") # 13.1 sone (77 phon) 49 49 50 50 # Señales variables en el tiempo: la sonoridad percentil N5 es el 51 51 # estándar para informes 52 52 res = loudness_zwicker(x, fs) # stationary=False (por defecto) 53 - print(res.n5, res.n10, res.loudness) # N5, N10, Nmax 53 + print(f"{res.n5:.1f} {res.n10:.1f} {res.loudness:.1f}") # 13.1 13.1 13.1 — N5, N10, Nmax 54 54 55 55 # Desde 28 niveles de tercio de octava (25 Hz .. 12.5 kHz) 56 56 res = loudness_zwicker_from_spectrum(levels_28, field="diffuse") ··· 131 131 ```python 132 132 from phonometry import sharpness_din 133 133 134 + # Usa `x`, `fs` y `sens` del snippet anterior. 134 135 s = sharpness_din(x, fs, calibration_factor=sens) # acum 135 136 s_aures = sharpness_din(x, fs, method="aures") # variante del Anexo B 136 137 ``` ··· 279 280 280 281 res = loudness_moore_glasberg_time(x, fs, field="free") 281 282 print(f"N_max = {res.n_max:.3f} sone ({res.loudness_level_max:.0f} phon)") # 1.000 sono (40 fonios) 282 - print(f"sonoridad de largo plazo superada el 5% del tiempo: {res.percentiles[5.0]:.3f} sone") 283 + print(f"sonoridad de largo plazo superada el 5% del tiempo: {res.percentiles[5.0]:.3f} sone") # 0.999 sone 283 284 284 285 res.plot() # sonoridad de corto plazo S'(t) y de largo plazo S''(t) frente al tiempo 285 286 ``` ··· 508 509 [índice de transmisión del habla](/phonometry/es/guides/speech-transmission/) 509 510 para el STI/STIPA, y [Teoría](/phonometry/es/reference/theory/) para la 510 511 matemática subyacente. 512 + 513 + --- 514 + 515 + **Normas.** ISO 532-1:2017, *Acoustics — Methods for calculating 516 + loudness — Part 1: Zwicker method* — sonoridad estacionaria y variable en el 517 + tiempo en sonos a partir del programa de referencia normativo del Anexo A.4, 518 + con la sonoridad percentil N5/N10, validada frente al conjunto del Anexo B. 519 + ISO 532-2:2017, *... Part 2: Moore-Glasberg method* — sonoridad estacionaria a 520 + partir de patrones de excitación roex sobre la escala del número ERB, con suma 521 + binaural explícita. ISO 532-3:2023, *... Part 3: 522 + Moore-Glasberg-Schlittenlacher method* — sonoridad variable en el tiempo de 523 + corto y largo plazo y el pico N_max. DIN 45692:2009, *Messtechnische 524 + Simulation der Hörempfindung Schärfe* — sharpness en acum (ponderación del 525 + apartado 6, variantes von Bismarck y Aures del Anexo B, objetivos de la 526 + Tabla A.2). ISO 226:2023, *Acoustics — Normal equal-loudness-level contours* — 527 + las curvas isofónicas (Fórmula 1), el nivel de sonoridad de tonos puros 528 + (Fórmula 2) y el umbral de audición. ECMA-418-2:2025, *Psychoacoustic metrics 529 + for ITT equipment — Part 2 (methods for describing human perception based on 530 + the Sottek Hearing Model)* — la sonoridad (sone_HMS), la tonalidad (tu_HMS) y 531 + la aspereza (asper) del modelo de Sottek.
+10
site/src/content/docs/es/guides/room-acoustics.md
··· 492 492 nivel tras los niveles de las salas emisora/receptora. 493 493 - [Teoría](/phonometry/es/reference/theory/) — la integración de Schroeder, las ventanas de 494 494 regresión y la derivación de la curva de referencia. 495 + 496 + --- 497 + 498 + **Normas.** ISO 18233:2006 (aplicación de nuevos métodos de medición — la 499 + adquisición de respuestas al impulso por barrido y MLS); ISO 3382-1:2009 e 500 + ISO 3382-2:2008 (tiempo de reverberación y parámetros de sala desde el 501 + decaimiento de Schroeder); ISO 3382-3:2022 (métricas de habla en oficinas 502 + abiertas); ISO 354:2003 (absorción sonora en cámara reverberante). Validado 503 + frente a decaimientos de forma cerrada y las definiciones de parámetros de 504 + las propias normas.
+125
site/src/content/docs/es/guides/sound-power.md
··· 109 109 res.plot() # barras de nivel de potencia sonora por banda; LWA en el título (requiere matplotlib) 110 110 ``` 111 111 112 + <img class="light-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/sound_power_pressure_result_es.png" alt="El espectro de nivel de potencia sonora por superficie envolvente del ejemplo de semiesfera de ISO 3744, una barra por banda de octava de 63 Hz a 8 kHz con pico cerca de 500 Hz, con el total ponderado A de 92,4 dB(A) en el título" style="width:88%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/sound_power_pressure_result_es_dark.png" alt="El espectro de nivel de potencia sonora por superficie envolvente del ejemplo de semiesfera de ISO 3744, una barra por banda de octava de 63 Hz a 8 kHz con pico cerca de 500 Hz, con el total ponderado A de 92,4 dB(A) en el título" style="width:88%"> 113 + 114 + *Una barra por banda: la presión superficial promediada en energía menos las 115 + correcciones de fondo (`K1`) y ambiental (`K2`) más el término de superficie 116 + `10 lg(S/S0)` dan `LW(f)`, y la suma energética ponderada A entre bandas da el 117 + número único `LWA` del título.* 118 + 119 + <details> 120 + <summary>Ver el código de esta figura</summary> 121 + 122 + ```python 123 + import matplotlib.pyplot as plt 124 + import numpy as np 125 + 126 + # res es el SoundPowerResult calculado arriba. Una línea: 127 + res.plot() 128 + plt.show() 129 + 130 + # A mano: un espectro de barras de LW con el total ponderado A en el título. 131 + positions = np.arange(freqs.size) 132 + fig, ax = plt.subplots() 133 + ax.bar(positions, res.sound_power_level, width=0.7, color="#1f77b4") 134 + ax.set_xticks(positions) 135 + ax.set_xticklabels([f"{f:g}" for f in freqs], rotation=45, ha="right") 136 + ax.set_xlabel("Frecuencia [Hz]") 137 + ax.set_ylabel("Nivel de potencia sonora LW [dB]") 138 + ax.set_title( 139 + f"Potencia sonora por superficie envolvente (ISO 3744) " 140 + f"LWA = {res.sound_power_level_a:.1f} dB(A)") 141 + plt.show() 142 + ``` 143 + 144 + </details> 145 + 112 146 El total ponderado A `LWA` se combina a partir de las potencias por banda con 113 147 las correcciones de ponderación A del Anexo E de ISO 3744, así que necesita 114 148 `frequencies`. Pasar los datos de sala (`reverberation_time` + `volume`, ··· 206 240 207 241 rev.plot() # espectro LW de la sala reverberante; LWA en el título (requiere matplotlib) 208 242 ``` 243 + 244 + <img class="light-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/sound_power_reverberation_result_es.png" alt="El espectro de nivel de potencia sonora en cámara reverberante del ejemplo de ISO 3741, una barra por banda de tercio de octava de 100 Hz a 10 kHz descendiendo suavemente con la frecuencia, con el total ponderado A de 92,1 dB(A) en el título" style="width:88%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/sound_power_reverberation_result_es_dark.png" alt="El espectro de nivel de potencia sonora en cámara reverberante del ejemplo de ISO 3741, una barra por banda de tercio de octava de 100 Hz a 10 kHz descendiendo suavemente con la frecuencia, con el total ponderado A de 92,1 dB(A) en el título" style="width:88%"> 245 + 246 + *El nivel medio de la sala llevado a través de los términos de área de 247 + absorción, de Waterhouse y meteorológicos de la Ec. 20 da el `LW(f)` por tercio 248 + de octava, y la suma energética ponderada A entre las 21 bandas da el `LWA` del 249 + título.* 250 + 251 + <details> 252 + <summary>Ver el código de esta figura</summary> 253 + 254 + ```python 255 + import matplotlib.pyplot as plt 256 + import numpy as np 257 + 258 + # rev es el ReverberationSoundPowerResult calculado arriba. Una línea: 259 + rev.plot() 260 + plt.show() 261 + 262 + # A mano: un espectro de barras de LW con el total ponderado A en el título. 263 + positions = np.arange(freqs.size) 264 + fig, ax = plt.subplots() 265 + ax.bar(positions, rev.sound_power_level, width=0.7, color="#1f77b4") 266 + ax.set_xticks(positions) 267 + ax.set_xticklabels([f"{f:g}" for f in freqs], rotation=45, ha="right") 268 + ax.set_xlabel("Frecuencia [Hz]") 269 + ax.set_ylabel("Nivel de potencia sonora LW [dB]") 270 + ax.set_title( 271 + f"Potencia sonora en cámara reverberante (ISO 3741) " 272 + f"LWA = {rev.sound_power_level_a:.1f} dB(A)") 273 + plt.show() 274 + ``` 275 + 276 + </details> 209 277 210 278 `levels` puede ser un espectro medio 1D o un array 2D `(NM, NB)` promediado 211 279 sobre las posiciones. Cuando el volumen de la sala, su tiempo de reverberación ··· 312 380 313 381 res.plot() # espectro LW; bandas no positivas (indeterminables) con trama (requiere matplotlib) 314 382 ``` 383 + 384 + <img class="light-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/sound_power_intensity_result_es.png" alt="El espectro de nivel de potencia sonora por barrido de intensidad del ejemplo de ISO 9614-2, una barra por banda de octava de 125 Hz a 4 kHz todas cerca de 85 dB, con el total ponderado A de 90,9 dB(A) en el título" style="width:88%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/sound_power_intensity_result_es_dark.png" alt="El espectro de nivel de potencia sonora por barrido de intensidad del ejemplo de ISO 9614-2, una barra por banda de octava de 125 Hz a 4 kHz todas cerca de 85 dB, con el total ponderado A de 90,9 dB(A) en el título" style="width:88%"> 385 + 386 + *Las potencias parciales `<In,i>·Si` de los seis segmentos suman el `LW` de 387 + cada banda; aquí todas las bandas tienen potencia neta positiva y superan los 388 + criterios de indicadores de campo en grado de ingeniería, así que las seis 389 + barras se sostienen, y el total ponderado A de 90,9 dB(A) encabeza el título.* 390 + 391 + <details> 392 + <summary>Ver el código de esta figura</summary> 393 + 394 + ```python 395 + import matplotlib.pyplot as plt 396 + import numpy as np 397 + 398 + # res es el SoundPowerIntensityResult calculado arriba. Una línea: 399 + res.plot() 400 + plt.show() 401 + 402 + # A mano: un espectro de barras de LW con el total ponderado A en el título. 403 + positions = np.arange(freqs.size) 404 + fig, ax = plt.subplots() 405 + ax.bar(positions, res.sound_power_level, width=0.7, color="#1f77b4") 406 + ax.set_xticks(positions) 407 + ax.set_xticklabels([f"{f:g}" for f in freqs], rotation=45, ha="right") 408 + ax.set_xlabel("Frecuencia [Hz]") 409 + ax.set_ylabel("Nivel de potencia sonora LW [dB]") 410 + ax.set_title( 411 + f"Potencia sonora por barrido de intensidad (ISO 9614-2) " 412 + f"LWA = {res.sound_power_level_a:.1f} dB(A)") 413 + plt.show() 414 + ``` 415 + 416 + </details> 315 417 316 418 Suministrar `normal_intensity_2` (el segundo barrido) promedia ambos para las 317 419 potencias parciales y evalúa el criterio 3; `pressure_levels` habilita `FpI`; ··· 569 671 tras `LWA`. 570 672 - [Teoría](/phonometry/es/reference/theory/) — las derivaciones de Waterhouse, K1/K2 y 571 673 C1/C2. 674 + 675 + --- 676 + 677 + **Normas.** ISO 3744:2010, *Acoustics — Determination of sound power levels 678 + and sound energy levels of noise sources using sound pressure — Engineering 679 + methods for an essentially free field over a reflecting plane* — el método de 680 + superficie envolvente: las áreas de la semiesfera y la caja, las correcciones 681 + `K1`/`K2`, las posiciones de micrófono del Anexo B y la ponderación A del 682 + Anexo E. ISO 3746:2010, *… Survey method using an enveloping measurement 683 + surface over a reflecting plane* — el grado de inspección que comparte las 684 + mismas fórmulas con criterios más laxos. ISO 3741:2010, *… Precision methods 685 + for reverberation test rooms* — los métodos directo (Ec. 20) y de comparación 686 + con las correcciones de Waterhouse y meteorológicas y los criterios de 687 + cualificación de la Tabla 1. ISO 3745:2012, *… Precision methods for anechoic 688 + rooms and hemi-anechoic rooms* — el nivel de potencia de la cláusula 8, la 689 + corrección de fondo por posición (Ec. 11), las correcciones meteorológicas y 690 + los conjuntos de micrófonos normalizados. ISO 9614-2:1996, *Acoustics — 691 + Determination of sound power levels of noise sources using sound intensity — 692 + Part 2: Measurement by scanning* — las potencias parciales, los indicadores de 693 + campo `FpI` y `F+/-` y los criterios de grado. ISO 9614-3:2002, *… Part 3: 694 + Precision method for measurement by scanning* — el método de barrido de 695 + grado 1, sus indicadores de campo y el marcado de no aplicable de la 696 + cláusula 9.2.
+43
site/src/content/docs/es/guides/speech-intelligibility.md
··· 162 162 163 163 <img class="light-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/sii_vocal_efforts_es.png" alt="Izquierda: los cuatro espectros de voz estándar de ANSI S3.5-1997 (normal, elevada, fuerte, grito) de 160 Hz a 8000 Hz, cada esfuerzo mayor sube todo el espectro. Derecha: el SII resultante en un ruido de banda ancha fijo, subiendo de 0,12 (normal) a 0,79 (grito)" style="width:96%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/sii_vocal_efforts_es_dark.png" alt="Izquierda: los cuatro espectros de voz estándar de ANSI S3.5-1997 (normal, elevada, fuerte, grito) de 160 Hz a 8000 Hz, cada esfuerzo mayor sube todo el espectro. Derecha: el SII resultante en un ruido de banda ancha fijo, subiendo de 0,12 (normal) a 0,79 (grito)" style="width:96%"> 164 164 165 + <details> 166 + <summary>Mostrar el código de esta figura</summary> 167 + 168 + ```python 169 + import numpy as np 170 + import matplotlib.pyplot as plt 171 + import phonometry as ph 172 + 173 + # Los cuatro espectros de la Tabla 3 de ANSI S3.5-1997 y el ruido fijo de arriba. 174 + noise = np.array([48.0, 47.0, 46.0, 44.0, 42.0, 40.0, 38.0, 36.0, 34.0, 175 + 32.0, 30.0, 28.0, 26.0, 24.0, 22.0, 20.0, 18.0, 16.0]) 176 + efforts = ph.sii.VOCAL_EFFORTS # ("normal", "raised", "loud", "shout") 177 + freqs = ph.sii.BAND_CENTERS # los 18 centros de banda de tercio de octava 178 + nombres = {"normal": "Normal", "raised": "Elevada", 179 + "loud": "Fuerte", "shout": "Grito"} 180 + 181 + fig, (ax_s, ax_i) = plt.subplots(1, 2, figsize=(12, 5)) 182 + 183 + # Izquierda: cada esfuerzo vocal mayor sube todo el espectro de voz. 184 + for effort in efforts: 185 + ax_s.plot(freqs, ph.standard_speech_spectrum(effort), "o-", 186 + label=nombres[effort]) 187 + ax_s.set_xscale("log") 188 + ax_s.set_xticks(list(freqs)) 189 + ax_s.set_xticklabels([f"{f:g}" for f in freqs], rotation=45, ha="right") 190 + ax_s.xaxis.set_minor_formatter(plt.NullFormatter()) 191 + ax_s.set_xlabel("Banda de tercio de octava [Hz]") 192 + ax_s.set_ylabel("Nivel espectral del habla [dB SPL]") 193 + ax_s.legend() 194 + 195 + # Derecha: el SII que alcanza cada espectro en el ruido fijo. 196 + sii = [ph.speech_intelligibility_index(e, noise).sii for e in efforts] 197 + pos = np.arange(len(efforts)) 198 + ax_i.bar(pos, sii) 199 + ax_i.set_xticks(pos) 200 + ax_i.set_xticklabels([nombres[e] for e in efforts]) 201 + ax_i.set_ylim(0.0, 1.0) 202 + ax_i.set_ylabel("Índice de inteligibilidad del habla") 203 + plt.show() 204 + ``` 205 + 206 + </details> 207 + 165 208 Los nombres de esfuerzo vocal funcionan en cualquier sitio donde se espere un 166 209 espectro de voz, incluido como primer argumento de 167 210 `speech_intelligibility_index`.
+108 -35
site/src/content/docs/es/guides/time-weighting.md
··· 7 7 específicas. phonometry implementa las constantes de tiempo exactas de la norma 8 8 **IEC 61672-1:2013**. 9 9 10 - <img class="light-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/time_weighting_analysis_es.png" alt="Respuestas de las ponderaciones temporales Fast, Slow e Impulse a una ráfaga de ruido" style="width:80%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/time_weighting_analysis_es_dark.png" alt="Respuestas de las ponderaciones temporales Fast, Slow e Impulse a una ráfaga de ruido" style="width:80%"> 11 - 12 - * **Fast (`fast`):** τ = 125 ms. Estándar para fluctuaciones de ruido. 13 - * **Slow (`slow`):** τ = 1000 ms. Estándar para ruido estacionario. 14 - * **Impulse (`impulse`):** ponderación temporal **asimétrica**. 35 ms de subida para 15 - capturar ataques rápidos y 1500 ms de caída para facilitar la lectura. 16 - 17 - ```python 18 - import numpy as np 19 - from phonometry import time_weighting 20 - 21 - # recording: una captura de micrófono calibrada (Pa) — grabada con tu cadena de medición. Sintetizada aquí para que la guía funcione por sí sola. 22 - fs = 48000 23 - recording = 0.2 * np.sin(2 * np.pi * 1000 * np.arange(fs) / fs) 24 - 25 - # Calcular la envolvente de energía (valor cuadrático medio) 26 - energy_envelope = time_weighting(recording, fs, mode='fast') 27 - # dB SPL respecto a 20 μPa 28 - spl_t = 10 * np.log10(energy_envelope / (2e-5)**2) 29 - ``` 30 - 31 - La ponderación temporal Impulse asimétrica usa dos constantes — ataque rápido y caída 32 - lenta — conmutando por muestra según el signo del cambio: 33 - 34 - $$ 35 - y[n] = y[n-1] + \alpha \ (x^2[n] - y[n-1]), \qquad 36 - \alpha = \begin{cases}1 - e^{-1/(f_s \cdot 0{,}035)} & x^2[n] > y[n-1]\\[2pt] 1 - e^{-1/(f_s \cdot 1{,}5)} & \text{en otro caso}\end{cases} 37 - $$ 38 - 39 - ## El detector exponencial 10 + ## 1. El detector exponencial 40 11 41 12 La aguja de un sonómetro no puede seguir la forma de onda de presión — muestra 42 13 un *valor cuadrático medio* móvil con memoria exponencial. Formalmente ··· 55 26 tras 8τ — por eso los análisis de nivel descartan los primeros instantes de una 56 27 grabación. 57 28 58 - ### Parámetros de `time_weighting()` / `TimeWeighting` 29 + ## 2. Las tres ponderaciones temporales 30 + 31 + <img class="light-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/time_weighting_analysis_es.png" alt="Respuestas de las ponderaciones temporales Fast, Slow e Impulse a una ráfaga de ruido" style="width:80%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/time_weighting_analysis_es_dark.png" alt="Respuestas de las ponderaciones temporales Fast, Slow e Impulse a una ráfaga de ruido" style="width:80%"> 32 + 33 + <details> 34 + <summary>Mostrar el código de esta figura</summary> 35 + 36 + ```python 37 + import numpy as np 38 + import matplotlib.pyplot as plt 39 + from phonometry import time_weighting 40 + 41 + fs = 48000 42 + t = np.arange(int(fs * 4)) / fs 43 + burst = np.zeros_like(t) # ráfaga de ruido de 0.5 s (Pa) que empieza en t = 1 s 44 + rng = np.random.default_rng(42) 45 + burst[fs:int(1.5 * fs)] = 0.2 * rng.standard_normal(int(0.5 * fs)) 46 + 47 + p0 = 2e-5 48 + plt.figure() 49 + for mode in ('fast', 'slow', 'impulse'): 50 + envelope = time_weighting(burst, fs, mode=mode) 51 + plt.plot(t, 10 * np.log10(np.maximum(envelope, 1e-12) / p0**2), label=mode) 52 + plt.xlabel('Tiempo [s]') 53 + plt.ylabel('Nivel [dB SPL]') 54 + plt.legend() 55 + plt.show() 56 + ``` 57 + 58 + </details> 59 + 60 + * **Fast (`fast`):** τ = 125 ms. Estándar para fluctuaciones de ruido. 61 + * **Slow (`slow`):** τ = 1000 ms. Estándar para ruido estacionario. 62 + * **Impulse (`impulse`):** ponderación temporal **asimétrica**. 35 ms de subida para 63 + capturar ataques rápidos y 1500 ms de caída para facilitar la lectura. 64 + 65 + ```python 66 + import numpy as np 67 + from phonometry import time_weighting 68 + 69 + # recording: una captura de micrófono calibrada (Pa) — grabada con tu cadena de medición. Sintetizada aquí para que la guía funcione por sí sola. 70 + fs = 48000 71 + recording = 0.2 * np.sin(2 * np.pi * 1000 * np.arange(fs) / fs) 72 + 73 + # Calcular la envolvente de energía (valor cuadrático medio) 74 + energy_envelope = time_weighting(recording, fs, mode='fast') 75 + # dB SPL respecto a 20 μPa 76 + spl_t = 10 * np.log10(energy_envelope / (2e-5)**2) 77 + 78 + print(f"Nivel Fast en régimen permanente: {spl_t[-1]:.1f} dB SPL") 79 + # Nivel Fast en régimen permanente: 77.0 dB SPL 80 + ``` 81 + 82 + La ponderación temporal Impulse asimétrica usa dos constantes — ataque rápido y caída 83 + lenta — conmutando por muestra según el signo del cambio: 84 + 85 + $$ 86 + y[n] = y[n-1] + \alpha \ (x^2[n] - y[n-1]), \qquad 87 + \alpha = \begin{cases}1 - e^{-1/(f_s \cdot 0{,}035)} & x^2[n] > y[n-1]\\[2pt] 1 - e^{-1/(f_s \cdot 1{,}5)} & \text{en otro caso}\end{cases} 88 + $$ 89 + 90 + ## 3. Parámetros de `time_weighting()` / `TimeWeighting` 59 91 60 92 | Parámetro | Tipo | Unidades | Rango / por defecto | Notas | 61 93 | :--- | :--- | :--- | :--- | :--- | ··· 67 99 La salida tiene las unidades de $x^2$: toma `10*log10(y / p0**2)` para SPL o 68 100 usa las funciones de nivel, que lo hacen por ti. 69 101 70 - ## Respuesta temporal verificada (IEC 61672-1, Tabla 4) 102 + ## 4. Respuesta temporal verificada (IEC 61672-1, Tabla 4) 71 103 72 104 La respuesta de la envolvente Fast a ráfagas de tono de 4 kHz cae exactamente 73 105 sobre los valores de referencia de la norma — verificado en CI para duraciones ··· 75 107 76 108 <img class="light-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/tone_burst_iec_es.png" alt="Respuestas de la envolvente Fast a ráfagas de 200, 50 y 10 ms alcanzando exactamente los valores de la Tabla 4 de IEC 61672-1" style="width:80%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/tone_burst_iec_es_dark.png" alt="Respuestas de la envolvente Fast a ráfagas de 200, 50 y 10 ms alcanzando exactamente los valores de la Tabla 4 de IEC 61672-1" style="width:80%"> 77 109 78 - ## Estado inicial 110 + <details> 111 + <summary>Mostrar el código de esta figura</summary> 112 + 113 + ```python 114 + import numpy as np 115 + import matplotlib.pyplot as plt 116 + from phonometry import time_weighting 117 + 118 + fs = 48000 119 + t = np.arange(int(fs * 2)) / fs 120 + tone = np.sin(2 * np.pi * 4000 * t) 121 + 122 + # Referencia Fast en régimen permanente del tono continuo 123 + reference = time_weighting(tone, fs, mode='fast')[int(1.5 * fs):].mean() 124 + 125 + # Ráfaga de 200 ms del mismo tono (objetivo de la Tabla 4 de IEC 61672-1: -1.0 dB) 126 + burst = np.zeros_like(t) 127 + burst[int(0.5 * fs):int(0.7 * fs)] = tone[int(0.5 * fs):int(0.7 * fs)] 128 + envelope = time_weighting(burst, fs, mode='fast') 129 + env_db = 10 * np.log10(np.maximum(envelope / reference, 1e-6)) 130 + 131 + plt.figure() 132 + plt.plot(t, env_db, label='Envolvente Fast') 133 + plt.axhline(-1.0, linestyle='--', label='Objetivo IEC −1.0 dB') 134 + plt.xlabel('Tiempo [s]') 135 + plt.ylabel('Nivel respecto al régimen permanente [dB]') 136 + plt.legend() 137 + plt.show() 138 + ``` 139 + 140 + </details> 141 + 142 + ## 5. Estado inicial 79 143 80 144 Por defecto, el integrador exponencial parte del reposo (`y[-1] = 0`). Pasar 81 145 `initial_state=None` deja ese comportamiento por defecto, mientras que ··· 84 148 la primera muestra: 85 149 86 150 ```python 151 + # Usa `recording` y `fs` del snippet anterior. 87 152 energy_envelope = time_weighting(recording, fs, mode='fast', initial_state='first') 88 153 ``` 89 154 90 - ## Procesado por bloques 155 + ## 6. Procesado por bloques 91 156 92 157 Para procesar por bloques, pasa el último valor de salida del bloque anterior 93 158 como `initial_state` del siguiente en lugar de reiniciar en cada bloque: ··· 124 189 continua (verificado para los tres modos, mono y multicanal). Llama a 125 190 `tw.reset()` para volver al reposo. 126 191 127 - ## Nota de rendimiento 192 + ## 7. Nota de rendimiento 128 193 129 194 El modo `impulse` usa un kernel asimétrico que se compila JIT cuando 130 195 [numba](https://numba.pydata.org/) está instalado (`pip install phonometry[perf]`). ··· 135 200 para las métricas Leq/LN construidas sobre estas envolventes, y 136 201 [Por qué phonometry](/phonometry/es/reference/why-phonometry/) para la 137 202 verificación con ráfagas de tono de IEC 61672-1. 203 + 204 + --- 205 + 206 + **Normas.** IEC 61672-1:2013, *Electroacoustics — Sound level meters — 207 + Part 1: Specifications* — el detector exponencial de ponderación temporal 208 + (cláusula 3.8) con las constantes de tiempo F y S (cláusula 5.7), y las 209 + respuestas de referencia a ráfagas de tono de 4 kHz de la Tabla 4 (límites de 210 + aceptación de clase 1) usadas para verificar la respuesta temporal en CI.
+157 -42
site/src/content/docs/es/guides/weighting.md
··· 9 9 10 10 <img class="light-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/weighting_responses_es.png" alt="Curvas de ponderación A, C y Z con zoom de la región positiva de la curva A (+1,27 dB en 2,5 kHz)" style="width:80%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/weighting_responses_es_dark.png" alt="Curvas de ponderación A, C y Z con zoom de la región positiva de la curva A (+1,27 dB en 2,5 kHz)" style="width:80%"> 11 11 12 + <details> 13 + <summary>Mostrar el código de esta figura</summary> 14 + 15 + ```python 16 + import matplotlib.pyplot as plt 17 + import numpy as np 18 + from phonometry import weighting_filter 19 + 20 + # Medimos la respuesta de cada curva: ponderamos un impulso unitario 21 + # centrado y tomamos su espectro (búfer de 1 s -> resolución de 1 Hz). 22 + fs = 48000 23 + impulse = np.zeros(fs) 24 + impulse[fs // 2] = 1.0 25 + freqs = np.fft.rfftfreq(fs, 1 / fs) 26 + 27 + fig, ax = plt.subplots(figsize=(9, 5)) 28 + for curve in ("A", "C", "Z"): 29 + spectrum = np.fft.rfft(weighting_filter(impulse, fs, curve=curve)) 30 + ax.semilogx(freqs[1:], 20 * np.log10(np.abs(spectrum[1:])), label=curve) 31 + ax.set(xlim=(10, 20000), ylim=(-80, 10), 32 + xlabel="Frecuencia [Hz]", ylabel="Respuesta [dB]") 33 + ax.grid(True, which="both", alpha=0.3) 34 + ax.legend() 35 + plt.show() 36 + ``` 37 + 38 + </details> 39 + 12 40 * **Ponderación A (`A`):** estándar para ruido ambiental (IEC 61672-1). 13 41 * **Ponderación C (`C`):** para presión sonora de pico y ruido de alto nivel. 14 42 * **Ponderación Z (`Z`):** ponderación cero, respuesta completamente plana. 15 43 * **Ponderación G (`G`):** ponderación de infrasonido según ISO 7196 (ver más abajo). 16 44 17 - ```python 18 - import numpy as np 19 - from phonometry import weighting_filter 20 - 21 - # recording: una captura de micrófono calibrada (Pa) — grabada con tu cadena de medición. Sintetizada aquí para que la guía funcione por sí sola. 22 - fs = 48000 23 - recording = 0.2 * np.sin(2 * np.pi * 1000 * np.arange(fs) / fs) 24 - 25 - # Aplicar ponderación A a la señal cruda 26 - weighted_signal = weighting_filter(recording, fs, curve='A') 27 - 28 - # Aplicar ponderación C para análisis de picos 29 - c_weighted_signal = weighting_filter(recording, fs, curve='C') 30 - ``` 31 - 32 - ## Infrasonido: ponderación G (ISO 7196) 33 - 34 - La **ponderación frecuencial G** (ISO 7196:1995) valora el infrasonido igual que 35 - la ponderación A valora el ruido audible. Se define por una configuración de 36 - polos y ceros con ganancia de 0 dB en 10 Hz, sube a 12 dB/octava entre 1 Hz y 37 - 20 Hz (siguiendo el crecimiento abrupto de la percepción en esa banda) y cae a 38 - 24 dB/octava fuera de ella. Úsala con fuentes con energía significativa por 39 - debajo de 20 Hz (aerogeneradores, climatización, voladuras): 40 - 41 - ```python 42 - from phonometry import weighting_filter 43 - 44 - g_weighted = weighting_filter(recording, fs, curve='G') 45 - ``` 46 - 47 - <img class="light-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/g_weighting_response_es.png" alt="Respuesta en frecuencia de la ponderación G de 0,1 Hz a 1 kHz con los valores nominales de la Tabla 2 de ISO 7196 superpuestos" style="width:80%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/g_weighting_response_es_dark.png" alt="Respuesta en frecuencia de la ponderación G de 0,1 Hz a 1 kHz con los valores nominales de la Tabla 2 de ISO 7196 superpuestos" style="width:80%"> 48 - 49 - La implementación sigue exactamente los polos/ceros de la Tabla 1 de ISO 7196 y 50 - se verifica en CI contra todos los valores nominales de respuesta de la Tabla 2 51 - (0,25 Hz a 315 Hz). `WeightingFilter(fs, "G")` admite el mismo procesado 52 - multicanal y por bloques que A/C. Los niveles medidos con la curva G se 53 - expresan como L<sub>pG</sub> (o L<sub>Geq</sub> para el nivel equivalente). 54 - 55 - ## De dónde vienen las curvas 45 + ## 1. De dónde vienen las curvas 56 46 57 47 Las curvas A y C son líneas isofónicas invertidas, congeladas en filtros: 58 48 **A** aproxima la inversa de la histórica línea isofónica de 40 fonios (niveles ··· 71 61 ausencia de ponderación. La derivación completa de polos y ceros está en la 72 62 página de [Teoría](/phonometry/es/reference/theory/). 73 63 74 - ### Parámetros de `weighting_filter()` / `WeightingFilter` 64 + ## 2. Uso básico 65 + 66 + ```python 67 + import numpy as np 68 + from phonometry import weighting_filter 69 + 70 + # recording: una captura de micrófono calibrada (Pa) — grabada con tu cadena de medición. Sintetizada aquí para que la guía funcione por sí sola. 71 + fs = 48000 72 + recording = 0.2 * np.sin(2 * np.pi * 1000 * np.arange(fs) / fs) 73 + 74 + # Aplicar ponderación A a la señal cruda 75 + weighted_signal = weighting_filter(recording, fs, curve='A') 76 + 77 + # Aplicar ponderación C para análisis de picos 78 + c_weighted_signal = weighting_filter(recording, fs, curve='C') 79 + ``` 80 + 81 + ## 3. Infrasonido: ponderación G (ISO 7196) 82 + 83 + La **ponderación frecuencial G** (ISO 7196:1995) valora el infrasonido igual que 84 + la ponderación A valora el ruido audible. Se define por una configuración de 85 + polos y ceros con ganancia de 0 dB en 10 Hz, sube a 12 dB/octava entre 1 Hz y 86 + 20 Hz (siguiendo el crecimiento abrupto de la percepción en esa banda) y cae a 87 + 24 dB/octava fuera de ella. Úsala con fuentes con energía significativa por 88 + debajo de 20 Hz (aerogeneradores, climatización, voladuras): 89 + 90 + ```python 91 + from phonometry import weighting_filter 92 + 93 + # Usa `recording` y `fs` del snippet anterior. 94 + g_weighted = weighting_filter(recording, fs, curve='G') 95 + ``` 96 + 97 + <img class="light-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/g_weighting_response_es.png" alt="Respuesta en frecuencia de la ponderación G de 0,1 Hz a 1 kHz con los valores nominales de la Tabla 2 de ISO 7196 superpuestos" style="width:80%"><img class="dark-only" src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/g_weighting_response_es_dark.png" alt="Respuesta en frecuencia de la ponderación G de 0,1 Hz a 1 kHz con los valores nominales de la Tabla 2 de ISO 7196 superpuestos" style="width:80%"> 98 + 99 + <details> 100 + <summary>Mostrar el código de esta figura</summary> 101 + 102 + ```python 103 + import matplotlib.pyplot as plt 104 + import numpy as np 105 + from phonometry import weighting_filter 106 + 107 + # Medimos la respuesta G: ponderamos un impulso unitario centrado y 108 + # tomamos su espectro. Un búfer largo da la resolución que necesita el 109 + # rango de infrasonido (20 s -> 0,05 Hz). 110 + fs = 4000 111 + impulse = np.zeros(20 * fs) 112 + impulse[impulse.size // 2] = 1.0 113 + freqs = np.fft.rfftfreq(impulse.size, 1 / fs) 114 + spectrum = np.fft.rfft(weighting_filter(impulse, fs, curve="G")) 115 + 116 + fig, ax = plt.subplots(figsize=(9, 5)) 117 + ax.semilogx(freqs[1:], 20 * np.log10(np.abs(spectrum[1:]))) 118 + ax.plot(10, 0, "o", color="tab:red", label="0 dB en 10 Hz") 119 + ax.set(xlim=(0.1, 1000), ylim=(-90, 15), 120 + xlabel="Frecuencia [Hz]", ylabel="Respuesta de la ponderación G [dB]") 121 + ax.grid(True, which="both", alpha=0.3) 122 + ax.legend() 123 + plt.show() 124 + ``` 125 + 126 + </details> 127 + 128 + La implementación sigue exactamente los polos/ceros de la Tabla 1 de ISO 7196 y 129 + se verifica en CI contra todos los valores nominales de respuesta de la Tabla 2 130 + (0,25 Hz a 315 Hz). `WeightingFilter(fs, "G")` admite el mismo procesado 131 + multicanal y por bloques que A/C. Los niveles medidos con la curva G se 132 + expresan como L<sub>pG</sub> (o L<sub>Geq</sub> para el nivel equivalente). 133 + 134 + ## 4. Parámetros de `weighting_filter()` / `WeightingFilter` 75 135 76 136 | Parámetro | Tipo | Unidades | Rango / por defecto | Notas | 77 137 | :--- | :--- | :--- | :--- | :--- | ··· 82 142 | `stateful` | bool (solo clase) | — | por defecto `False` | Conserva el estado del filtro entre bloques (streaming) | 83 143 | `steady_ic` | bool (solo clase) | — | por defecto `False` | Condiciones iniciales estacionarias (sin transitorio de arranque) | 84 144 85 - ## Objeto de filtro reutilizable 145 + ## 5. Objeto de filtro reutilizable 86 146 87 147 Si ponderas muchas señales con los mismos parámetros, diseña el filtro una sola vez: 88 148 89 149 ```python 90 150 from phonometry import WeightingFilter 91 151 152 + # Usa `recording` y `fs` del snippet anterior. 92 153 wf = WeightingFilter(fs, "A") 93 154 signals = [recording] # tu lote de grabaciones 94 155 for recording in signals: 95 156 weighted = wf.filter(recording) 96 157 ``` 97 158 98 - ## Precisión en alta frecuencia (`high_accuracy`) 159 + ## 6. Precisión en alta frecuencia (`high_accuracy`) 99 160 100 161 Un diseño con transformación bilineal simple comprime la respuesta cerca de 101 162 Nyquist: a fs = 48 kHz el error de la curva A a 12,5 kHz alcanza −2,7 dB, fuera ··· 111 172 *El diseño bilineal simple (rojo) cruza la tolerancia de clase 1 cerca de 112 173 12,5 kHz; el diseño sobremuestreado (azul) se mantiene junto a la curva analítica.* 113 174 175 + <details> 176 + <summary>Mostrar el código de esta figura</summary> 177 + 178 + ```python 179 + import matplotlib.pyplot as plt 180 + import numpy as np 181 + from phonometry import weighting_filter 182 + 183 + # Respuesta medida de ambos diseños a fs = 48 kHz: ponderamos un impulso 184 + # unitario centrado y tomamos su espectro... 185 + fs = 48000 186 + impulse = np.zeros(fs) 187 + impulse[fs // 2] = 1.0 188 + freqs = np.fft.rfftfreq(fs, 1 / fs)[1:] 189 + 190 + # ...frente a la curva A analítica de IEC 61672-1 construida con las 191 + # cuatro frecuencias de esquina de la sección 1, normalizada a 0 dB en 1 kHz. 192 + f1, f2, f3, f4 = 20.599, 107.653, 737.862, 12194.217 193 + gain = (f4**2 * freqs**4) / ((freqs**2 + f1**2) 194 + * np.sqrt((freqs**2 + f2**2) * (freqs**2 + f3**2)) 195 + * (freqs**2 + f4**2)) 196 + analytic = 20 * np.log10(gain / gain[np.argmin(np.abs(freqs - 1000))]) 197 + 198 + fig, ax = plt.subplots(figsize=(9, 5)) 199 + ax.semilogx(freqs, analytic, "k--", label="Analítica (IEC 61672-1)") 200 + for high_accuracy, label in ((False, "Bilineal simple"), 201 + (True, "Sobremuestreado (por defecto)")): 202 + weighted = weighting_filter(impulse, fs, curve="A", 203 + high_accuracy=high_accuracy) 204 + response = 20 * np.log10(np.abs(np.fft.rfft(weighted)))[1:] 205 + ax.semilogx(freqs, response, label=label) 206 + ax.set(xlim=(1000, 20000), ylim=(-12, 3), 207 + xlabel="Frecuencia [Hz]", ylabel="Respuesta de la ponderación A [dB]") 208 + ax.grid(True, which="both", alpha=0.3) 209 + ax.legend() 210 + plt.show() 211 + ``` 212 + 213 + </details> 214 + 114 215 - `high_accuracy=False` restaura el comportamiento bilineal clásico. 115 216 - El **procesado por bloques (stateful)** usa siempre el diseño clásico: el 116 217 remuestreo FIR interno es incompatible con la continuidad entre bloques. Pasar 117 218 `high_accuracy=True` junto con `stateful=True` lanza un `ValueError`. 118 219 119 220 ```python 221 + from phonometry import WeightingFilter, weighting_filter 222 + 223 + # Usa `recording` y `fs` del snippet anterior. 120 224 # Comportamiento clásico explícito 121 225 y = weighting_filter(recording, fs, curve="A", high_accuracy=False) 122 226 ··· 130 234 Consulta [Procesado por bloques](/phonometry/es/guides/block-processing/) para 131 235 el flujo en streaming y [Teoría](/phonometry/es/reference/theory/) para las 132 236 definiciones analíticas de las curvas. 237 + 238 + --- 239 + 240 + **Normas.** IEC 61672-1:2013, *Electroacoustics — Sound level meters — 241 + Part 1: Specifications* — las curvas de ponderación frecuencial A, C y Z (la 242 + definición analítica del Anexo E a partir de cuatro frecuencias de esquina, 243 + normalizadas a 0 dB en 1 kHz) y las tolerancias de clase 1 que el diseño 244 + `high_accuracy` mantiene hasta 16 kHz. ISO 7196:1995, *Acoustics — 245 + Frequency-weighting characteristic for infrasound measurements* — la 246 + definición de polos y ceros de la ponderación G (Tabla 1), verificada frente a 247 + todos los valores nominales de respuesta de la Tabla 2 (0,25 Hz a 315 Hz).