Saturday, July 21, 2018

Programming with triadic transformations

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:

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.