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 NameHz
60C4261.626
61C♯/D♭4277.183
62D4293.665
63D♯/E♭4311.127
64E4329.628
65F4349.228
66F♯/G♭4369.994
67G4391.995
68G♯/A♭4415.305
69A4440
70A♯/B♭4466.164
71B4493.883
72C5523.251
73C♯/D♭5554.365
74D5587.33
75D♯/E♭5622.254
76E5659.255
77F5698.457
78F♯/G♭5739.989
79G5783.991
80G♯/A♭5830.609
81A5880

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:

Um2M2m3M3P4a4P5m6M6m7M7
CC#DD#EFF#GG#AA#B

Ratios to unison for intervals:

IntervalRatioHz (over 440)12-TET Hz
P443\frac{4}{3} 586.6664402512440 \cdot 2^{\frac{5}{12}} = 587.329
P532\frac{3}{2} 660.6664402612440 \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 ClassFifthOctaveUnisonm2M2m3M3P4a4
6-60F♯/G♭0G0G♯/A♭0A0A♯/B♭0B0C1
1-51C♯/D♭1D1D♯/E♭1E1F1F♯/G♭1G1
8-41G♯/A♭1A1A♯/B♭1B1C2C♯/D♭2D2
3-32D♯/E♭2E2F2F♯/G♭2G2G♯/A♭2A2
10-22A♯/B♭2B2C3C♯/D♭3D3D♯/E♭3E3
5-13F3F♯/G♭3G3G♯/A♭3A3A♯/B♭3B3
004C4C♯/D♭4D4D♯/E♭4E4F4F♯/G♭4
714G4G♯/A♭4A4A♯/B♭4B4C5C♯/D♭5
225D5D♯/E♭5E5F5F♯/G♭5G5G♯/A♭5
935A5A♯/B♭5B5C6C♯/D♭6D6D♯/E♭6
446E6F6F♯/G♭6G6G♯/A♭6A6A♯/B♭6
1156B6C7C♯/D♭7D7D♯/E♭7E7F7
667F♯/G♭7G7G♯/A♭7A7A♯/B♭7B7C8

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.

ToneFifthOctave ShiftRatioReducedWikipedia
C00(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} ‼️
D2-1(21)1(32)2(\frac{2}{1})^{-1} \cdot (\frac{3}{2})^2 98\frac{9}{8} 98\frac{9}{8}
D♯/E♭-31(21)1(32)3(\frac{2}{1})^{1} \cdot (\frac{3}{2})^{-3} 3227\frac{32}{27} 3227\frac{32}{27}
E4-2(21)2(32)4(\frac{2}{1})^{-2} \cdot (\frac{3}{2})^4 8164\frac{81}{64} 8164\frac{81}{64}
F-11(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…

ToneIntervalFifthOctave ShiftRatioReducedWikipedia
CU00(21)0(32)0(\frac{2}{1})^0 \cdot (\frac{3}{2})^011\frac{1}{1}11\frac{1}{1}
C♯/D♭m2-53(21)3(32)5(\frac{2}{1})^{3} \cdot (\frac{3}{2})^{-5}256243\frac{256}{243}256243\frac{256}{243}
DM22-1(21)1(32)2(\frac{2}{1})^{-1} \cdot (\frac{3}{2})^298\frac{9}{8}98\frac{9}{8}
D♯/E♭m3-31(21)1(32)3(\frac{2}{1})^{1} \cdot (\frac{3}{2})^{-3}3227\frac{32}{27}3227\frac{32}{27}
EM34-2(21)2(32)4(\frac{2}{1})^{-2} \cdot (\frac{3}{2})^48164\frac{81}{64}8164\frac{81}{64}
FP4-11(21)1(32)1(\frac{2}{1})^{1} \cdot (\frac{3}{2})^{-1}43\frac{4}{3}43\frac{4}{3}
F♯/G♭a46-3(21)3(32)6(\frac{2}{1})^{-3} \cdot (\frac{3}{2})^6729512\frac{729}{512}729512\frac{729}{512}
GP510(21)0(32)1(\frac{2}{1})^{0} \cdot (\frac{3}{2})^{1}32\frac{3}{2}32\frac{3}{2}
G♯/A♭m6-43(21)3(32)4(\frac{2}{1})^{3} \cdot (\frac{3}{2})^{-4}12881\frac{128}{81}12881\frac{128}{81}
AM63-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-22(21)2(32)2(\frac{2}{1})^{2} \cdot (\frac{3}{2})^{-2}169\frac{16}{9}169\frac{16}{9}
BM75-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…

Interval12-TET HzDiff
C4–D4C422/12=293.669745...C_4 \cdot 2^{2/12} = 293.669745... 32.039745
C4–D5C4214/12=587.339491...C_4 \cdot 2^{14/12} = 587.339491... 325.709491
C4–D6C4226/12=1174.678982...C_4 \cdot 2^{26/12} = 1174.678982... 913.048982

Pythagorean…

IntervalPythagorean HzDiff∂ 12-TET
C4–D4C498=294.33375C_4 \cdot \frac{9}{8} = 294.33375 32.70375-3.9 cents
C4–D5C494=588.6675C_4 \cdot \frac{9}{4} = 588.6675 317.0375-3.9 cents
C4–D6C492=1177.335C_4 \cdot \frac{9}{2} = 1177.335 905.705-3.9 cents