Skip to main content

sva_samples/measure/
partials.rs

1// Concern: tracks each partial as frequency and amplitude knot tables, tonal or breathy, with its residual | Non-concern: naming notes (pitch.rs) | IO: (planes, rate, start, Asked) -> Partials
2
3use 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
10/// Two peaks, or two fragments, are one partial within this many cents or `NEAR_HZ`.
11pub const NEAR_CENTS: f64 = 25.0;
12pub const NEAR_HZ: f64 = 8.0;
13/// A peak counts where it stands this far over the median bin within a sixth of an octave.
14pub const PROMINENCE_DB: f64 = 6.0;
15/// A peak stands this many times over the Hann sidelobes louder peaks put under it: a
16/// partial that decays or swells within a frame raises its skirts over a steady one's.
17pub const LEAK_MARGIN: f64 = 4.0;
18/// The demodulating Gaussian's half-width: this share of the gap to the nearest track, clamped.
19pub const GAP_SHARE: f64 = 0.4;
20pub const RESOLUTION_HZ: (f64, f64) = (6.0, 40.0);
21/// Groups closer than this share one band at the narrowest Gaussian, and are one track.
22pub const LEAST_GAP_HZ: f64 = RESOLUTION_HZ.0 / GAP_SHARE;
23/// A hop's frequency is the band's mean phase slope over this many Gaussian time deviations:
24/// a change faster than the Gaussian's width is not in the band to be read.
25pub const SLOPE_SIGMAS: f64 = 4.0;
26/// A hop's frequency is read only where its slope's span lies this many Gaussian time
27/// deviations inside the target, the band there untouched by its ends, and held outward.
28pub const EDGE_SIGMAS: f64 = 3.0;
29/// A phase slope past this many half-widths from the detected frequency reads a neighbour
30/// leaking into a band its own track has left silent, and is not the track's.
31pub const CAPTURE_SIGMAS: f64 = 2.0;
32/// Jacobi passes against the residual: each cancels what the last left of every tonal
33/// neighbour's band in a track's own, the neighbours' leak back into it a factor of their
34/// Gaussian's weight at the gap smaller each pass.
35pub const CANCELLATIONS: usize = 2;
36/// A frequency knot table stays this close to the measured curve, in cents, or within twice
37/// the curve's own noise where that is wider.
38pub const KNOT_CENTS: f64 = 1.0;
39/// An amplitude knot table stays within this share of the track's peak, or the floor if higher.
40pub const KNOT_SHARE: f64 = 0.02;
41/// Flutter: the dB spread of `FLUTTER_FRAME_SECS` means about their `FLUTTER_MEAN_FRAMES`
42/// running mean over the loudest `FLUTTER_SPAN_SECS`; over `BREATHY_FLUTTER_DB` is breathy.
43pub 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;
47/// Coherence: the band's correlation with itself `COHERENCE_LAG_SECS` on, over the loudest
48/// `COHERENCE_SPAN_SECS`, the band at least `COHERENCE_HALF_HZ` either side.
49pub const COHERENCE_LAG_SECS: f64 = 0.01;
50pub const COHERENCE_SPAN_SECS: f64 = 0.25;
51pub const COHERENCE_HALF_HZ: f64 = 15.0;
52/// The cancelled reads take a track's flags at this half-width at least: a narrowband noise's
53/// envelope under a narrower Gaussian moves slower than the flutter's 30 ms running mean.
54pub const FLAG_LEAST_HZ: f64 = 20.0;
55/// A breathy track's level is its band's rms over this span.
56pub const BREATHY_POWER_SECS: f64 = 0.03;
57/// The decimated band is read at least this often.
58const LEAST_BAND_RATE_HZ: f64 = 1000.0;
59/// Past this many Gaussian deviations a weight is under e^-18 and is left out.
60const SIGMAS: f64 = 6.0;
61/// Zero padding, in the narrowest Gaussian's time deviations, so no band wraps onto the target.
62const PAD_SIGMAS: f64 = 12.0;
63/// A sixth of an octave either side, and this many bins more, is a peak's neighbourhood.
64const 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/// What a caller asks of `measure`.
161#[derive(Clone, Copy, Debug, PartialEq)]
162pub struct Asked {
163    pub frame_secs: f64,
164    /// The span of samples whose frames' power each hop averages.
165    pub window_secs: f64,
166    pub hop_secs: f64,
167    pub floor_db: f64,
168    pub tracks: usize,
169}
170
171/// A knot table: `pwl(t, times[0], values[0], ...)`; `error` the most it leaves the curve.
172#[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    /// Energy-weighted over the hops it sounds.
198    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    /// The band's energy at hops ruled a louder neighbour's, over what the track keeps.
209    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
223/// Whole samples: a frame of at least 2, a hop of at least 1, a window of whole hops of frames.
224pub 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    /// The target's own samples.
267    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
279/// Transforms of one length, its twiddles computed once.
280struct 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
306/// One frame's power per bin, a sinusoid's amplitude squared at its own bin, and the
307/// samples its Hann spans; `None` where it spans under two.
308type Power = Option<(Vec<f64>, usize)>;
309
310/// Each hop's peaks off the power of every frame centred within `reach` hops, averaged.
311fn 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
404/// Loudest first, the local maxima over the floor standing `LEAK_MARGIN` over the Hann
405/// sidelobes the kept peaks, and a DC bin 0 holds as its maximum, put under them; then the
406/// prominent ones 2 frame bins clear of 0 Hz and Nyquist.
407fn 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
539/// Each hop's own peak, energy-weighted: (frequency, amplitude).
540fn 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
562/// Per hop: whether the group holds a peak, where alone the phase slope reads its frequency;
563/// every other group's peak; the least modulus that is the track's own, 0 within its peaks'
564/// span, `CLAIM_MARGIN` times `amp e^(-d^2 / (2 sigma^2))` at the gap d within `reach` of
565/// them, infinite past.
566struct 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 * libm::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
618/// `None` hops interpolated between their held neighbours, held past the ends.
619fn 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
647/// A knot table read at rising instants: `value` its straight lines, held past its ends;
648/// `integral` its running integral from the first knot, extended at the end values.
649struct 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    /// The segment holding `t`, the cursor moved there, back if `t` came before it.
672    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
751/// One transform of the whole target, zero-padded; each track reads its band off it.
752struct 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/// One track's band: decimated `every` samples, shifted down by `centre_bin`, flat over
768/// `lo_hz..hi_hz` and 6 of the widest Gaussians more; `sigma` its own Gaussian.
769#[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
778/// What every track's read shares: the grid, the transforms' lengths and plans, the floor.
779struct 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    /// The band's analytic signal, flat over the track's range and 240 Hz more, Gaussian past
807    /// it, shifted down by the centre bin and decimated.
808    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 = libm::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    /// The band times e^(-i 2 pi (pwl_integral(t) - centre t)): demodulated along a table.
834    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    /// The band's own transform, to be lowpassed more than once.
847    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    /// `keep(hz)` each bin's weight, back to the band's samples.
855    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| libm::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
889/// One track while it is read: its tables, flags, and its band along its table.
890struct 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    /// The band demodulated along `freq` and lowpassed by the band's own Gaussian.
901    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    /// The band's samples from the amplitude table's first knot to its last.
1070    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
1078/// A tonal track's band as `band` reads it: its demodulated band remodulated by its phase
1079/// and shifted down by the band's centre bin, so the residual's band plus this is its band
1080/// again; a breathy track is left in the residual.
1081fn 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
1096/// Every tonal track's band as samples, Re z(t) e^(i 2 pi pwl_integral(t)), z read between
1097/// the band's samples on straight lines.
1098fn 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
1116/// Every tonal track's knot tables as samples: amplitude sin(2 pi pwl_integral + phase).
1117fn 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
1138/// Reads one track off `raw`, its band: frequency refined by the band's phase slope along
1139/// `curve`, then amplitude, phase and flags, read at `flag_sigma`, along the table it fits.
1140fn 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
1164/// The frequency per hop where the band sounds: `along`'s plus the band's mean phase slope
1165/// demodulated along it, held between; its table, fitted; and its energy-weighted mean.
1166fn 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
1236/// Amplitude, phase and flags along a frequency table; `kind` where already known, and the
1237/// flags, flutter and coherence, where already read, else the Gaussian they are read at.
1238fn 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)), &amps, 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], &amps[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
1398/// A robust deviation of a curve's noise about its trend where `counts`: the median absolute
1399/// second difference over 0.6745 sqrt 6, as independent errors e give var(e_{k-1} - 2 e_k +
1400/// e_{k+1}) = 6 var(e); 0 under three counted points in a row.
1401fn 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
1414/// The table's values refit, its times held, by least squares weighted by `weights` over
1415/// every point, the line through two knots continuous; a knot no weight reaches keeps its
1416/// value. `error` the largest gap it leaves at a weighted point.
1417fn 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(&times, &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
1464/// The symmetric tridiagonal system diag, off (off[k] couples k and k + 1), by Thomas.
1465fn 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
1484/// The fewest of the points, first and last kept, whose straight lines pass within
1485/// `tolerance` of every point `counts` marks: each segment runs to the last point whose line
1486/// from the segment's start every marked point between admits. `error` the largest gap left.
1487fn 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}