Skip to main content

sva_samples/measure/harmony/
mod.rs

1// Concern: the partials a harmonic reading starts from, per instant or frame, and their floor | Non-concern: what a reading makes of them (siblings) | IO: (lines) -> partials
2
3pub mod chords;
4pub mod dissonance;
5pub mod inharmonicity;
6pub mod measured;
7pub mod tonnetz;
8pub mod voice_leading;
9
10use std::collections::HashMap;
11
12use sva_formula::C64;
13
14#[derive(Clone, Copy, Debug, PartialEq)]
15pub struct Partial {
16    pub hz: f64,
17    pub amplitude: f64,
18    /// Where the true values lie, `(lo, hi)`; a point where the partial is exact.
19    pub hz_bounds: (f64, f64),
20    pub amplitude_bounds: (f64, f64),
21}
22
23#[derive(Clone, Debug, PartialEq)]
24pub struct Moment {
25    pub t_secs: f64,
26    pub partials: Vec<Partial>,
27}
28
29#[derive(Clone, Copy, Debug, PartialEq)]
30pub struct Framing {
31    pub hop_secs: f64,
32    pub window: Option<Window>,
33}
34
35impl Framing {
36    pub fn method(&self) -> &'static str {
37        match self.window {
38            None => EXACT_METHOD,
39            Some(_) => measured::METHOD,
40        }
41    }
42}
43
44pub const EXACT_METHOD: &str = "exact: at each instant every atom of each channel of the closed \
45                                form is a partial at its carrier frequency, its value there \
46                                summed with the channel's others at that frequency, and a \
47                                cosine's two conjugate lines one partial; every channel in \
48                                power, each partial's squared amplitude summed over the \
49                                channels at its frequency, then divided by the channel count";
50
51#[derive(Clone, Copy, Debug, PartialEq)]
52pub struct Window {
53    pub size: usize,
54    pub secs: f64,
55    pub resolution_hz: f64,
56}
57
58/// Lines at one frequency sum as complex amplitudes, so a cancelled pair is no partial; a
59/// real wave's amplitude is its two conjugate lines' moduli summed.
60pub fn partials(lines: impl IntoIterator<Item = (f64, C64)>) -> Vec<Partial> {
61    let mut at: HashMap<u64, C64> = HashMap::new();
62    let mut order: Vec<f64> = Vec::new();
63    for (hz, amp) in lines {
64        let summed = at.entry(hz.to_bits()).or_insert_with(|| {
65            order.push(hz);
66            C64::ZERO
67        });
68        *summed = *summed + amp;
69    }
70    let modulus = |hz: f64| {
71        at.get(&hz.to_bits())
72            .map_or(0.0, |c| libm::hypot(c.re, c.im))
73    };
74    order
75        .into_iter()
76        .filter(|hz| *hz > 0.0)
77        .map(|hz| {
78            let amplitude = modulus(hz) + modulus(-hz);
79            Partial {
80                hz,
81                amplitude,
82                hz_bounds: (hz, hz),
83                amplitude_bounds: (amplitude, amplitude),
84            }
85        })
86        .filter(|p| p.amplitude > 0.0)
87        .collect()
88}
89
90/// Every channel in power, as a measured reading's spectrum reads them; one channel is itself.
91pub fn averaged(per_channel: Vec<Vec<Partial>>) -> Vec<Partial> {
92    if per_channel.len() == 1 {
93        return per_channel.into_iter().flatten().collect();
94    }
95    let count = per_channel.len() as f64;
96    let mut power: HashMap<u64, f64> = HashMap::new();
97    let mut order: Vec<f64> = Vec::new();
98    for p in per_channel.iter().flatten() {
99        *power.entry(p.hz.to_bits()).or_insert_with(|| {
100            order.push(p.hz);
101            0.0
102        }) += p.amplitude * p.amplitude;
103    }
104    order
105        .into_iter()
106        .map(|hz| {
107            let amplitude = (power[&hz.to_bits()] / count).sqrt();
108            Partial {
109                hz,
110                amplitude,
111                hz_bounds: (hz, hz),
112                amplitude_bounds: (amplitude, amplitude),
113            }
114        })
115        .collect()
116}
117
118pub fn above(moment: &Moment, floor_db: f64) -> Vec<Partial> {
119    let floor = libm::pow(10f64, floor_db / 20.0);
120    moment
121        .partials
122        .iter()
123        .filter(|p| p.amplitude >= floor)
124        .copied()
125        .collect()
126}