I’m a self-taught guitarist of many years, and like a lot of self-taught musicians, am woefully inept at (Western) music theory.
So naturally, I decided to write some code.
This article explains the very basics of Western music theory in around 200 lines of Python.
We will first look at the notes in Western music theory, use them to derive the chromatic scale in a given key, and then to combine it with interval formulas to derive common scales and chords.
Finally, we will look at modes, which are whole collections of scales derived from common scales, that can be used to evoke more subtle moods and atmospheres than the happy-sad dichotomy that major and minor scales provide.
The musical alphabet of Western music consists of the letters A through G, and they represent different pitches of notes.
We can represent the musical alphabet with the following list in Python:
alphabet = ['A', 'B', 'C', 'D', 'E', 'F', 'G']
However, these notes are not evenly spaced in their frequencies. To get a more even spacing between pitches, we have the following twelve notes:
notes_basic = [ ['A'], ['A#', 'Bb'], ['B'], ['C'], ['C#', 'Db'], ['D'], ['D#', 'Eb'], ['E'], ['F'], ['F#', 'Gb'], ['G'], ['G#', 'Ab'],
]
There are four things to note here: first, each of the notes are a half step or semitone apart, second, the way this is represented is by an optional trailing symbol (called an accidental) to indicate a half-step raise (sharp, ♯) or a half-step lowering (flat, ♭) of the base note, and third, the above notes simply loop back and start over, but at a higher octave.
Finally, you’ll notice that some of these notes are represented by lists containing more than one name: these are enharmonic equivalents, a fancy way of saying that the same note can have different “spellings”. So for example the note half a step above A is A♯, but it can also be thought of as half a step below B, and can thus be referred to as B♭. For historical reasons, there are no sharps or flats between the notes B/C, and E/F.
The key thing to remember on why we need these equivalents is that, when we start to derive common scales (major, minor and the modes), consecutive notes must start with consecutive letters. Having enharmonic equivalents with different alphabets allows us to derive these scales correctly.
In fact, for certain keys, the above enharmonic notes are not sufficient. In order to satisfy the “different-alphabets-for-consecutive-notes rule”, we end up having to use double sharps and double flats that raise or lower a note by a full step. These scales generally have equivalents that do not require these double accidentals, but for completeness, we can include all possible enharmonic equivalents by rewriting our notes as follows:
notes = [ ['B#', 'C', 'Dbb'], ['B##', 'C#', 'Db'], ['C##', 'D', 'Ebb'], ['D#', 'Eb', 'Fbb'], ['D##', 'E', 'Fb'], ['E#', 'F', 'Gbb'], ['E##', 'F#', 'Gb'], ['F##', 'G', 'Abb'], ['G#', 'Ab'], ['G##', 'A', 'Bbb'], ['A#', 'Bb', 'Cbb'], ['A##', 'B', 'Cb'],
]
The chromatic scale is the easiest scale possible, and simply consists of all the (twelve) semitones between an octave of a given key (the main note in a scale, also called the tonic).
We can generate a chromatic scale for any given key very easily: (i) find the index of
the note in our notes
list, and then (ii) left-rotate the notes
list that many times.
Let’s write a simple function to find a particular note in this list:
def find_note_index(scale, search_note): ''' Given a scale, find the index of a particular note ''' for index, note in enumerate(scale): if type(note) == list: if search_note in note: return index elif type(note) == str: if search_note == note: return index
The find_note_index()
function takes as parameters a sequence of notes (scale
),
and a note to search for (search_note
), and returns the index via a simple linear
search. We handle two cases within the loop: (i) where the provided scale
consists
of individual notes (like our alphabet
list above), or (ii) where it consists of
a list of enharmonic equivalents (like our notes
or notes_basic
lists above).
Here is an example of how the function works for both:
>>> find_note_index(notes, 'A') # notes is a list of lists
9
>>> find_note_index(alphabet, 'A') # alphabet is a list of notes
0
We can now write a function to rotate a given scale
by n
steps:
def rotate(scale, n): ''' Left-rotate a scale by n positions. ''' return scale[n:] + scale[:n]
We slice the scale
list at position n
and exchange the two halves. Here is
an example of rotating our alphabet
list three places (which brings the note D
to the front):
>>> alphabet
['A', 'B', 'C', 'D', 'E', 'F', 'G']
>>> rotate(alphabet, 3)
['D', 'E', 'F', 'G', 'A', 'B', 'C']
We can now finally write our chromatic()
function that generates a chromatic
scale for a given key by rotating the notes
array:
def chromatic(key): ''' Generate a chromatic scale in a given key. ''' num_rotations = find_note_index(notes, key) return rotate(notes, num_rotations)
The chromatic()
function above finds the index of the provided key in the notes
list (using our find_note_index()
function), and then rotates it by that amount
to bring it to the front (using our rotate()
function). Here is an example of
generating the D chromatic scale:
>>> import pprint
>>> pprint.pprint(chromatic('D'))
[['C##', 'D', 'Ebb'], ['D#', 'Eb', 'Fbb'], ['D##', 'E', 'Fb'], ['E#', 'F', 'Gbb'], ['E##', 'F#', 'Gb'], ['F##', 'G', 'Abb'], ['G#', 'Ab'], ['G##', 'A', 'Bbb'], ['A#', 'Bb', 'Cbb'], ['A##', 'B', 'Cb'], ['B#', 'C', 'Dbb'], ['B##', 'C#', 'Db']]
For chromatic scales, one typically uses sharps when ascending and flats when descending. However, for now, we leave enharmonic equivalents just as they are; we will see how to pick the correct note to use later.
Intervals specify the relative distance between notes.
The notes of a chromatic scale can therfore be given names based on their relative distance from
the tonic or root note. Below are the standard names for each note, ordered
identical to the indexes in the notes
list:
intervals = [ ['P1', 'd2'], ['m2', 'A1'], ['M2', 'd3'], ['m3', 'A2'], ['M3', 'd4'], ['P4', 'A3'], ['d5', 'A4'], ['P5', 'd6'], ['m6', 'A5'], ['M6', 'd7'], ['m7', 'A6'], ['M7', 'd8'], ['P8', 'A7'], ]
Again, the same note can have different interval names. For example, the root note can be thought of as a perfect unison or an diminished 2nd.
Given a chromatic scale in a given key, and an interval name in the above array, we can pin point the exact note to use (and filter it out from a set of enharmonic equivalents). Let’s look at the basic way to do this.
As an example, let’s look at how to find the note corresponding to M3
, or
the major third interval, from the D
chromatic scale.
intervals
array, we can see that the index at which we find M3
is 4. That is 'M3' in intervals[4] == True
.chromatic('D')[4]
is the list of notes ['E##', 'F#', 'Gb']
.M3
(i.e., the 3) indicates the alphabet we need to use, with 1 indicating the root alphabet. So for example, for the key of D, 1=D
, 2=E
, 3=F
, 4=G
, 5=A
, 6=B
, 7=C
, 8=D
… and so on. So we need to look for a note in our list of notes (['E##', 'F#', 'Gb']
) containing the alphabet F
. That’s the note F#.We can write a relatively simple function to apply this logic for us programmatically, and give us a dict mapping all interval names to the right note names in a given key:
def make_intervals_standard(key): labels = {} chromatic_scale = chromatic(key) alphabet_key = rotate(alphabet, find_note_index(alphabet, key[0])) for index, interval_list in enumerate(intervals): notes_to_search = chromatic_scale[index % len(chromatic_scale)] for interval_name in interval_list: degree = int(interval_name[1]) - 1 alphabet_to_search = alphabet_key[degree % len(alphabet_key)] try: note = [x for x in notes_to_search if x[0] == alphabet_to_search][0] except: note = notes_to_search[0] labels[interval_name] = note return labels
And here is the dict we get back for the key of C:
>>> import pprint
>>> pprint.pprint(make_intervals_standard('C'), sort_dicts=False)
{'P1': 'C', 'd2': 'Dbb', 'm2': 'Db', 'A1': 'C#', 'M2': 'D', 'd3': 'Ebb', 'm3': 'Eb', 'A2': 'D#', 'M3': 'E', 'd4': 'Fb', 'P4': 'F', 'A3': 'E#', 'd5': 'Gb', 'A4': 'F#', 'P5': 'G', 'd6': 'Abb', 'm6': 'Ab', 'A5': 'G#', 'M6': 'A', 'd7': 'Bbb', 'm7': 'Bb', 'A6': 'A#', 'M7': 'B', 'd8': 'Cb', 'P8': 'C', 'A7': 'B#'}
We can now specify formulas, or groups of notes, using interval names, and be able to map them to any key that we want:
def make_formula(formula, labeled): ''' Given a comma-separated interval formula, and a set of labeled notes in a key, return the notes of the formula. ''' return [labeled[x] for x in formula.split(',')]
For example, the formula for a major scale is:
formula = 'P1,M2,M3,P4,P5,M6,M7,P8'
We can use this to generate the major scale easily for different keys as shown below:
>>> for key in alphabet:
>>> print(key, make_formula(formula, make_intervals_standard(key)))
C ['C', 'D', 'E', 'F', 'G', 'A', 'B', 'C']
D ['D', 'E', 'F#', 'G', 'A', 'B', 'C#', 'D']
E ['E', 'F#', 'G#', 'A', 'B', 'C#', 'D#', 'E']
F ['F', 'G', 'A', 'Bb', 'C', 'D', 'E', 'F']
G ['G', 'A', 'B', 'C', 'D', 'E', 'F#', 'G']
A ['A', 'B', 'C#', 'D', 'E', 'F#', 'G#', 'A']
B ['B', 'C#', 'D#', 'E', 'F#', 'G#', 'A#', 'B']
Let’s also quickly write a function to print scales in a nicer way:
def dump(scale, separator=' '): ''' Pretty-print the notes of a scale. Replaces b and # characters for unicode flat and sharp symbols. ''' return separator.join(['{:<3s}'.format(x) for x in scale]) \ .replace('b', '\u266d') \ .replace('#', '\u266f')
Here is a nicer output using the correct unicode characters:
>>> for key in alphabet:
>>> scale = make_formula(formula, make_intervals_standard(key))
>>> print('{}: {}'.format(key, dump(scale)))
C: C D E F G A B C
D: D E F♯ G A B C♯ D
E: E F♯ G♯ A B C♯ D♯ E
F: F G A B♭ C D E F
G: G A B C D E F♯ G
A: A B C♯ D E F♯ G♯ A
B: B C♯ D♯ E F♯ G♯ A♯ B
An alternative approach to naming formulas is based on the notes of the major scale. This is easier when playing instruments as you can derive scales and chords in a given key if youre familiar with its major scale.
Here are the interval names relative to the major scale in a given key:
intervals_major = [ [ '1', 'bb2'], ['b2', '#1'], [ '2', 'bb3', '9'], ['b3', '#2'], [ '3', 'b4'], [ '4', '#3', '11'], ['b5', '#4', '#11'], [ '5', 'bb6'], ['b6', '#5'], [ '6', 'bb7', '13'], ['b7', '#6'], [ '7', 'b8'], [ '8', '#7'],
]
I’ve also added common intervals used in more complex chords (9th’s, 11th’s, and 13th’s). These are essentially wrapped around modulo eight. So, for example, a 9th is just a 2nd, but an octave higher.
We can also modify our make_intervals()
function to use this:
def make_intervals(key, interval_type='standard'): ... for index, interval_list in enumerate(intervals): ... intervs = intervals if interval_type == 'standard' else intervals_major for interval_name in intervs: if interval_type == 'standard': degree = int(interval_name[1]) - 1 elif interval_type == 'major': degree = int(re.sub('[b#]', '', interval_name)) - 1 ... return labels
Above, we’ve just added a new parameter (interval_type
) to the make_intervals()
function, and calculate the degree
differently in the inner loop. If interval_type
is specified as 'major'
, we just remove all b
and characters before converting to an integer to get the degree.
Here are a bunch of formulas covering the most common scales and chords:
formulas = { 'scales': { 'major': '1,2,3,4,5,6,7', 'minor': '1,2,b3,4,5,b6,b7', 'melodic_minor': '1,2,b3,4,5,6,7', 'harmonic_minor': '1,2,b3,4,5,b6,7', 'major_blues': '1,2,b3,3,5,6', 'minor_blues': '1,b3,4,b5,5,b7', 'pentatonic_major': '1,2,3,5,6', 'pentatonic_minor': '1,b3,4,5,b7', 'pentatonic_blues': '1,b3,4,b5,5,b7', }, 'chords': { 'major': '1,3,5', 'major_6': '1,3,5,6', 'major_6_9': '1,3,5,6,9', 'major_7': '1,3,5,7', 'major_9': '1,3,5,7,9', 'major_13': '1,3,5,7,9,11,13', 'major_7_#11': '1,3,5,7,#11', 'minor': '1,b3,5', 'minor_6': '1,b3,5,6', 'minor_6_9': '1,b3,5,6,9', 'minor_7': '1,b3,5,b7', 'minor_9': '1,b3,5,b7,9', 'minor_11': '1,b3,5,b7,9,11', 'minor_7_b5': '1,b3,b5,b7', 'dominant_7': '1,3,5,b7', 'dominant_9': '1,3,5,b7,9', 'dominant_11': '1,3,5,b7,9,11', 'dominant_13': '1,3,5,b7,9,11,13', 'dominant_7_#11': '1,3,5,b7,#11', 'diminished': '1,b3,b5', 'diminished_7': '1,b3,b5,bb7', 'diminished_7_half': '1,b3,b5,b7', 'augmented': '1,3,#5', 'sus2': '1,2,5', 'sus4': '1,4,5', '7sus2': '1,2,5,b7', '7sus4': '1,4,5,b7', },
}
Here is the output when generating all these scales and chords in the key of C:
intervs = make_intervals('C', 'major')
for ftype in formulas: print(ftype) for name, formula in formulas[ftype].items(): v = make_formula(formula, intervs) print('\t{}: {}'.format(name, dump(v)))
scales major: C D E F G A B minor: C D E♭ F G A♭ B♭ melodic_minor: C D E♭ F G A B harmonic_minor: C D E♭ F G A♭ B major_blues: C D E♭ E G A minor_blues: C E♭ F G♭ G B♭ pentatonic_major: C D E G A pentatonic_minor: C E♭ F G B♭ pentatonic_blues: C E♭ F G♭ G B♭
chords major: C E G major_6: C E G A major_6_9: C E G A D major_7: C E G B major_9: C E G B D major_13: C E G B D F A major_7_#11: C E G B F♯ minor: C E♭ G minor_6: C E♭ G A minor_6_9: C E♭ G A D minor_7: C E♭ G B♭ minor_9: C E♭ G B♭ D minor_11: C E♭ G B♭ D F minor_7_b5: C E♭ G♭ B♭ dominant_7: C E G B♭ dominant_9: C E G B♭ D dominant_11: C E G B♭ D F dominant_13: C E G B♭ D F A dominant_7_#11: C E G B♭ F♯ diminished: C E♭ G♭ diminished_7: C E♭ G♭ B♭♭ diminished_7_half: C E♭ G♭ B♭ augmented: C E G♯ sus2: C D G sus4: C F G 7sus2: C D G B♭ 7sus4: C F G B♭
Modes are essentially left-rotations of a scale.
mode = rotate
The thing to note is that the resulting rotated scale, or mode, is in a different key since the root note changes after the rotation.
For every key, there are exactly seven modes of the major scale depending on the number of left-rotations applied, and each has a specific name:
major_mode_rotations = { 'Ionian': 0, 'Dorian': 1, 'Phrygian': 2, 'Lydian': 3, 'Mixolydian': 4, 'Aeolian': 5, 'Locrian': 6,
}
Using this, we can now generate modes of the major scale for any given key. Here’s an example for the C major scale:
intervs = make_intervals('C', 'major')
c_major_scale = make_formula(formulas['scales']['major'], intervs)
for m in major_mode_rotations: v = mode(c_major_scale, major_mode_rotations[m]) print('{} {}: {}'.format(dump([v[0]]), m, dump(v)))
And here is the result. Remember that the root note changes with each rotation:
C Ionian: C D E F G A B
D Dorian: D E F G A B C
E Phrygian: E F G A B C D
F Lydian: F G A B C D E
G Mixolydian: G A B C D E F
A Aeolian: A B C D E F G
B Locrian: B C D E F G A
Above, we’re looking at modes that derive from a given scale. However, in practice, what you care about are modes for a given key. So given the key of C, we would want to know the C Ionian, the C Dorian, the C Mixolydian and so on.
Another way to put this is that a “C Mixolidian”, for example, is not the same as “the Mixolydian of C”. The former means a Mixolydian scale where the root note is a C. The latter means the Mixolydian of the C major scale (i.e., G Mixolydian from above).
We can also generate modes in a given key quite easily.
keys = [ 'B#', 'C', 'C#', 'Db', 'D', 'D#', 'Eb', 'E', 'Fb', 'E#', 'F', 'F#', 'Gb', 'G', 'G#', 'Ab', 'A', 'A#', 'Bb', 'B', 'Cb',
] modes = {}
for key in keys: intervs = make_intervals(key, 'major') c_major_scale = make_formula(formulas['scales']['major'], intervs) for m in major_mode_rotations: v = mode(c_major_scale, major_mode_rotations[m]) if v[0] not in modes: modes[v[0]] = {} modes[v[0]][m] = v
Above, we go through each key, and build up a dict containing the modes of each key as we come across them (by checking the first note of the mode).
Now, for example, if we print out modes['C']
, we get the following:
{'Aeolian': ['C', 'D', 'Eb', 'F', 'G', 'Ab', 'Bb'], 'Dorian': ['C', 'D', 'Eb', 'F', 'G', 'A', 'Bb'], 'Ionian': ['C', 'D', 'E', 'F', 'G', 'A', 'B'], 'Locrian': ['C', 'Db', 'Eb', 'F', 'Gb', 'Ab', 'Bb'], 'Lydian': ['C', 'D', 'E', 'F#', 'G', 'A', 'B'], 'Mixolydian': ['C', 'D', 'E', 'F', 'G', 'A', 'Bb'], 'Phrygian': ['C', 'Db', 'Eb', 'F', 'G', 'Ab', 'Bb']}
So we’ve looked at basic notes in Western music theory. How to derive chromatic scales from these notes. How to use interval names to pick out the right notes from enharmonic equivalents. Then we looked at how to generate scales and chords of various kinds using interval formulas, both using standard interval names and intervals relative to the major-scale. Finally, we saw that modes are simply rotations of a scale, and can be viewed in two way for a given key: the mode derived by rotating the scale of a given key (which will be in a another key), and the mode derived from some key such that the first note is the key we want.