Skip to Content

The ABCs of Music Theory, Part 2

If you didn't see the last post, I recommend you start there before jumping into here. We covered some basic scale and chord generation, but from a software point of view. In this one, I want to go through get one level more abstract. The goal is to be able to turn something supposedly unreadable like this:

Circle of Fifths

into something we can generate and understand with code. This one will primarily focus on teaching it through recursion instead of abstraction.

Going Round

Last time, we created a few classes called MajorKey and MinorKey that inherit from a base class MusicKey. If we wanted to check out a new set of scales, we had to create a new class every time. But there definitely seemed to be some patterns hiding under there, so let's see if we can find out what's up. I have no idea what those strange sets of lines and shapes mean, so let's treat it like a puzzle. If we start by looking at a couple notes that are close to each, then scales seem like a good place to start. I'm removing the boiler plate, since that's mostly the same since last time!

Cmaj = MajorKey('C')
Cmaj.generate_scale()

Gmaj = MajorKey('G')
Gmaj.generate_scale()

['C', 'D', 'E', 'F', 'G', 'A', 'B', 'C']
['G', 'A', 'B', 'C', 'D', 'E', 'F#', 'G']

These look pretty similar, don't they? Let's line them up so that they both start with the same note. We'll also need a rotate function, so thanks Internet for providing that. I also made each string print out two characters wide now. That should make life simpler when trying to compare things.

def rotate(l, n):
    """Left rotate a list by n elements"""
    return l[n:] + l[:n]

print(Gmaj.scale, "- Gmaj")
for rot in range(len(Cmaj.scale)):
    rotated_scale = rotate(Cmaj.scale, rot)
    print(f"{rotated_scale} - Cmaj start on Note {rot+1}")

Don't mind that one-indexing, that's just what real life is like. What do we find?

['G ', 'A ', 'B ', 'C ', 'D ', 'E ', 'F#'] - Gmaj
['C ', 'D ', 'E ', 'F ', 'G ', 'A ', 'B '] - Cmaj start on Note 1
['D ', 'E ', 'F ', 'G ', 'A ', 'B ', 'C '] - Cmaj start on Note 2
['E ', 'F ', 'G ', 'A ', 'B ', 'C ', 'D '] - Cmaj start on Note 3
['F ', 'G ', 'A ', 'B ', 'C ', 'D ', 'E '] - Cmaj start on Note 4
['G ', 'A ', 'B ', 'C ', 'D ', 'E ', 'F '] - Cmaj start on Note 5
['A ', 'B ', 'C ', 'D ', 'E ', 'F ', 'G '] - Cmaj start on Note 6
['B ', 'C ', 'D ', 'E ', 'F ', 'G ', 'A '] - Cmaj start on Note 7

Hmm, a whole lot of noise. Well, let's look at the closest two:

['G ', 'A ', 'B ', 'C ', 'D ', 'E ', 'F#']
['G ', 'A ', 'B ', 'C ', 'D ', 'E ', 'F '] - Start on Note 5

Ok, so they look pretty similar with just one note difference. Let's try the next pair: Gmaj and Dmaj.

print(Dmaj.scale, "- Dmaj")
rot_Gmaj = rotate(Gmaj.scale, 4)
print(f"{rot_Gmaj} - Gmaj start on Note {4+1}")

I skipped the iterations through, but it turns out that they require the same shift amount, interesting!

['D ', 'E ', 'F#', 'G ', 'A ', 'B ', 'C#'] - Dmaj
['D ', 'E ', 'F#', 'G ', 'A ', 'B ', 'C '] - Gmaj start on Note 5

Hm, what's even neater is that we now have a pattern. The only thing that changed between these two is the last note. Going from Gmaj to Dmaj moved it up a half step. Reminder: a half step is an increment by one, so we went from F to F#. That's the same thing that happened last time - going from Cmaj to Gmaj was also raising the last note by a half step.

Let's try to generate this programmatically. What are the rules here?

  1. We'll start at the same place everytime, Cmaj. That's a base case!
  2. Shift the scale by 5 notes (one-indexed rotate)
  3. Increment the last note by one (relative to all pitches, not the scale)
  4. If the first note is the desired note, we're done. Otherwise, back to step 2!

Pretty standard recursive problem. Let's try it out!

def recursive_scales(scale, desired_note):
    """Recursively generates the scale for desired_note"""
    if scale[0] == desired_note:
        return scale
    else:
        scale = rotate(scale, 4)
        last_note_idx = (NOTES.index(scale[6]) + 1) % NUM_NOTES
        scale[6] = NOTES[last_note_idx]
        return recursive_scales(scale, desired_note)

Ok, only a few lines of code in Python. But does it actually work? Let's pick something pretty far along that circle.

Bmaj = MajorKey('B ')
print(Bmaj.scale)

scale = recursive_scales(Cmaj.scale, 'B ')
print(scale)

['B ', 'C#', 'D#', 'E ', 'F#', 'G#', 'A#']
['B ', 'C#', 'D#', 'E ', 'F#', 'G#', 'A#']

Dang, it really works! Can we get every scale doing this? Sure, there are probably proofs about this1, but let's just try going all the way around. We'll start on one to the right and try to get all the way around:

scale = recursive_scales(Gmaj.scale, 'C ')
print(scale)

['C ', 'D ', 'E ', 'F ', 'G ', 'A ', 'B ']

And back around we are! Awesome. That original crazy symbol at the top isn't seeming so crazy anymore I hope. It's just describing a relationship between all the different scales, and it just so turns out, you can generate them recursively. It also turns out that if you keep doing this, you can go around the circle forever.

There's nothing particularly special about starting on Cmaj by the way. It just happens to be the one without any sharps, so that it's pretty simple to start there.

Major, Minor, Who's Counting

What about the inner loop though? It seems like we've only solved half the puzzle so far, so what's up with that? Let's go back to our chord definitions from last time. Major scales had this pattern:

Major minor minor Major Major minor diminished

And minor scales had this pattern:

minor diminished Major minor minor Major Major

They both have 3 major chords, 3 minor chords, and a diminished chord. Actually… hey wait a second, they're just shifted versions of each other! Getting pretty good at noticing these shifts now. So if we want to generate the minor scale for a note, all we need to do is:

def minorify_scale(scale):
    """Turns a major scale into a minor scale"""
    return rotate(scale, 5)

Amin = MinorKey('A ')
Amin.generate_scale()

print(Cmaj.scale)
print(minorify_scale(Cmaj.scale))
print(Amin.scale)

['C ', 'D ', 'E ', 'F ', 'G ', 'A ', 'B ']
['A ', 'B ', 'C ', 'D ', 'E ', 'F ', 'G ']
['A ', 'B ', 'C ', 'D ', 'E ', 'F ', 'G ']

Nice! Compared to everything to get to this point, it almost seems trivial. It's not! We now can tie together you probably hear about in music: Keys, Scales, Chords, Fifths. It all just comes down to twelve notes and some simple patterns between them.

If you have an instrument to play along with at home, I highly recommend picking a bizarre key you would never play in. Then, try and derive the key. Then try and play around with it. After awhile, it'll feel as natural as going back and forth between 12 and 24 hour time.

Oh, and if you don't have an instrument, it's never too late! Go get one. Start a band. Live the dream!


  1. I tried finding something, but couldn't see anything exact. It involves coprime modulo operations for sure! ↩︎