Pachelbel's Canon in D

音樂可以用代碼來表示嗎? — Can music be expressed in code?

♩ Canon in D Major

Johann Pachelbel · Web Audio API · ~30 seconds

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.