Skip to main content

sva_samples/measure/harmony/
chords.rs

1// Concern: the chords a curve's moments hold, a run of moments naming one note set each | Non-concern: what a chord moves to (siblings) | IO: (moments, hop, floor) -> chords
2
3use super::{Moment, Partial, above};
4use crate::measure::pitch::{Note, midi_of, name_peaks};
5use crate::measure::spectrum::{Peak, db};
6
7pub const METHOD: &str = "per moment: the partials at or above the floor, named as `pitch` names \
8                          peaks (25 Hz to 5 kHz, a peak at a near-whole multiple of another in \
9                          the series the greatest subharmonic sum heads, or as sharp of it as \
10                          the stiff string that series fits, dropped as its harmonic), each \
11                          rounded to its nearest equal-tempered note; consecutive moments naming one set are one chord, and a moment \
12                          naming none ends one; a note whose partial's frequency bounds round \
13                          to another note in any of its moments is listed as unresolved";
14
15/// `notes` are MIDI numbers, ascending, each once; `end_secs` is the next moment's instant
16/// or the render's end;
17/// `unresolved` are those whose frequency bounds round to another note in some moment.
18#[derive(Clone, Debug, PartialEq)]
19pub struct Chord {
20    pub start_secs: f64,
21    pub end_secs: f64,
22    pub notes: Vec<i64>,
23    pub unresolved: Vec<i64>,
24}
25
26fn nearest(hz: f64) -> i64 {
27    midi_of(hz).round() as i64
28}
29
30/// The partials at or above the floor that `pitch` names, loudest first, each with its name:
31/// a note, or a harmonic in a series. Partials outside its band are left out.
32pub fn named(moment: &Moment, floor_db: f64) -> Vec<(Partial, Note)> {
33    let mut kept = above(moment, floor_db);
34    kept.sort_by(|a, b| b.amplitude.total_cmp(&a.amplitude));
35    let found: Vec<Peak> = kept
36        .iter()
37        .map(|p| Peak {
38            hz: p.hz,
39            db: db(p.amplitude),
40        })
41        .collect();
42    name_peaks(&found, usize::MAX)
43        .into_iter()
44        .map(|note| {
45            let partial = *kept
46                .iter()
47                .find(|p| p.hz == note.hz)
48                .expect("each name is one kept partial's");
49            (partial, note)
50        })
51        .collect()
52}
53
54pub fn notes_at(moment: &Moment, floor_db: f64) -> (Vec<i64>, Vec<i64>) {
55    let (mut notes, mut unresolved) = (Vec::new(), Vec::new());
56    for (partial, named) in named(moment, floor_db) {
57        if named.harmonic_of.is_some() {
58            continue;
59        }
60        let note = nearest(named.hz);
61        notes.push(note);
62        let bounds = partial.hz_bounds;
63        if nearest(bounds.0) != note || nearest(bounds.1) != note {
64            unresolved.push(note);
65        }
66    }
67    for held in [&mut notes, &mut unresolved] {
68        held.sort_unstable();
69        held.dedup();
70    }
71    (notes, unresolved)
72}
73
74pub fn segment(moments: &[Moment], hop_secs: f64, until_secs: f64, floor_db: f64) -> Vec<Chord> {
75    let mut chords: Vec<Chord> = Vec::new();
76    let mut open = false;
77    for moment in moments {
78        let (notes, unresolved) = notes_at(moment, floor_db);
79        let end_secs = (moment.t_secs + hop_secs).min(until_secs);
80        match chords.last_mut() {
81            Some(last) if open && last.notes == notes => {
82                last.end_secs = end_secs;
83                last.unresolved.extend(unresolved);
84                last.unresolved.sort_unstable();
85                last.unresolved.dedup();
86            }
87            _ if notes.is_empty() => {}
88            _ => chords.push(Chord {
89                start_secs: moment.t_secs,
90                end_secs,
91                notes: notes.clone(),
92                unresolved,
93            }),
94        }
95        open = !notes.is_empty();
96    }
97    chords
98}