Giant Steps

This post is a messy work in progress:

Github repo with all code used in this article

What a song. I had always heard about the geometric underpinnings of this tune, and last year I learned it on piano.

Much has been written about the music theory in this Coltrane’s Giant Steps. This excellent post by Roel Hollander lays things out very nicely. The song uses a multi-tonic structure, sometimes referred to as the “Coltrane Matrix”.

I always wondered why these relationships were chosen. And if the relationships were different, how would it sound? Changing one element of the pattern, without modifying the others is tedious to do by hand, but if this song could be translated into a parameterized function, we could iterate on the chordal relationships in a very granular way.

I’ve been working on a blog post profiling my friend Dan Gorelick and his live coding work. He introduced me to Tidal cycles and I started experimenting in the browser with Strudel REPL.

The genesis of this post arrived purely by accident while randomly shuffling through the provided code samples provided in web tool.

I tried asking GPT-4 to modify this in several ways but it had a hard time with the format. GPT wanted to use functions to generate the chords but the Strudel language didn’t seem to have a great functional interface. I haven’t read all the docs but it seems to operate more like a declarative config style language. I switched gears towards code generation and began building a generator function in Javascript.

So the goal would be to take a string like this:

    "[B^7 D7] [G^7 Bb7] Eb^7 [Am7 D7]",
    "[G^7 Bb7] [Eb^7 F#7] B^7 [Fm7 Bb7]",
    "Eb^7 [Am7 D7] G^7 [C#m7 F#7]",
    "B^7 [Fm7 Bb7] Eb^7 [C#m7 F#7]"

and turn it into a series of functions which could be called according to their tonal center, and degree of scale from that center. We’re assuming a few things here, namely that the tonal center is an important idea, and that in this song there is a clear tonal center for each note and chord. According to Roel and other’s analysis, that seems to be the case. I’m pondering how this code would work (or not work) for music with more abstract tonal definition.

B^7 in the Strudel string notation would have to be written something like this:

    generateChord({
        tonalCenter: "B",
        romanNumeral: 1,
        chordType: "major"
    })

I re-arranged the Giant Steps Strudel format string into an array based data structure:

    const giantStepsChordArray = [
        ['B^7', 'D7'],
        ['G^7', 'Bb7'],
        'Eb^7',
        ['Am7', 'D7'],
        ['G^7', 'Bb7'],
        ['Eb^7', 'F#7'],
        'B^7',
        ['Fm7', 'Bb7'],
        'Eb^7',
        ['Am7', 'D7'],
        'G^7',
        ['C#m7', 'F#7'],
        'B^7',
        ['Fm7', 'Bb7'],
        'Eb^7',
        ['C#m7', 'F#7']
    ];

and then set about modifying each chord in this array to form a comprehensive configuration object I could later pass to a function:

    const giantStepsDataStructure = [
        // ['B^7', 'D7'],
        [{
            // tonalCenter: "?",
            // romanNumeral: ?,
            chordType: '^'
        }, {
            // tonalCenter: "?",
            // romanNumeral: ?,
            chordType: '7'
        }],
        etc...

Well, this is tedious. Now we need to actually figure out what the tonal center of each of these chords is, so we can set the relative roman numeral position correctly. a little more reading at the various blogs…

https://j.eu/en/blog-saxophone/Coltrane-Geometry/ https://www.learnjazzstandards.com/blog/understanding-coltrane-changes-part-1/

I hope I’m not losing you all by sucking all the soul out of this timeless jazz standard and reducing it to code… This does feel like some sort of perverse fetish to compulsively parameterize and automate all things… Once this is finished I’ll be able to tweak a parameter or two and everything will probably sound like trash, (in a perfectly systematic and repeatable way!). So definitely a gamble here, but once this code is all set up we’ll be able to quickly iterate on weird and radical reharmonizations of Giant Steps.

Ok, so for the first three chords BMajor7 -> D7 -> GMajor7 we’re starting on the tonic of one key (B), moving up a minor third to D7 (the fifth degree of our next scale), then resolving down the perfect fifth to G (the new root), which from the perspective of our original tonic B is a downwards movement of a Major Third. This pattern repeats twice landing on a long Eb^7 before shifting keys again across a 2->5 progression.

This can be reflected in our javascript config object as such:

    const giantStepsDataStructure = [
        // ['B^7', 'D7'],
        [{
            tonalCenter:  note.B,
            romanNumeral: 1,
            chordType:    chordType.majorSeventh
        }, {
            tonalCenter:  note.G,
            romanNumeral: 5,
            chordType:    chordType.dominantSeventh
        }],

        // ['G^7', 'Bb7'],
        [{
            tonalCenter:  note.G,
            romanNumeral: 1,
            chordType:    chordType.majorSeventh
        }, {
            tonalCenter:  note.Eb,
            romanNumeral: 5,
            chordType:    chordType.dominantSeventh
        }],

        // 'Eb^7',
        {
            tonalCenter:  note.Eb,
            romanNumeral: 1,
            chordType:    chordType.majorSeventh
        },

        // ['Am7', 'D7'],
        [{
            tonalCenter:  note.G,
            romanNumeral: 2,
            chordType:    chordType.minorSeventh
        }, {
            tonalCenter:  note.G,
            romanNumeral: 5,
            chordType:    chordType.dominantSeventh
        }],
        etc...

So I think it’s time to build a formatter to process this data set and render strings friendly to Strudel

    const note = {
        C  : "C",
        C$ : "C#",
        Db : "Db",
        D  : "D",
        D$ : "D#",
        Eb : "Eb",
        E  : "E",
        E$ : "E#",
        F  : "F",
        F$ : "F#",
        G  : "G",
        G$ : "G#",
        A  : "A",
        A$ : "A#",
        B  : "B",
        B$ : "B#"
    }

    const noteToMidiNumber = {
        [note.C]  : 0,
        [note.C$] : 1,
        [note.Db] : 1,
        [note.D]  : 2,
        [note.D$] : 3,
        [note.Eb] : 3,
        [note.E]  : 4,
        [note.E$] : 5,
        [note.Fb] : 4,
        [note.F]  : 5,
        [note.F$] : 6,
        [note.Gb] : 6,
        [note.G]  : 7,
        [note.G$] : 8,
        [note.Ab] : 8,
        [note.A]  : 9,
        [note.A$] : 10,
        [note.Bb] : 10,
        [note.B]  : 11,
        [note.B$] : 12,
    };

    function noteToMidi(noteName) {
        // hardcoding all octaves to 1 for now
        // not yet ready to deal with this complexity
        const octave = 1;
        return noteToMidiNumber[noteName] + 12 * (octave + 1);
    }
    
    function midiToNote(midiNoteNumber) {
        const numberToNote = [
            note.C, note.C$, note.D, note.D$, note.E, note.F, note.F$, note.G, note.G$, note.A, note.A$, note.B
        ];

        // const octave = Math.floor(midiNoteNumber / 12) - 1;
        const noteName = numberToNote[midiNoteNumber % 12];

        // return noteName + octave;
        return noteName;
    }
    
    const romanNumeralToInterval = {
        1: 0,
        2: 2,
        3: 4,
        4: 5,
        5: 7,
        6: 9,
        7: 11
    };

    const chordType = {
        minorSeventh    : '-',
        majorSeventh    : '^',
        dominantSeventh : '7',
        minSevenFlat5   : "ø"
    }

    const chordQualityMapping = {
        [chordType.majorSeventh]    : "^7",
        [chordType.minorSeventh]    : "m7",
        [chordType.minSevenFlat5]   : "m7b5",
        [chordType.dominantSeventh] : "7"
    };

    const generateChord = ({ tonalCenter, romanNumeral, chordType }) => {

        // extract midi note from note string
        const rootMidiNote = noteToMidi(tonalCenter, 1);
        
        // modify root midi note by it's roman numeral interval offset
        const newRootMidiNumber = rootMidiNote + romanNumeralToInterval[romanNumeral];

        const newRootNoteName = midiToNote(newRootMidiNumber)

        // now append the chord type information to this string
        const fullChordDefinition = newRootNoteName + chordQualityMapping[chordType]

        // return fullChordDefinition;
        return fullChordDefinition;
    }

    const recursiveGiantStepsParser = (data) => {
        if(Array.isArray(data)) {
            const result = [];

            // add opening bracket
            result.push('[')
            
            // loop over each item and recurse if it's not a string
            data.forEach((eachElem) => {
                result.push(recursiveGiantStepsParser(eachElem));
            })

            // add closing bracket
            result.push(']')
            
            // join with a space so it's readable
            return result.join(' ');
        } else {
            // Assuming we don't have anything but arrays and chord param objects here
            return generateChord({
                tonalCenter: data.tonalCenter,
                romanNumeral: data.romanNumeral,
                chordType: data.chordType
            });
        }
    }

And after all that code, we get this:

    [ [ B^7 D7 ] [ G^7 A#7 ] D#^7 [ Am7 D7 ] [ G^7 A#7 ] [ D#^7 F#7 ] B^7 [ Fm7 A#7 ] D#^7 [ Am7 D7 ] G^7 [ C#m7 F#7 ] B^7 [ Fm7 A#7 ] D#7 [ C#m7 F#7 ] ]

Let’s compare to our original source string:

    "[B^7 D7] [G^7 Bb7] Eb^7 [Am7 D7]",
    "[G^7 Bb7] [Eb^7 F#7] B^7 [Fm7 Bb7]",
    "Eb^7 [Am7 D7] G^7 [C#m7 F#7]",
    "B^7 [Fm7 Bb7] Eb^7 [C#m7 F#7]"

Ok, so it’s a one liner, but not too bad

The chords are rendering slightly differently but they are technically the same. Bb7 equates to A#7 for example. I should really fix the code to operate in flats since the original track is in flats. This output is going to be so modulated once we start experimenting that it hardly matters. I’ll have a few friends try to solo over this when it’s done. I hope they don’t mind that it’s all sharps. Maybe we can modify the code later.

So, the first modulation I want to attempt is changing the roman numerals across the song. In this case, my code is still not sufficiently parameterized, so we’ll add this romanNumeralMap object:

    const romanNumeralMap = {
        1: 1,
        2: 2,
        3: 3,
        4: 4,
        5: 5,
        6: 6,
        7: 7
    }
    const giantStepsDataStructure = [
    // ['B^7', 'D7'],
    [{
        tonalCenter:  note.B,
        romanNumeral: romanNumeralMap[1],
        chordType:    chordType.majorSeventh
    }, {
        tonalCenter:  note.G,
        romanNumeral: romanNumeralMap[5],
        chordType:    chordType.dominantSeventh
    }],

This allows a single line re-configuration of the song. For example:

    const romanNumeralMap = {
        1: 1,
        2: 2,
        3: 3,
        4: 4,
        5: 5 + 1,
        6: 6,
        7: 7
    }

For example switching all 5th degree chords to sixths! Output rendered looks like this:

    [ [ B^7 E7 ] [ G^7 C7 ] D#^7 [ Am7 E7 ] [ G^7 C7 ] [ D#^7 G#7 ] B^7 [ Fm7 C7 ] D#^7 [ Am7 E7 ] G^7 [ C#m7 G#7 ] B^7 [ Fm7 C7 ] D#7 [ C#m7 G#7 ] ]

I should really get this working with flats… fixed that and regenerated the chords:

    [ [ B^7 E7 ] [ G^7 C7 ] Eb^7 [ Am7 E7 ] [ G^7 C7 ] [ Eb^7 undefined7 ] B^7 [ Fm7 C7 ] Eb^7 [ Am7 E7 ] G^7 [ Dbm7 undefined7 ] B^7 [ Fm7 C7 ] Eb7 [ Dbm7 undefined7 ] ]

Oops… good luck with undefined7. Fixed it again, and now we have the correctly augmented 5th scale degrees rendered back into the original chord progression. Cool.

    [ [ B^7 E7 ] [ G^7 C7 ] Eb^7 [ Am7 E7 ] [ G^7 C7 ] [ Eb^7 Ab7 ] B^7 [ Fm7 C7 ] Eb^7 [ Am7 E7 ] G^7 [ Dbm7 Ab7 ] B^7 [ Fm7 C7 ] Eb7 [ Dbm7 Ab7 ] ]

Ok, so now it’s time to plug some of this into Strudel and see what it sounds like. This time let’s try lowering all the 5th degree chords by three positions! (completely random choice)

    const romanNumeralMap = {
        1: 1,
        2: 2,
        3: 3,
        4: 4,
        5: 5 - 3,
        6: 6,
        7: 7
    }

So, lowering the fifth degree of the scale everywhere it’s used sounds like this. And remember that’s the fifth based on the tonic center which is defined in our config object and constantly shifting throughout the song:

Pretty weird

the original again for reference:

Well, this is fun, but the changes are not so obvious here.. I should probably start generating the bass line and the melody in a similar way. Back to the drawing board…

This is the melody in strudel format:

    "[F#5 D5] [B4 G4] Bb4 [B4 A4]",
    "[D5 Bb4] [G4 Eb4] F#4 [G4 F4]",
    "Bb4 [B4 A4] D5 [D#5 C#5]",
    "F#5 [G5 F5] Bb5 [F#5 F#5]",

I’ll re-factor my code to handle this. Will need to define the tonal center for each note in a similar way it was done with the chords. In fact, I need to copy this tonal center and re-use it everywhere. The tonal center should not change for melody and bass. This is a little annoying to write in my code notation because the amount of notes per measure varies in the bassline. I’m doing a lot of manual transposition to set all this up.

Ok, a few minutes later and we’ve completed the process of manually transposing the original notes into tonal-center-aware javascript config format:

    const bassConfig = [
    // B2 D2
    [
        {
            tonalCenter: note.B,
            scaleDegree: bassScaleDegreeMap[1],
            octave: 2
        },
        {
            tonalCenter: note.G,
            scaleDegree: bassScaleDegreeMap[5],
            octave: 2
        }
    ],
    etc...

    const melodyConfig = [
    // [F#5 D5],
    [{
        tonalCenter: note.B,
        scaleDegree: melodyScaleDegreeMap[5],
        octave: 5
    }, {
        tonalCenter: note.G,
        scaleDegree: melodyScaleDegreeMap[5],
        octave: 5
    }],
    etc...

    const melodyScaleDegreeMap = {
        1: 1,
        2: 2,
        3: 3,
        4: 4,
        5: 5,
        6: 6,
        7: 7
    }

    const bassScaleDegreeMap = {
        1: 1,
        2: 2,
        3: 3,
        4: 4,
        5: 5,
        6: 6,
        7: 7
    }

I’ve added separate scale config objects for each section of the song so we can modulate melody without effecting bass, etc.

    // John Coltrane - Brian Fogg - Variated Steps
    setVoicingRange('lefthand', ['E3', 'G4']);

    stack(
        // melody
        seq("[ [ E6 C6 ] [ A4 E4 ] Ab4 [ Bb4 A4 ] [ C6 Ab4 ] [ Gb4 C5 ] E5 [ Gb4 F4 ] Ab4 [ Bb4 A4 ] C6 [ D6 Db6 ] E6 [ Gb5 F5 ] Ab5 [ E6 E6 ] ]")
        // chords
        seq("[ [ B1^7 D27 ] [ G1^7 Bb17 ] Eb1^7 [ A1m7 D27 ] [ G1^7 Bb17 ] [ Eb1^7 Gb27 ] B1^7 [ F1m7 Bb17 ] Eb1^7 [ A1m7 D27 ] G1^7 [ Db2m7 Gb27 ] B1^7 [ F1m7 Bb17 ] Eb17 [ Db2m7 Gb27 ] ]")
        .voicings('lefthand')
        // bass
        seq("[ [ C3 C3 ] [ Ab2 Ab2 ] [ E2 Ab3 ] [ A2 C3 ] [ Ab2 Ab2 ] [ E2 Eb3 ] [ C3 Eb3 ] [ F2 Ab2 ] [ E2 ] [ A2 C3 ] [ Ab2 C3 ] [ Db3 E3 ] [ C3 E3 ] [ F2 Ab2 ] [ E2 Ab3 ] [ Db3 E3 ] ]")
    )
    .slow(20)
    .note()

I’m certain I’ve introduced errors during this transposition process. I’ll need to manually compare the rendered outputs to the original source. Maybe the sharp eared among you will simply hear the errors.

TODO: add the above code to Strudel interactive player

Ok, now it’s time to start modulating things… Let’s try something very easy, a move of a perfect fifth in the melody without touching anything else. That should be easy to hear and ideally not too musically obtuse.

We’ll take the second degree of the melody scale range and boost it by a perfect fifth. This means that any note which should be the 2nd degree of a scale will now be played as a sixth. It’s a musically reasonable move and not too “outside”. This one line change will effect several notes in the melody, and with the constantly shifting tonal centers, I don’t know how many notes will be modifie, so it’s very hard to predict exactly how this will sound.

    const melodyScaleDegreeMap = {
        1: 1,
        2: 2 + 5,
        3: 3,
        4: 4,
        5: 5,
        6: 6,
        7: 7
    }

That’s the change!

TODO: generate output

    // John Coltrane - Brian Fogg - Variated Steps
    setVoicingRange('lefthand', ['E3', 'G4']);

    stack(
        // melody
        seq("[ [ E6 C6 ] [ A4 E4 ] Ab4 [ Bb4 A4 ] [ C6 Ab4 ] [ Gb4 C5 ] E5 [ Gb4 F4 ] Ab4 [ Bb4 A4 ] C6 [ D6 Db6 ] E6 [ Gb5 F5 ] Ab5 [ E6 E6 ] ]")
        // chords
        seq("[ [ B1^7 D27 ] [ G1^7 Bb17 ] Eb1^7 [ A1m7 D27 ] [ G1^7 Bb17 ] [ Eb1^7 Gb27 ] B1^7 [ F1m7 Bb17 ] Eb1^7 [ A1m7 D27 ] G1^7 [ Db2m7 Gb27 ] B1^7 [ F1m7 Bb17 ] Eb17 [ Db2m7 Gb27 ] ]")
        .voicings('lefthand')
        // bass
        seq("[ [ C3 C3 ] [ Ab2 Ab2 ] [ E2 Ab3 ] [ A2 C3 ] [ Ab2 Ab2 ] [ E2 Eb3 ] [ C3 Eb3 ] [ F2 Ab2 ] [ E2 ] [ A2 C3 ] [ Ab2 C3 ] [ Db3 E3 ] [ C3 E3 ] [ F2 Ab2 ] [ E2 Ab3 ] [ Db3 E3 ] ]")
    )
    .slow(20)
    .note()

Ok, I realize this is not very impressive yet, but this is as far as I’ve gotten as of last night. I’ll update later…

-Brian