Pachelbel's Canon in D
音樂可以用代碼來表示嗎? — Can music be expressed in code?
♩ Canon in D Major
This is Pachelbel's Canon in D — one of the most recognisable pieces in Western classical music — rendered entirely in the browser using the Web Audio API with no audio files or external libraries.
The entire melody is encoded in a single base-36 string: each character maps to a note value. Below is the same idea expressed in Python — it turns each note into a sine wave and mixes them into an audio buffer at the right time offsets, then writes the whole canon to a WAV file using only the standard library. (The live player above still runs in JavaScript via the Web Audio API.)
碼農愛音樂的最高目標就是讀懂《Gödel, Escher, Bach》了。這段代碼正是這本書精神的縮影:用最簡單的規則,生成無限的美。
The Code — in Python
# Pachelbel's Canon in D — rendered to a WAV file (standard library only)
import math
import struct
import wave
# ── The melody, packed into one base-36 string ──────────────────────────────
# Each character is a single base-36 digit read with int(c, 36):
# '0'-'9' -> 0..9, 'a'-'z' -> 10..35.
# That value n encodes BOTH the pitch and the length of one note:
#
# pitch : (n - 1) % 20 = how many semitones BELOW A5 (880 Hz) to sound,
# so freq = 880 * 2 ** (-((n - 1) % 20) / 12).
# e.g. n = 1 or 21 -> A5 (880 Hz); n = 8 -> D5; n = 13 -> A4.
# duration : n > 20 -> a quarter note (Q); otherwise an eighth note (Q / 2).
# '0' : sentinel that marks the end of the melody.
#
# So the opening "l43 l43 1..." reads A(quarter) F# G | A(quarter) F# G | A F# D...
# The 59 notes run as a melody; the last three share a start time, ringing
# together as the closing D-major chord (A5 + F#5 + D5).
PATTERN = "l43l431db98643o86ogfdbdfdgfdzbdzgigikigfdbzbdv98db9864311480"
SAMPLE_RATE = 44100
Q = 6 / 13 # quarter-note length, in seconds
def render(path="canon.wav"):
buffer = {} # sample index -> summed amplitude
t = 0.0 # start time of the current note
i = 0
while True:
n = int(PATTERN[i], 36) # base-36 digit -> note value
if not n: # '0' marks the end of the melody
break
i += 1
freq = 880 * 2 ** (-((n - 1) % 20) / 12)
dur = Q * 8 if i > 56 else (Q if n > 20 else Q / 2)
start = int(t * SAMPLE_RATE)
for k in range(int(dur * SAMPLE_RATE)):
j = start + k
buffer[j] = buffer.get(j, 0.0) + math.sin(2 * math.pi * freq * k / SAMPLE_RATE)
if i <= 56: # final chord rings together; melody advances
t += dur
frames = [buffer.get(s, 0.0) for s in range(max(buffer) + 1)]
peak = max(abs(v) for v in frames) or 1.0
with wave.open(path, "w") as wav:
wav.setnchannels(1)
wav.setsampwidth(2)
wav.setframerate(SAMPLE_RATE)
wav.writeframes(b"".join(
struct.pack("<h", int(v / peak * 0.8 * 32767)) for v in frames))The full runnable script lives in canon.py — run python3 canon.py to generate canon.wav.