1use sva_formula::filter::Shape;
4
5use crate::biquad::{Coeffs, State, design};
6
7pub fn erb_hz(hz: f64) -> f64 {
9 24.7 * (4.37 * hz / 1000.0 + 1.0)
10}
11
12pub fn cam(hz: f64) -> f64 {
14 21.4 * (1.0 + 4.37 * hz / 1000.0).log10()
15}
16
17pub fn hz_at_cam(cam: f64) -> f64 {
18 (10f64.powf(cam / 21.4) - 1.0) * 1000.0 / 4.37
19}
20
21pub const FIRST_CAM: f64 = 1.0;
23pub const BAND_COUNT: usize = 40;
24
25pub const DECIMATED_HZ: f64 = 2000.0;
27
28fn floor_secs(erb: f64) -> f64 {
30 (16.0 / erb).clamp(0.02, 2.0)
31}
32
33#[derive(Clone, Copy, Debug, PartialEq)]
35pub struct BandFloor {
36 pub peak: f64,
37 pub time_to_peak_secs: f64,
38 pub rise_10_90_secs: Option<f64>,
39}
40
41#[derive(Clone, Debug, PartialEq)]
42pub struct BandTrack {
43 pub centre_hz: f64,
44 pub cam: f64,
45 pub erb_hz: f64,
46 pub q: f64,
47 pub peak: f64,
48 pub time_to_peak_secs: Option<f64>,
49 pub rise_10_90_secs: Option<f64>,
50 pub floor: BandFloor,
51 pub rms: Vec<f64>,
52}
53
54#[derive(Clone, Debug, PartialEq)]
55pub struct Bands {
56 pub rate_hz: f64,
57 pub start_secs: f64,
58 pub bands: Vec<BandTrack>,
59}
60
61#[derive(Clone, Copy, Debug, PartialEq)]
63pub struct Smoothing {
64 pub hz: Option<f64>,
65 pub poles: usize,
66}
67
68const OWN_WIDTH: Smoothing = Smoothing { hz: None, poles: 1 };
69
70struct Chain {
72 pass: Coeffs,
73 smooth: Coeffs,
74 a: State,
75 b: State,
76 energy: Vec<State>,
77}
78
79impl Chain {
80 fn new(centre_hz: f64, sr: f64, smoothing: Smoothing) -> Chain {
81 let erb = erb_hz(centre_hz);
82 let hz = smoothing.hz.unwrap_or(erb.min(DECIMATED_HZ / 4.0));
83 Chain {
84 pass: design(Shape::Bandpass, centre_hz, centre_hz / erb, 0.0, sr),
85 smooth: design(Shape::OnePole, hz, 0.0, 0.0, sr),
86 a: State::default(),
87 b: State::default(),
88 energy: vec![State::default(); smoothing.poles],
89 }
90 }
91
92 #[inline]
93 fn step(&mut self, x: f64) -> f64 {
94 let banded = self.b.step(&self.pass, self.a.step(&self.pass, x));
95 self.energy
96 .iter_mut()
97 .fold(banded * banded, |e, pole| pole.step(&self.smooth, e))
98 }
99}
100
101fn trace(
102 centre_hz: f64,
103 sr: f64,
104 smoothing: Smoothing,
105 (hop, n, width): (usize, usize, usize),
106 at: impl Fn(usize, usize) -> f64,
107) -> Vec<f64> {
108 let mut chains: Vec<Chain> = (0..width)
109 .map(|_| Chain::new(centre_hz, sr, smoothing))
110 .collect();
111 let mut out = Vec::with_capacity(n / hop + 1);
112 for i in 0..n {
113 let mut each = chains
114 .iter_mut()
115 .enumerate()
116 .map(|(c, chain)| chain.step(at(c, i)));
117 let first = each.next().expect("a channel");
118 let energy = each.fold(first, |sum, e| sum + e) / width as f64;
119 if i % hop == 0 {
120 out.push(energy.max(0.0).sqrt());
121 }
122 }
123 out
124}
125
126fn crossing(track: &[f64], until: usize, level: f64, rate: f64) -> Option<f64> {
128 let hit = track[..=until].iter().position(|&v| v >= level)?;
129 if hit == 0 {
130 return Some(0.0);
131 }
132 let (lo, hi) = (track[hit - 1], track[hit]);
133 let frac = match hi > lo {
134 true => (level - lo) / (hi - lo),
135 false => 0.0,
136 };
137 Some((hit as f64 - 1.0 + frac) / rate)
138}
139
140fn stats(track: &[f64], rate: f64) -> (f64, Option<f64>, Option<f64>) {
141 let peak = track.iter().copied().fold(0.0, f64::max);
142 if peak <= 0.0 {
143 return (peak, None, None);
144 }
145 let at = track
146 .iter()
147 .position(|&v| v == peak)
148 .expect("the peak is one of the frames");
149 let t10 = crossing(track, at, 0.1 * peak, rate);
150 let t90 = crossing(track, at, 0.9 * peak, rate);
151 let rise = t10.zip(t90).map(|(a, b)| (b - a).max(0.0));
152 (peak, Some(at as f64 / rate), rise)
153}
154
155fn floor_of(centre_hz: f64, sr: f64, smoothing: Smoothing, hop: usize, rate: f64) -> BandFloor {
156 let n = (floor_secs(erb_hz(centre_hz)) * sr).round().max(4.0) as usize;
157 let track = trace(centre_hz, sr, smoothing, (hop, n, 1), |_, i| {
158 f64::from(u8::from(i == 0))
159 });
160 let (peak, at, rise) = stats(&track, rate);
161 BandFloor {
162 peak,
163 time_to_peak_secs: at.unwrap_or(0.0),
164 rise_10_90_secs: rise,
165 }
166}
167
168pub const HIGHEST_CENTRE_OF_RATE: f64 = 0.45;
169
170pub(crate) fn centres(sr: f64) -> Vec<f64> {
172 (0..BAND_COUNT)
173 .map(|k| hz_at_cam(FIRST_CAM + k as f64))
174 .filter(|hz| *hz < HIGHEST_CENTRE_OF_RATE * sr)
175 .collect()
176}
177
178pub fn least_rate_hz() -> f64 {
179 hz_at_cam(FIRST_CAM) / HIGHEST_CENTRE_OF_RATE
180}
181
182pub fn analyze(planes: &[&[f64]], sr: f64, start_secs: f64) -> Bands {
183 smoothed(planes, sr, start_secs, OWN_WIDTH)
184}
185
186pub const PRUNED_MODEL: &str = "each band's track as `bands` traces it, read at every source \
187 sample instead of every 2 kHz one, in dB, 20 log10 of its \
188 rms, any level under `floor_db` (the band's peak plus the \
189 asked floor) cut to it. Its knots are a swing filter's \
190 (Elmeleegy, H. et al. (2009), Online piece-wise linear \
191 approximation of numerical streams with precision \
192 guarantees, Proc. VLDB Endow. 2(1) 145-156): from each knot \
193 the next is the last sample some line from it stays within \
194 eps_db - g/2 of every sample since, its value that line's, \
195 rounded to a grid of g dB, g the largest power of ten, at \
196 most 1, not over eps_db / 5, so the polyline through the \
197 stored knots is within eps_db of the cut track at every \
198 source sample. `max_error_db` is the largest distance \
199 between the two, evaluated there, the knots joined \
200 linearly in dB. Peak, time to peak and rise are read off \
201 every source sample, the impulse floor too";
202
203fn grid_per_db(eps_db: f64) -> f64 {
206 let decimals = (5.0 / eps_db).log10().ceil().max(0.0) as usize;
207 (0..decimals).fold(1.0, |per, _| per * 10.0)
208}
209
210pub const LEAST_PRUNE_DB: f64 = 1e-6;
213
214#[derive(Clone, Copy, Debug, PartialEq)]
216pub struct Prune {
217 pub eps_db: f64,
218 pub floor_db: f64,
219}
220
221pub type Knot = (usize, f64);
223
224#[derive(Clone, Debug, PartialEq)]
225pub struct PrunedBand {
226 pub centre_hz: f64,
227 pub cam: f64,
228 pub erb_hz: f64,
229 pub q: f64,
230 pub peak: f64,
231 pub time_to_peak_secs: Option<f64>,
232 pub rise_10_90_secs: Option<f64>,
233 pub floor: BandFloor,
234 pub floor_db: Option<f64>,
236 pub max_error_db: f64,
237 pub knots: Vec<Knot>,
238}
239
240#[derive(Clone, Debug, PartialEq)]
242pub struct PrunedBands {
243 pub rate_hz: f64,
244 pub start_secs: f64,
245 pub prune: Prune,
246 pub bands: Vec<PrunedBand>,
247}
248
249pub fn pruned(planes: &[&[f64]], sr: f64, start_secs: f64, prune: Prune) -> PrunedBands {
250 let shape = (crate::measure::held(planes), planes.len());
251 PrunedBands {
252 rate_hz: sr,
253 start_secs,
254 prune,
255 bands: centres(sr)
256 .into_iter()
257 .map(|centre_hz| {
258 let (n, width) = shape;
259 let track = trace(centre_hz, sr, OWN_WIDTH, (1, n, width), |c, i| planes[c][i]);
260 let (peak, at, rise) = stats(&track, sr);
261 let floor_db = (peak > 0.0).then(|| 20.0 * peak.log10() + prune.floor_db);
262 let levels = |floor: f64| track.iter().map(move |v| (20.0 * v.log10()).max(floor));
263 let (knots, max_error_db) = match floor_db {
264 Some(floor) => {
265 let knots = swing(levels(floor), prune.eps_db);
266 let worst = worst(levels(floor), &knots);
267 (knots, worst)
268 }
269 None => (Vec::new(), 0.0),
270 };
271 let erb = erb_hz(centre_hz);
272 PrunedBand {
273 centre_hz,
274 cam: cam(centre_hz),
275 erb_hz: erb,
276 q: centre_hz / erb,
277 peak,
278 time_to_peak_secs: at,
279 rise_10_90_secs: rise,
280 floor: floor_of(centre_hz, sr, OWN_WIDTH, 1, sr),
281 floor_db,
282 max_error_db,
283 knots,
284 }
285 })
286 .collect(),
287 }
288}
289
290fn swing(levels: impl Iterator<Item = f64>, eps_db: f64) -> Vec<Knot> {
293 let per = grid_per_db(eps_db);
294 let on_grid = |db: f64| (db * per).round() / per;
295 let within = (eps_db - 0.5 / per) * (1.0 - 1e-9);
296 let mut levels = levels.enumerate();
297 let Some((_, first)) = levels.next() else {
298 return Vec::new();
299 };
300 let mut anchor = (0, on_grid(first));
301 let mut knots = vec![anchor];
302 let (mut last, mut lo, mut hi) = ((0, first), f64::NEG_INFINITY, f64::INFINITY);
303 let close = |anchor: Knot, (at, db): Knot, lo: f64, hi: f64| {
304 let run = (at - anchor.0) as f64;
305 let slope = ((db - anchor.1) / run).clamp(lo, hi);
306 (at, on_grid(anchor.1 + slope * run))
307 };
308 for (at, db) in levels {
309 let run = (at - anchor.0) as f64;
310 let (low, high) = (
311 (db - within - anchor.1) / run,
312 (db + within - anchor.1) / run,
313 );
314 if low.max(lo) > high.min(hi) {
315 anchor = close(anchor, last, lo, hi);
316 knots.push(anchor);
317 let run = (at - anchor.0) as f64;
318 (lo, hi) = (
319 (db - within - anchor.1) / run,
320 (db + within - anchor.1) / run,
321 );
322 } else {
323 (lo, hi) = (low.max(lo), high.min(hi));
324 }
325 last = (at, db);
326 }
327 if last.0 > anchor.0 {
328 knots.push(close(anchor, last, lo, hi));
329 }
330 knots
331}
332
333fn worst(levels: impl Iterator<Item = f64>, knots: &[Knot]) -> f64 {
335 let mut joins = knots.windows(2).peekable();
336 let mut most = 0f64;
337 for (at, db) in levels.enumerate() {
338 let line = match joins.peek() {
339 None => knots[0].1,
340 Some(&[a, b]) => a.1 + (b.1 - a.1) * (at - a.0) as f64 / (b.0 - a.0) as f64,
341 Some(_) => unreachable!("windows of two"),
342 };
343 most = most.max((line - db).abs());
344 if joins.peek().is_some_and(|pair| at == pair[1].0) {
345 joins.next();
346 }
347 }
348 most
349}
350
351pub fn smoothed(planes: &[&[f64]], sr: f64, start_secs: f64, smoothing: Smoothing) -> Bands {
352 let shape = (crate::measure::held(planes), planes.len());
353 let hop = (sr / DECIMATED_HZ).round().max(1.0) as usize;
354 let rate = sr / hop as f64;
355 Bands {
356 rate_hz: rate,
357 start_secs,
358 bands: centres(sr)
359 .into_iter()
360 .map(|centre_hz| {
361 let (n, width) = shape;
362 let track = trace(centre_hz, sr, smoothing, (hop, n, width), |c, i| {
363 planes[c][i]
364 });
365 let (peak, at, rise) = stats(&track, rate);
366 let erb = erb_hz(centre_hz);
367 BandTrack {
368 centre_hz,
369 cam: cam(centre_hz),
370 erb_hz: erb,
371 q: centre_hz / erb,
372 peak,
373 time_to_peak_secs: at,
374 rise_10_90_secs: rise,
375 floor: floor_of(centre_hz, sr, smoothing, hop, rate),
376 rms: track,
377 }
378 })
379 .collect(),
380 }
381}