Skip to main content

sva_samples/measure/harmony/
voice_leading.rs

1// Concern: the smallest voice leading between each two successive chords, in pitch space | Non-concern: finding the chords (chords.rs) | IO: (chords, norm) -> VoiceLeading
2
3use super::Framing;
4use super::chords::Chord;
5
6pub const MODEL: &str = "Tymoczko, D. (2006), The Geometry of Musical Chords, Science 313 \
7                         72-74, and A Geometry of Music (2011) ch. 2: the smallest voice \
8                         leading in pitch space, a note doubled where sizes differ so every \
9                         note of both chords moves to or from one of the other, its size the \
10                         L1 or L2 norm of the moves in semitones";
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub enum Norm {
14    L1,
15    L2,
16}
17
18impl Norm {
19    pub fn key(self) -> &'static str {
20        match self {
21            Norm::L1 => "l1",
22            Norm::L2 => "l2",
23        }
24    }
25
26    fn cost(self, from: i64, to: i64) -> f64 {
27        let d = (from - to).abs() as f64;
28        match self {
29            Norm::L1 => d,
30            Norm::L2 => d * d,
31        }
32    }
33
34    fn size(self, summed: f64) -> f64 {
35        match self {
36            Norm::L1 => summed,
37            Norm::L2 => summed.sqrt(),
38        }
39    }
40}
41
42#[derive(Clone, Debug, PartialEq)]
43pub struct Transition {
44    pub at_secs: f64,
45    pub from: Vec<i64>,
46    pub to: Vec<i64>,
47    pub distance: f64,
48    pub mapping: Vec<(i64, i64)>,
49}
50
51#[derive(Clone, Debug, PartialEq)]
52pub struct VoiceLeading {
53    pub framing: Framing,
54    pub floor_db: f64,
55    pub norm: Norm,
56    pub chords: Vec<Chord>,
57    pub transitions: Vec<Transition>,
58    pub total: f64,
59}
60
61/// An edge cover of two ascending chords: crossing moves uncross at no greater convex cost
62/// with the same notes covered (the cost is Monge), so a smallest cover runs in order along
63/// both, and this dynamic program over them finds one exactly.
64pub fn lead(from: &[i64], to: &[i64], norm: Norm) -> (f64, Vec<(i64, i64)>) {
65    let (m, n) = (from.len(), to.len());
66    let mut best = vec![vec![f64::INFINITY; n]; m];
67    for i in 0..m {
68        for j in 0..n {
69            let before = match (i, j) {
70                (0, 0) => 0.0,
71                (0, _) => best[0][j - 1],
72                (_, 0) => best[i - 1][0],
73                _ => best[i - 1][j - 1].min(best[i - 1][j]).min(best[i][j - 1]),
74            };
75            best[i][j] = before + norm.cost(from[i], to[j]);
76        }
77    }
78    let (mut i, mut j) = (m - 1, n - 1);
79    let mut mapping = vec![(from[i], to[j])];
80    while (i, j) != (0, 0) {
81        (i, j) = match (i, j) {
82            (0, _) => (0, j - 1),
83            (_, 0) => (i - 1, 0),
84            _ => {
85                let diagonal = best[i - 1][j - 1];
86                if diagonal <= best[i - 1][j] && diagonal <= best[i][j - 1] {
87                    (i - 1, j - 1)
88                } else if best[i - 1][j] <= best[i][j - 1] {
89                    (i - 1, j)
90                } else {
91                    (i, j - 1)
92                }
93            }
94        };
95        mapping.push((from[i], to[j]));
96    }
97    mapping.reverse();
98    (norm.size(best[m - 1][n - 1]), mapping)
99}
100
101pub fn analyze(chords: Vec<Chord>, framing: Framing, floor_db: f64, norm: Norm) -> VoiceLeading {
102    let transitions: Vec<Transition> = chords
103        .windows(2)
104        .map(|pair| {
105            let (distance, mapping) = lead(&pair[0].notes, &pair[1].notes, norm);
106            Transition {
107                at_secs: pair[1].start_secs,
108                from: pair[0].notes.clone(),
109                to: pair[1].notes.clone(),
110                distance,
111                mapping,
112            }
113        })
114        .collect();
115    VoiceLeading {
116        framing,
117        floor_db,
118        norm,
119        total: transitions.iter().map(|t| t.distance).sum(),
120        chords,
121        transitions,
122    }
123}