feat: electroacoustics — distortion (IEC 60268-3 / AES17) and frequency response (Bendat & Piersol) (#143)
* docs: design spec for electroacoustics distortion and frequency response
* feat: electroacoustic distortion and frequency response (IEC 60268-3 / AES17 / Bendat & Piersol)
Add two modules for audio-equipment characterisation from a captured signal.
distortion.py (IEC 60268-3:2013 + AES17-2015):
- thd (THD_F/THD_R), harmonic_distortion (nth-order)
- thd_plus_noise, sinad (AES17 standard notch, transient-trimmed residual)
- weighted_thd (A/C weighting of the residual)
- modulation_distortion (SMPTE), difference_frequency_distortion (CCIF),
total_difference_frequency_distortion, dynamic_intermodulation_distortion (DIM,
Table 2 nine products of the 15 kHz / 3.15 kHz signal)
- harmonic_analysis -> HarmonicDistortionResult with .plot()
frequency_response.py (Bendat & Piersol, Random Data 4e):
- transfer_function (H1 = Gxy/Gxx, H2 = Gyy/Gyx)
- coherence (ordinary gamma^2)
- FrequencyResponseResult with a Bode + coherence .plot()
Every quantity is verified against an exact analytic oracle (synthetic signals
with known harmonic/intermodulation amplitudes; a known LTI path with
gamma^2 = 1 noiseless and SNR/(1+SNR) with output noise). Adds a new conformance
domain (7 checks), tests, four-variant SVG figures, docs (EN + site EN/ES),
api-reference, indices and the astro sidebar.
* review: harden electroacoustics per AI-reviewer feedback
- distortion: reject a fundamental at/above Nyquist before the notch (clear
error instead of a SciPy crash); validate notch_q in weighted_thd for parity
with thd_plus_noise; narrow the harmonic peak-search window to ±0.1·f0 so a
nearby non-harmonic tone is not latched onto a harmonic bin.
- frequency_response: clamp noverlap to nperseg-1 (high overlap no longer
raises); use gyx != 0 instead of abs(gyx) > 0 for the H2 zero-guard.
- tests: assert the full nine-product DIM Table 2 list; add high-overlap,
above-Nyquist and weighted_thd notch_q validation cases.
- conformance: extract a shared _electro_tone helper (de-duplication).
- api-reference: document the window parameter of thd_plus_noise/harmonic_analysis.
* review: avoid float-equality zero-guard flagged by SonarCloud
Use |gyx|^2 > 0 for the H2 denominator guard instead of gyx != 0 — keeps the
cheaper (no sqrt) form gemini asked for while satisfying SonarCloud's
no-float-equality reliability rule.
feat: psychoacoustic annoyance and fluctuation strength (Fastl & Zwicker) (#142)
* docs: design spec for psychoacoustic annoyance and fluctuation strength
* feat: psychoacoustic annoyance and fluctuation strength (Fastl & Zwicker)
Close the modern sound-quality set with two new modules.
Psychoacoustic annoyance (Fastl & Zwicker Eqs 16.2-16.4; origin Widmann 1992):
`psychoacoustic_annoyance(n5, sharpness, fluctuation_strength, roughness)`
computes PA = N5·sqrt(1 + wS² + wFR²) exactly, and
`psychoacoustic_annoyance_from_signal` derives the four sensations from a
calibrated signal (documented model-mixing caveat; F is free-field).
Fluctuation strength (Fastl & Zwicker Ch. 10; no ISO standard):
`fluctuation_strength_am_noise` is the exact closed form for AM broadband
noise (Eq. 10.2); `fluctuation_strength` is the Osses et al. (2016) signal
model, implemented clean-room and cross-checked against the paper's Table 1
literature values and the open SQAT reference (used only as a numeric oracle).
Calibrated so the 1 kHz / 60 dB / m=1 / 4 Hz AM tone reads 1.00 vacil by
construction; the AM-tone 70 dB modulation sweep tracks the literature at
Pearson r ≈ 0.98 with the 4 Hz band-pass peak. The Osses model is free-field
only (no diffuse variant), so the function takes no `field` argument.
- 27 new tests (exact PA/closed-form + signal cross-check); full suite 2266 pass
- conformance +3 (PA exact, Eq. 10.2 exact, calibration): 148/148, no drift
- EN/ES guides, api-reference, README, sidebar; two figures (4 variants each)
- `.plot()` on both result dataclasses
* fix: address PR review findings (PA/fluctuation strength)
- fluctuation_strength: guard _cross_covariance against catastrophic
cancellation — a (near-)constant band envelope could make dx·dy slightly
negative, and sqrt() then returned NaN (which slipped past the `denom <= 0`
test and poisoned the sum). Clamp the product to 0 (gemini/coderabbit).
- fluctuation_strength: precompute the Bark-inversion grid at module load
instead of rebuilding a 20k-point grid on every _bark_center_hz call (copilot).
- _plotting: let callers override color/edgecolor in plot_psychoacoustic_annoyance
via **kwargs instead of raising TypeError on a duplicate keyword (coderabbit).
- generate_graphs: the closed-form Eq. 10.2 maximum is at ∛50 ≈ 3.68 Hz, so
relabel the 4 Hz guide line as a reference (the marker already sits on the
true peak); update the ES string. Regenerate the 4 figure variants (coderabbit).
- docs: add the missing combined legend to the fluctuation-strength figure
snippet in the EN/ES guides (coderabbit).
- reference_data: sync the wFR worked-example comment with the constant (copilot).
feat: underwater acoustics — reference levels, ship radiated noise & pile driving (ISO 18405/17208/18406) (#144)
New underwater-acoustics domain (PR-A of #19), clean-room from the standard texts:
- underwater_acoustics — ISO 18405:2017 reference levels re 1 µPa: SPL, SEL (re 1 µPa²·s), zero-to-peak, and 1 µPa <-> 20 µPa re-referencing.
- ship_radiated_noise — ISO 17208-1/-2: radiated noise level (re 1 µPa·m), equivalent monopole source level via the Lloyd's-mirror correction (d_s = 0.7·D), three-hydrophone geometry, tabulated uncertainty.
- pile_driving_noise — ISO 18406:2017: single-strike SEL, cumulative SEL (energy sum and N-identical), and per-strike metrics (SEL, peak SPL, Leq, 90 %-energy pulse duration) with .plot().
Conformance domain with 6 checks (161/161). Analytic oracles independent of the implementation, including an externally hand-computed Lloyd's-mirror transcription guard. Figures in EN/ES × light/dark; EN + site EN/ES guides, API reference and README.
Aeroacoustics (ICAO EPNL, IEC 61265/61400-11) is a separate follow-up PR (PR-B of #19).