Doodles: Tuning Systems

Some prelimiaries…

%pip install tabulate

import math
import itertools
from IPython.display import display, HTML, display_html, display_markdown, 
    Markdown
from tabulate import tabulate

type Hz = float
type Note = int

A_440_Note = 69
A_440_Hz = 440.0

def note_class(n: Note) -> tuple[int, int]:
    octaves, steps = divmod(n, 12)
    return octaves - 1, steps

def note_name(n: Note) -> str:
    # MIDI note 24 is C1
    note_names = ['C', 'C♯/D♭', 'D', 'D♯/E♭', 'E', 'F', 'F♯/G♭', 'G', 'G♯/A♭',
        'A', 'A♯/B♭', 'B'] octaves, steps = note_class(n)
    return f"{note_names[steps]}{octaves}"    

assert note_name(24) == 'C1'

12-TET Tempering

12-tone Equal Temperament divides an octave into a twelve logarithmically-equal parts. This is convenient because it has a very compact closed-form expression.

P(n)=P02n12 P(n) = P_0 \cdot 2^{\frac{n}{12}}

where P0P_0 is the reference pitch, the pitch in hertz at which n=0n=0. In fact this works for any number of steps per octave:

P(n,k)=P02nk P(n, k) = P_0 \cdot 2^{\frac{n}{k}}

We can implement it in python with a higher-order function like this:

from typing import Callable

def equal_tempered(n: int) -> Callable[[Note], Hz]:
    return lambda x: A_440_Hz * pow(2.0, (float(x - A_440_Note) / float(n)))

def tempered12tet(n: Note) -> Hz:
    return equal_tempered(12)(n)
table = tabulate(
    [(str(n), note_name(n), f"{tempered12tet(n): 0.04f}") for n in range(60, 82)],
    tablefmt='html',
    headers=('MIDI Note', 'Note Name', 'Hz'))

display(HTML(table))
MIDI NoteNote Name Hz
60C4 261.626
61C♯/D♭4 277.183
62D4 293.665
63D♯/E♭4 311.127
64E4 329.628
65F4 349.228
66F♯/G♭4 369.994
67G4 391.995
68G♯/A♭4 415.305
69A4 440
70A♯/B♭4 466.164
71B4 493.883
72C5 523.251
73C♯/D♭5 554.365
74D5 587.33
75D♯/E♭5 622.254
76E5 659.255
77F5 698.457
78F♯/G♭5 739.989
79G5 783.991
80G♯/A♭5 830.609
81A5 880

Pythagorean Tuning

Pythagorean tuning constructs intervals between notes purely out of combinations of ratios of 32\frac{3}{2} and 12\frac{1}{2}.

Chromatic Scale with diatonic intervals:

U m2 M2 m3 M3 P4 a4 P5 m6 M6 m7 M7
C C# D D# E F F# G G# A A# B

Ratios to unison for intervals:

Interval Ratio Hz (over 440) 12-TET Hz
P4 43\frac{4}{3} 586.666 4402512440 \cdot 2^{\frac{5}{12}} = 587.329
P5 32\frac{3}{2} 660.666 4402612440 \cdot 2^{\frac{6}{12}} = 622.253

Notes that do not exist in C:

E♯/F♭ B♯/C♭

Circle of Fifths

Semitones_In_P5 = 7 # There are seven semitones in a perfect fifth
Center_Note = 60    # C4
Fifths_Window = 6   # We want the table to show 6 fifths above and below Center_Note

# implementation

Start_Note = Center_Note - Fifths_Window * Semitones_In_P5
End_Note   = Center_Note + (Fifths_Window + 1) * Semitones_In_P5

note_range = range(Start_Note, End_Note)
fifths_batches = itertools.batched(note_range, Semitones_In_P5)

Headings = ('Note Class', 'Fifth', 'Octave', 'Unison', 'm2', 'M2', 'm3', 'M3', 'P4', 'a4')

rows = []

for i, fifth_batch in enumerate(fifths_batches):
    row = []
    unison = fifth_batch[0]
    octave, step = note_class(unison)
    row.append(str(step))
    row.append(str(i-Fifths_Window))
    row.append(str(octave))
    row.extend([note_name(n) for n in fifth_batch])
    rows.append(row)

table = tabulate(rows, 
                 tablefmt='html',
                 headers=Headings)
display(HTML(table))
Note Class Fifth OctaveUnison m2 M2 m3 M3 P4 a4
6 -6 0F♯/G♭0 G0 G♯/A♭0A0 A♯/B♭0B0 C1
1 -5 1C♯/D♭1 D1 D♯/E♭1E1 F1 F♯/G♭1G1
8 -4 1G♯/A♭1 A1 A♯/B♭1B1 C2 C♯/D♭2D2
3 -3 2D♯/E♭2 E2 F2 F♯/G♭2G2 G♯/A♭2A2
10 -2 2A♯/B♭2 B2 C3 C♯/D♭3D3 D♯/E♭3E3
5 -1 3F3 F♯/G♭3G3 G♯/A♭3A3 A♯/B♭3B3
0 0 4C4 C♯/D♭4D4 D♯/E♭4E4 F4 F♯/G♭4
7 1 4G4 G♯/A♭4A4 A♯/B♭4B4 C5 C♯/D♭5
2 2 5D5 D♯/E♭5E5 F5 F♯/G♭5G5 G♯/A♭5
9 3 5A5 A♯/B♭5B5 C6 C♯/D♭6D6 D♯/E♭6
4 4 6E6 F6 F♯/G♭6G6 G♯/A♭6A6 A♯/B♭6
11 5 6B6 C7 C♯/D♭7D7 D♯/E♭7E7 F7
6 6 7F♯/G♭7 G7 G♯/A♭7A7 A♯/B♭7B7 C8

We obtain the tuning ratio for each tone by either going up or down the circle of fifths to the tone we wish to use, and for each step around the circle we either multiply by 32\frac{3}{2} (going up) or 321=23\frac{3}{2}^{-1} = \frac{2}{3} (going down). We then multiply by an integral power of 2 in order to bring the interval back into the root octave. I’m going to work out some of these and compare them with Wikipedia.

Tone Fifth Octave Shift Ratio Reduced Wikipedia
C 0 0 (21)0(32)0(\frac{2}{1})^0 \cdot (\frac{3}{2})^0 11\frac{1}{1} 11\frac{1}{1}
C♯/D♭ 7 -4 (21)4(32)7(\frac{2}{1})^{-4} \cdot (\frac{3}{2})^7 21872048\frac{2187}{2048} 256243\frac{256}{243} ‼️
D 2 -1 (21)1(32)2(\frac{2}{1})^{-1} \cdot (\frac{3}{2})^2 98\frac{9}{8} 98\frac{9}{8}
D♯/E♭ -3 1 (21)1(32)3(\frac{2}{1})^{1} \cdot (\frac{3}{2})^{-3} 3227\frac{32}{27} 3227\frac{32}{27}
E 4 -2 (21)2(32)4(\frac{2}{1})^{-2} \cdot (\frac{3}{2})^4 8164\frac{81}{64} 8164\frac{81}{64}
F -1 1 (21)1(32)1(\frac{2}{1})^{1} \cdot (\frac{3}{2})^{-1} 43\frac{4}{3} 43\frac{4}{3}

These figures for C♯ differ from Wikipedia, this is because I marched seven fifths around the circle in the positive direction to get to C♯, instead of five fifths back, and this results in the interval being different than the closer one by exactly 7153497664\frac{7153}{497664} or about 1.4%. 21872048\frac{2187}{2048} is a little larger than the semitone and is called the augmented unison.

If I start over…

Tone Interval Fifth Octave Shift Ratio Reduced Wikipedia
C U 0 0 (21)0(32)0(\frac{2}{1})^0 \cdot (\frac{3}{2})^0 11\frac{1}{1} 11\frac{1}{1}
C♯/D♭ m2 -5 3 (21)3(32)5(\frac{2}{1})^{3} \cdot (\frac{3}{2})^{-5} 256243\frac{256}{243} 256243\frac{256}{243}
D M2 2 -1 (21)1(32)2(\frac{2}{1})^{-1} \cdot (\frac{3}{2})^2 98\frac{9}{8} 98\frac{9}{8}
D♯/E♭ m3 -3 1 (21)1(32)3(\frac{2}{1})^{1} \cdot (\frac{3}{2})^{-3} 3227\frac{32}{27} 3227\frac{32}{27}
E M3 4 -2 (21)2(32)4(\frac{2}{1})^{-2} \cdot (\frac{3}{2})^4 8164\frac{81}{64} 8164\frac{81}{64}
F P4 -1 1 (21)1(32)1(\frac{2}{1})^{1} \cdot (\frac{3}{2})^{-1} 43\frac{4}{3} 43\frac{4}{3}
F♯/G♭ a4 6 -3 (21)3(32)6(\frac{2}{1})^{-3} \cdot (\frac{3}{2})^6 729512\frac{729}{512} 729512\frac{729}{512}
G P5 1 0 (21)0(32)1(\frac{2}{1})^{0} \cdot (\frac{3}{2})^{1} 32\frac{3}{2} 32\frac{3}{2}
G♯/A♭ m6 -4 3 (21)3(32)4(\frac{2}{1})^{3} \cdot (\frac{3}{2})^{-4} 12881\frac{128}{81} 12881\frac{128}{81}
A M6 3 -1 (21)1(32)3(\frac{2}{1})^{-1} \cdot (\frac{3}{2})^{3} 2716\frac{27}{16} 2716\frac{27}{16}
A♯/B♭ m7 -2 2 (21)2(32)2(\frac{2}{1})^{2} \cdot (\frac{3}{2})^{-2} 169\frac{16}{9} 169\frac{16}{9}
B M7 5 -2 (21)2(32)5(\frac{2}{1})^{-2} \cdot (\frac{3}{2})^{5} 243128\frac{243}{128} 243128\frac{243}{128}

And now everything is looking better.

Automating the process

I did these charts by hand referring to the Circle of Fifths chart, but is it possible to do this programmatically?

I had the thought that as intervals get wider, the Pythagorean tones might drift further away from the 12-TET ones.

C4=261.63 hertzC_4 = 261.63 \ \text{hertz}

12-TET…

Interval 12-TET Hz Diff
C4–D4 C422/12=293.669745...C_4 \cdot 2^{2/12} = 293.669745... 32.039745
C4–D5 C4214/12=587.339491...C_4 \cdot 2^{14/12} = 587.339491... 325.709491
C4–D6 C4226/12=1174.678982...C_4 \cdot 2^{26/12} = 1174.678982... 913.048982

Pythagorean…

Interval Pythagorean Hz Diff ∂ 12-TET
C4–D4 C498=294.33375C_4 \cdot \frac{9}{8} = 294.33375 32.70375 -3.9 cents
C4–D5 C494=588.6675C_4 \cdot \frac{9}{4} = 588.6675 317.0375 -3.9 cents
C4–D6 C492=1177.335C_4 \cdot \frac{9}{2} = 1177.335 905.705 -3.9 cents