1use super::fit::golden_section;
4use crate::cast;
5use crate::units::{Cents, Hz, Seconds};
6
7pub(crate) const VIBRATO_RATE_MIN: Hz = Hz::from_const(3.0);
9pub(crate) const VIBRATO_RATE_MAX: Hz = Hz::from_const(12.0);
11pub(crate) const RATE_SLACK: f64 = 0.01;
16pub(crate) const VIBRATO_EXTENT_MIN: Cents = Cents::from_const(10.0);
18pub const VIBRATO_SPAN_MIN: Seconds = Seconds::from_const(0.5);
20pub(crate) const VIBRATO_SHARE_MIN: f64 = 0.5;
23pub(crate) const VIBRATO_CYCLES_MIN: f64 = 2.5;
29const SEARCH_LO: f64 = 1.0;
32const SEARCH_CYCLES: f64 = 1.5;
34const SEARCH_HI: f64 = 20.0;
36const SEARCH_STEP: f64 = 0.01;
38
39pub const VIBRATO_CONVENTION: &str = "the voiced f0 frames of a steady span, a voiced run of \
41the track split where two stable pitches lie 150 cents or more apart, without 50 ms at each end, \
42without the frames where a glide's fitted curve lies more than 10 cents from both its pitches \
43and without a drop: the span before it ends 50 ms before its onset, and the span after starts \
44once it has settled, five time constants and at least 100 ms after the onset; in cents; start_s and end_s are its first and last frame in audio time; a least-squares line and sine fitted \
45together, at the rate whose sine explains the most power beyond the line, searched from 1 Hz or \
461.5 cycles over the span up to 20 Hz in 0.01 Hz steps and refined by golden-section search; extent_raw_ct is that sine's amplitude, half the peak-to-peak swing; extent_ct is \
47extent_raw_ct times extent_factor, which undoes the smoothing of the f0 track at that rate; a \
48vibrato counts at 2.97 to 12.12 Hz, 3 to 12 Hz with 1 % slack, with extent_ct of 10 or more and a share, the power the sine \
49explains beyond the line, the line's residual less that of the line and sine together, over the \
50power left after the line alone, of 0.5 or more, over 2.5 cycles or more";
51
52#[derive(Clone, Copy, Debug, PartialEq)]
54pub struct VibratoFit {
55 pub start: Seconds,
58 pub end: Seconds,
60 pub rate: Hz,
62 pub extent_raw: Cents,
64 pub factor: f64,
66 pub share: f64,
68 pub kurtosis: f64,
71 pub pitch: Hz,
73}
74
75impl VibratoFit {
76 #[must_use]
78 pub fn extent(&self) -> Cents {
79 Cents::clamped(self.extent_raw.get() * self.factor)
80 }
81
82 #[must_use]
84 pub fn cycles(&self) -> f64 {
85 self.rate.get() * (self.end.get() - self.start.get())
86 }
87
88 #[must_use]
90 pub fn accepted(&self) -> bool {
91 let rate = self.rate.get();
92 (VIBRATO_RATE_MIN.get() * (1.0 - RATE_SLACK)..=VIBRATO_RATE_MAX.get() * (1.0 + RATE_SLACK))
93 .contains(&rate)
94 && self.extent() >= VIBRATO_EXTENT_MIN
95 && self.share >= VIBRATO_SHARE_MIN
96 && self.cycles() >= VIBRATO_CYCLES_MIN
97 }
98}
99
100pub const EXTENT_SHARE: [(f64, f64); 9] = [
109 (3.0, 1.003),
110 (4.0, 0.973),
111 (5.0, 0.944),
112 (5.5, 0.939),
113 (6.0, 0.909),
114 (7.0, 0.812),
115 (8.0, 0.817),
116 (10.0, 0.800),
117 (12.0, 0.732),
118];
119
120#[must_use]
123pub(crate) fn extent_factor(rate: Hz) -> f64 {
124 let r = rate.get();
125 let share = match EXTENT_SHARE.iter().position(|&(at, _)| at >= r) {
126 None => EXTENT_SHARE[EXTENT_SHARE.len() - 1].1,
127 Some(0) => EXTENT_SHARE[0].1,
128 Some(k) => {
129 let ((r0, s0), (r1, s1)) = (EXTENT_SHARE[k - 1], EXTENT_SHARE[k]);
130 s0 + (s1 - s0) * (r - r0) / (r1 - r0)
131 }
132 };
133 1.0 / share
134}
135
136#[must_use]
140pub fn fit_vibrato(frames: &[(Seconds, Hz)]) -> Option<VibratoFit> {
141 let (first, last) = (frames.first()?, frames.last()?);
142 let span = last.0.get() - first.0.get();
143 if span < VIBRATO_SPAN_MIN.get() {
144 return None;
145 }
146 let mut cents: Vec<f64> = frames
147 .iter()
148 .filter(|(_, f0)| f0.get() > 0.0)
149 .map(|(_, f0)| 1200.0 * f0.get().log2())
150 .collect();
151 let times: Vec<f64> = frames
152 .iter()
153 .filter(|(_, f0)| f0.get() > 0.0)
154 .map(|(t, _)| t.get() - first.0.get())
155 .collect();
156 let mean = cents.iter().sum::<f64>() / cast::usize_to_f64(cents.len());
157 for c in &mut cents {
158 *c -= mean;
159 }
160 let residuals = detrend(×, ¢s);
161 let power: f64 = residuals.iter().map(|r| r * r).sum();
162 if power <= 0.0 {
163 return None;
164 }
165 let explained = |rate: f64| {
166 sine_fit(×, ¢s, rate).map_or(f64::NEG_INFINITY, |(left, _)| power - left)
167 };
168 let lo = SEARCH_LO.max(SEARCH_CYCLES / span);
169 let steps = cast::floor_to_usize((SEARCH_HI - lo) / SEARCH_STEP)?;
170 let (best, _) = (0..=steps)
171 .map(|k| lo + SEARCH_STEP * cast::usize_to_f64(k))
172 .map(|rate| (rate, explained(rate)))
173 .max_by(|a, b| a.1.total_cmp(&b.1))?;
174 let (rate, _) = golden_section(
175 |rate| -explained(rate),
176 (best - SEARCH_STEP).max(lo),
177 (best + SEARCH_STEP).min(SEARCH_HI),
178 1e-6,
179 );
180 let (left, amplitude) = sine_fit(×, ¢s, rate)?;
181 let explained_power = power - left;
182 let kurtosis = kurtosis(&residuals);
183 let rate = Hz::new(rate).ok()?;
184 Some(VibratoFit {
185 start: first.0,
186 end: last.0,
187 rate,
188 extent_raw: Cents::new(amplitude).ok()?,
189 factor: extent_factor(rate),
190 share: (explained_power / power).min(1.0),
191 kurtosis,
192 pitch: Hz::new(2_f64.powf(mean / 1200.0)).ok()?,
193 })
194}
195
196fn sine_fit(times: &[f64], values: &[f64], rate: f64) -> Option<(f64, f64)> {
204 let mut normal = [[0.0; 5]; 4];
205 for (&time, &value) in times.iter().zip(values) {
206 let (sine, cosine) = (std::f64::consts::TAU * rate * time).sin_cos();
207 let basis = [1.0, time, cosine, sine];
208 for (row, &u) in normal.iter_mut().zip(&basis) {
209 for (cell, &v) in row.iter_mut().zip(&basis) {
210 *cell += u * v;
211 }
212 row[4] += u * value;
213 }
214 }
215 let [offset, slope, along_cos, along_sin] = solve4(normal)?;
216 let left = times
217 .iter()
218 .zip(values)
219 .map(|(&time, &value)| {
220 let (sine, cosine) = (std::f64::consts::TAU * rate * time).sin_cos();
221 (value - offset - slope * time - along_cos * cosine - along_sin * sine).powi(2)
222 })
223 .sum();
224 Some((left, along_cos.hypot(along_sin)))
225}
226
227fn detrend(t: &[f64], y: &[f64]) -> Vec<f64> {
230 let n = cast::usize_to_f64(t.len());
231 let (st, sy) = (t.iter().sum::<f64>(), y.iter().sum::<f64>());
232 let stt: f64 = t.iter().map(|t| t * t).sum();
233 let sty: f64 = t.iter().zip(y).map(|(t, y)| t * y).sum();
234 let det = n * stt - st * st;
235 let slope = if det > 0.0 {
236 (n * sty - st * sy) / det
237 } else {
238 0.0
239 };
240 let intercept = (sy - slope * st) / n;
241 t.iter()
242 .zip(y)
243 .map(|(t, y)| y - intercept - slope * t)
244 .collect()
245}
246
247fn kurtosis(residuals: &[f64]) -> f64 {
250 let n = cast::usize_to_f64(residuals.len());
251 let (mut m2, mut m4) = (0.0, 0.0);
252 for r in residuals {
253 m2 += r * r;
254 m4 += r.powi(4);
255 }
256 if m2 > 0.0 { n * m4 / (m2 * m2) } else { 0.0 }
257}
258
259fn solve4(mut m: [[f64; 5]; 4]) -> Option<[f64; 4]> {
262 for col in 0..4 {
263 let pivot = (col..4).max_by(|&a, &b| m[a][col].abs().total_cmp(&m[b][col].abs()))?;
264 if m[pivot][col].abs() < 1e-12 {
265 return None;
266 }
267 m.swap(col, pivot);
268 for row in col + 1..4 {
269 let factor = m[row][col] / m[col][col];
270 for k in col..5 {
271 m[row][k] -= factor * m[col][k];
272 }
273 }
274 }
275 let mut x = [0.0; 4];
276 for row in (0..4).rev() {
277 let tail: f64 = (row + 1..4).map(|k| m[row][k] * x[k]).sum();
278 x[row] = (m[row][4] - tail) / m[row][row];
279 }
280 Some(x)
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286
287 #[test]
288 fn the_share_counts_only_the_power_beyond_the_line() {
289 let reads = |noise: f64| fit_vibrato(&noisy(0.0, noise)).unwrap();
295 let clear = reads(18.0);
296 assert!(
297 clear.accepted() && (0.55..0.59).contains(&clear.share),
298 "{clear:?}"
299 );
300 let borderline = reads(20.6);
301 assert!((0.48..0.5).contains(&borderline.share), "{borderline:?}");
302 assert!(!borderline.accepted(), "{borderline:?}");
303 }
304
305 fn noisy(phase: f64, noise: f64) -> Vec<(Seconds, Hz)> {
308 let hop = 128.0 / 22_050.0;
309 let mut seed: u64 = 7;
310 let count = cast::floor_to_usize(3.0 / 5.5 / hop).unwrap();
311 (0..=count)
312 .map(|k| {
313 seed = seed
314 .wrapping_mul(6_364_136_223_846_793_005)
315 .wrapping_add(1_442_695_040_888_963_407);
316 let u = cast::u64_to_f64(seed >> 11) / cast::u64_to_f64(1 << 53) * 2.0 - 1.0;
317 let t = cast::usize_to_f64(k) * hop;
318 let cents = 20.0 * (std::f64::consts::TAU * 5.5 * t + phase).sin() + noise * u;
319 (
320 Seconds::new(0.2 + t).unwrap(),
321 Hz::new(220.0 * 2f64.powf(cents / 1200.0)).unwrap(),
322 )
323 })
324 .collect()
325 }
326
327 #[test]
328 fn the_rate_search_maximises_the_power_beyond_the_line() {
329 let hop = 128.0 / 22_050.0;
334 let count = cast::floor_to_usize(0.8 / hop).unwrap();
335 let frames: Vec<(Seconds, Hz)> = (0..=count)
336 .map(|k| {
337 let t = cast::usize_to_f64(k) * hop;
338 let cents =
339 300.0 * (t - 0.4).powi(2) + 10.0 * (std::f64::consts::TAU * 6.0 * t).sin();
340 (
341 Seconds::new(0.2 + t).unwrap(),
342 Hz::new(220.0 * 2f64.powf(cents / 1200.0)).unwrap(),
343 )
344 })
345 .collect();
346 let fit = fit_vibrato(&frames).unwrap();
347 assert!((fit.rate.get() - 6.0).abs() < 0.3, "{fit:?}");
348 }
349
350 #[test]
351 fn a_singular_joint_fit_reads_no_vibrato() {
352 let frames: Vec<(Seconds, Hz)> = [0.2, 0.5, 0.8]
354 .iter()
355 .zip([220.0, 221.0, 219.0])
356 .map(|(&t, f)| (Seconds::new(t).unwrap(), Hz::new(f).unwrap()))
357 .collect();
358 assert_eq!(fit_vibrato(&frames), None);
359 }
360
361 #[test]
362 fn the_convention_states_the_constants() {
363 use super::super::glide::STEADY_SPLIT;
364 use super::super::{RUN_TRIM, SETTLE_MIN, num, says};
365 let ms = |s: f64| num(1000.0 * s);
366 says(
367 VIBRATO_CONVENTION,
368 &[
369 format!("lie {} cents or more apart", num(STEADY_SPLIT.get())),
370 format!("without {} ms at each end", ms(RUN_TRIM.get())),
371 format!(
372 "a glide's fitted curve lies more than {} cents from both its pitches",
373 num(VIBRATO_EXTENT_MIN.get())
374 ),
375 format!("five time constants and at least {} ms", ms(SETTLE_MIN)),
376 format!(
377 "searched from {} Hz or {} cycles over the span up to {} Hz in {} Hz steps",
378 num(SEARCH_LO),
379 num(SEARCH_CYCLES),
380 num(SEARCH_HI),
381 num(SEARCH_STEP)
382 ),
383 format!(
384 "counts at {} to {} Hz, {} to {} Hz with {} % slack",
385 num(VIBRATO_RATE_MIN.get() * (1.0 - RATE_SLACK)),
386 num(VIBRATO_RATE_MAX.get() * (1.0 + RATE_SLACK)),
387 num(VIBRATO_RATE_MIN.get()),
388 num(VIBRATO_RATE_MAX.get()),
389 num(100.0 * RATE_SLACK)
390 ),
391 format!("extent_ct of {} or more", num(VIBRATO_EXTENT_MIN.get())),
392 format!("of {} or more, over", num(VIBRATO_SHARE_MIN)),
393 format!("over {} cycles or more", num(VIBRATO_CYCLES_MIN)),
394 ],
395 );
396 }
397
398 fn frames(rate: f64, extent: f64, span: f64) -> Vec<(Seconds, Hz)> {
399 let hop = 128.0 / 22_050.0;
400 let count = cast::floor_to_usize(span / hop).unwrap();
401 (0..=count)
402 .map(|k| {
403 let t = 0.2 + cast::usize_to_f64(k) * hop;
404 let cents = 0.3 * t + extent * (std::f64::consts::TAU * rate * t).sin();
405 (
406 Seconds::new(t).unwrap(),
407 Hz::new(220.0 * 2f64.powf(cents / 1200.0)).unwrap(),
408 )
409 })
410 .collect()
411 }
412
413 #[test]
414 fn an_exact_sine_contour_reads_its_rate_and_extent() {
415 let fit = fit_vibrato(&frames(5.5, 35.0, 1.0)).unwrap();
416 assert!((fit.rate.get() - 5.5).abs() < 1e-3, "{fit:?}");
417 assert!((fit.extent_raw.get() - 35.0).abs() < 0.1, "{fit:?}");
418 assert!((fit.factor - 1.0 / 0.939).abs() < 1e-3, "{fit:?}");
419 assert!(fit.share > 0.99 && fit.accepted(), "{fit:?}");
420 assert!((fit.kurtosis - 1.5).abs() < 0.02, "{fit:?}");
421 }
422
423 #[test]
424 fn a_vibrato_needs_two_and_a_half_cycles() {
425 let short = fit_vibrato(&frames(3.0, 35.0, 0.6)).unwrap();
427 assert!(
428 short.cycles() < VIBRATO_CYCLES_MIN && !short.accepted(),
429 "{short:?}"
430 );
431 let long = fit_vibrato(&frames(3.0, 35.0, 1.0)).unwrap();
432 assert!(long.accepted(), "{long:?}");
433 }
434
435 #[test]
436 fn a_contour_that_jumps_between_two_levels_reads_a_kurtosis_near_1() {
437 let square: Vec<(Seconds, Hz)> = frames(5.5, 35.0, 1.0)
438 .into_iter()
439 .map(|(t, _)| {
440 let up = (2.0 * 5.5 * t.get()).floor() % 2.0;
441 (t, Hz::new(220.0 * 2f64.powf(up / 12.0)).unwrap())
442 })
443 .collect();
444 let fit = fit_vibrato(&square).unwrap();
445 assert!(fit.kurtosis < 1.1, "{fit:?}");
446 }
447
448 #[test]
449 fn the_factor_interpolates_between_rates_and_holds_at_the_ends() {
450 let factor = |rate: f64| extent_factor(Hz::new(rate).unwrap());
451 assert!((factor(5.25) - 1.0 / 0.9415).abs() < 1e-12);
452 assert!((factor(2.0) - 1.0 / 1.003).abs() < 1e-12);
453 assert!((factor(15.0) - 1.0 / 0.732).abs() < 1e-12);
454 }
455
456 #[test]
457 fn slow_fast_shallow_and_short_motion_is_not_a_vibrato() {
458 assert!(!fit_vibrato(&frames(2.0, 35.0, 1.0)).unwrap().accepted());
459 assert!(!fit_vibrato(&frames(15.0, 35.0, 1.0)).unwrap().accepted());
460 assert!(!fit_vibrato(&frames(5.5, 5.0, 1.0)).unwrap().accepted());
461 assert_eq!(fit_vibrato(&frames(5.5, 35.0, 0.4)), None);
462 assert_eq!(fit_vibrato(&[]), None);
463 }
464}