sva_samples/measure/
pitch.rs1use crate::measure::spectrum::{Peak, magnitudes, peaks};
4
5const NAMES: [&str; 12] = [
6 "C", "Cs", "D", "Ds", "E", "F", "Fs", "G", "Gs", "A", "As", "B",
7];
8const LOWEST_HZ: f64 = 25.0;
9const HIGHEST_HZ: f64 = 5000.0;
10const HARMONIC_TOLERANCE: f64 = 0.04;
11const HIGHEST_HARMONIC: f64 = 8.0;
12
13#[derive(Clone, Debug, PartialEq)]
14pub struct Note {
15 pub hz: f64,
16 pub name: String,
17 pub cents: f64,
18 pub db: f64,
19 pub harmonic_of: Option<f64>,
20}
21
22#[derive(Clone, Debug, PartialEq)]
23pub struct PitchFrame {
24 pub t_secs: f64,
25 pub notes: Vec<Note>,
26}
27
28pub fn track(
29 samples: &[f64],
30 sample_rate: f64,
31 start_secs: f64,
32 frame_secs: f64,
33 max_notes: usize,
34) -> Vec<PitchFrame> {
35 let stride = ((frame_secs * sample_rate).round() as usize).max(1);
36 samples
37 .chunks(stride)
38 .enumerate()
39 .map(|(n, chunk)| {
40 let (mags, bin_hz, ..) = magnitudes(chunk, sample_rate, None);
41 let found = peaks(&mags, bin_hz, max_notes * 4);
42 PitchFrame {
43 t_secs: start_secs + (n * stride) as f64 / sample_rate,
44 notes: name_all(&found, max_notes),
45 }
46 })
47 .collect()
48}
49
50pub fn name_peaks(found: &[Peak], max_notes: usize) -> Vec<Note> {
52 name_all(found, max_notes)
53}
54
55fn name_all(found: &[Peak], max_notes: usize) -> Vec<Note> {
58 let audible: Vec<&Peak> = found
59 .iter()
60 .filter(|p| (LOWEST_HZ..=HIGHEST_HZ).contains(&p.hz))
61 .collect();
62
63 audible
64 .iter()
65 .take(max_notes)
66 .map(|p| {
67 let midi = 69.0 + 12.0 * (p.hz / 440.0).log2();
68 let nearest = midi.round();
69 Note {
70 hz: p.hz,
71 name: format!(
72 "{}{}",
73 NAMES[(nearest as i64).rem_euclid(12) as usize],
74 (nearest as i64) / 12 - 1
75 ),
76 cents: (midi - nearest) * 100.0,
77 db: p.db,
78 harmonic_of: harmonic_of(p, &audible),
79 }
80 })
81 .collect()
82}
83
84fn harmonic_of(peak: &Peak, louder_first: &[&Peak]) -> Option<f64> {
85 louder_first
86 .iter()
87 .take_while(|other| other.db > peak.db)
88 .find(|other| {
89 let n = peak.hz / other.hz;
90 (1.5..=HIGHEST_HARMONIC).contains(&n) && (n - n.round()).abs() < HARMONIC_TOLERANCE
91 })
92 .map(|other| other.hz)
93}