sva_samples/measure/harmony/
voice_leading.rs1use 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
61pub 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}