1use std::collections::VecDeque;
4use std::f64::consts::{FRAC_PI_2, PI, SQRT_2, TAU};
5
6use super::peaks;
7use crate::fft::{fft_with, ifft_with, twiddles};
8use crate::stft::hann_periodic;
9
10pub const NEAR_CENTS: f64 = 25.0;
12pub const NEAR_HZ: f64 = 8.0;
13pub const PROMINENCE_DB: f64 = 6.0;
15pub const LEAK_MARGIN: f64 = 4.0;
18pub const GAP_SHARE: f64 = 0.4;
20pub const RESOLUTION_HZ: (f64, f64) = (6.0, 40.0);
21pub const LEAST_GAP_HZ: f64 = RESOLUTION_HZ.0 / GAP_SHARE;
23pub const SLOPE_SIGMAS: f64 = 4.0;
26pub const EDGE_SIGMAS: f64 = 3.0;
29pub const CAPTURE_SIGMAS: f64 = 2.0;
32pub const CANCELLATIONS: usize = 2;
36pub const KNOT_CENTS: f64 = 1.0;
39pub const KNOT_SHARE: f64 = 0.02;
41pub const FLUTTER_FRAME_SECS: f64 = 0.002;
44pub const FLUTTER_MEAN_FRAMES: usize = 15;
45pub const FLUTTER_SPAN_SECS: f64 = 0.2;
46pub const BREATHY_FLUTTER_DB: f64 = 2.0;
47pub const COHERENCE_LAG_SECS: f64 = 0.01;
50pub const COHERENCE_SPAN_SECS: f64 = 0.25;
51pub const COHERENCE_HALF_HZ: f64 = 15.0;
52pub const FLAG_LEAST_HZ: f64 = 20.0;
55pub const BREATHY_POWER_SECS: f64 = 0.03;
57const LEAST_BAND_RATE_HZ: f64 = 1000.0;
59const SIGMAS: f64 = 6.0;
61const PAD_SIGMAS: f64 = 12.0;
63const PROMINENCE_BINS: f64 = 4.0;
65
66pub const MODEL: &str = "heterodyne partial tracking, as the fit harness's analysis seed \
67 tracks, read without a score. Detect: every `hop_secs` from the \
68 target's start, the power of every periodic Hann frame of `frame_secs` \
69 centred within `window_secs` of samples (Welch, P. D. (1967), The use \
70 of fast Fourier transform for the estimation of power spectra, IEEE \
71 Trans. Audio Electroacoust. 15(2) 70-73), a frame past the target's \
72 ends spanning the samples it holds, zero-padded to at least its own \
73 length, over every channel's samples averaged, those frames' power \
74 averaged so a noise's chance peaks average away and a held partial's \
75 stay; its peaks the local maxima at or above the floor, in dB against \
76 full scale, standing 4 times over the Hann sidelobes the louder peaks \
77 kept and a DC put under them (`spectrum`'s bound, Harris 1978, with \
78 that margin for a partial that moves within a frame), 6 dB over the \
79 median bin within a sixth of an octave and 4 bins, and 2 frame bins \
80 from 0 Hz and Nyquist. Peaks link hop to hop into fragments, loudest \
81 first, each to the nearest open fragment within 25 cents or 8 Hz, a \
82 fragment open across gaps of half a frame. Fragments sorted by \
83 energy-weighted frequency join a group while within 25 cents or 8 Hz \
84 of its running mean, or 15 Hz, where the narrowest Gaussian below \
85 reads both as one band (one partial across notes; a glide is a group \
86 whose peaks move, which a window longer than the time it takes to \
87 cross a frame's resolution smears). The `peaks` groups of most energy \
88 are tracks; `detected` counts them all. Read: per track the target's \
89 analytic signal demodulated along the track's phase 2 pi \
90 pwl_integral(t, frequency), then a Gaussian lowpass of half-width \
91 sigma = `resolution_hz`, 0.4 of the gap to the nearest track's \
92 frequency clamped to 6-40 Hz, its time deviation `resolution_secs` = 1 \
93 / (2 pi sigma); all of it off one transform of the target zero-padded \
94 by 12 time deviations of the narrowest Gaussian, each band taken flat \
95 to 240 Hz past the track's frequencies, Gaussian past that, and \
96 decimated to at least its width, so the band is exact for every \
97 lowpass here. For a lone sinusoid of constant frequency and amplitude \
98 that band is a e^(i(phase - pi/2)) exactly, wherever the target \
99 reaches past the Gaussian's tails, about 6 time deviations: its \
100 per-hop frequency, amplitude and phase are exact there, and a \
101 neighbour d Hz away moves the band by at most its amplitude times \
102 e^(-d^2 / (2 sigma^2)). Frequency: the group's peaks per hop, plus the \
103 band's mean phase slope over 2 pi across 4 time deviations about the \
104 hop, where the group holds a peak, the band is over the floor, that \
105 span lies 3 time deviations inside the target (anywhere it sounds, \
106 where no hop does) and the slope is within 2 sigma and lands nearer \
107 the track than any other group's peak there, else the band reads a \
108 neighbour's frequency and not its own; held between and outward. \
109 Cancel: then twice, every tonal track's band, Re z(t) e^(i 2 pi \
110 pwl_integral(t)), is taken from the target and each track read again \
111 off that residual plus its own band (Jacobi), so what a tonal \
112 neighbour leaks into a track's band is cancelled, the second pass \
113 cancelling what the first took of the track's own leak into the \
114 neighbour's; a breathy track stays in the residual. Tonal or breathy, \
115 as the harness read them: flutter is the dB spread of 2 ms means of \
116 the band's modulus about their 15-frame running mean, the 7 frames at \
117 either end left out, over the 200 ms from the track's loudest hop, or \
118 the target's last 200 ms where that is later; coherence |sum b(t)* b(t \
119 + 10 ms)| / sqrt(sum |b(t)|^2 sum |b(t + 10 ms)|^2) of the band \
120 brickwalled to max(sigma, 15 Hz) either side over the 250 ms so \
121 placed, 1 under three lags; the first read takes both at sigma, the \
122 cancelled reads at sigma or 20 Hz if wider, where a narrowband noise's \
123 envelope moves faster than the 30 ms running mean and a tone's, its \
124 tonal neighbours cancelled, holds. A tone's modulus holds over 2 ms \
125 frames and a narrowband noise's flutters (Rayleigh, some 5.6 dB): over \
126 2 dB is breathy, its frequency the energy-weighted mean, its \
127 `bandwidth_hz` the larger of -ln(coherence) / (pi 10 ms), a Lorentzian \
128 line's width, and 2 sigma / 3. A tonal track's amplitude is the band's \
129 modulus, the sinusoid's peak amplitude, and `phase` is the \
130 energy-weighted mean of its argument plus pi/2, so amplitude sin(2 pi \
131 pwl_integral(t, frequency) + phase) is the track; a breathy track's \
132 amplitude is its band's rms over 30 ms, and it has no phase. Knots: \
133 per hop an amplitude under the floor is zero, and each table is the \
134 fewest hops, greedily, through which straight lines stay within the \
135 tolerance of the per-hop curve: the amplitude within 2% of the track's \
136 peak or the floor's amplitude, from the track's first sounding hop \
137 less one to its last plus one; the frequency within 1 cent, or twice \
138 its own noise where wider (the median absolute second difference over \
139 0.6745 sqrt 6), where the track sounds, its knots then refit by least \
140 squares weighted by the band's energy. `error` is the largest gap a \
141 table leaves where it is held to the curve. The frequency table spans \
142 the whole target from its first hop, so pwl_integral's phase starts \
143 there. Bounds: `residual_db` is the energy the knot tables leave in a \
144 tonal track's band, the target less every tonal track's tables \
145 demodulated as above, over the energy its band holds, over the \
146 amplitude table's span, the whole of what the track misses there, \
147 noise, a neighbour's error and its own; null for a breathy track, a \
148 band whose waveform no table claims. Before its group's first peak and \
149 after its last a track sounds only within a window and half a frame of \
150 them, and only where its band holds over twice what any other group's \
151 peak there puts through its Gaussian, amp e^(-d^2 / (2 sigma^2)) at \
152 the gap d: a track with no peaks of its own never reads a louder \
153 neighbour filling its band as a second synthesis of it; \
154 `unclaimed_db` is its band's energy over the floor at the hops so \
155 ruled out, over the energy at the hops it keeps, null where none is. \
156 The \
157 target's `residual_db` is its energy less the tonal tracks' tables' \
158 over its energy, breathy energy left in.";
159
160#[derive(Clone, Copy, Debug, PartialEq)]
162pub struct Asked {
163 pub frame_secs: f64,
164 pub window_secs: f64,
166 pub hop_secs: f64,
167 pub floor_db: f64,
168 pub tracks: usize,
169}
170
171#[derive(Clone, Debug, PartialEq)]
173pub struct Curve {
174 pub times: Vec<f64>,
175 pub values: Vec<f64>,
176 pub error: f64,
177}
178
179#[derive(Clone, Copy, Debug, PartialEq, Eq)]
180pub enum Kind {
181 Tonal,
182 Breathy,
183}
184
185impl Kind {
186 pub fn key(self) -> &'static str {
187 match self {
188 Kind::Tonal => "tonal",
189 Kind::Breathy => "breathy",
190 }
191 }
192}
193
194#[derive(Clone, Debug, PartialEq)]
195pub struct Track {
196 pub kind: Kind,
197 pub hz: f64,
199 pub frequency: Curve,
200 pub amplitude: Curve,
201 pub phase: Option<f64>,
202 pub flutter_db: f64,
203 pub coherence: f64,
204 pub bandwidth_hz: Option<f64>,
205 pub resolution_hz: f64,
206 pub resolution_secs: f64,
207 pub residual_db: Option<f64>,
208 pub unclaimed_db: Option<f64>,
210}
211
212#[derive(Clone, Debug, PartialEq)]
213pub struct Partials {
214 pub frame_secs: f64,
215 pub window_secs: f64,
216 pub hop_secs: f64,
217 pub floor_db: f64,
218 pub detected: usize,
219 pub residual_db: Option<f64>,
220 pub tracks: Vec<Track>,
221}
222
223pub fn measure(planes: &[&[f64]], sr: f64, start: f64, asked: Asked) -> Partials {
225 let x = mono(planes);
226 let frame = ((asked.frame_secs * sr).round() as usize).max(2);
227 let hop = ((asked.hop_secs * sr).round() as usize).max(1);
228 let reach = ((asked.window_secs * sr - frame as f64).max(0.0) / (2 * hop) as f64).round();
229 let reach = reach as usize;
230 let floor = libm::pow(10.0, asked.floor_db / 20.0);
231 let grid = Grid {
232 sr,
233 samples: x.len(),
234 hop,
235 hops: x.len().saturating_sub(1).div_ceil(hop) + 1,
236 start,
237 };
238 let peaks = detect(&x, &grid, (frame, reach), (floor, asked.tracks));
239 let mut groups = group(link(&peaks, (frame / (2 * hop)).max(1)));
240 let detected = groups.len();
241 groups.sort_by(|a, b| b.energy.total_cmp(&a.energy).then(a.hz.total_cmp(&b.hz)));
242 groups.truncate(asked.tracks);
243 groups.sort_by(|a, b| a.hz.total_cmp(&b.hz));
244 let (tracks, residual_db) = read(&x, &grid, (&groups, reach + frame.div_ceil(2 * hop)), floor);
245 Partials {
246 frame_secs: frame as f64 / sr,
247 window_secs: (2 * reach * hop + frame) as f64 / sr,
248 hop_secs: hop as f64 / sr,
249 floor_db: asked.floor_db,
250 detected,
251 residual_db,
252 tracks,
253 }
254}
255
256fn mono(planes: &[&[f64]]) -> Vec<f64> {
257 let len = super::held(planes);
258 let count = planes.len() as f64;
259 (0..len)
260 .map(|n| planes.iter().map(|p| p[n]).sum::<f64>() / count)
261 .collect()
262}
263
264struct Grid {
265 sr: f64,
266 samples: usize,
268 hop: usize,
269 hops: usize,
270 start: f64,
271}
272
273impl Grid {
274 fn secs(&self, k: usize) -> f64 {
275 (k * self.hop) as f64 / self.sr
276 }
277}
278
279struct Plan {
281 twiddle: Vec<(f64, f64)>,
282}
283
284impl Plan {
285 fn new(n: usize) -> Plan {
286 Plan {
287 twiddle: twiddles(n),
288 }
289 }
290
291 fn forward(&self, re: &mut [f64], im: &mut [f64]) {
292 fft_with(re, im, &self.twiddle);
293 }
294
295 fn inverse(&self, re: &mut [f64], im: &mut [f64]) {
296 ifft_with(re, im, &self.twiddle);
297 }
298}
299
300#[derive(Clone, Copy)]
301struct Found {
302 hz: f64,
303 amp: f64,
304}
305
306type Power = Option<(Vec<f64>, usize)>;
309
310fn detect(
312 x: &[f64],
313 grid: &Grid,
314 (frame, reach): (usize, usize),
315 (floor, cap): (f64, usize),
316) -> Vec<Vec<Found>> {
317 let size = frame.next_power_of_two();
318 let plan = Plan::new(size);
319 let bin_hz = grid.sr / size as f64;
320 let span = |k: usize| {
321 let centre = k * grid.hop;
322 let from = centre.saturating_sub(frame / 2).min(x.len());
323 (from, (centre + frame.div_ceil(2)).min(x.len()) - from)
324 };
325 let (mut re, mut im) = (vec![0.0; size], vec![0.0; size]);
326 let mut pair = |k: usize| -> (Power, Power) {
327 re.iter_mut().for_each(|v| *v = 0.0);
328 im.iter_mut().for_each(|v| *v = 0.0);
329 let ((a, na), (b, nb)) = (span(k), span(k + 1));
330 if na >= 2 {
331 for (i, w) in hann_periodic(na).iter().enumerate() {
332 re[i] = x[a + i] * w;
333 }
334 }
335 if k + 1 < grid.hops && nb >= 2 {
336 for (i, w) in hann_periodic(nb).iter().enumerate() {
337 im[i] = x[b + i] * w;
338 }
339 }
340 plan.forward(&mut re, &mut im);
341 let unpacked = |n: usize, second: bool| {
342 (n >= 2).then(|| {
343 let scale = (4.0 / n as f64) * (4.0 / n as f64);
344 let bins = (0..=size / 2).map(|j| {
345 let (zr, zi, wr, wi) =
346 (re[j], im[j], re[(size - j) % size], im[(size - j) % size]);
347 let (pr, pi) = match second {
348 false => (zr + wr, zi - wi),
349 true => (zi + wi, wr - zr),
350 };
351 (pr * pr + pi * pi) * 0.25 * scale
352 });
353 (bins.collect(), n)
354 })
355 };
356 let second = (k + 1 < grid.hops).then(|| unpacked(nb, true)).flatten();
357 (unpacked(na, false), second)
358 };
359 let mut held: VecDeque<(usize, Power)> = VecDeque::new();
360 let mut spare: Option<Power> = None;
361 let mut sum = vec![0.0; size / 2 + 1];
362 let (mut count, mut next) = (0usize, 0usize);
363 let mut out = Vec::with_capacity(grid.hops);
364 for k in 0..grid.hops {
365 while next < grid.hops && next <= k + reach {
366 let p = match spare.take() {
367 Some(p) => p,
368 None => {
369 let (first, second) = pair(next);
370 spare = Some(second);
371 first
372 }
373 };
374 if let Some((p, _)) = &p {
375 sum.iter_mut().zip(p).for_each(|(a, b)| *a += b);
376 count += 1;
377 }
378 held.push_back((next, p));
379 next += 1;
380 }
381 while held.front().is_some_and(|(j, _)| j + reach < k) {
382 if let Some((_, Some((p, _)))) = held.pop_front() {
383 sum.iter_mut().zip(&p).for_each(|(a, b)| *a -= b);
384 count -= 1;
385 }
386 }
387 let spanned = held
388 .iter()
389 .filter_map(|(_, p)| p.as_ref().map(|p| p.1))
390 .min();
391 let (Some(spanned), true) = (spanned, count > 0) else {
392 out.push(Vec::new());
393 continue;
394 };
395 let mags: Vec<f64> = sum
396 .iter()
397 .map(|p| (p.max(0.0) / count as f64).sqrt())
398 .collect();
399 out.push(picked(&mags, (bin_hz, grid.sr), spanned, (floor, cap)));
400 }
401 out
402}
403
404fn picked(
408 mags: &[f64],
409 (bin_hz, sr): (f64, f64),
410 spanned: usize,
411 (floor, cap): (f64, usize),
412) -> Vec<Found> {
413 let r = spanned as f64 / (2 * (mags.len() - 1)) as f64;
414 let half = r / 2.0;
415 let dc = match mags {
416 [zero, one, ..] if zero >= one => *zero,
417 _ => 0.0,
418 };
419 let edge = 2.0 * sr / spanned as f64;
420 let mut kept: Vec<(usize, Found)> = Vec::new();
421 for (k, p) in peaks::located(mags, bin_hz) {
422 let amp = libm::pow(10.0, p.db / 20.0);
423 if amp < floor || kept.len() == 4 * cap {
424 break;
425 }
426 let under = |q: usize| {
427 let near = (k.abs_diff(q) as f64 * r - half).max(0.0);
428 let image = (k + q) as f64 * r - half;
429 mags[q] * (peaks::leakage(near) + peaks::leakage(image))
430 };
431 let leaked = kept.iter().map(|&(q, _)| under(q)).sum::<f64>()
432 + dc * peaks::leakage(k as f64 * r - half);
433 if mags[k] > LEAK_MARGIN * leaked / peaks::kernel(half) {
434 kept.push((k, Found { hz: p.hz, amp }));
435 }
436 }
437 kept.into_iter()
438 .map(|(_, p)| p)
439 .filter(|p| p.hz >= edge && p.hz <= sr / 2.0 - edge && prominent(mags, bin_hz, p))
440 .collect()
441}
442
443fn prominent(mags: &[f64], bin_hz: f64, p: &Found) -> bool {
444 let lo = (p.hz * libm::pow(2.0, -1.0 / 6.0) / bin_hz - PROMINENCE_BINS).max(0.0) as usize;
445 let hi = (p.hz * libm::pow(2.0, 1.0 / 6.0) / bin_hz + PROMINENCE_BINS) as usize;
446 let mut near: Vec<f64> = mags[lo..=hi.min(mags.len() - 1)].to_vec();
447 let middle = near.len() / 2;
448 let (_, median, _) = near.select_nth_unstable_by(middle, f64::total_cmp);
449 p.amp >= *median * libm::pow(10.0, PROMINENCE_DB / 20.0)
450}
451
452fn cents(a: f64, b: f64) -> f64 {
453 1200.0 * libm::log2(a / b)
454}
455
456fn near(a: f64, b: f64) -> bool {
457 cents(a, b).abs() < NEAR_CENTS || (a - b).abs() < NEAR_HZ
458}
459
460struct Fragment {
461 hops: Vec<usize>,
462 hz: Vec<f64>,
463 amp: Vec<f64>,
464}
465
466impl Fragment {
467 fn last_hz(&self) -> f64 {
468 self.hz[self.hz.len() - 1]
469 }
470}
471
472fn link(peaks: &[Vec<Found>], gap: usize) -> Vec<Fragment> {
473 let mut all: Vec<Fragment> = Vec::new();
474 let mut open: Vec<usize> = Vec::new();
475 for (k, found) in peaks.iter().enumerate() {
476 open.retain(|&f| k - all[f].hops[all[f].hops.len() - 1] <= gap);
477 let mut order: Vec<&Found> = found.iter().collect();
478 order.sort_by(|a, b| b.amp.total_cmp(&a.amp).then(a.hz.total_cmp(&b.hz)));
479 let mut taken: Vec<usize> = Vec::new();
480 for p in order {
481 let off = |f: usize| cents(p.hz, all[f].last_hz()).abs();
482 let best = open
483 .iter()
484 .copied()
485 .filter(|f| !taken.contains(f) && near(p.hz, all[*f].last_hz()))
486 .min_by(|&a, &b| off(a).total_cmp(&off(b)).then(a.cmp(&b)));
487 let f = best.unwrap_or_else(|| {
488 all.push(Fragment {
489 hops: Vec::new(),
490 hz: Vec::new(),
491 amp: Vec::new(),
492 });
493 open.push(all.len() - 1);
494 all.len() - 1
495 });
496 all[f].hops.push(k);
497 all[f].hz.push(p.hz);
498 all[f].amp.push(p.amp);
499 taken.push(f);
500 }
501 }
502 all
503}
504
505struct Group {
506 hz: f64,
507 energy: f64,
508 members: Vec<Fragment>,
509}
510
511fn group(fragments: Vec<Fragment>) -> Vec<Group> {
512 let mut weighed: Vec<(f64, f64, Fragment)> = fragments
513 .into_iter()
514 .map(|f| {
515 let energy: f64 = f.amp.iter().map(|a| a * a).sum();
516 let hz = f.hz.iter().zip(&f.amp).map(|(h, a)| h * a * a).sum::<f64>() / energy;
517 (hz, energy, f)
518 })
519 .collect();
520 weighed.sort_by(|a, b| a.0.total_cmp(&b.0).then(a.2.hops[0].cmp(&b.2.hops[0])));
521 let mut groups: Vec<Group> = Vec::new();
522 for (hz, energy, f) in weighed {
523 match groups.last_mut() {
524 Some(held) if near(hz, held.hz) || hz - held.hz < LEAST_GAP_HZ => {
525 held.hz = (held.hz * held.energy + hz * energy) / (held.energy + energy);
526 held.energy += energy;
527 held.members.push(f);
528 }
529 _ => groups.push(Group {
530 hz,
531 energy,
532 members: vec![f],
533 }),
534 }
535 }
536 groups
537}
538
539fn peaks_of(g: &Group, hops: usize) -> Vec<Option<(f64, f64)>> {
541 let mut sum = vec![0.0; hops];
542 let mut weight = vec![0.0; hops];
543 for f in &g.members {
544 for ((k, hz), a) in f.hops.iter().zip(&f.hz).zip(&f.amp) {
545 sum[*k] += hz * a * a;
546 weight[*k] += a * a;
547 }
548 }
549 sum.iter()
550 .zip(&weight)
551 .map(|(s, w)| (*w > 0.0).then(|| (s / w, w.sqrt())))
552 .collect()
553}
554
555fn coarse(own: &[Option<(f64, f64)>], otherwise: f64) -> Vec<f64> {
556 let held: Vec<Option<f64>> = own.iter().map(|p| p.map(|(hz, _)| hz)).collect();
557 filled(&held, otherwise)
558}
559
560const CLAIM_MARGIN: f64 = 2.0;
561
562struct Claim {
567 peak: bool,
568 rivals: Vec<f64>,
569 least: f64,
570}
571
572fn claims(
573 held: &[Vec<Option<(f64, f64)>>],
574 curves: &[Vec<f64>],
575 sigmas: &[f64],
576 reach: usize,
577) -> Vec<Vec<Claim>> {
578 (0..held.len())
579 .map(|g| {
580 let hops = held[g].len();
581 let peaks: Vec<usize> = (0..hops).filter(|&k| held[g][k].is_some()).collect();
582 (0..hops)
583 .map(|k| {
584 let others: Vec<(f64, f64)> = (0..held.len())
585 .filter(|&h| h != g)
586 .filter_map(|h| held[h][k])
587 .collect();
588 let at = peaks.partition_point(|&j| j < k);
589 let peak = held[g][k].is_some();
590 let near = |j: usize| j.abs_diff(k) <= reach;
591 let reached =
592 (at < peaks.len() && near(peaks[at])) || (at > 0 && near(peaks[at - 1]));
593 let least = if peak || (at > 0 && at < peaks.len()) {
594 0.0
595 } else if !reached {
596 f64::INFINITY
597 } else {
598 let leak = others
599 .iter()
600 .map(|&(hz, amp)| {
601 let d = (hz - curves[g][k]) / sigmas[g];
602 amp * sva_formula::exp(-0.5 * d * d)
603 })
604 .fold(0.0, f64::max);
605 CLAIM_MARGIN * leak
606 };
607 Claim {
608 peak,
609 rivals: others.into_iter().map(|(hz, _)| hz).collect(),
610 least,
611 }
612 })
613 .collect()
614 })
615 .collect()
616}
617
618fn filled(held: &[Option<f64>], otherwise: f64) -> Vec<f64> {
620 let known: Vec<(usize, f64)> = held
621 .iter()
622 .enumerate()
623 .filter_map(|(k, v)| v.map(|v| (k, v)))
624 .collect();
625 let (Some(&first), Some(&last)) = (known.first(), known.last()) else {
626 return vec![otherwise; held.len()];
627 };
628 let mut next = 0;
629 (0..held.len())
630 .map(|k| {
631 while next < known.len() && known[next].0 < k {
632 next += 1;
633 }
634 match (held[k], next) {
635 (Some(v), _) => v,
636 (None, 0) => first.1,
637 (None, n) if n == known.len() => last.1,
638 (None, n) => {
639 let ((a, va), (b, vb)) = (known[n - 1], known[n]);
640 va + (vb - va) * (k - a) as f64 / (b - a) as f64
641 }
642 }
643 })
644 .collect()
645}
646
647struct Walk<'a> {
650 times: &'a [f64],
651 values: &'a [f64],
652 prefix: Vec<f64>,
653 at: usize,
654}
655
656impl<'a> Walk<'a> {
657 fn new(times: &'a [f64], values: &'a [f64]) -> Walk<'a> {
658 let mut prefix = vec![0.0];
659 for k in 1..times.len() {
660 let dt = times[k] - times[k - 1];
661 prefix.push(prefix[k - 1] + dt * (values[k] + values[k - 1]) * 0.5);
662 }
663 Walk {
664 times,
665 values,
666 prefix,
667 at: 0,
668 }
669 }
670
671 fn segment(&mut self, t: f64) -> usize {
673 let last = self.times.len() - 1;
674 if t < self.times[self.at] {
675 self.at = self.times.partition_point(|&s| s <= t).saturating_sub(1);
676 }
677 while self.at < last && self.times[self.at + 1] <= t {
678 self.at += 1;
679 }
680 self.at
681 }
682
683 fn value(&mut self, t: f64) -> f64 {
684 let k = self.segment(t);
685 if t <= self.times[0] || k == self.times.len() - 1 {
686 return self.values[k];
687 }
688 let u = (t - self.times[k]) / (self.times[k + 1] - self.times[k]);
689 self.values[k] + (self.values[k + 1] - self.values[k]) * u
690 }
691
692 fn integral(&mut self, t: f64) -> f64 {
693 let k = self.segment(t);
694 let u = t - self.times[k];
695 if t <= self.times[0] || k == self.times.len() - 1 {
696 return self.prefix[k] + self.values[k] * u;
697 }
698 let slope = (self.values[k + 1] - self.values[k]) / (self.times[k + 1] - self.times[k]);
699 self.prefix[k] + u * (self.values[k] + 0.5 * slope * u)
700 }
701}
702
703#[derive(Clone, Copy)]
704struct C {
705 re: f64,
706 im: f64,
707}
708
709const ZERO: C = C { re: 0.0, im: 0.0 };
710
711impl C {
712 fn of_turns(turns: f64) -> C {
713 let (im, re) = libm::sincos(TAU * turns.rem_euclid(1.0));
714 C { re, im }
715 }
716 fn mul(self, o: C) -> C {
717 C {
718 re: self.re * o.re - self.im * o.im,
719 im: self.re * o.im + self.im * o.re,
720 }
721 }
722 fn scale(self, s: f64) -> C {
723 C {
724 re: self.re * s,
725 im: self.im * s,
726 }
727 }
728 fn add(self, o: C) -> C {
729 C {
730 re: self.re + o.re,
731 im: self.im + o.im,
732 }
733 }
734 fn conj(self) -> C {
735 C {
736 re: self.re,
737 im: -self.im,
738 }
739 }
740 fn norm(self) -> f64 {
741 self.re * self.re + self.im * self.im
742 }
743 fn abs(self) -> f64 {
744 self.norm().sqrt()
745 }
746 fn arg(self) -> f64 {
747 libm::atan2(self.im, self.re)
748 }
749}
750
751struct Whole {
753 re: Vec<f64>,
754 im: Vec<f64>,
755}
756
757impl Whole {
758 fn of(x: &[f64], plan: &Plan, size: usize) -> Whole {
759 let mut re = vec![0.0; size];
760 let mut im = vec![0.0; size];
761 re[..x.len()].copy_from_slice(x);
762 plan.forward(&mut re, &mut im);
763 Whole { re, im }
764 }
765}
766
767#[derive(Clone, Copy)]
770struct Band {
771 every: usize,
772 centre_bin: usize,
773 lo_hz: f64,
774 hi_hz: f64,
775 sigma: f64,
776}
777
778struct Reader<'a> {
780 grid: &'a Grid,
781 size: usize,
782 whole: Plan,
783 bands: Vec<(usize, Plan)>,
784 floor: f64,
785 hop_secs: Vec<f64>,
786}
787
788impl Reader<'_> {
789 fn rate(&self, b: &Band) -> f64 {
790 self.grid.sr / b.every as f64
791 }
792
793 fn centre_hz(&self, b: &Band) -> f64 {
794 b.centre_bin as f64 * self.grid.sr / self.size as f64
795 }
796
797 fn plan(&self, m: usize) -> &Plan {
798 let (_, plan) = self
799 .bands
800 .iter()
801 .find(|(n, _)| *n == m)
802 .expect("every band length was planned");
803 plan
804 }
805
806 fn band(&self, whole: &Whole, b: &Band) -> Vec<C> {
809 let n = self.size;
810 let m = n / b.every;
811 let bin_hz = self.grid.sr / n as f64;
812 let widest = RESOLUTION_HZ.1;
813 let flat = (b.lo_hz - SIGMAS * widest, b.hi_hz + SIGMAS * widest);
814 let first = ((flat.0 - SIGMAS * widest) / bin_hz).floor().max(0.0) as usize;
815 let last = (((flat.1 + SIGMAS * widest) / bin_hz).ceil() as usize).min(n / 2);
816 let (mut re, mut im) = (vec![0.0; m], vec![0.0; m]);
817 let gain = 2.0 * m as f64 / n as f64;
818 for bin in first..=last {
819 let f = bin as f64 * bin_hz;
820 let off = (flat.0 - f).max(f - flat.1).max(0.0);
821 let w = sva_formula::exp(-0.5 * (off / widest) * (off / widest)) * gain;
822 let j = (bin as isize - b.centre_bin as isize).rem_euclid(m as isize) as usize;
823 re[j] += whole.re[bin] * w;
824 im[j] += whole.im[bin] * w;
825 }
826 self.plan(m).inverse(&mut re, &mut im);
827 re.into_iter()
828 .zip(im)
829 .map(|(re, im)| C { re, im })
830 .collect()
831 }
832
833 fn mixed(&self, raw: &[C], b: &Band, (times, values): (&[f64], &[f64])) -> Vec<C> {
835 let (centre, rate) = (self.centre_hz(b), self.rate(b));
836 let mut walk = Walk::new(times, values);
837 raw.iter()
838 .enumerate()
839 .map(|(m, z)| {
840 let t = m as f64 / rate;
841 z.mul(C::of_turns(centre * t - walk.integral(t)))
842 })
843 .collect()
844 }
845
846 fn spectrum(&self, z: &[C]) -> (Vec<f64>, Vec<f64>) {
848 let mut re: Vec<f64> = z.iter().map(|c| c.re).collect();
849 let mut im: Vec<f64> = z.iter().map(|c| c.im).collect();
850 self.plan(z.len()).forward(&mut re, &mut im);
851 (re, im)
852 }
853
854 fn lowpass(
856 &self,
857 (re, im): &(Vec<f64>, Vec<f64>),
858 rate: f64,
859 keep: impl Fn(f64) -> f64,
860 ) -> Vec<C> {
861 let m = re.len();
862 let (mut re, mut im) = (re.clone(), im.clone());
863 for j in 0..m {
864 let signed = if j <= m / 2 {
865 j as f64
866 } else {
867 j as f64 - m as f64
868 };
869 let w = keep(signed * rate / m as f64);
870 re[j] *= w;
871 im[j] *= w;
872 }
873 self.plan(m).inverse(&mut re, &mut im);
874 re.into_iter()
875 .zip(im)
876 .map(|(re, im)| C { re, im })
877 .collect()
878 }
879}
880
881fn gaussian(sigma: f64) -> impl Fn(f64) -> f64 {
882 move |f| sva_formula::exp(-0.5 * (f / sigma) * (f / sigma))
883}
884
885fn brickwall(half: f64) -> impl Fn(f64) -> f64 {
886 move |f| if f.abs() < half { 1.0 } else { 0.0 }
887}
888
889struct Reading {
891 band: Band,
892 freq: Curve,
893 kind: Kind,
894 amp: Curve,
895 phase: f64,
896 flutter_db: f64,
897 coherence: f64,
898 hz: f64,
899 unclaimed_db: Option<f64>,
900 z: Vec<C>,
902}
903
904fn read(
905 x: &[f64],
906 grid: &Grid,
907 (groups, reach): (&[Group], usize),
908 floor: f64,
909) -> (Vec<Track>, Option<f64>) {
910 if groups.is_empty() {
911 return (Vec::new(), energy_ratio(x, x));
912 }
913 let held: Vec<Vec<Option<(f64, f64)>>> =
914 groups.iter().map(|g| peaks_of(g, grid.hops)).collect();
915 let curves: Vec<Vec<f64>> = held
916 .iter()
917 .zip(groups)
918 .map(|(o, g)| coarse(o, g.hz))
919 .collect();
920 let sigmas: Vec<f64> = groups
921 .iter()
922 .map(|g| {
923 let gap = groups
924 .iter()
925 .filter(|o| !std::ptr::eq(*o, g))
926 .map(|o| (g.hz - o.hz).abs())
927 .fold(f64::MAX, f64::min);
928 (GAP_SHARE * gap).clamp(RESOLUTION_HZ.0, RESOLUTION_HZ.1)
929 })
930 .collect();
931 let claimed = claims(&held, &curves, &sigmas, reach);
932 let narrowest = sigmas.iter().copied().fold(f64::MAX, f64::min);
933 let pad = (PAD_SIGMAS / (TAU * narrowest) * grid.sr).ceil() as usize;
934 let size = (x.len() + pad).next_power_of_two();
935 let bands: Vec<Band> = curves
936 .iter()
937 .zip(&sigmas)
938 .map(|(curve, &sigma)| {
939 let lo = curve.iter().copied().fold(f64::MAX, f64::min);
940 let hi = curve.iter().copied().fold(0f64, f64::max);
941 let width = (hi - lo + 4.0 * SIGMAS * RESOLUTION_HZ.1).max(LEAST_BAND_RATE_HZ);
942 let mut every = 1usize;
943 while grid.hop.is_multiple_of(2 * every)
944 && size.is_multiple_of(2 * every)
945 && grid.sr / (2 * every) as f64 >= width
946 {
947 every *= 2;
948 }
949 Band {
950 every,
951 centre_bin: ((lo + hi) / 2.0 * size as f64 / grid.sr).round() as usize,
952 lo_hz: lo,
953 hi_hz: hi,
954 sigma,
955 }
956 })
957 .collect();
958 let mut lengths: Vec<usize> = bands.iter().map(|b| size / b.every).collect();
959 lengths.sort_unstable();
960 lengths.dedup();
961 let reader = Reader {
962 grid,
963 size,
964 whole: Plan::new(size),
965 bands: lengths.into_iter().map(|m| (m, Plan::new(m))).collect(),
966 floor,
967 hop_secs: (0..grid.hops).map(|k| grid.secs(k)).collect(),
968 };
969
970 let first = Whole::of(x, &reader.whole, size);
971 let mut readings: Vec<Reading> = bands
972 .iter()
973 .zip(curves.iter().zip(&claimed))
974 .map(|(b, (curve, claim))| {
975 let raw = reader.band(&first, b);
976 read_one(&reader, raw, *b, (curve, claim, b.sigma))
977 })
978 .collect();
979 for _ in 0..CANCELLATIONS {
980 let left: Vec<f64> = x
981 .iter()
982 .zip(bands_of(&readings, grid, x.len()))
983 .map(|(a, s)| a - s)
984 .collect();
985 let left = Whole::of(&left, &reader.whole, size);
986 readings = readings
987 .iter()
988 .zip(curves.iter().zip(&claimed))
989 .map(|(r, (curve, claim))| {
990 let again = add(&reader.band(&left, &r.band), &own(&reader, r));
991 let curve = match r.kind {
992 Kind::Tonal => curve.clone(),
993 Kind::Breathy => {
994 let mut held = Walk::new(&r.freq.times, &r.freq.values);
995 reader.hop_secs.iter().map(|t| held.value(*t)).collect()
996 }
997 };
998 let flags = r.band.sigma.max(FLAG_LEAST_HZ);
999 read_one(&reader, again, r.band, (&curve, claim, flags))
1000 })
1001 .collect();
1002 }
1003
1004 let left: Vec<f64> = x
1005 .iter()
1006 .zip(synthesis(&readings, grid, x.len()))
1007 .map(|(a, s)| a - s)
1008 .collect();
1009 let whole_left = Whole::of(&left, &reader.whole, size);
1010 let tracks = readings
1011 .iter()
1012 .filter(|r| r.amp.values.iter().any(|v| *v > 0.0))
1013 .map(|r| {
1014 let residual_db = (r.kind == Kind::Tonal)
1015 .then(|| {
1016 let mixed = reader.mixed(
1017 &reader.band(&whole_left, &r.band),
1018 &r.band,
1019 (&r.freq.times, &r.freq.values),
1020 );
1021 let rest = reader.lowpass(
1022 &reader.spectrum(&mixed),
1023 reader.rate(&r.band),
1024 gaussian(r.band.sigma),
1025 );
1026 let span = r.span(&reader);
1027 let held: f64 = r.z[span.clone()].iter().map(|c| c.norm()).sum();
1028 let lost: f64 = rest[span].iter().map(|c| c.norm()).sum();
1029 (held > 0.0).then(|| 10.0 * libm::log10(lost / held))
1030 })
1031 .flatten();
1032 track(r, grid.start, residual_db)
1033 })
1034 .collect();
1035 (tracks, energy_ratio(&left, x))
1036}
1037
1038fn track(r: &Reading, start: f64, residual_db: Option<f64>) -> Track {
1039 let shifted = |c: &Curve| Curve {
1040 times: c.times.iter().map(|t| t + start).collect(),
1041 ..c.clone()
1042 };
1043 Track {
1044 kind: r.kind,
1045 hz: r.hz,
1046 frequency: shifted(&r.freq),
1047 amplitude: shifted(&r.amp),
1048 phase: (r.kind == Kind::Tonal).then_some(r.phase),
1049 flutter_db: r.flutter_db,
1050 coherence: r.coherence,
1051 bandwidth_hz: (r.kind == Kind::Breathy).then(|| {
1052 let lorentz = -libm::log(r.coherence.max(1e-3)) / (PI * COHERENCE_LAG_SECS);
1053 lorentz.max(2.0 * r.band.sigma / 3.0)
1054 }),
1055 resolution_hz: r.band.sigma,
1056 resolution_secs: 1.0 / (TAU * r.band.sigma),
1057 residual_db,
1058 unclaimed_db: r.unclaimed_db,
1059 }
1060}
1061
1062fn energy_ratio(rest: &[f64], whole: &[f64]) -> Option<f64> {
1063 let held: f64 = whole.iter().map(|v| v * v).sum();
1064 let lost: f64 = rest.iter().map(|v| v * v).sum();
1065 (held > 0.0).then(|| 10.0 * libm::log10(lost / held))
1066}
1067
1068impl Reading {
1069 fn span(&self, reader: &Reader) -> std::ops::Range<usize> {
1071 let rate = reader.rate(&self.band);
1072 let at = |t: f64| ((t * rate).round() as usize).min(self.z.len() - 1);
1073 let (a, b) = (self.amp.times[0], self.amp.times[self.amp.times.len() - 1]);
1074 at(a)..at(b) + 1
1075 }
1076}
1077
1078fn own(reader: &Reader, r: &Reading) -> Vec<C> {
1082 if r.kind == Kind::Breathy {
1083 return vec![ZERO; r.z.len()];
1084 }
1085 let (centre, rate) = (reader.centre_hz(&r.band), reader.rate(&r.band));
1086 let mut walk = Walk::new(&r.freq.times, &r.freq.values);
1087 r.z.iter()
1088 .enumerate()
1089 .map(|(m, z)| {
1090 let t = m as f64 / rate;
1091 z.mul(C::of_turns(walk.integral(t) - centre * t))
1092 })
1093 .collect()
1094}
1095
1096fn bands_of(readings: &[Reading], grid: &Grid, n: usize) -> Vec<f64> {
1099 let mut out = vec![0.0; n];
1100 for r in readings.iter().filter(|r| r.kind == Kind::Tonal) {
1101 let mut walk = Walk::new(&r.freq.times, &r.freq.values);
1102 let every = r.band.every as f64;
1103 let last = r.z.len() - 1;
1104 for (i, slot) in out.iter_mut().enumerate() {
1105 let at = i as f64 / every;
1106 let m = (at as usize).min(last);
1107 let u = at - m as f64;
1108 let z = r.z[m].scale(1.0 - u).add(r.z[(m + 1).min(last)].scale(u));
1109 let e = C::of_turns(walk.integral(i as f64 / grid.sr));
1110 *slot += z.re * e.re - z.im * e.im;
1111 }
1112 }
1113 out
1114}
1115
1116fn synthesis(readings: &[Reading], grid: &Grid, n: usize) -> Vec<f64> {
1118 let mut out = vec![0.0; n];
1119 for r in readings.iter().filter(|r| r.kind == Kind::Tonal) {
1120 let mut phase = Walk::new(&r.freq.times, &r.freq.values);
1121 let mut amp = Walk::new(&r.amp.times, &r.amp.values);
1122 let (a0, a1) = (r.amp.times[0], r.amp.times[r.amp.times.len() - 1]);
1123 let from = ((a0 * grid.sr).ceil().max(0.0) as usize).min(n);
1124 let to = (((a1 * grid.sr).floor() as usize) + 1).min(n);
1125 for (i, slot) in out.iter_mut().enumerate().take(to).skip(from) {
1126 let t = i as f64 / grid.sr;
1127 let turns = phase.integral(t).rem_euclid(1.0);
1128 *slot += amp.value(t) * libm::sin(TAU * turns + r.phase);
1129 }
1130 }
1131 out
1132}
1133
1134fn add(a: &[C], b: &[C]) -> Vec<C> {
1135 a.iter().zip(b).map(|(x, y)| x.add(*y)).collect()
1136}
1137
1138fn read_one(
1141 reader: &Reader,
1142 raw: Vec<C>,
1143 b: Band,
1144 (curve, claim, flag_sigma): (&[f64], &[Claim], f64),
1145) -> Reading {
1146 let (freq, mean_hz) = refined(reader, &raw, b, (curve, claim));
1147 let r = measured(reader, &raw, b, (freq, claim), (None, Err(flag_sigma)));
1148 let r = match r.kind {
1149 Kind::Tonal => r,
1150 Kind::Breathy => {
1151 let ends = reader.hop_secs[reader.hop_secs.len() - 1].max(1.0 / reader.grid.sr);
1152 let held = Curve {
1153 times: vec![0.0, ends],
1154 values: vec![mean_hz; 2],
1155 error: 0.0,
1156 };
1157 let flags = Ok((r.flutter_db, r.coherence));
1158 measured(reader, &raw, b, (held, claim), (Some(Kind::Breathy), flags))
1159 }
1160 };
1161 Reading { hz: mean_hz, ..r }
1162}
1163
1164fn refined(
1167 reader: &Reader,
1168 raw: &[C],
1169 b: Band,
1170 (along, claim): (&[f64], &[Claim]),
1171) -> (Curve, f64) {
1172 let (grid, hops) = (reader.grid, &reader.hop_secs);
1173 let rate = reader.rate(&b);
1174 let per_hop = grid.hop / b.every;
1175 let mixed = reader.mixed(raw, &b, (hops, along));
1176 let zc = reader.lowpass(&reader.spectrum(&mixed), rate, gaussian(b.sigma));
1177 let at_hop = |k: usize| zc[(k * per_hop).min(zc.len() - 1)];
1178 let sounding: Vec<bool> = (0..grid.hops)
1179 .map(|k| {
1180 let m = at_hop(k).abs();
1181 m >= reader.floor && m > claim[k].least
1182 })
1183 .collect();
1184 let mut turns = vec![0.0; zc.len()];
1185 for m in 1..zc.len() {
1186 turns[m] = turns[m - 1] + zc[m].mul(zc[m - 1].conj()).arg() / TAU;
1187 }
1188 let deviation = rate / (TAU * b.sigma);
1189 let reach = ((SLOPE_SIGMAS * deviation / 2.0).round() as usize)
1190 .max(per_hop.div_ceil(2))
1191 .max(1);
1192 let edge = (EDGE_SIGMAS * deviation).ceil() as usize + reach;
1193 let end = grid.samples.div_ceil(b.every);
1194 let inside = |k: usize| k * per_hop >= edge && k * per_hop + edge <= end;
1195 let counted: Vec<bool> = match (0..grid.hops).any(|k| sounding[k] && inside(k)) {
1196 true => (0..grid.hops).map(|k| sounding[k] && inside(k)).collect(),
1197 false => sounding.clone(),
1198 };
1199 let slope = |k: usize| {
1200 let lo = (k * per_hop).saturating_sub(reach);
1201 let hi = (k * per_hop + reach).min(zc.len() - 1).max(lo + 1);
1202 (turns[hi] - turns[lo]) * rate / (hi - lo) as f64
1203 };
1204 let counted: Vec<bool> = (0..grid.hops)
1205 .map(|k| {
1206 let (off, at) = (slope(k), along[k] + slope(k));
1207 let rival = claim[k].rivals.iter().any(|hz| (hz - at).abs() < off.abs());
1208 counted[k] && claim[k].peak && off.abs() <= CAPTURE_SIGMAS * b.sigma && !rival
1209 })
1210 .collect();
1211 let held: Vec<Option<f64>> = (0..grid.hops)
1212 .map(|k| counted[k].then(|| along[k] + slope(k)))
1213 .collect();
1214 let refined = filled(&held, along[0]);
1215 let weights: Vec<f64> = (0..grid.hops)
1216 .map(|k| if counted[k] { at_hop(k).norm() } else { 0.0 })
1217 .collect();
1218 let total: f64 = weights.iter().sum();
1219 let mean_hz = match total > 0.0 {
1220 true => {
1221 refined
1222 .iter()
1223 .zip(&weights)
1224 .map(|(f, w)| f * w)
1225 .sum::<f64>()
1226 / total
1227 }
1228 false => refined[0],
1229 };
1230 let cent = mean_hz * (libm::pow(2.0, KNOT_CENTS / 1200.0) - 1.0);
1231 let tolerance = cent.max(2.0 * spread(&refined, &counted));
1232 let table = knots(hops, &refined, &counted, tolerance);
1233 (refit(table, hops, &refined, &weights), mean_hz)
1234}
1235
1236fn measured(
1239 reader: &Reader,
1240 raw: &[C],
1241 b: Band,
1242 (freq, claim): (Curve, &[Claim]),
1243 (kind, flags): (Option<Kind>, Result<(f64, f64), f64>),
1244) -> Reading {
1245 let grid = reader.grid;
1246 let rate = reader.rate(&b);
1247 let per_hop = grid.hop / b.every;
1248 let spectrum = reader.spectrum(&reader.mixed(raw, &b, (&freq.times, &freq.values)));
1249 let z = reader.lowpass(&spectrum, rate, gaussian(b.sigma));
1250 let at_hop = |k: usize| z[(k * per_hop).min(z.len() - 1)];
1251 let moduli: Vec<f64> = (0..grid.hops).map(|k| at_hop(k).abs()).collect();
1252 let loudest = (0..grid.hops)
1253 .max_by(|&a, &b| moduli[a].total_cmp(&moduli[b]).then(b.cmp(&a)))
1254 .unwrap_or(0);
1255 let (flutter_db, coherence) = flags.unwrap_or_else(|flag_sigma| {
1256 let held = grid.samples.div_ceil(b.every).min(z.len());
1257 let from = |secs: f64| (loudest * per_hop).min(held.saturating_sub((secs * rate) as usize));
1258 let flagged = match flag_sigma == b.sigma {
1259 true => z.clone(),
1260 false => reader.lowpass(&spectrum, rate, gaussian(flag_sigma)),
1261 };
1262 let banded = reader.lowpass(
1263 &spectrum,
1264 rate,
1265 brickwall(flag_sigma.max(COHERENCE_HALF_HZ)),
1266 );
1267 (
1268 flutter(&flagged[..held], rate, from(FLUTTER_SPAN_SECS)),
1269 coherence(&banded[..held], rate, from(COHERENCE_SPAN_SECS)),
1270 )
1271 });
1272 let kind = kind.unwrap_or(match flutter_db > BREATHY_FLUTTER_DB {
1273 true => Kind::Breathy,
1274 false => Kind::Tonal,
1275 });
1276 let claimed = |k: usize| moduli[k] > claim[k].least;
1277 let levels: Vec<f64> = match kind {
1278 Kind::Tonal => moduli.clone(),
1279 Kind::Breathy => {
1280 let half = ((BREATHY_POWER_SECS / 2.0) * rate).round() as usize;
1281 (0..grid.hops)
1282 .map(|k| {
1283 let c = (k * per_hop).min(z.len() - 1);
1284 let (lo, hi) = (c.saturating_sub(half), (c + half).min(z.len() - 1));
1285 let power =
1286 z[lo..=hi].iter().map(|v| v.norm()).sum::<f64>() / (hi - lo + 1) as f64;
1287 (power / 2.0).sqrt()
1288 })
1289 .collect()
1290 }
1291 };
1292 let peak = |level: f64| match kind {
1293 Kind::Tonal => level,
1294 Kind::Breathy => level * SQRT_2,
1295 };
1296 let heard = |k: usize| peak(levels[k]) >= reader.floor;
1297 let amps: Vec<f64> = (0..grid.hops)
1298 .map(|k| {
1299 if heard(k) && claimed(k) {
1300 levels[k]
1301 } else {
1302 0.0
1303 }
1304 })
1305 .collect();
1306 let amp = amplitude_knots((&reader.hop_secs, grid.secs(1)), &s, reader.floor);
1307 let weighted = (0..grid.hops)
1308 .filter(|&k| amps[k] > 0.0)
1309 .map(|k| at_hop(k).scale(at_hop(k).abs()))
1310 .fold(ZERO, C::add);
1311 let (mut kept, mut ruled) = (0.0, 0.0);
1312 for k in (0..grid.hops).filter(|&k| heard(k)) {
1313 match claimed(k) {
1314 true => kept += levels[k] * levels[k],
1315 false => ruled += levels[k] * levels[k],
1316 }
1317 }
1318 Reading {
1319 band: b,
1320 freq,
1321 kind,
1322 amp,
1323 phase: (weighted.arg() + FRAC_PI_2).rem_euclid(TAU),
1324 flutter_db,
1325 coherence,
1326 hz: 0.0,
1327 unclaimed_db: (ruled > 0.0 && kept > 0.0).then(|| 10.0 * libm::log10(ruled / kept)),
1328 z,
1329 }
1330}
1331
1332fn flutter(z: &[C], rate: f64, from: usize) -> f64 {
1333 let frame = ((FLUTTER_FRAME_SECS * rate).round() as usize).max(1);
1334 let span = (FLUTTER_SPAN_SECS * rate) as usize;
1335 let frames = (from + span).min(z.len()).saturating_sub(from) / frame;
1336 let edge = FLUTTER_MEAN_FRAMES / 2;
1337 if frames <= 2 * edge + 1 || frames * frame * 10 < span {
1338 return 0.0;
1339 }
1340 let levels: Vec<f64> = (0..frames)
1341 .map(|f| {
1342 let s = &z[from + f * frame..from + (f + 1) * frame];
1343 let mean = s.iter().map(|c| c.abs()).sum::<f64>() / frame as f64;
1344 20.0 * libm::log10(mean + 1e-12)
1345 })
1346 .collect();
1347 let offs: Vec<f64> = (edge..frames - edge)
1348 .map(|f| {
1349 let around = levels[f - edge..=f + edge].iter().sum::<f64>();
1350 levels[f] - around / FLUTTER_MEAN_FRAMES as f64
1351 })
1352 .collect();
1353 let mean = offs.iter().sum::<f64>() / offs.len() as f64;
1354 (offs.iter().map(|o| (o - mean) * (o - mean)).sum::<f64>() / offs.len() as f64).sqrt()
1355}
1356
1357fn coherence(b: &[C], rate: f64, from: usize) -> f64 {
1358 let lag = ((COHERENCE_LAG_SECS * rate).round() as usize).max(1);
1359 let to = (from + (COHERENCE_SPAN_SECS * rate) as usize).min(b.len());
1360 if to < from + 3 * lag {
1361 return 1.0;
1362 }
1363 let (mut cross, mut early, mut late) = (ZERO, 0.0, 0.0);
1364 for m in from..to - lag {
1365 cross = cross.add(b[m].conj().mul(b[m + lag]));
1366 early += b[m].norm();
1367 late += b[m + lag].norm();
1368 }
1369 let denom = (early * late).sqrt();
1370 if denom > 0.0 {
1371 cross.abs() / denom
1372 } else {
1373 1.0
1374 }
1375}
1376
1377fn amplitude_knots((hop_secs, step): (&[f64], f64), amps: &[f64], floor: f64) -> Curve {
1378 let mut sounding = (0..amps.len()).filter(|&k| amps[k] > 0.0);
1379 let (Some(first), Some(last)) = (sounding.clone().next(), sounding.next_back()) else {
1380 return Curve {
1381 times: vec![hop_secs[0], hop_secs[hop_secs.len() - 1]],
1382 values: vec![0.0, 0.0],
1383 error: 0.0,
1384 };
1385 };
1386 let (a, b) = (first.saturating_sub(1), (last + 1).min(amps.len() - 1));
1387 let peak = amps.iter().copied().fold(0.0, f64::max);
1388 let all = vec![true; b - a + 1];
1389 let tolerance = (KNOT_SHARE * peak).max(floor);
1390 let mut table = knots(&hop_secs[a..=b], &s[a..=b], &all, tolerance);
1391 if table.times.len() == 1 {
1392 table.times.push(table.times[0] + step);
1393 table.values.push(0.0);
1394 }
1395 table
1396}
1397
1398fn spread(values: &[f64], counts: &[bool]) -> f64 {
1402 let mut d: Vec<f64> = (1..values.len().saturating_sub(1))
1403 .filter(|&k| counts[k - 1] && counts[k] && counts[k + 1])
1404 .map(|k| (values[k - 1] - 2.0 * values[k] + values[k + 1]).abs())
1405 .collect();
1406 if d.is_empty() {
1407 return 0.0;
1408 }
1409 let middle = d.len() / 2;
1410 let (_, median, _) = d.select_nth_unstable_by(middle, f64::total_cmp);
1411 *median / (0.6745 * 6f64.sqrt())
1412}
1413
1414fn refit(table: Curve, at: &[f64], measured: &[f64], weights: &[f64]) -> Curve {
1418 let times = table.times;
1419 let n = times.len();
1420 let (mut diag, mut off, mut rhs) = (vec![0.0; n], vec![0.0; n], vec![0.0; n]);
1421 let mut seg = 0;
1422 for ((t, y), w) in at.iter().zip(measured).zip(weights) {
1423 while seg + 2 < n && *t > times[seg + 1] {
1424 seg += 1;
1425 }
1426 if *w <= 0.0 {
1427 continue;
1428 }
1429 let u = ((t - times[seg]) / (times[seg + 1] - times[seg])).clamp(0.0, 1.0);
1430 diag[seg] += w * (1.0 - u) * (1.0 - u);
1431 diag[seg + 1] += w * u * u;
1432 off[seg] += w * (1.0 - u) * u;
1433 rhs[seg] += w * (1.0 - u) * y;
1434 rhs[seg + 1] += w * u * y;
1435 }
1436 let ridge = diag.iter().copied().fold(0.0, f64::max) * 1e-9;
1437 if ridge == 0.0 {
1438 return Curve {
1439 times,
1440 values: table.values,
1441 error: 0.0,
1442 };
1443 }
1444 for k in 0..n {
1445 diag[k] += ridge;
1446 rhs[k] += ridge * table.values[k];
1447 }
1448 let values = tridiagonal(&diag, &off, &rhs);
1449 let mut walk = Walk::new(×, &values);
1450 let error = at
1451 .iter()
1452 .zip(measured)
1453 .zip(weights)
1454 .filter(|(_, w)| **w > 0.0)
1455 .map(|((t, y), _)| (walk.value(*t) - y).abs())
1456 .fold(0.0, f64::max);
1457 Curve {
1458 times,
1459 values,
1460 error,
1461 }
1462}
1463
1464fn tridiagonal(diag: &[f64], off: &[f64], rhs: &[f64]) -> Vec<f64> {
1466 let n = diag.len();
1467 let (mut c, mut d) = (vec![0.0; n], vec![0.0; n]);
1468 for k in 0..n {
1469 let (below, carried, held) = match k {
1470 0 => (0.0, 0.0, 0.0),
1471 _ => (off[k - 1], c[k - 1], d[k - 1]),
1472 };
1473 let pivot = diag[k] - below * carried;
1474 c[k] = if k + 1 < n { off[k] / pivot } else { 0.0 };
1475 d[k] = (rhs[k] - below * held) / pivot;
1476 }
1477 let mut x = vec![0.0; n];
1478 for k in (0..n).rev() {
1479 x[k] = d[k] - if k + 1 < n { c[k] * x[k + 1] } else { 0.0 };
1480 }
1481 x
1482}
1483
1484fn knots(times: &[f64], values: &[f64], counts: &[bool], tolerance: f64) -> Curve {
1488 let n = times.len();
1489 if n <= 2 {
1490 return Curve {
1491 times: times.to_vec(),
1492 values: values.to_vec(),
1493 error: 0.0,
1494 };
1495 }
1496 let mut kept = vec![0usize];
1497 let mut s = 0;
1498 while s < n - 1 {
1499 let (mut lo, mut hi) = (f64::MIN, f64::MAX);
1500 let mut end = s + 1;
1501 for e in s + 1..n {
1502 let dt = times[e] - times[s];
1503 let slope = (values[e] - values[s]) / dt;
1504 if slope >= lo && slope <= hi {
1505 end = e;
1506 }
1507 if counts[e] {
1508 lo = lo.max((values[e] - tolerance - values[s]) / dt);
1509 hi = hi.min((values[e] + tolerance - values[s]) / dt);
1510 }
1511 if lo > hi {
1512 break;
1513 }
1514 }
1515 kept.push(end);
1516 s = end;
1517 }
1518 let mut error = 0f64;
1519 for w in kept.windows(2) {
1520 for j in (w[0]..=w[1]).filter(|&j| counts[j]) {
1521 let u = (times[j] - times[w[0]]) / (times[w[1]] - times[w[0]]);
1522 let line = values[w[0]] + (values[w[1]] - values[w[0]]) * u;
1523 error = error.max((values[j] - line).abs());
1524 }
1525 }
1526 Curve {
1527 times: kept.iter().map(|&k| times[k]).collect(),
1528 values: kept.iter().map(|&k| values[k]).collect(),
1529 error,
1530 }
1531}