sva_samples/measure/harmony/
tonnetz.rs1use std::collections::VecDeque;
4
5use super::Framing;
6use super::chords::Chord;
7
8pub const MODEL: &str = "Tonnetz (Euler 1739, Riemann): the 24 major and minor triads, two \
9 adjacent where one of P, L, R maps one to the other (Cohn, R. (1998), \
10 Introduction to Neo-Riemannian Theory, J. Music Theory 42(2) \
11 167-180): P keeps root and fifth (C-c), R the major triad's root and \
12 third (C-a), L its third and fifth (C-e); distance is the fewest steps. \
13 A chord whose pitch classes are no major or minor triad has no place on \
14 it, and its steps and tension are null";
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17pub enum Quality {
18 Major,
19 Minor,
20}
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23pub struct Triad {
24 pub root: u8,
25 pub quality: Quality,
26}
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum Step {
30 P,
31 L,
32 R,
33}
34
35const STEPS: [Step; 3] = [Step::P, Step::L, Step::R];
36
37impl Step {
38 pub fn letter(self) -> char {
39 match self {
40 Step::P => 'P',
41 Step::L => 'L',
42 Step::R => 'R',
43 }
44 }
45
46 pub fn of(self, t: Triad) -> Triad {
47 let (shift, quality) = match (self, t.quality) {
48 (Step::P, Quality::Major) => (0, Quality::Minor),
49 (Step::P, Quality::Minor) => (0, Quality::Major),
50 (Step::R, Quality::Major) => (9, Quality::Minor),
51 (Step::R, Quality::Minor) => (3, Quality::Major),
52 (Step::L, Quality::Major) => (4, Quality::Minor),
53 (Step::L, Quality::Minor) => (8, Quality::Major),
54 };
55 Triad {
56 root: (t.root + shift) % 12,
57 quality,
58 }
59 }
60}
61
62impl Triad {
63 fn index(self) -> usize {
64 usize::from(self.root) * 2 + usize::from(self.quality == Quality::Minor)
65 }
66
67 pub fn of(notes: &[i64]) -> Option<Triad> {
68 let mut classes: Vec<u8> = notes.iter().map(|n| n.rem_euclid(12) as u8).collect();
69 classes.sort_unstable();
70 classes.dedup();
71 if classes.len() != 3 {
72 return None;
73 }
74 let holds = |root: u8, third: u8| {
75 let mut want = [root, (root + third) % 12, (root + 7) % 12];
76 want.sort_unstable();
77 want[..] == classes[..]
78 };
79 classes.iter().find_map(|&root| {
80 [(4, Quality::Major), (3, Quality::Minor)]
81 .into_iter()
82 .find(|(third, _)| holds(root, *third))
83 .map(|(_, quality)| Triad { root, quality })
84 })
85 }
86}
87
88pub fn path(from: Triad, to: Triad) -> Vec<Step> {
90 let mut came: [Option<(Triad, Step)>; 24] = [None; 24];
91 let mut seen = [false; 24];
92 seen[from.index()] = true;
93 let mut queue = VecDeque::from([from]);
94 while let Some(at) = queue.pop_front() {
95 if at == to {
96 break;
97 }
98 for step in STEPS {
99 let next = step.of(at);
100 if !seen[next.index()] {
101 seen[next.index()] = true;
102 came[next.index()] = Some((at, step));
103 queue.push_back(next);
104 }
105 }
106 }
107 let mut steps = Vec::new();
108 let mut at = to;
109 while let Some((before, step)) = came[at.index()] {
110 steps.push(step);
111 at = before;
112 }
113 steps.reverse();
114 steps
115}
116
117#[derive(Clone, Debug, PartialEq)]
118pub struct TonalChord {
119 pub chord: Chord,
120 pub triad: Option<Triad>,
121 pub tension: Option<usize>,
123}
124
125#[derive(Clone, Debug, PartialEq)]
126pub struct TonalTransition {
127 pub at_secs: f64,
128 pub from: Option<Triad>,
129 pub to: Option<Triad>,
130 pub path: Option<Vec<Step>>,
131}
132
133#[derive(Clone, Debug, PartialEq)]
134pub struct TonalDistance {
135 pub framing: Framing,
136 pub floor_db: f64,
137 pub tonic: Option<(Triad, f64)>,
139 pub chords: Vec<TonalChord>,
140 pub transitions: Vec<TonalTransition>,
141}
142
143pub fn analyze(chords: Vec<Chord>, framing: Framing, floor_db: f64) -> TonalDistance {
144 let triads: Vec<Option<Triad>> = chords.iter().map(|c| Triad::of(&c.notes)).collect();
145 let tonic = chords
146 .iter()
147 .zip(&triads)
148 .find_map(|(c, t)| t.map(|t| (t, c.start_secs)));
149 let transitions = chords
150 .windows(2)
151 .zip(triads.windows(2))
152 .map(|(c, t)| TonalTransition {
153 at_secs: c[1].start_secs,
154 from: t[0],
155 to: t[1],
156 path: t[0].zip(t[1]).map(|(a, b)| path(a, b)),
157 })
158 .collect();
159 let chords = chords
160 .into_iter()
161 .zip(triads)
162 .map(|(chord, triad)| TonalChord {
163 tension: tonic.zip(triad).map(|((home, _), t)| path(home, t).len()),
164 chord,
165 triad,
166 })
167 .collect();
168 TonalDistance {
169 framing,
170 floor_db,
171 tonic,
172 chords,
173 transitions,
174 }
175}