1use std::f64::consts::{PI, TAU};
4use std::ops::Range;
5
6use sva_formula::C64;
7use sva_formula::filter::Shape;
8
9use super::bands::{self, Bands, Smoothing};
10use super::periodicity::SPLIT_HZ;
11use super::sliding::{self, Span, Stats, Unread, stats};
12use crate::biquad::{Coeffs, design};
13use crate::fft::fft;
14use crate::stft::hann_periodic;
15
16pub const SPLITS_HZ: [f64; 2] = [SPLIT_HZ, 2000.0];
18pub const MIN_HZ: f64 = 0.5;
19pub const MAX_HZ: f64 = 20.0;
20pub const PEAKS: usize = 3;
21pub const SMOOTHING: Smoothing = Smoothing {
25 hz: Some(SMOOTHING_HZ),
26 poles: 4,
27};
28const SMOOTHING_HZ: f64 = 200.0;
29const ENVELOPE_HZ: f64 = 100.0;
31const LOWPASS_HZ: f64 = 50.0;
32const HALF_TAPS: usize = 128;
34
35pub const MODEL: &str = "modulation spectrum: the spectrum of each band's amplitude envelope \
36 over a sliding window, the representation of Atlas, L. and Shamma, S. \
37 A. (2003), Joint acoustic and modulation frequency, EURASIP J. Appl. \
38 Signal Process. 2003(7) 668-675, read over the 0.5-20 Hz modulation \
39 range the envelope filterbank of Dau, T., Kollmeier, B. and \
40 Kohlrausch, A. (1997), Modeling auditory processing of amplitude \
41 modulation I, J. Acoust. Soc. Am. 102(5) 2892-2905, models. Envelopes: \
42 the RMS of each ERB band of `bands` (Glasberg and Moore's filter \
43 widths), its energy smoothed by `smoothing.poles` one-pole lowpasses at \
44 `smoothing.hz` in place of the one at the band's own width, decimated \
45 to 2 kHz, then by a 257-tap Blackman-windowed sinc lowpass at 50 Hz to \
46 `envelope_hz` (Harris, F. J. (1978), On the use of windows for \
47 harmonic analysis with the discrete Fourier transform, Proc. IEEE \
48 66(1) 51-83: stopband some 74 dB down, from about 71 Hz, so what \
49 would fold into 0-20 Hz is that far down), the taps renormalized \
50 where they reach past the target. G(f) = |H(f)| L(f), H the \
51 smoothing's response on the energy and L the 50 Hz lowpass's on the \
52 envelope, off their coefficients, is what a modulation at f keeps to \
53 first order, and each envelope peak's depth is divided by G at its \
54 rate, a centroid peak's by L; `passband_gain_at_max_hz` is G at \
55 `max_hz`. Every \
56 channel's band energy averaged. Groups: a band is `low` where its centre is under \
57 `splits_hz[0]`, `mid` under `splits_hz[1]` and `high` from it; the \
58 filters' skirts overlap, so a tone leaks attenuated into the \
59 neighbouring group, and its modulation with it. Each window is \
60 `window_secs` of envelope centred on t, t stepping by `hop_secs` \
61 from the target's start as every framed reading steps, clipped to \
62 the target at its ends, `from`/`to` its span; a periodic Hann window \
63 w over it, each band's Hann-weighted mean c = sum w e / sum w \
64 subtracted (so the mean leaves no lobe), zero-padded to `bin_step_hz` \
65 and transformed by FFT. depth(f) = 2 sum over the group's bands of \
66 |W(f)| over (sum w times sum of c): the modulation index m of an \
67 envelope c (1 + m cos 2 pi f t), read as the c-weighted mean of the \
68 bands' indices, magnitudes summed so bands moving out of phase, as \
69 under a filter sweep, do not cancel; an envelope near silent \
70 between swells reads past 1, up to 2 for a pulse train. `level` is \
71 the sum of c, linear amplitude. peaks: the three local maxima of \
72 depth from `min_hz` to `max_hz` whose bins read deepest, the deepest \
73 after correction first, each placed by inverting the periodic Hann's \
74 exact transform W through its bin and the deeper neighbour, as \
75 Grandke, T. (1983), Interpolation algorithms for discrete Fourier \
76 transforms of weighted signals, IEEE Trans. Instrum. Meas. 32(2) \
77 350-355, does between unpadded bins: the offset u, within q of the \
78 bin, where |W(q - u)| / |W(u)| is the two bins' ratio, q the padded \
79 bin in window bins, that ratio rising with u; its depth the bin's \
80 over |W(u)| / W(0), then over G. Bounds: under the model that in the \
81 window each band's envelope ahead of the smoothing is a carrier's \
82 amplitude c (1 + m cos(2 pi f t + phi)), one m <= 1 in every band, \
83 the carrier's ripple wholly removed by the smoothing, and a centroid \
84 is mean + depth cos(2 pi f t + phi), the two bins read the main lobe \
85 moved by: the modulation's image at -f and the removed mean, at most \
86 r(2 (x - q)) + 2 r(x - q)^2 of its amplitude, x the bin in window \
87 bins, r(y) the transform's envelope over its sidelobes (|W| with \
88 |sin pi y| taken as 1, falling for 1 < y < n/2), the mean also moving \
89 the level by m r(x - q); and for an envelope the square law: the \
90 smoothing h, positive of unit sum, acts on energy, so each envelope \
91 is h * a plus d, 0 <= d = var_h(a) / (sqrt(h * a^2) + h * a) <= c \
92 min(m^2 V / (2 (1 - m)), m sqrt V), V = (1 - |H(f)|^2 + |H(2f) - \
93 H(f)^2|) / 2 at the bin's rate plus q, V rising with f, so the \
94 lowpass (S the sum of its taps' magnitudes) moves a bin by at most \
95 (1 + 3 r(x - q)) S c min(...) and the level by S c min(...). The \
96 ratio widened by those, \
97 inverted, gives `hz_bounds`; the bin over the kernel's most and \
98 least across them and over G's (|H| falling with f, L moving no \
99 faster than its taps' slope) gives `depth_bounds`; m's ceiling is \
100 1, or lower where the square law's sqrt V branch bounds it, and each \
101 round's depth ceiling bounds the next. `period_beats_bounds` follow \
102 from `hz_bounds`. They cover the bins, the interpolation, the \
103 correction and the square law alone, under that model: a tone's \
104 sidebands f either side meet each band's skirt unevenly, so the \
105 bands' envelopes carry a tone's index changed, by up to several \
106 percent near `max_hz` with where the tone falls among the bands, \
107 which is the bands' view and not an error the bounds cover; a second \
108 modulation, partial or component near f is outside the model. They \
109 are null where the window is clipped, where no round bounds the \
110 peak (the perturbations swamping the main lobe), where the two \
111 bins' ratio, widened by those, is one no main lobe reads at bins q \
112 apart, as at a sidelobe, or where the depth's floor stands over its \
113 ceiling, refuting the model. Deep, fast \
114 modulation bounds wide: near `max_hz` the square law's bound reaches \
115 a fifth of m. A strong peak's Hann sidelobes, some \
116 31 dB under it and 1.5 / window beside it, read as peaks of their \
117 own; a modulation that is no sinusoid reads at its rate and its \
118 harmonics; two modulations closer than \
119 `resolution_hz`, the Hann main lobe's half-width 2 / window, or one \
120 slower than that, interfere and are not resolved, and a window \
121 clipped at the target's ends reads from 2 / (to - from) Hz. \
122 period_beats, where `bpm` is given: bpm / (60 hz), so 0.5 is an \
123 eighth-note wobble. centroid: per instant the group's spectral \
124 centroid, sum of centre e over sum of e across its bands at 2 kHz, \
125 lowpassed and decimated as the envelopes are; `mean_hz` its \
126 Hann-weighted mean, and its peaks read as the envelopes' are, their \
127 `depth_hz` the amplitude in Hz of a centroid swinging as mean + depth \
128 cos 2 pi f t, which a filter sweep moves and tremolo does not; null \
129 where any instant in the window has no energy in the group. Beating \
130 between partials inside one band reads as modulation at their \
131 difference, as it is heard; a modulation under `min_hz` (a pad \
132 evolving over seconds) reads in `level` and `centroid.mean_hz` from \
133 window to window, not in the peaks. No threshold: a static sound \
134 reads peaks of small depth, never none. summary: the leading peak's \
135 hz and depth over the windows with a peak, the centroid's mean_hz \
136 over the windows with one, the leading centroid peak's depth_hz over \
137 the windows with one, each its mean, min and max with the t each \
138 stands at. Exact path: none; a closed form is read \
139 off its rendered samples, because an envelope's band-limited \
140 spectrum over a window is stated by no closed form";
141
142#[derive(Clone, Copy, Debug, PartialEq, Eq)]
143pub enum Group {
144 Low,
145 Mid,
146 High,
147}
148
149impl Group {
150 pub const ALL: [Group; 3] = [Group::Low, Group::Mid, Group::High];
151
152 pub fn key(self) -> &'static str {
153 match self {
154 Group::Low => "low",
155 Group::Mid => "mid",
156 Group::High => "high",
157 }
158 }
159}
160
161#[derive(Clone, Copy, Debug, PartialEq)]
164pub struct Peak {
165 pub hz: f64,
166 pub hz_bounds: Option<(f64, f64)>,
167 pub period_beats: Option<f64>,
168 pub period_beats_bounds: Option<(f64, f64)>,
169 pub depth: f64,
170 pub depth_bounds: Option<(f64, f64)>,
171}
172
173#[derive(Clone, Debug, PartialEq)]
174pub struct Centroid {
175 pub mean_hz: f64,
176 pub peaks: Vec<Peak>,
178}
179
180#[derive(Clone, Debug, PartialEq)]
181pub struct Window {
182 pub t_secs: f64,
183 pub from_secs: f64,
184 pub to_secs: f64,
185 pub level: f64,
186 pub peaks: Vec<Peak>,
188 pub centroid: Option<Centroid>,
189}
190
191#[derive(Clone, Copy, Debug, PartialEq)]
192pub struct Summary {
193 pub windows_with_peak: usize,
194 pub hz: Option<Stats>,
195 pub depth: Option<Stats>,
196 pub centroid_hz: Option<Stats>,
197 pub centroid_depth_hz: Option<Stats>,
198}
199
200#[derive(Clone, Debug, PartialEq)]
201pub struct Track {
202 pub group: Group,
203 pub centres_hz: Vec<f64>,
204 pub windows: Vec<Window>,
205 pub summary: Summary,
206}
207
208#[derive(Clone, Debug, PartialEq)]
209pub struct Modulation {
210 pub hop_secs: f64,
211 pub window_secs: f64,
212 pub envelope_hz: f64,
213 pub bin_step_hz: f64,
214 pub resolution_hz: f64,
215 pub min_hz: f64,
216 pub max_hz: f64,
217 pub bpm: Option<f64>,
218 pub passband_gain_at_max_hz: f64,
219 pub tracks: Vec<Track>,
220}
221
222pub fn measure(
223 planes: &[&[f64]],
224 sample_rate: f64,
225 start_secs: f64,
226 hop_secs: f64,
227 window_secs: f64,
228 bpm: Option<f64>,
229) -> Result<Modulation, Unread> {
230 let held = crate::measure::held(planes);
231 sliding::fits(held, sample_rate, window_secs, 1.0 / MIN_HZ)?;
232 let split = bands::smoothed(planes, sample_rate, start_secs, SMOOTHING);
233 let lowpass = Lowpass::new(split.rate_hz);
234 let groups = grouped(&split, &lowpass);
235 let rate = split.rate_hz / lowpass.step as f64;
236 let len = groups.first().map_or(0, |g| g.centroid.len());
237 let grid = sliding::spans(
238 held,
239 sample_rate,
240 start_secs,
241 hop_secs,
242 rate,
243 window_secs,
244 len,
245 );
246 let size = (grid.window_secs * rate).round() as usize;
247 let n = (2 * size).next_power_of_two();
248 let step = rate / n as f64;
249 let read = Read {
250 step,
251 bins: 1..((MAX_HZ / step).ceil() as usize + 2).min(n / 2),
252 n,
253 bpm,
254 size,
255 rate,
256 sample_rate,
257 smoothing: design(Shape::OnePole, SMOOTHING_HZ, 0.0, 0.0, sample_rate),
258 lowpass: &lowpass,
259 };
260 let tracks = Group::ALL
261 .into_iter()
262 .zip(groups)
263 .map(|(group, held)| {
264 let windows: Vec<Window> = grid.spans.iter().map(|s| read.window(&held, s)).collect();
265 Track {
266 group,
267 centres_hz: held.centres_hz,
268 summary: summary(&windows),
269 windows,
270 }
271 })
272 .collect();
273 Ok(Modulation {
274 hop_secs: grid.hop_secs,
275 window_secs: grid.window_secs,
276 envelope_hz: rate,
277 bin_step_hz: step,
278 resolution_hz: 2.0 / grid.window_secs,
279 min_hz: MIN_HZ.max(2.0 / grid.window_secs),
280 max_hz: MAX_HZ,
281 bpm,
282 passband_gain_at_max_hz: read.gain(MAX_HZ, Kind::Envelope),
283 tracks,
284 })
285}
286
287struct Lowpass {
289 taps: Vec<f64>,
290 step: usize,
291 rate: f64,
292}
293
294impl Lowpass {
295 fn new(rate: f64) -> Lowpass {
296 let cut = LOWPASS_HZ / rate;
297 let half = HALF_TAPS as f64;
298 let raw: Vec<f64> = (0..=2 * HALF_TAPS)
299 .map(|i| {
300 let j = i as f64 - half;
301 let sinc = match j == 0.0 {
302 true => 2.0 * cut,
303 false => (TAU * cut * j).sin() / (PI * j),
304 };
305 let x = PI * j / half;
306 sinc * (0.42 + 0.5 * x.cos() + 0.08 * (2.0 * x).cos())
307 })
308 .collect();
309 let sum: f64 = raw.iter().sum();
310 Lowpass {
311 taps: raw.into_iter().map(|h| h / sum).collect(),
312 step: (rate / ENVELOPE_HZ).round().max(1.0) as usize,
313 rate,
314 }
315 }
316
317 fn gain(&self, hz: f64) -> f64 {
319 self.taps
320 .iter()
321 .enumerate()
322 .map(|(i, h)| h * (TAU * hz * (i as f64 - HALF_TAPS as f64) / self.rate).cos())
323 .sum()
324 }
325
326 fn slope(&self) -> f64 {
328 self.taps
329 .iter()
330 .enumerate()
331 .map(|(i, h)| h.abs() * TAU * (i as f64 - HALF_TAPS as f64).abs() / self.rate)
332 .sum()
333 }
334
335 fn spread(&self) -> f64 {
337 self.taps.iter().map(|h| h.abs()).sum()
338 }
339
340 fn apply(&self, x: &[f64]) -> Vec<f64> {
344 (0..x.len())
345 .step_by(self.step)
346 .map(|m| {
347 let lo = m.saturating_sub(HALF_TAPS);
348 let hi = (m + HALF_TAPS + 1).min(x.len());
349 let taps = &self.taps[lo + HALF_TAPS - m..hi + HALF_TAPS - m];
350 let sum: f64 = taps.iter().sum();
351 taps.iter().zip(&x[lo..hi]).map(|(h, v)| h * v).sum::<f64>() / sum
352 })
353 .collect()
354 }
355}
356
357struct Held {
359 centres_hz: Vec<f64>,
360 envelopes: Vec<Vec<f64>>,
361 centroid: Vec<f64>,
363}
364
365fn grouped(split: &Bands, lowpass: &Lowpass) -> Vec<Held> {
366 let tracks = &split.bands;
367 let edge = |hz: f64| {
368 tracks
369 .iter()
370 .position(|b| b.centre_hz >= hz)
371 .unwrap_or(tracks.len())
372 };
373 let (low, mid) = (edge(SPLITS_HZ[0]), edge(SPLITS_HZ[1]));
374 let len = tracks.first().map_or(0, |b| b.rms.len());
375 [0..low, low..mid, mid..tracks.len()]
376 .into_iter()
377 .map(|range: Range<usize>| {
378 let held = &tracks[range];
379 let centroid: Vec<f64> = (0..len)
380 .map(|i| {
381 let (weighted, sum) = held.iter().fold((0.0, 0.0), |(w, s), b| {
382 (w + b.centre_hz * b.rms[i], s + b.rms[i])
383 });
384 match sum > 0.0 {
385 true => weighted / sum,
386 false => f64::NAN,
387 }
388 })
389 .collect();
390 Held {
391 centres_hz: held.iter().map(|b| b.centre_hz).collect(),
392 envelopes: held.iter().map(|b| lowpass.apply(&b.rms)).collect(),
393 centroid: lowpass.apply(¢roid),
394 }
395 })
396 .collect()
397}
398
399struct Read<'a> {
400 step: f64,
401 bins: Range<usize>,
403 n: usize,
404 bpm: Option<f64>,
405 size: usize,
407 rate: f64,
408 sample_rate: f64,
409 smoothing: Coeffs,
410 lowpass: &'a Lowpass,
411}
412
413#[derive(Clone, Copy, Debug, PartialEq, Eq)]
415enum Kind {
416 Envelope,
417 Centroid,
418}
419
420const ROUNDS: usize = 8;
422
423impl Read<'_> {
424 fn window(&self, held: &Held, s: &Span) -> Window {
425 let w = hann_periodic(s.to - s.from);
426 let total: f64 = w.iter().sum();
427 let lowest = MIN_HZ.max(2.0 / (s.to_secs - s.from_secs));
428 let segments: Vec<&[f64]> = held.envelopes.iter().map(|e| &e[s.from..s.to]).collect();
429 let means: Vec<f64> = segments.iter().map(|e| weighted_mean(&w, e)).collect();
430 let level: f64 = means.iter().sum();
431 let peaks = match level > 0.0 {
432 true => {
433 let mut depth = vec![0.0; self.bins.len()];
434 for spectrum in self.magnitudes(&w, &segments, &means) {
435 for (d, m) in depth.iter_mut().zip(spectrum) {
436 *d += 2.0 * m / (total * level);
437 }
438 }
439 self.peaks(&depth, lowest, s, Kind::Envelope)
440 }
441 false => Vec::new(),
442 };
443 let track = &held.centroid[s.from..s.to];
444 let centroid = (!track.iter().any(|c| c.is_nan())).then(|| {
445 let mean_hz = weighted_mean(&w, track);
446 let spectrum = self.magnitudes(&w, &[track], &[mean_hz]).remove(0);
447 let depth: Vec<f64> = spectrum.into_iter().map(|m| 2.0 * m / total).collect();
448 Centroid {
449 mean_hz,
450 peaks: self.peaks(&depth, lowest, s, Kind::Centroid),
451 }
452 });
453 Window {
454 t_secs: s.t_secs,
455 from_secs: s.from_secs,
456 to_secs: s.to_secs,
457 level,
458 peaks,
459 centroid,
460 }
461 }
462
463 fn magnitudes(&self, w: &[f64], segments: &[&[f64]], means: &[f64]) -> Vec<Vec<f64>> {
466 let fill = |out: &mut [f64], at: usize| {
467 for ((o, x), h) in out.iter_mut().zip(segments[at]).zip(w) {
468 *o = h * (x - means[at]);
469 }
470 };
471 let mut out = Vec::with_capacity(segments.len());
472 for pair in (0..segments.len()).step_by(2) {
473 let (mut re, mut im) = (vec![0.0; self.n], vec![0.0; self.n]);
474 fill(&mut re, pair);
475 let both = pair + 1 < segments.len();
476 if both {
477 fill(&mut im, pair + 1);
478 }
479 fft(&mut re, &mut im);
480 let half = |sign: f64| -> Vec<f64> {
481 self.bins
482 .clone()
483 .map(|k| {
484 let (r, i) = (re[self.n - k], -im[self.n - k]);
485 (re[k] + sign * r).hypot(im[k] + sign * i) / 2.0
486 })
487 .collect()
488 };
489 out.push(half(1.0));
490 if both {
491 out.push(half(-1.0));
492 }
493 }
494 out
495 }
496
497 fn peaks(&self, depth: &[f64], lowest: f64, s: &Span, kind: Kind) -> Vec<Peak> {
500 let first = self.bins.start;
501 let mut found: Vec<usize> = (1..depth.len().saturating_sub(1))
502 .filter(|&i| {
503 let hz = (first + i) as f64 * self.step;
504 (lowest..=MAX_HZ).contains(&hz)
505 && depth[i] > depth[i - 1]
506 && depth[i] >= depth[i + 1]
507 })
508 .collect();
509 found.sort_by(|a, b| depth[*b].total_cmp(&depth[*a]).then(a.cmp(b)));
510 let mut peaks: Vec<Peak> = found
511 .into_iter()
512 .take(PEAKS)
513 .map(|i| self.located(depth, i, s.to - s.from, kind))
514 .collect();
515 peaks.sort_by(|a, b| b.depth.total_cmp(&a.depth).then(a.hz.total_cmp(&b.hz)));
516 peaks
517 }
518
519 fn located(&self, depth: &[f64], i: usize, size: usize, kind: Kind) -> Peak {
522 let q = size as f64 / self.n as f64;
523 let x = (self.bins.start + i) as f64 * q;
524 let side = match depth[i + 1] >= depth[i - 1] {
525 true => 1.0,
526 false => -1.0,
527 };
528 let d0 = depth[i];
529 let ratio = depth[(i as f64 + side) as usize] / d0;
530 let u = offset(ratio, q, size);
531 let hz = (x + side * u) * self.rate / size as f64;
532 let bounds = match size == self.size {
533 true => self.bounded(d0, ratio, x, side, size, kind),
534 false => None,
535 };
536 let beats = |hz: f64| self.bpm.map(|bpm| bpm / (60.0 * hz));
537 Peak {
538 hz,
539 hz_bounds: bounds.map(|b| b.0),
540 period_beats: beats(hz),
541 period_beats_bounds: bounds.and_then(|((lo, hi), _)| beats(hi).zip(beats(lo))),
542 depth: d0 / kernel(u, size) / self.gain(hz, kind),
543 depth_bounds: bounds.map(|b| b.1),
544 }
545 }
546
547 fn bounded(
550 &self,
551 d0: f64,
552 ratio: f64,
553 x: f64,
554 side: f64,
555 size: usize,
556 kind: Kind,
557 ) -> Option<((f64, f64), (f64, f64))> {
558 let q = size as f64 / self.n as f64;
559 let to_hz = |nu: f64| nu * self.rate / size as f64;
560 let kappa = reach(x - q, size);
561 let lambda = reach(2.0 * (x - q), size) + 2.0 * kappa * kappa;
562 let spread = self.lowpass.spread();
563 let v = match kind {
564 Kind::Envelope => self.square_law(to_hz(x + q)),
565 Kind::Centroid => 0.0,
566 };
567 let law = |m: f64| {
568 let tight = match m < 1.0 {
569 true => m * m * v / (2.0 * (1.0 - m)),
570 false => f64::INFINITY,
571 };
572 tight.min(m * v.sqrt())
573 };
574 let square = |m: f64| (1.0 + 3.0 * kappa) * spread * law(m);
575 let mut m = match kind {
576 Kind::Envelope => {
577 let (lo, _) = self.gains((to_hz(x - q), to_hz(x + q)), kind);
578 let room = lo * kernel(q, size)
579 - d0 * (kappa + spread * v.sqrt())
580 - spread * lambda
581 - square(1.0);
582 match room > 0.0 {
583 true => (d0 / room).min(1.0),
584 false => 1.0,
585 }
586 }
587 Kind::Centroid => f64::INFINITY,
588 };
589 let mut out = None;
590 for _ in 0..ROUNDS {
591 let (level, amp) = match kind {
592 Kind::Envelope => (m * kappa + spread * law(m), m * spread),
593 Kind::Centroid => (0.0, d0 / (kernel(q, size) - lambda)),
594 };
595 let e = amp * lambda + square(m);
596 let floor = d0 * (1.0 - level) - e;
597 if !(floor > 0.0 && amp > 0.0 && amp.is_finite()) {
598 break;
599 }
600 let wide = (1.0 + ratio) * e / floor;
601 let (least, most) = ratios(q, size);
602 if ratio + wide < least || ratio - wide > most {
603 return None;
604 }
605 let (ua, ub) = (offset(ratio - wide, q, size), offset(ratio + wide, q, size));
606 let near = match ua <= 0.0 && ub >= 0.0 {
607 true => 0.0,
608 false => ua.abs().min(ub.abs()),
609 };
610 let far = ua.abs().max(ub.abs());
611 let (ha, hb) = (to_hz(x + side * ua), to_hz(x + side * ub));
612 let hz = (ha.min(hb), ha.max(hb));
613 let g = self.gains(hz, kind);
614 let amps = (
615 floor / kernel(near, size),
616 (d0 * (1.0 + level) + e) / kernel(far, size),
617 );
618 let depth = (amps.0 / g.1, amps.1 / g.0);
619 if depth.0 > m {
620 return None;
621 }
622 out = Some((hz, (depth.0, depth.1.min(m))));
623 if kind == Kind::Centroid || depth.1 >= m {
624 break;
625 }
626 m = depth.1;
627 }
628 out
629 }
630
631 fn smoothed(&self, hz: f64) -> C64 {
633 let w = TAU * hz / self.sample_rate;
634 let back = C64::new(w.cos(), -w.sin()).scale(self.smoothing.a1);
635 (C64::real(self.smoothing.b0) / (C64::ONE + back)).powi(SMOOTHING.poles as u32)
636 }
637
638 fn gain(&self, hz: f64, kind: Kind) -> f64 {
641 match kind {
642 Kind::Envelope => self.smoothed(hz).abs() * self.lowpass.gain(hz),
643 Kind::Centroid => self.lowpass.gain(hz),
644 }
645 }
646
647 fn gains(&self, hz: (f64, f64), kind: Kind) -> (f64, f64) {
650 let (mid, half) = ((hz.0 + hz.1) / 2.0, (hz.1 - hz.0) / 2.0);
651 let pass = self.lowpass.gain(mid);
652 let moved = self.lowpass.slope() * half;
653 let (lo, hi) = (pass - moved, pass + moved);
654 match kind {
655 Kind::Envelope => (
656 self.smoothed(hz.1).abs() * lo,
657 self.smoothed(hz.0).abs() * hi,
658 ),
659 Kind::Centroid => (lo, hi),
660 }
661 }
662
663 fn square_law(&self, hz: f64) -> f64 {
665 let (one, two) = (self.smoothed(hz), self.smoothed(2.0 * hz));
666 (1.0 - one.norm_sqr() + (two - one * one).abs()) / 2.0
667 }
668}
669
670fn dirichlet(x: f64, n: usize) -> C64 {
673 let n = n as f64;
674 let turn = C64::new((PI / n).cos(), (PI / n).sin());
675 let s = |y: f64| (PI * y / n).sin();
676 C64::real(0.5 / s(x)) - turn.conj().over(4.0 * s(x - 1.0)) - turn.over(4.0 * s(x + 1.0))
677}
678
679fn kernel(x: f64, n: usize) -> f64 {
681 match x.abs() {
682 0.0 => 1.0,
683 1.0 => 0.5,
684 x => 2.0 / n as f64 * (PI * x).sin().abs() * dirichlet(x, n).abs(),
685 }
686}
687
688fn reach(x: f64, n: usize) -> f64 {
690 2.0 / n as f64 * dirichlet(x, n).abs()
691}
692
693fn ratios(q: f64, n: usize) -> (f64, f64) {
696 (kernel(2.0 * q, n) / kernel(q, n), 1.0 / kernel(q, n))
697}
698
699fn offset(ratio: f64, q: f64, n: usize) -> f64 {
702 let (mut lo, mut hi) = (-q, q);
703 for _ in 0..60 {
704 let mid = (lo + hi) / 2.0;
705 match kernel(q - mid, n) / kernel(mid, n) < ratio {
706 true => lo = mid,
707 false => hi = mid,
708 }
709 }
710 (lo + hi) / 2.0
711}
712
713fn weighted_mean(w: &[f64], x: &[f64]) -> f64 {
714 w.iter().zip(x).map(|(h, v)| h * v).sum::<f64>() / w.iter().sum::<f64>()
715}
716
717fn summary(windows: &[Window]) -> Summary {
718 let leads = || {
719 windows
720 .iter()
721 .filter_map(|w| w.peaks.first().map(|p| (w.t_secs, *p)))
722 };
723 let centroids = || {
724 windows
725 .iter()
726 .filter_map(|w| w.centroid.as_ref().map(|c| (w.t_secs, c)))
727 };
728 Summary {
729 windows_with_peak: leads().count(),
730 hz: stats(leads().map(|(t, p)| (t, p.hz))),
731 depth: stats(leads().map(|(t, p)| (t, p.depth))),
732 centroid_hz: stats(centroids().map(|(t, c)| (t, c.mean_hz))),
733 centroid_depth_hz: stats(
734 centroids().filter_map(|(t, c)| c.peaks.first().map(|p| (t, p.depth))),
735 ),
736 }
737}
738
739#[cfg(test)]
740mod tests {
741 use super::*;
742
743 #[test]
746 fn the_kernel_its_reach_and_the_square_law_hold_as_model_states() {
747 for n in [250, 400, 401] {
748 let w = hann_periodic(n);
749 let total: f64 = w.iter().sum();
750 for x in [0.0, 0.01, 0.39, 0.5, 1.0, 1.7, 2.3, 7.5, 61.2] {
751 let summed = w.iter().enumerate().fold(C64::ZERO, |acc, (k, h)| {
752 let turn = -TAU * x * k as f64 / n as f64;
753 acc + C64::new(turn.cos(), turn.sin()).scale(*h)
754 });
755 assert!(
756 (kernel(x, n) - summed.abs() / total).abs() < 1e-12,
757 "{n} {x}"
758 );
759 }
760 let ys: Vec<f64> = (1..(n * 5)).map(|k| 1.0 + k as f64 / 10.0).collect();
761 for pair in ys.windows(2).filter(|p| p[1] < n as f64 / 2.0) {
762 assert!(reach(pair[1], n) < reach(pair[0], n), "{n} {pair:?}");
763 assert!(kernel(pair[1], n) <= reach(pair[1], n) * (1.0 + 1e-12));
764 }
765 }
766 let lowpass = Lowpass::new(2000.0);
767 for sample_rate in [8_192.0, 16_000.0, 44_100.0, 48_000.0, 96_000.0] {
768 let read = Read {
769 step: 1.0,
770 bins: 1..2,
771 n: 2,
772 bpm: None,
773 size: 1,
774 rate: 100.0,
775 sample_rate,
776 smoothing: design(Shape::OnePole, SMOOTHING_HZ, 0.0, 0.0, sample_rate),
777 lowpass: &lowpass,
778 };
779 let v: Vec<f64> = (1..=2000)
780 .map(|k| read.square_law(k as f64 * 0.01))
781 .collect();
782 assert!(v.windows(2).all(|p| p[1] > p[0]), "{sample_rate}");
783 }
784 }
785}