Skip to main content

sva_samples/measure/harmony/
tonnetz.rs

1// Concern: the fewest neo-Riemannian steps between successive triads, and each from the tonic | Non-concern: finding the chords (chords.rs) | IO: (chords) -> TonalDistance
2
3use 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
88/// Breadth first in P, L, R order.
89pub 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    /// Steps from the tonic; `None` where this chord or the tonic is no triad.
122    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    /// The first chord that is a triad, and its start.
138    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}