sva_samples/measure/harmony/
measured.rs1use super::{Framing, Moment, Partial, Window};
4use crate::measure::peaks::{kernel, leakage, peaks};
5use crate::measure::spectrum::{magnitudes_of, pinned_frame};
6
7pub const WINDOW_SECS: f64 = 0.05;
8
9pub const METHOD: &str = "measured: a Hann frame of `window.size` samples of each channel \
10 centred on each instant, zero past its ends, their power per bin \
11 averaged over the channels; its local \
12 maxima, each refined by the two-bin Hann \
13 interpolation of Grandke, T. (1983), IEEE Trans. Instrum. Meas. 32(2) \
14 350-355, exact for a lone sinusoid; a peak no louder than the Hann \
15 kernel's bound 1/(pi m (m^2 - 1)) at m bins (Harris, F. J. (1978), \
16 Proc. IEEE 66(1) 51-83) lets the louder peaks kept leak there is \
17 dropped as leakage. Each partial's bounds are Grandke's estimate over \
18 its two bins each widened by what that bound lets the other kept \
19 partials, at their estimated levels, leak into it: they hold where the \
20 frame is stationary sinusoids at the kept partials, and are a point \
21 for a lone one";
22
23fn offset(a: f64) -> f64 {
24 match a.is_finite() {
25 true => ((2.0 * a - 1.0) / (a + 1.0)).clamp(-1.0, 1.0),
26 false => 1.0,
27 }
28}
29
30struct Peaked {
32 k: usize,
33 sign: f64,
34 hz: f64,
35 amplitude: f64,
36}
37
38fn refined(mags: &[f64], bin_hz: f64, hz: f64) -> Peaked {
41 let near = (hz / bin_hz).round() as usize;
42 let k = (near.saturating_sub(1)..=near + 1)
43 .filter(|k| (1..mags.len() - 1).contains(k))
44 .max_by(|a, b| mags[*a].total_cmp(&mags[*b]))
45 .expect("a peak stands off both ends");
46 let sign = match mags[k + 1] >= mags[k - 1] {
47 true => 1.0,
48 false => -1.0,
49 };
50 let d = offset(mags[(k as f64 + sign) as usize] / mags[k]);
51 Peaked {
52 k,
53 sign,
54 hz: (k as f64 + sign * d) * bin_hz,
55 amplitude: mags[k] / kernel(d),
56 }
57}
58
59fn bounded(p: &Peaked, mags: &[f64], bin_hz: f64, leaked: impl Fn(f64) -> f64) -> Partial {
62 let toward = p.k as f64 + p.sign;
63 let widened = |x: f64, by: f64| ((x - by).max(0.0), x + by);
64 let own = widened(mags[p.k], leaked(p.k as f64));
65 let side = widened(mags[toward as usize], leaked(toward));
66 let a_hi = match own.0 > 0.0 {
67 true => side.1 / own.0,
68 false => f64::INFINITY,
69 };
70 let d = (offset(side.0 / own.1), offset(a_hi));
71 let ends = [
72 (p.k as f64 + p.sign * d.0) * bin_hz,
73 (p.k as f64 + p.sign * d.1) * bin_hz,
74 ];
75 let nearest = match d.0 <= 0.0 && d.1 >= 0.0 {
76 true => 0.0,
77 false => d.0.abs().min(d.1.abs()),
78 };
79 Partial {
80 hz: p.hz,
81 amplitude: p.amplitude,
82 hz_bounds: (ends[0].min(ends[1]), ends[0].max(ends[1])),
83 amplitude_bounds: (
84 own.0 / kernel(nearest),
85 own.1 / kernel(d.0.abs().max(d.1.abs())),
86 ),
87 }
88}
89
90pub fn moments(
91 planes: &[&[f64]],
92 sample_rate: f64,
93 start_secs: f64,
94 hop_secs: f64,
95) -> (Vec<Moment>, Framing) {
96 let len = crate::measure::held(planes);
97 let hop = ((hop_secs * sample_rate).round() as usize).max(1);
98 let size = pinned_frame(WINDOW_SECS, sample_rate);
99 let half = (size / 2) as i64;
100 let mut held = Vec::new();
101 for centre in (0..len).step_by(hop) {
102 let frames: Vec<Vec<f64>> = planes
103 .iter()
104 .map(|samples| {
105 (0..size as i64)
106 .map(|k| {
107 let at = centre as i64 - half + k;
108 usize::try_from(at)
109 .ok()
110 .and_then(|at| samples.get(at))
111 .copied()
112 .unwrap_or(0.0)
113 })
114 .collect()
115 })
116 .collect();
117 let frames: Vec<&[f64]> = frames.iter().map(Vec::as_slice).collect();
118 let framed = magnitudes_of(&frames, sample_rate, Some(WINDOW_SECS));
119 let (mags, bin_hz) = (framed.mags, framed.bin_hz);
120 let mut kept: Vec<Peaked> = Vec::new();
121 for peak in peaks(&mags, bin_hz, usize::MAX) {
122 let found = refined(&mags, bin_hz, peak.hz);
123 let leaked: f64 = kept
124 .iter()
125 .map(|q| q.amplitude * leakage((found.hz - q.hz).abs() / bin_hz))
126 .sum();
127 if found.amplitude > leaked {
128 kept.push(found);
129 }
130 }
131 let partials = kept
132 .iter()
133 .enumerate()
134 .map(|(i, p)| {
135 let leaked = |bin: f64| -> f64 {
136 kept.iter()
137 .enumerate()
138 .filter(|(j, _)| *j != i)
139 .map(|(_, q)| q.amplitude * leakage((bin - q.hz / bin_hz).abs()))
140 .sum()
141 };
142 bounded(p, &mags, bin_hz, leaked)
143 })
144 .collect();
145 held.push(Moment {
146 t_secs: start_secs + centre as f64 / sample_rate,
147 partials,
148 });
149 }
150 let framing = Framing {
151 hop_secs: hop as f64 / sample_rate,
152 window: Some(Window {
153 size,
154 secs: size as f64 / sample_rate,
155 resolution_hz: sample_rate / size as f64,
156 }),
157 };
158 (held, framing)
159}