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 * libm::exp(-0.6 * (f - 3.3) * (f - 3.3)) + 1e-3 * f * f * f * f
58}
59
60fn centre_hz(cam: f64) -> f64 {
63 (libm::pow(10.0, cam / 21.4) - 1.0) * 1000.0 / 4.37
64}
65
66fn intensity(db: f64) -> f64 {
67 libm::pow(10.0, db / 10.0)
68}
69
70struct Ear {
71 centres: Vec<f64>,
72 threshold: f64,
73 a: f64,
74 c: f64,
75}
76
77impl Ear {
78 fn new(sr: f64) -> Ear {
79 let centres: Vec<f64> = (0..BAND_COUNT)
80 .map(|k| centre_hz(FIRST_CAM + k as f64))
81 .filter(|hz| *hz < sr / 2.0)
82 .collect();
83 let threshold = intensity(quiet_db(CALIBRATION_HZ));
84 let mut ear = Ear {
85 centres,
86 threshold,
87 a: 2.0 * threshold,
88 c: 1.0,
89 };
90 let tone = [(CALIBRATION_HZ, intensity(CALIBRATION_DB))];
91 let total: f64 = ear
92 .specific(&ear.excitation(&tone, &[CALIBRATION_DB]))
93 .iter()
94 .sum();
95 ear.c = 1.0 / total;
96 ear
97 }
98
99 fn excitation(&self, parts: &[(f64, f64)], per_erb_db: &[f64]) -> Vec<f64> {
102 let p_1k = 4.0 * 1000.0 / erb_hz(1000.0);
103 self.centres
104 .iter()
105 .map(|&fc| {
106 let p = 4.0 * fc / erb_hz(fc);
107 parts
108 .iter()
109 .zip(per_erb_db)
110 .map(|(&(hz, i), &x)| {
111 let g = (hz - fc).abs() / fc;
112 let slope = match hz < fc {
113 true => (p - LOWER_SLOPE * (p / p_1k) * (x - LOWER_SLOPE_DB))
114 .max(LEAST_LOWER_SHARE * p),
115 false => p,
116 };
117 let pg = slope * g;
118 i * (1.0 + pg) * libm::exp(-pg)
119 })
120 .sum()
121 })
122 .collect()
123 }
124
125 fn specific(&self, excitation: &[f64]) -> Vec<f64> {
126 excitation
127 .iter()
128 .map(|&e| {
129 let grown = libm::pow(e + self.a, ALPHA) - libm::pow(self.a, ALPHA);
130 let near = match e < self.threshold {
131 true => libm::pow(2.0 * e / (e + self.threshold), 1.5),
132 false => 1.0,
133 };
134 self.c * near * grown
135 })
136 .collect()
137 }
138
139 fn at_threshold(&self) -> f64 {
140 self.specific(&[self.threshold])[0]
141 }
142}
143
144pub fn measure(
146 planes: &[&[f64]],
147 sr: f64,
148 start: f64,
149 hop_secs: f64,
150 spl_db: f64,
151) -> LoudnessPattern {
152 let len = super::held(planes);
153 let ear = Ear::new(sr);
154 let n = super::scaled_pow2(WINDOW_SAMPLES_48K, sr);
155 let hop = ((hop_secs * sr).round() as usize).max(1);
156 let w: Vec<f64> = (0..n)
157 .map(|k| 0.5 - 0.5 * libm::cos(TAU * k as f64 / n as f64))
158 .collect();
159 let norm = 2.0 / (n as f64 * n as f64 * 0.375) / 0.5 * intensity(spl_db);
160 let bin_hz = sr / n as f64;
161 let weights: Vec<f64> = (0..=n / 2)
162 .map(|k| intensity(quiet_db(CALIBRATION_HZ) - quiet_db(k as f64 * bin_hz)))
163 .collect();
164 let frames = (0..=len.saturating_sub(1) / hop)
165 .map(|f| {
166 let centre = (f * hop) as i64;
167 let mut power = vec![0.0; n / 2 + 1];
168 for plane in planes {
169 let (mut re, mut im) = (vec![0.0; n], vec![0.0; n]);
170 for (i, wi) in w.iter().enumerate() {
171 let at = centre + i as i64 - (n / 2) as i64;
172 if at >= 0 && (at as usize) < len {
173 re[i] = plane[at as usize] * wi;
174 }
175 }
176 fft(&mut re, &mut im);
177 for k in 0..=n / 2 {
178 power[k] += (re[k] * re[k] + im[k] * im[k]) / planes.len() as f64;
179 }
180 }
181 let parts: Vec<(f64, f64)> = (1..n / 2)
182 .map(|k| (k as f64 * bin_hz, power[k] * norm * weights[k]))
183 .collect();
184 let mut running = vec![0.0; parts.len() + 1];
185 for (i, p) in parts.iter().enumerate() {
186 running[i + 1] = running[i] + p.1;
187 }
188 let per_erb: Vec<f64> = parts
189 .iter()
190 .map(|&(hz, _)| {
191 let half = erb_hz(hz) / 2.0;
192 let first = ((hz - half) / bin_hz).ceil().max(1.0) as usize;
193 let last = (((hz + half) / bin_hz).floor() as usize).min(parts.len());
194 let held = running[last] - running[(first - 1).min(last)];
195 10.0 * libm::log10(held + 1e-30)
196 })
197 .collect();
198 let specific = ear.specific(&ear.excitation(&parts, &per_erb));
199 PatternFrame {
200 t_secs: start + (f * hop) as f64 / sr,
201 total_sone: specific.iter().sum(),
202 specific,
203 }
204 })
205 .collect();
206 LoudnessPattern {
207 frame_secs: hop as f64 / sr,
208 window_secs: n as f64 / sr,
209 spl_db,
210 threshold_sone: ear.at_threshold(),
211 centres_hz: ear.centres,
212 frames,
213 }
214}
215
216pub fn sharpness(centres_hz: &[f64], specific: &[f64]) -> Option<f64> {
218 let total: f64 = specific.iter().sum();
219 if total <= 0.0 {
220 return None;
221 }
222 let moment: f64 = centres_hz
223 .iter()
224 .zip(specific)
225 .map(|(&hz, &n)| {
226 let z = 26.81 * hz / (1960.0 + hz) - 0.53;
227 let g = match z > 15.8 {
228 true => 0.15 * libm::exp(0.42 * (z - 15.8)) + 0.85,
229 false => 1.0,
230 };
231 n * g * z
232 })
233 .sum();
234 Some(0.11 * moment / total)
235}