I decided to make use of a 5-hour train ride from Toronto to Ottawa recently by writing a small program that performs all the fun Neo-Riemannian triadic transformations music theorists at the graduate level know and love.
At the heart of the program is the Triad class which coordinates the triadic operators:
Transposing musical pitches programmatically always takes some creativity because of the peculiar way our system of music is organized. Specifically, some intervals between pitch letters are whole-tones (e.g. C-D) while some others are semi-tones (e.g. E-F). For this application I solved that problem by asking the developer to provide both the number of letter changes (e.g. C-D would be a value of 1) in addition to the number of semi-tone changes (e.g. C-D would be a value of 2). This makes it easy to reconcile those aforementioned peculiarities and any accidentals that ensue following a transposition.
With that transpositional logic, if I wanted to invoke a somewhat unusual interval like an augmented 2nd (e.g. C-D#) then as a developer I'd just have to provide the value 1 for the number of letter changes, and 3 for the number of semi-tone changes. In code this could look like cNatural.transpose(1, 3). I feel like this is a pretty reasonable and readable solution.
From a set theory perspective all I would need is the number of semi-tone changes to perform a transformation, but because music deals with letters and accidentals that can have enharmonic spellings (e.g. B# == C), two arguments are necessary to get exactly what the composer/musician/developer is asking for.
A few neat takeaways from this are that:
1) The R (relative) and L (leading tone) transformations end up just being rotations of the pitch classes the triad is composed of (with one triad member being transposed). For example, after an R transformation the root becomes either the third or fifth and the remaining triad members follow the same shift.
2) The N, S, and H transformations use chaining and as you might expect the programmatic approach to performing this is very clean and easy (see code above).
I haven't experimented with how exhaustive one can be composing transformations like the N, S, and H transformations, but a few others I came up with in pseudocode were:
dominant = LR
tritone = PRPR
neapolitan = SP (or, more primitively, LPRP)
You can conveniently find all this code on one of my public GitHub repositories.
At the heart of the program is the Triad class which coordinates the triadic operators:
data class Triad(val root: PitchClass, val third: PitchClass, val fifth: PitchClass) { fun parallel() = Triad(root = root, third = when { isMinor() -> third.transpose(desiredInterval = 1) isMajor() -> third.transpose(desiredInterval = -1) else -> onNonMinorOrMajorTriad() }, fifth = fifth) fun relative() = when { isMinor() -> Triad(root = third, third = fifth, fifth = root.transpose(-1, -2)) isMajor() -> Triad(root = fifth.transpose(1, 2), third = root, fifth = third) else -> onNonMinorOrMajorTriad() } fun leadingTone() = when { isMinor() -> Triad(root = fifth.transpose(1, 1), third = root, fifth = third) isMajor() -> Triad(root = third, third = fifth, fifth = root.transpose(-1, -1)) else -> onNonMinorOrMajorTriad() } fun nebenverwandt() = relative().leadingTone().parallel() fun slide() = leadingTone().parallel().relative() fun hexatonicPole() = leadingTone().parallel().leadingTone() private fun isMinor() = root.isMinorThird(third) && third.isMajorThird(fifth) private fun isMajor() = root.isMajorThird(third) && third.isMinorThird(fifth) private fun isDiminished() = root.isMinorThird(third) && third.isMinorThird(fifth) private fun isAugmented() = root.isMajorThird(third) && fifth.isMajorThird(fifth) private fun onNonMinorOrMajorTriad(): Nothing = error("Triad was neither minor nor major") }
Transposing musical pitches programmatically always takes some creativity because of the peculiar way our system of music is organized. Specifically, some intervals between pitch letters are whole-tones (e.g. C-D) while some others are semi-tones (e.g. E-F). For this application I solved that problem by asking the developer to provide both the number of letter changes (e.g. C-D would be a value of 1) in addition to the number of semi-tone changes (e.g. C-D would be a value of 2). This makes it easy to reconcile those aforementioned peculiarities and any accidentals that ensue following a transposition.
With that transpositional logic, if I wanted to invoke a somewhat unusual interval like an augmented 2nd (e.g. C-D#) then as a developer I'd just have to provide the value 1 for the number of letter changes, and 3 for the number of semi-tone changes. In code this could look like cNatural.transpose(1, 3). I feel like this is a pretty reasonable and readable solution.
From a set theory perspective all I would need is the number of semi-tone changes to perform a transformation, but because music deals with letters and accidentals that can have enharmonic spellings (e.g. B# == C), two arguments are necessary to get exactly what the composer/musician/developer is asking for.
A few neat takeaways from this are that:
1) The R (relative) and L (leading tone) transformations end up just being rotations of the pitch classes the triad is composed of (with one triad member being transposed). For example, after an R transformation the root becomes either the third or fifth and the remaining triad members follow the same shift.
2) The N, S, and H transformations use chaining and as you might expect the programmatic approach to performing this is very clean and easy (see code above).
I haven't experimented with how exhaustive one can be composing transformations like the N, S, and H transformations, but a few others I came up with in pseudocode were:
dominant = LR
tritone = PRPR
neapolitan = SP (or, more primitively, LPRP)
You can conveniently find all this code on one of my public GitHub repositories.