Archived blog posts from my CPSC 431 (Fall 2021) course blog. Part I details my process and submission for an earlier assignment, while Part II builds on it for my final project in the class. All sample code is in SuperCollider.
For this assignment, I designed a mealy machine that can modulate between any two (major or minor) triads using Neo-Riemannian transformations, regardless of key.
In short, Neo-Riemannian theory is the idea that rather than analyzing harmonic progressions based on their relationship to a tonic chord, harmonies can be related directly to each other via certain types of chord transformations. It can be particularly useful for analyzing tonal music with many modulations or a lot of chromaticism, often found in late romantic music (Liszt, Wagner, etc) but also in a range of other styles from minimalism (Philip Glass, etc) to contemporary music for films.
There are three principal transformations (and a handful of secondary ones which are combinations of these). Examples stolen from the wiki page:
What's nifty about these transformations is that 1) they operate independently of chord voicing, i.e. chord inversions don't affect harmonic motion and 2) because two pitches stay the same between each transformation, lots of harmonic ground can be covered while the progression still sounds smooth.
The biggest challenge for this was finding a way to decide which of the three transformations to apply, given a starting and ending chord. To visualize, we can construct a Neo-Riemannian tonnetz (or "tone net") that maps outs all 12 pitches, their possible triads, and the relationships between each one.
Once again, stolen from the wiki page. Making this by hand was way too much work
In this visualization, the goal is then to start with three adjacent nodes in the graph (each small triangle forms a triad) and find a way to transform them until we path-find our way to some target three notes. The algorithm I've used is as follows:
Another challenging aspect was making functions that communicated between MIDI values, pitch classes (mod 12), symbols, dictionaries... yeah it was a little bit of a mess. For example, here is a function that given a 3-note MIDI chord, returned a matching chord state in the mealy machine:
// chord is 3-note array, returns matching chord state
midi2state: {|self, chord|
var pcs = (chord % 12).asSet;
self.states.select({ |item|
pcs.includes(item.root) &&
pcs.includes(item.third) &&
pcs.includes(item.fifth)}).getPairs[0]; // .select() returns a dict, so getPairs[0] gives us just the single key we're looking for
},
And here is another guy that when given a pitch class (pc), found the relevant index in a MIDI chord array:
pc2index: {|self, pc|
var chord_pcs = self.chord.asArray % 12; // array of three MIDI pitches, reduced to mod 12 pitch classes
switch(pc % 12,
chord_pcs[0], {i = 0},
chord_pcs[1], {i = 1},
chord_pcs[2], {i = 2});
},