Tags

, ,

Mathematician and educator John Baez has an excellent series of blog posts about music theory. The seventh concerns generating scales by using notes separated by fifths. Shifting the start point generates the seven major scale modes. Shifting the root key generates those seven modes in the twelve keys (a total of 7×12=84 scales).

John asked if any of his readers would be interested in creating that table of all 84 rows. It sounded like — and turned out to be — a fun exercise. This post explores in detail the Python solution I came up with.

It’s actually the third solution I came up with. The first and second used different approaches to the problem — the second wasn’t entirely successful. Exploring those seems like a good illustration of different programming approaches to the same problem, but also topics for another post. I’ll stick here to the third (and final) solution.

First, here’s the table the code is to create:

Lydian C D E F♯ G A B
Ionian C D E F G A B
Mixolydian C D E F G A B♭
Dorian C D E♭ F G A B♭
Aeolian C D E♭ F G A♭ B♭
Phrygian C D♭ E♭ F G A♭ B♭
Locrian C D♭ E♭ F G♭ A♭ B♭
Lydian B C♯ D♯ E♯ F♯ G♯ A♯
Ionian B C♯ D♯ E F♯ G♯ A♯
Mixolydian B C♯ D♯ E F♯ G♯ A
Dorian B C♯ D E F♯ G♯ A
Aeolian B C♯ D E F♯ G A
Phrygian B C D E F♯ G A
Locrian B C D E F G A
Lydian B♭ C D E F G A
Ionian B♭ C D E♭ F G A
Mixolydian B♭ C D E♭ F G A♭
Dorian B♭ C D♭ E♭ F G A♭
Aeolian B♭ C D♭ E♭ F G♭ A♭
Phrygian B♭ C♭ D♭ E♭ F G♭ A♭
Locrian B♭ C♭ D♭ E♭ F♭ G♭ A♭
Lydian A B C♯ D♯ E F♯ G♯
Ionian A B C♯ D E F♯ G♯
Mixolydian A B C♯ D E F♯ G
Dorian A B C D E F♯ G
Aeolian A B C D E F G
Phrygian A B♭ C D E F G
Locrian A B♭ C D E♭ F G
Lydian G♯ A♯ B♯ C♯♯ D♯ E♯ F♯♯
Ionian G♯ A♯ B♯ C♯ D♯ E♯ F♯♯
Mixolydian G♯ A♯ B♯ C♯ D♯ E♯ F♯
Dorian G♯ A♯ B C♯ D♯ E♯ F♯
Aeolian G♯ A♯ B C♯ D♯ E F♯
Phrygian G♯ A B C♯ D♯ E F♯
Locrian G♯ A B C♯ D E F♯
Lydian G A B C♯ D E F♯
Ionian G A B C D E F♯
Mixolydian G A B C D E F
Dorian G A B♭ C D E F
Aeolian G A B♭ C D E♭ F
Phrygian G A♭ B♭ C D E♭ F
Locrian G A♭ B♭ C D♭ E♭ F
Lydian F♯ G♯ A♯ B♯ C♯ D♯ E♯
Ionian F♯ G♯ A♯ B C♯ D♯ E♯
Mixolydian F♯ G♯ A♯ B C♯ D♯ E
Dorian F♯ G♯ A B C♯ D♯ E
Aeolian F♯ G♯ A B C♯ D E
Phrygian F♯ G A B C♯ D E
Locrian F♯ G A B C D E
Lydian F G A B C D E
Ionian F G A B♭ C D E
Mixolydian F G A B♭ C D E♭
Dorian F G A♭ B♭ C D E♭
Aeolian F G A♭ B♭ C D♭ E♭
Phrygian F G♭ A♭ B♭ C D♭ E♭
Locrian F G♭ A♭ B♭ C♭ D♭ E♭
Lydian E F♯ G♯ A♯ B C♯ D♯
Ionian E F♯ G♯ A B C♯ D♯
Mixolydian E F♯ G♯ A B C♯ D
Dorian E F♯ G A B C♯ D
Aeolian E F♯ G A B C D
Phrygian E F G A B C D
Locrian E F G A B♭ C D
Lydian E♭ F G A B♭ C D
Ionian E♭ F G A♭ B♭ C D
Mixolydian E♭ F G A♭ B♭ C D♭
Dorian E♭ F G♭ A♭ B♭ C D♭
Aeolian E♭ F G♭ A♭ B♭ C♭ D♭
Phrygian E♭ F♭ G♭ A♭ B♭ C♭ D♭
Locrian E♭ F♭ G♭ A♭ B♭♭ C♭ D♭
Lydian D E F♯ G♯ A B C♯
Ionian D E F♯ G A B C♯
Mixolydian D E F♯ G A B C
Dorian D E F G A B C
Aeolian D E F G A B♭ C
Phrygian D E♭ F G A B♭ C
Locrian D E♭ F G A♭ B♭ C
Lydian C♯ D♯ E♯ F♯♯ G♯ A♯ B♯
Ionian C♯ D♯ E♯ F♯ G♯ A♯ B♯
Mixolydian C♯ D♯ E♯ F♯ G♯ A♯ B
Dorian C♯ D♯ E F♯ G♯ A♯ B
Aeolian C♯ D♯ E F♯ G♯ A B
Phrygian C♯ D E F♯ G♯ A B
Locrian C♯ D E F♯ G A B

For each of the twelve keys (A, A♯, B, C, … G, G♯) there are seven scale modes: Lydian, Ionian, Mixolydian, Dorian, Aeolian, Phrygian, and Locrian.

The first solution, which worked fine, simply reverse-engineered the table. Each column follows a regular pattern that’s easy to generate. That pattern has a different starting index and note in each of the seven columns, but otherwise it’s the same. It was easy to color the table because the color pattern follows the column pattern.

The second solution used the technique John described in his post of picking notes in intervals of fifths. This solution matches the problem description and has a nice algorithm for generating the scale rows, but it struggles to pick between, for example, F♯ or G♭ — the same note but spelled differently depending on context. Frustration over trying to resolve that issue led to a final solution.

§

The third solution trades code complexity for data size, a common design tradeoff. (As an example, a function can use code to calculate a sine function or simply look answers up in a large data table.) The scales now come from a seven-row data table containing the note intervals. Producing a given scale just requires the starting note.

Because the program is short, and to make it easier to copy, here it is in its entirety:

001| from sys import stdout
002| 
003| ModeNames = [
004|     ‘Lydian’,‘Ionian’,‘Mixolydian’,‘Dorian’,
005|     ‘Aeolian’,‘Phrygian’,‘Locrian’
006| ]
007| 
008| ModeIntervals = [
009|     [1, 2, 2, 2, 1, 2, 2, 1],   # Lydian
010|     [1, 2, 2, 1, 2, 2, 2, 1],   # Ionian
011|     [2, 2, 2, 1, 2, 2, 1, 2],   # Mixolydian
012|     [2, 2, 1, 2, 2, 2, 1, 2],   # Dorian
013|     [2, 2, 1, 2, 2, 1, 2, 2],   # Aeolian
014|     [2, 1, 2, 2, 2, 1, 2, 2],   # Phrygian
015|     [2, 1, 2, 2, 1, 2, 2, 2],   # Locrian
016| ]
017| 
018| Flat  = ‘\u266d’
019| Sharp = ‘\u266f’
020| Flat2  = ‘\u266d\u266d’
021| Sharp2 = ‘\u266f\u266f’ #’\U0001D12a’
022| 
023| BaseNotes = [
024|     ‘A’,‘B’+Flat,‘B’,‘C’,‘C’+Sharp,‘D’,
025|     ‘E’+Flat,‘E’,‘F’,‘F’+Sharp,‘G’,‘G’+Sharp
026| ]
027| 
028| NoteTable = {
029|     ‘A’:(‘A’+Flat2,‘A’+Flat,‘A’,‘A’+Sharp,‘A’+Sharp2),
030|     ‘B’:(‘B’+Flat2,‘B’+Flat,‘B’,‘B’+Sharp,‘B’+Sharp2),
031|     ‘C’:(‘C’+Flat2,‘C’+Flat,‘C’,‘C’+Sharp,‘C’+Sharp2),
032|     ‘D’:(‘D’+Flat2,‘D’+Flat,‘D’,‘D’+Sharp,‘D’+Sharp2),
033|     ‘E’:(‘E’+Flat2,‘E’+Flat,‘E’,‘E’+Sharp,‘E’+Sharp2),
034|     ‘F’:(‘F’+Flat2,‘F’+Flat,‘F’,‘F’+Sharp,‘F’+Sharp2),
035|     ‘G’:(‘G’+Flat2,‘G’+Flat,‘G’,‘G’+Sharp,‘G’+Sharp2),
036| }
037| def Note2Str (t):
038|     base = t[0]
039|     more = 2 + t[1]
040|     return NoteTable[base][more]
041| 
042| def Str2Note (nt):
043|     base = nt[0]
044|     more = nt[1:2]
045|     if more == Sharp: return (base, +1)
046|     if more == Flat: return (base, 1)
047|     return (base, 0)
048| 
049| def NoteAbove (nt):
050|     if nt == ‘G’: return ‘A’
051|     return chr(ord(nt)+1)
052| 
053| def generate_scale (first_note=(‘C’,0), mode_index=0):
054|     mode = ModeIntervals[mode_index]
055|     mode_name = ModeNames[mode_index]
056| 
057|     def next_note (nt, d):
058|         nxnt = NoteAbove(nt[0])
059|         if nt[0] in [‘B’, ‘E’]:
060|             flag = nt[1]+(d1)
061|         else:
062|             flag = nt[1](2d)
063|         return (nxnt, flag)
064| 
065|     scale = [first_note]
066|     for dist in mode[1:7]:
067|         note = next_note(scale[1], dist)
068|         scale.append(note)
069|     return (mode_name, [Note2Str(n) for n in scale])
070| 
071| def generate_section (first_note=(‘C’,0)):
072|     scales = []
073|     for mode in range(7):
074|         notes = generate_scale(first_note, mode)
075|         scales.append(notes)
076|     return scales
077| 
078| def generate_table ():
079|     table = []
080|     for key in [3,2,1,0,11,10,9,8,7,6,5,4]:
081|         note = Str2Note(BaseNotes[key])
082|         sect = generate_section(note)
083|         table.append(sect)
084|     return table
085| 
086| fmt = ‘%-11s| %-3s %-3s %-3s %-3s %-3s %-3s %-3s|’
087| 
088| def display_table (table, stream=stdout):
089|     for section in table:
090|         for mode,scale in section:
091|             print(fmt % (mode,*scale), file=stream)
092|         print(file=stream)
093|     print(file=stream)
094| 
095| table = generate_table()
096| display_table(table)
097| 
098| 

When run, it generates the above table (but without color).

Lines #3-#6: ModeNames, a table of the seven scale names.

Lines #8-#16: ModeIntervals, a table of the intervals for the seven scales. Note that each scale has eight intervals in order to capture the interval from the last note to the first note in the next occurrence of the scale.

Lines #18-21: Four constants with the Unicode character codes for the musical sharp, flat, double-flat, and double-sharp characters.

Lines #23-26: BaseNotes, a table of the twelve notes as (Unicode) characters. Where notes have alternate spellings (such as F♯ and G♭), the table uses an “obvious” choice.

Lines #28-36: NoteTable, another table of notes, but with two big differences from BaseNotes. Firstly, there are only seven entries, one for each natural note (A, B, C, D, E, F, & G). Secondly, each entry has five spellings, the natural note in the center with flat and double-flat to the left and sharp and double-sharp to the right.

The idea is that a natural note has a “tuning” index of zero. A flat note has an index of -1, while a sharp note has an index of +1. Double flats and sharps have indexes of -2 and +2, respectively. The NoteTable rows each index -2, -1, 0, +1, +2.

Lines #37-40: The Note2Str function, which takes a note tuple and returns the Unicode character for that note. This function is the only one that directly accesses NoteTable.

Lines #42-47: The Str2Note function, which takes a Unicode note string and returns a note tuple (suitable for indexing the NoteTable). The opposite of the Note2Str function.

Lines #49-51: The NoteAbove function, which takes a Unicode note string and returns the note above it. For example, given D returns E. Given G returns A.

Lines #53-69: The generate_scale function, which takes a root note and scale mode index and returns the indexed scale starting at the given note. This is the heart of the algorithm, so I’ll come back to it below.

Lines #71-76: The generate_section function, which takes a root note and returns all seven scales starting at that note. This function calls generate_scale for each of the seven scales.

Lines #78-84: The generate_table function, which has no parameters but returns a table with the seven scales starting with all twelve notes (84 scales total). This function calls generate_section for each of the twelve notes.

Line #86: The fmt string configures the output of the 84 scales.

Lines #88-93: The display_table function, which takes an 84-scale table and prints it to an output stream (stdout by default).

Line #95: Call generate_table and store the result in table.

Line #96: Call display_table and pass it table, to print the data.

§

Now let’s dig deeper into the generate_scale function. Here it is again for easy reference:

053| def generate_scale (first_note=(‘C’,0), mode_index=0):
054|     mode = ModeIntervals[mode_index]
055|     mode_name = ModeNames[mode_index]
056| 
057|     def next_note (nt, d):
058|         nxnt = NoteAbove(nt[0])
059|         if nt[0] in [‘B’, ‘E’]:
060|             flag = nt[1]+(d1)
061|         else:
062|             flag = nt[1](2d)
063|         return (nxnt, flag)
064| 
065|     scale = [first_note]
066|     for dist in mode[1:7]:
067|         note = next_note(scale[1], dist)
068|         scale.append(note)
069|     return (mode_name, [Note2Str(n) for n in scale])
070| 

The function has two input parameters, first_note and mode_index, both with default values. The former is the starting note of the scale in tuple form. The latter, which determines which scale the function generates, is an index from zero to six.

Line #54: Use mode_index to get the scale intervals from the ModeIntervals table and put the list of scale intervals in the mode variable.

Line #55: Use mode_index to get the scale name from the ModeNames table. Put the name string in the mode_name variable.

Lines #57-#63: The (local) next_note function, which takes a note and an interval (a “distance”) and returns the next note of the scale.

Line #65: Start the output scale list, scale, with the first_note.

Lines #66-#68: Iterate through intervals of mode, calculate the next note, and add it to the output, scale.

Line #69: Return a tuple with the scale name and scale. (Convert the scale from note tuple form to Unicode character form, first.)

§

Lastly, a few words about the note representation. Externally, notes are character strings, like ‘A’ or ‘C♯’ or ‘E♭’. Internally, notes are represented as two-member tuples. The first member is a base note, a single-character string which can be ‘A’ through ‘G’. The second member, an integer from -2 to +2, specifies whether the base note is double-flat (-2), flat (-1), natural (0), sharp (+1), or double-sharp (+2).

In theory, the representation could support triple-sharp (or more), but the NoteTable only supports single and double flats and sharps.

The reason for the representation is that using the fifths intervals to construct a scale can result in undesirable spellings of some notes. They are undesirable because some base notes are left out while others are duplicated. For example, generating the C Lydian scale might return this:

C D E G G♭ A B

Ideally, any scale should contain all seven base notes and no duplicates. (Ensuring this sometimes requires a double-flat or double-sharp.) In the case above, the F is missing, and the G appears twice. The G♭ should be an F♯.

The second approach I took, which used intervals of fifths to generate scales, suffered from this problem. It generated over two-dozen problem cases among the 84 scales. I only solved it by throwing an exceptions table at it, an ugly solution. The note representation of this third approach doesn’t suffer from the issue at all.

Ø