Code Lab: Tuning Systems Calculator
Western music has never settled on a single way to tune its twelve notes. Each tuning system makes a different trade-off between pure intervals and the freedom to change key. In this lab you will build a Python calculator that computes the frequencies for four historically important tuning systems -- equal temperament, Pythagorean, just intonation, and quarter-comma meantone -- then measure the cent differences between them and visualize the deviations on a single chart.
Prerequisites: numpy, matplotlib. Install with pip install numpy matplotlib.
The Cent: A Universal Unit for Pitch Comparison
Before we begin, recall that a cent is 1/1200 of an octave. The formula to convert a frequency ratio to cents is:
$$\text{cents} = 1200 \times \log_2\!\left(\frac{f}{f_{\text{ref}}}\right)$$
Equal temperament spaces its semitones exactly 100 cents apart, so any deviation from 100-cent multiples tells us how a tuning system differs from the modern piano.
import numpy as np
import matplotlib.pyplot as plt
def ratio_to_cents(ratio):
"""Convert a frequency ratio to cents (1200 cents = one octave)."""
return 1200.0 * np.log2(ratio)
NOTE_NAMES = ["C", "C#", "D", "D#", "E", "F",
"F#", "G", "G#", "A", "A#", "B"]
Equal Temperament (12-TET)
The modern standard divides the octave into twelve geometrically equal steps. Every semitone has the ratio 2^(1/12). This system makes every key equally (im)pure, which is why it dominates keyboard instruments.
def equal_temperament(reference_freq=261.63):
"""
Compute 12-tone equal temperament frequencies.
Default reference: C4 = 261.63 Hz (derived from A4 = 440 Hz).
"""
ratios = [2 ** (n / 12.0) for n in range(12)]
freqs = [reference_freq * r for r in ratios]
cents = [ratio_to_cents(r) for r in ratios]
return dict(zip(NOTE_NAMES, zip(freqs, ratios, cents)))
et = equal_temperament()
print("Equal Temperament (reference C4 = 261.63 Hz)")
print(f"{'Note':<5} {'Freq (Hz)':>10} {'Ratio':>10} {'Cents':>8}")
for name in NOTE_NAMES:
freq, ratio, cent = et[name]
print(f"{name:<5} {freq:>10.2f} {ratio:>10.6f} {cent:>8.1f}")
Pythagorean Tuning
Pythagorean tuning builds every interval from stacked perfect fifths (ratio 3:2). Starting from C, each new note is a fifth above the previous one, transposed down by octaves as needed. The resulting fifths are acoustically pure, but the major thirds are noticeably sharp (408 cents instead of the pure 386 cents).
def pythagorean(reference_freq=261.63):
"""
Compute Pythagorean tuning by stacking pure fifths (3:2).
Circle of fifths order: F-C-G-D-A-E-B-F#-C#-G#-D#-A#
"""
# Number of fifths above C for each note in the circle
fifth_counts = {
"C": 0, "G": 1, "D": 2, "A": 3, "E": 4, "B": 5,
"F#": 6, "C#": 7, "G#": 8, "D#": 9, "A#": 10, "F": -1,
}
results = {}
for name in NOTE_NAMES:
n_fifths = fifth_counts[name]
# Ratio = (3/2)^n_fifths, reduced into a single octave
raw_ratio = (3.0 / 2.0) ** n_fifths
# Bring into the range [1, 2) by dividing by powers of 2
ratio = raw_ratio / (2 ** int(np.log2(raw_ratio))) if raw_ratio >= 2 else raw_ratio
if ratio < 1.0:
ratio *= 2.0
freq = reference_freq * ratio
cents = ratio_to_cents(ratio)
results[name] = (freq, ratio, cents)
return results
pyth = pythagorean()
print("\nPythagorean Tuning")
print(f"{'Note':<5} {'Freq (Hz)':>10} {'Ratio':>10} {'Cents':>8}")
for name in NOTE_NAMES:
freq, ratio, cent = pyth[name]
print(f"{name:<5} {freq:>10.2f} {ratio:>10.6f} {cent:>8.1f}")
Just Intonation (5-limit)
Just intonation tunes each interval to the simplest possible whole-number ratio involving the primes 2, 3, and 5. Major thirds become a pure 5:4 and minor thirds a pure 6:5. The trade-off: the tuning is locked to one key. Modulating to a distant key produces "wolf" intervals that sound painfully out of tune.
def just_intonation(reference_freq=261.63):
"""
5-limit just intonation ratios relative to the tonic (C major).
These are the standard textbook ratios for a major-mode JI scale.
"""
ji_ratios = {
"C": 1/1,
"C#": 16/15,
"D": 9/8,
"D#": 6/5,
"E": 5/4,
"F": 4/3,
"F#": 45/32,
"G": 3/2,
"G#": 8/5,
"A": 5/3,
"A#": 9/5,
"B": 15/8,
}
results = {}
for name in NOTE_NAMES:
ratio = ji_ratios[name]
freq = reference_freq * ratio
cents = ratio_to_cents(ratio)
results[name] = (freq, ratio, cents)
return results
ji = just_intonation()
print("\nJust Intonation (5-limit, C major)")
print(f"{'Note':<5} {'Freq (Hz)':>10} {'Ratio':>12} {'Cents':>8}")
for name in NOTE_NAMES:
freq, ratio, cent = ji[name]
print(f"{name:<5} {freq:>10.2f} {ratio:>12.6f} {cent:>8.1f}")
Quarter-Comma Meantone
Meantone temperament, dominant in European music from roughly 1500 to 1750, narrows each fifth by a quarter of the syntonic comma so that major thirds come out pure (5:4). The fifths are slightly flat (696.6 cents instead of 702), but the thirds are beautiful. The system breaks down at the "wolf fifth" (G# to D#), which absorbs all the accumulated error.
def quarter_comma_meantone(reference_freq=261.63):
"""
Quarter-comma meantone: each fifth is narrowed by 1/4 of the
syntonic comma (81/80). Fifth ratio = 5^(1/4) ≈ 1.49535.
"""
meantone_fifth = 5 ** 0.25 # ≈ 1.49535 (compared to pure 1.5)
# Same circle-of-fifths construction as Pythagorean
fifth_counts = {
"C": 0, "G": 1, "D": 2, "A": 3, "E": 4, "B": 5,
"F#": 6, "C#": 7, "G#": 8, "D#": 9, "A#": 10, "F": -1,
}
results = {}
for name in NOTE_NAMES:
n = fifth_counts[name]
raw_ratio = meantone_fifth ** n
# Reduce to one octave
while raw_ratio >= 2.0:
raw_ratio /= 2.0
while raw_ratio < 1.0:
raw_ratio *= 2.0
freq = reference_freq * raw_ratio
cents = ratio_to_cents(raw_ratio)
results[name] = (freq, raw_ratio, cents)
return results
mt = quarter_comma_meantone()
print("\nQuarter-Comma Meantone")
print(f"{'Note':<5} {'Freq (Hz)':>10} {'Ratio':>10} {'Cents':>8}")
for name in NOTE_NAMES:
freq, ratio, cent = mt[name]
print(f"{name:<5} {freq:>10.2f} {ratio:>10.6f} {cent:>8.1f}")
Comparing the Four Systems: Cent Deviations from Equal Temperament
The most revealing comparison uses equal temperament as the zero baseline and plots how many cents each system deviates at every chromatic pitch.
def deviation_from_et(system, et_system):
"""Return cent deviations of *system* relative to *et_system*."""
devs = {}
for name in NOTE_NAMES:
et_cents = et_system[name][2]
sys_cents = system[name][2]
devs[name] = sys_cents - et_cents
return devs
dev_pyth = deviation_from_et(pyth, et)
dev_ji = deviation_from_et(ji, et)
dev_mt = deviation_from_et(mt, et)
# ---- Visualization ----
x = np.arange(len(NOTE_NAMES))
width = 0.25
fig, ax = plt.subplots(figsize=(13, 6))
bars1 = ax.bar(x - width, [dev_pyth[n] for n in NOTE_NAMES],
width, label="Pythagorean", color="#2196F3", alpha=0.85)
bars2 = ax.bar(x, [dev_ji[n] for n in NOTE_NAMES],
width, label="Just Intonation", color="#FF5722", alpha=0.85)
bars3 = ax.bar(x + width, [dev_mt[n] for n in NOTE_NAMES],
width, label="Quarter-Comma Meantone", color="#4CAF50", alpha=0.85)
ax.axhline(0, color="black", linewidth=0.8, linestyle="-")
ax.set_xticks(x)
ax.set_xticklabels(NOTE_NAMES, fontsize=11)
ax.set_xlabel("Note", fontsize=12)
ax.set_ylabel("Deviation from Equal Temperament (cents)", fontsize=12)
ax.set_title("Tuning System Deviations from 12-TET\n"
"Positive = sharper than ET; Negative = flatter than ET",
fontsize=14, weight="bold")
ax.legend(fontsize=10)
ax.grid(True, axis="y", alpha=0.3)
plt.tight_layout()
plt.show()
What to notice:
- Pythagorean tuning is very close to equal temperament for fifths and fourths (G, D, A, F) but diverges sharply at the thirds (E, G#, B).
- Just intonation nails the intervals that matter most in C major (E is 14 cents flat of ET, giving a pure 5:4 major third) but the remote notes (F#, C#) drift considerably.
- Quarter-comma meantone compromises: its thirds are pure and its fifths are only 3.4 cents flat, but the accumulated error at G# and D# creates the notorious wolf interval.
Printing a Summary Table
print(f"\n{'Note':<5} {'ET':>8} {'Pyth':>8} {'JI':>8} {'MT':>8} "
f"{'Pyth-ET':>8} {'JI-ET':>8} {'MT-ET':>8}")
print("-" * 72)
for name in NOTE_NAMES:
et_c = et[name][2]
pyth_c = pyth[name][2]
ji_c = ji[name][2]
mt_c = mt[name][2]
print(f"{name:<5} {et_c:>8.1f} {pyth_c:>8.1f} {ji_c:>8.1f} {mt_c:>8.1f} "
f"{dev_pyth[name]:>+8.1f} {dev_ji[name]:>+8.1f} {dev_mt[name]:>+8.1f}")
Try It Yourself
-
The Pythagorean comma. If you stack twelve pure fifths (3:2) you should return to the same note seven octaves higher, but you overshoot by the Pythagorean comma. Calculate
(3/2)**12 / 2**7and convert the result to cents. How large is this comma, and why does it make Pythagorean tuning impossible to use in all twelve keys? -
Just intonation in a different key. Rewrite the
just_intonationfunction so that the pure ratios are centered on G major instead of C major (hint: the G-major JI scale uses the same ratio set but shifted so that G = 1/1). Plot the cent deviations against equal temperament and compare with the C-major version. Which notes move the most? -
Historical listening test. Using the frequency values from each tuning system, generate a C-major triad (C-E-G) as a sum of three sine waves at 0.5 seconds each. Use
numpyto create the audio arrays andmatplotlibto plot the combined waveforms. Compare the beat patterns: the just-intonation triad should show a smooth, steady envelope (no beats), while equal temperament should exhibit slow amplitude fluctuations caused by the slightly mistuned third. Calculate the expected beat rate from the frequency difference between the ET and JI versions of E.