sva_samples/measure/
loudness_pattern.rs1use std::f64::consts::TAU;
4
5use super::bands::{BAND_COUNT, FIRST_CAM, erb_hz};
6use crate::fft::fft;
7
8pub const MODEL: &str = "specific loudness after Moore, Glasberg and Baer (1997), J. Audio Eng. \
9 Soc. 45(4) 224-240, per frame: a periodic Hann of 2048 samples at 48 kHz \
10 (its length scaled to the rate) centred on each frame time, every \
11 channel's power per bin averaged, a full-scale sine at `spl_db`; the \
12 outer and middle ear as Terhardt's (1979) threshold in quiet, 3.64 \
13 f^-0.8 - 6.5 e^(-0.6 (f - 3.3)^2) + 0.001 f^4 dB (f in kHz), taken \
14 against its value at 1 kHz; each band's excitation the power through a \
15 rounded-exponential auditory filter (1 + p g) e^(-p g), g the \
16 frequency's distance from the centre over the centre, p = 4 fc / \
17 ERB(fc) above it and p - 0.35 (p / p(1 kHz)) (X - 51) below it, X the \
18 level in dB SPL within one ERB of the component, its upward spread of \
19 masking; N' = C ((E + A)^0.2 - A^0.2), A twice the excitation at \
20 threshold, times (2 E / (E + E_T))^1.5 under threshold E_T, C so a 1 \
21 kHz sine at 40 dB SPL is 1 sone; the total the sum over bands 1 Cam \
22 apart. A cell at or under `threshold_sone` is inaudible: a comparison \
23 counts it zero where both sides are. Measured, not ISO 532-2: no \
24 binaural inhibition and no temporal integration, and the threshold in \
25 quiet stands for the low-frequency internal noise.";
26
27const WINDOW_SAMPLES_48K: f64 = 2048.0;
28pub const DEFAULT_SPL_DB: f64 = 100.0;
29const ALPHA: f64 = 0.2;
30const LOWER_SLOPE_DB: f64 = 51.0;
31const LOWER_SLOPE: f64 = 0.35;
32const LEAST_LOWER_SHARE: f64 = 0.1;
33const CALIBRATION_HZ: f64 = 1000.0;
34const CALIBRATION_DB: f64 = 40.0;
35
36#[derive(Clone, Debug, PartialEq)]
37pub struct PatternFrame {
38 pub t_secs: f64,
39 pub total_sone: f64,
40 pub specific: Vec<f64>,
42}
43
44#[derive(Clone, Debug, PartialEq)]
45pub struct LoudnessPattern {
46 pub frame_secs: f64,
47 pub window_secs: f64,
48 pub spl_db: f64,
49 pub centres_hz: Vec<f64>,
50 pub threshold_sone: f64,
51 pub frames: Vec<PatternFrame>,
52}
53
54fn quiet_db(hz: f64) -> f64 {
56 let f = hz.max(20.0) / 1000.0;
57 3.64 * libm::pow(f, -0.8) - 6.5 * sva_formula::exp(-0.6 * (f - 3.3) * (f - 3.3))
58 + 1e-3 * f * f * f * f
59}
60
61fn centre_hz(cam: f64) -> f64 {
64 (libm::pow(10.0, cam / 21.4) - 1.0) * 1000.0 / 4.37
65}
66
67fn intensity(db: f64) -> f64 {
68 libm::pow(10.0, db / 10.0)
69}
70
71struct Ear {
72 centres: Vec<f64>,
73 threshold: f64,
74 a: f64,
75 c: f64,
76}
77
78impl Ear {
79 fn new(sr: f64) -> Ear {
80 let centres: Vec<f64> = (0..BAND_COUNT)
81 .map(|k| centre_hz(FIRST_CAM + k as f64))
82 .filter(|hz| *hz < sr / 2.0)
83 .collect();
84 let threshold = intensity(quiet_db(CALIBRATION_HZ));
85 let mut ear = Ear {
86 centres,
87 threshold,
88 a: 2.0 * threshold,
89 c: 1.0,
90 };
91 let tone = [(CALIBRATION_HZ, intensity(CALIBRATION_DB))];
92 let total: f64 = ear
93 .specific(&ear.excitation(&tone, &[CALIBRATION_DB]))
94 .iter()
95 .sum();
96 ear.c = 1.0 / total;
97 ear
98 }
99
100 fn excitation(&self, parts: &[(f64, f64)], per_erb_db: &[f64]) -> Vec<f64> {
103 let p_1k = 4.0 * 1000.0 / erb_hz(1000.0);
104 self.centres
105 .iter()
106 .map(|&fc| {
107 let p = 4.0 * fc / erb_hz(fc);
108 parts
109 .iter()
110 .zip(per_erb_db)
111 .map(|(&(hz, i), &x)| {
112 let g = (hz - fc).abs() / fc;
113 let slope = match hz < fc {
114 true => (p - LOWER_SLOPE * (p / p_1k) * (x - LOWER_SLOPE_DB))
115 .max(LEAST_LOWER_SHARE * p),
116 false => p,
117 };
118 let pg = slope * g;
119 i * (1.0 + pg) * sva_formula::exp(-pg)
120 })
121 .sum()
122 })
123 .collect()
124 }
125
126 fn specific(&self, excitation: &[f64]) -> Vec<f64> {
127 excitation
128 .iter()
129 .map(|&e| {
130 let grown = libm::pow(e + self.a, ALPHA) - libm::pow(self.a, ALPHA);
131 let near = match e < self.threshold {
132 true => libm::pow(2.0 * e / (e + self.threshold), 1.5),
133 false => 1.0,
134 };
135 self.c * near * grown
136 })
137 .collect()
138 }
139
140 fn at_threshold(&self) -> f64 {
141 self.specific(&[self.threshold])[0]
142 }
143}
144
145pub fn measure(
147 planes: &[&[f64]],
148 sr: f64,
149 start: f64,
150 hop_secs: f64,
151 spl_db: f64,
152) -> LoudnessPattern {
153 let len = super::held(planes);
154 let ear = Ear::new(sr);
155 let n = super::scaled_pow2(WINDOW_SAMPLES_48K, sr);
156 let hop = ((hop_secs * sr).round() as usize).max(1);
157 let w: Vec<f64> = (0..n)
158 .map(|k| 0.5 - 0.5 * libm::cos(TAU * k as f64 / n as f64))
159 .collect();
160 let norm = 2.0 / (n as f64 * n as f64 * 0.375) / 0.5 * intensity(spl_db);
161 let bin_hz = sr / n as f64;
162 let weights: Vec<f64> = (0..=n / 2)
163 .map(|k| intensity(quiet_db(CALIBRATION_HZ) - quiet_db(k as f64 * bin_hz)))
164 .collect();
165 let frames = (0..=len.saturating_sub(1) / hop)
166 .map(|f| {
167 let centre = (f * hop) as i64;
168 let mut power = vec![0.0; n / 2 + 1];
169 for plane in planes {
170 let (mut re, mut im) = (vec![0.0; n], vec![0.0; n]);
171 for (i, wi) in w.iter().enumerate() {
172 let at = centre + i as i64 - (n / 2) as i64;
173 if at >= 0 && (at as usize) < len {
174 re[i] = plane[at as usize] * wi;
175 }
176 }
177 fft(&mut re, &mut im);
178 for k in 0..=n / 2 {
179 power[k] += (re[k] * re[k] + im[k] * im[k]) / planes.len() as f64;
180 }
181 }
182 let parts: Vec<(f64, f64)> = (1..n / 2)
183 .map(|k| (k as f64 * bin_hz, power[k] * norm * weights[k]))
184 .collect();
185 let mut running = vec![0.0; parts.len() + 1];
186 for (i, p) in parts.iter().enumerate() {
187 running[i + 1] = running[i] + p.1;
188 }
189 let per_erb: Vec<f64> = parts
190 .iter()
191 .map(|&(hz, _)| {
192 let half = erb_hz(hz) / 2.0;
193 let first = ((hz - half) / bin_hz).ceil().max(1.0) as usize;
194 let last = (((hz + half) / bin_hz).floor() as usize).min(parts.len());
195 let held = running[last] - running[(first - 1).min(last)];
196 10.0 * libm::log10(held + 1e-30)
197 })
198 .collect();
199 let specific = ear.specific(&ear.excitation(&parts, &per_erb));
200 PatternFrame {
201 t_secs: start + (f * hop) as f64 / sr,
202 total_sone: specific.iter().sum(),
203 specific,
204 }
205 })
206 .collect();
207 LoudnessPattern {
208 frame_secs: hop as f64 / sr,
209 window_secs: n as f64 / sr,
210 spl_db,
211 threshold_sone: ear.at_threshold(),
212 centres_hz: ear.centres,
213 frames,
214 }
215}