Skip to content

Instantly share code, notes, and snippets.

@ndbroadbent
Created September 26, 2026 22:59
Show Gist options
  • Select an option

  • Save ndbroadbent/363c72ca950bed4d2ce28f82e5b297c8 to your computer and use it in GitHub Desktop.

Select an option

Save ndbroadbent/363c72ca950bed4d2ce28f82e5b297c8 to your computer and use it in GitHub Desktop.
Track composed by opus 5.5
"""Original score + sound design for the DocSpring reel.
Modern/minimal: deep short kick, swung hats + shaker, sparse wood clicks,
rolling sine sub, FM electric-piano chord stabs, a sparse bell motif through
ping-pong delay, soft swells instead of risers. Everything is synthesized from
scratch (no samples), and every hit lines up with src/timeline.json, which the
animation code reads too.
python3 music/compose.py -> public/audio/reel.wav
"""
import json
import os
import wave
import numpy as np
from scipy.signal import butter, fftconvolve, sosfilt
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
TL = json.load(open(os.path.join(ROOT, "src", "timeline.json")))
SR = 48000
BPM = TL["bpm"]
BEAT = 60.0 / BPM
TOTAL_BEATS = TL["totalBeats"]
N = int(SR * TOTAL_BEATS * BEAT)
S = TL["scenes"]
X = TL["sfx"]
rng = np.random.default_rng(11)
# ---------------------------------------------------------------- utilities
def t_of(beat):
return int(round(beat * BEAT * SR))
def midi_hz(m):
return 440.0 * 2 ** ((m - 69) / 12)
def buf():
return np.zeros((N, 2))
def place(bus, sig, beat, gain=1.0, pan=0.0):
start = t_of(beat)
if start >= N or start < 0:
return
if sig.ndim == 1:
l = np.cos((pan + 1) * np.pi / 4) * 1.414
r = np.sin((pan + 1) * np.pi / 4) * 1.414
sig = np.stack([sig * l, sig * r], axis=1)
end = min(N, start + len(sig))
bus[start:end] += sig[: end - start] * gain
def env(n, tau):
return np.exp(-np.arange(n) / (tau * SR))
def lp(x, f, order=2):
return sosfilt(butter(order, min(f, SR / 2 - 100) / (SR / 2), "low", output="sos"), x, axis=0)
def hp(x, f, order=2):
return sosfilt(butter(order, f / (SR / 2), "high", output="sos"), x, axis=0)
def bp(x, lo, hi, order=2):
return sosfilt(butter(order, [lo / (SR / 2), hi / (SR / 2)], "band", output="sos"), x, axis=0)
def in_scene(beat, *names):
return any(S[n][0] <= beat < S[n][1] for n in names)
def ping_pong(x, delay_beats=0.75, fb=0.45, taps=6, tone=3500):
"""Stereo ping-pong delay (dotted-8th by default), darker with each repeat."""
if x.ndim == 1:
x = np.stack([x, x], axis=1)
d = t_of(delay_beats)
out = x.copy()
tap = x.mean(axis=1)
for k in range(1, taps + 1):
tap = lp(tap, tone)
shifted = np.zeros(len(x))
if d * k < len(x):
shifted[d * k :] = tap[: len(x) - d * k]
ch = k % 2
out[:, ch] += shifted * fb**k
return out
def reverb(x, seconds=2.4, wet=0.3, predelay=0.025, tone=5500):
n = int(SR * seconds)
t = np.arange(n) / SR
ir = rng.standard_normal((n, 2)) * np.exp(-t / (seconds / 6.9))[:, None]
ir = lp(ir, tone)
ir[: int(predelay * SR)] = 0
ir /= np.sqrt((ir**2).sum(axis=0))
y = np.stack([fftconvolve(x[:, c], ir[:, c])[: len(x)] for c in range(2)], axis=1)
return x * (1 - wet) + y * wet * 1.5
# ---------------------------------------------------------------- instruments
def kick(big=False):
n = int(SR * (0.7 if big else 0.32))
t = np.arange(n) / SR
f = 44 + 95 * np.exp(-t / 0.022)
body = np.sin(2 * np.pi * np.cumsum(f) / SR) * env(n, 0.26 if big else 0.13)
click = bp(rng.standard_normal(n), 1500, 6000) * env(n, 0.0025) * 0.15
return np.tanh((body + click) * 1.3)
def snap():
"""Tight modern clap/snap: short noise bursts + small room."""
n = int(SR * 0.22)
x = bp(rng.standard_normal(n), 1400, 7000)
e = np.zeros(n)
for off, tau in [(0, 0.004), (0.008, 0.004), (0.016, 0.05)]:
s = int(off * SR)
e[s:] += env(n - s, tau)
return x * e * 0.4
def rim():
n = int(SR * 0.05)
t = np.arange(n) / SR
x = np.sin(2 * np.pi * 1700 * t) * env(n, 0.008) + bp(rng.standard_normal(n), 2000, 6000) * env(n, 0.003) * 0.4
return x * 0.35
def hat(vel=1.0):
n = int(SR * 0.04)
x = hp(rng.standard_normal(n), 9000, 4)
return x * env(n, 0.009) * 0.12 * vel
def shaker(vel=1.0):
n = int(SR * 0.07)
x = bp(rng.standard_normal(n), 5000, 11000)
a = np.minimum(1, np.arange(n) / (0.012 * SR))
return x * a * env(n, 0.02) * 0.07 * vel
def wood(pitch=1100):
n = int(SR * 0.06)
t = np.arange(n) / SR
return (np.sin(2 * np.pi * pitch * t) * 0.8 + np.sin(2 * np.pi * pitch * 2.76 * t) * 0.2) * env(n, 0.012) * 0.3
def sub(m, dur):
n = int(SR * dur)
t = np.arange(n) / SR
f = midi_hz(m)
x = np.sin(2 * np.pi * f * t) + 0.12 * np.sin(2 * np.pi * f * 2 * t)
a = np.minimum(1, t / 0.006) * np.minimum(1, (dur - t) / 0.02)
return np.tanh(x * 1.3) * a * 0.5
def ep(notes, dur, bright=1.0, decay=1.4):
"""FM electric piano: 1:1 modulator with decaying index + a soft tine."""
n = int(SR * dur)
t = np.arange(n) / SR
L = np.zeros(n)
R = np.zeros(n)
for k, m in enumerate(notes):
f = midi_hz(m)
idx = (0.4 + 2.2 * bright * np.exp(-t / 0.18))
for ch, det in ((0, 0.9993), (1, 1.0007)):
mod = np.sin(2 * np.pi * f * det * t) * idx
car = np.sin(2 * np.pi * f * det * t + mod)
tine = np.sin(2 * np.pi * f * 14 * t) * np.exp(-t / 0.012) * 0.08 * bright
(L if ch == 0 else R)[:] += car + tine
a = np.minimum(1, t / 0.004) * env(n, decay) * np.minimum(1, (dur - t) / 0.08)
x = np.stack([L * a, R * a], axis=1) / len(notes)
return lp(x, 6500) * 0.55
def pad(notes, dur, cutoff=900):
n = int(SR * dur)
t = np.arange(n) / SR
L = np.zeros(n)
R = np.zeros(n)
for m in notes:
f = midi_hz(m)
for det, ch in ((0.996, 0), (1.004, 1), (1.0, 0), (1.0, 1)):
ph = rng.random()
s = 2 * ((t * f * det + ph) % 1.0) - 1
(L if ch == 0 else R)[:] += s
x = lp(np.stack([L, R], axis=1) / (len(notes) * 2), cutoff, 2)
a = np.minimum(1, t / (dur * 0.45)) * np.minimum(1, (dur - t) / 0.6)
return x * a[:, None] * 0.45
def bell(m, dur=0.9):
n = int(SR * dur)
t = np.arange(n) / SR
f = midi_hz(m)
mod = np.sin(2 * np.pi * f * 3.5 * t) * 1.2 * np.exp(-t / 0.08)
x = np.sin(2 * np.pi * f * t + mod) * env(n, 0.25)
return x * 0.22
def swell(beats, lo=300, hi=6000):
"""Soft filtered-noise swell (reverse-reverb feel) into a section."""
n = t_of(beats)
noise = rng.standard_normal((n, 2))
x = bp(noise, lo, hi)
t = np.linspace(0, 1, n)
return x * (t**3)[:, None] * 0.22
def sub_drop(dur=1.6):
n = int(SR * dur)
t = np.arange(n) / SR
f = 26 + 38 * np.exp(-t / 0.25)
x = np.sin(2 * np.pi * np.cumsum(f) / SR) * env(n, 0.55)
air = lp(bp(rng.standard_normal(n), 200, 3000) * env(n, 0.3), 2000) * 0.15
return np.tanh(x * 1.2 + air) * 0.8
def whoosh(dur=0.45):
n = int(SR * dur)
x = rng.standard_normal((n, 2))
t = np.linspace(0, 1, n)
out = np.zeros_like(x)
block = 480
center = 500 + 4000 * np.sin(np.pi * t) ** 2
for i in range(0, n, block):
c = center[i]
out[i : i + block] = bp(x[i : i + block], c * 0.6, min(SR / 2 - 200, c * 1.5), 1)
amp = np.sin(np.pi * t) ** 2
pan = np.stack([1 - t * 0.7, 0.3 + t * 0.7], axis=1)
return out * amp[:, None] * pan * 0.28
def click(pitch=2400, dur=0.02, gain=0.18):
n = int(SR * dur)
t = np.arange(n) / SR
return np.sin(2 * np.pi * pitch * t) * env(n, dur / 4) * gain
def tock(m):
"""Soft UI 'placement' sound: pitched knock + tiny noise."""
n = int(SR * 0.09)
t = np.arange(n) / SR
f = midi_hz(m) * (1 + 0.25 * np.exp(-t / 0.006))
x = np.sin(2 * np.pi * np.cumsum(f) / SR) * env(n, 0.03)
x += hp(rng.standard_normal(n), 4000) * env(n, 0.002) * 0.2
return x * 0.22
def key_tick():
n = int(SR * 0.025)
return hp(rng.standard_normal(n), 2500) * env(n, 0.003) * 0.12
def scribble(dur=0.9):
n = int(SR * dur)
x = bp(rng.standard_normal(n), 2500, 7000)
t = np.arange(n) / SR
am = 0.5 + 0.5 * np.sin(2 * np.pi * 8 * t + 3 * np.sin(2 * np.pi * 2.1 * t))
return x * am * 0.07 * np.minimum(1, (dur - t) / 0.1)
# ---------------------------------------------------------------- harmony
F2 = 41
# minimal two-chord world with a lift in the features section
CHORDS = {
"Fm9": [53, 56, 60, 63, 67], # F3 Ab3 C4 Eb4 G4
"Dbmaj9": [49, 53, 56, 60, 63], # Db3 F3 Ab3 C4 Eb4
"Bbm9": [46, 53, 56, 60, 61], # Bb2 F3 Ab3 C4 Db4
"Ebsus": [51, 56, 58, 63, 65], # Eb3 Ab3 Bb3 Eb4 F4
}
ROOTS = {"Fm9": F2 - 12, "Dbmaj9": F2 - 16, "Bbm9": F2 - 7 - 12, "Ebsus": F2 - 2 - 12}
def chord_at(beat):
if in_scene(beat, "features"):
return ["Fm9", "Dbmaj9", "Bbm9", "Ebsus"][int((beat - S["features"][0]) // 4) % 4]
return ["Fm9", "Fm9", "Dbmaj9", "Dbmaj9"][int(beat // 4) % 4]
# ---------------------------------------------------------------- arrangement
drums = buf()
bass = buf()
keys = buf()
motif = buf()
fx = buf()
kick_times = []
GROOVE = ("drop", "editor", "api", "scale", "features")
GAPS = {7.5, 7.75, 47.5, 47.75}
SWING = 0.035 # beats of swing on the off-16ths
for b16 in range(TOTAL_BEATS * 4):
beat = b16 / 4
pos = b16 % 16
api_break = S["api"][0] <= beat < 32
gap = beat in GAPS
swing = SWING if pos % 2 == 1 else 0.0
ch = chord_at(beat)
if in_scene(beat, *GROOVE) and not gap:
if pos % 4 == 0 and not api_break:
kick_times.append(beat)
place(drums, kick(big=beat in X["impact"]), beat)
if pos % 8 == 4 and not api_break:
place(drums, snap() if not in_scene(beat, "drop") else rim(), beat, 0.9)
# hats: 16ths, accented off-beats, swung
place(drums, hat(1.0 if pos % 4 == 2 else 0.45), beat + swing, 1.0, pan=0.25)
if not in_scene(beat, "drop"):
place(drums, shaker(0.9 if pos % 2 else 0.5), beat + swing, 1.0, pan=-0.3)
# sparse wood clicks: a syncopated minimal figure
if pos in (3, 6, 11) and not api_break:
place(drums, wood(1100 if pos != 6 else 820), beat + swing, 0.8, pan=0.5 if pos == 6 else -0.4)
# rolling sub: off-beat 8ths, fifth on the last 16th of every other bar
if not api_break:
root = ROOTS[ch]
if pos % 4 == 2:
place(bass, sub(root, BEAT * 0.42), beat)
if pos == 15 and int(beat // 4) % 2 == 1:
place(bass, sub(root + 7, BEAT * 0.2), beat)
# intro: shaker + clicks come in at bar 1, like a clock
if in_scene(beat, "intro") and beat >= 4 and not gap:
if pos % 2 == 1:
place(drums, shaker(0.3 + 0.5 * (beat - 4) / 4), beat + swing, 1.0, pan=0.2)
if pos % 4 == 0:
place(drums, wood(1400), beat, 0.35)
# chords: syncopated EP stabs in the groove, sustained in intro/logo
for bar in range(TOTAL_BEATS // 4):
b0 = bar * 4
ch = chord_at(b0)
if in_scene(b0, "intro"):
place(keys, pad(CHORDS[ch], 4 * BEAT + 0.4), b0, 0.9)
place(keys, ep(CHORDS[ch], 4 * BEAT, bright=0.5, decay=2.0), b0, 0.5 if b0 == 0 else 0.35)
elif in_scene(b0, *GROOVE):
api_break = S["api"][0] <= b0 < 32
for off, g, br in ((0, 0.8, 1.0), (1.5, 0.55, 0.7), (2.75, 0.4, 0.6)):
if api_break and off > 0:
continue
place(keys, ep(CHORDS[ch], BEAT * 1.4, bright=br, decay=0.5), b0 + off, g)
place(keys, pad(CHORDS[ch], 4 * BEAT, cutoff=700), b0, 0.35)
# sparse bell motif, only in editor + features, through ping-pong delay
MOTIF = [(0, 72), (0.75, 75), (2.5, 79)] # C5 Eb5 G5
for b0 in list(range(S["editor"][0], S["editor"][1], 8)) + list(range(S["features"][0], S["features"][1], 4)):
for off, m in MOTIF:
place(motif, bell(m), b0 + 2 + off, 0.8)
# logo finale: warm sustained chord, sub, and one final hit
L0 = S["logo"][0]
place(keys, pad(CHORDS["Fm9"], 8 * BEAT, cutoff=1400), L0, 1.0)
place(keys, ep(CHORDS["Fm9"], 4 * BEAT, bright=0.9, decay=2.2), L0, 0.8)
place(keys, ep([c + 12 for c in CHORDS["Dbmaj9"]], 4 * BEAT, bright=0.6, decay=2.5), X["stinger"][0], 0.6)
place(bass, sub(ROOTS["Fm9"], 3.5 * BEAT), L0)
place(bass, sub(ROOTS["Dbmaj9"], 3.8 * BEAT), X["stinger"][0])
place(drums, kick(big=True), L0)
place(drums, kick(big=True), X["stinger"][0])
place(drums, snap(), X["stinger"][0], 0.9)
for i, m in enumerate([84, 79, 75, 72]):
place(motif, bell(m, 1.4), X["stinger"][0] + 0.5 + i * 0.5, 0.7)
# ---------------------------------------------------------------- sound design
for b in X["whoosh"]:
place(fx, whoosh(), b - 0.05, 0.9)
place(fx, swell(3.5, 400, 5000), 4.0, 1.0)
place(fx, swell(2.5, 300, 4000), 29.5, 0.7)
place(fx, swell(3.0, 500, 7000), 44.5, 1.0)
place(fx, swell(1.0, 800, 8000), 63.0, 0.9)
for b in X["impact"]:
place(fx, sub_drop(), b, 0.9 if b in (8, 64) else 0.6)
for i, b in enumerate(X["toolbarPops"]):
place(fx, click(2600 + (i % 3) * 300), b, 0.9, pan=-0.5 + i * 0.12)
for i, b in enumerate(X["fieldPops"]):
place(fx, tock([72, 75, 79, 80, 84, 79, 75, 72][i]), b, 1.0, pan=(-1) ** i * 0.25)
ty = X["typing"]
for b in np.arange(ty["from"], ty["to"] + 1e-6, ty["step"]):
for jitter in (0, 0.1):
if rng.random() < 0.85:
place(fx, key_tick(), b + jitter, 0.8 + rng.random() * 0.4, pan=rng.uniform(-0.3, 0.3))
for i, b in enumerate(X["dataLands"]):
place(fx, tock([79, 80, 84, 87, 84][i]), b, 1.0, pan=0.4)
for b in X["scribble"] + X["signature"]:
place(fx, scribble(), b, 1.0, pan=0.3)
for i, m in enumerate([84, 87, 91]):
place(fx, bell(m, 1.2), X["chime"][0] + i * 0.08, 0.7)
for i, b in enumerate(X["ticks"]):
place(fx, click(3000 + i * 60, 0.015, 0.14), b, 1.0, pan=0.5 * np.sin(i))
for b in X["langPops"] + X["integrationPops"]:
place(fx, click(2200 + rng.integers(0, 4) * 250, 0.03, 0.16), b, 1.0, pan=rng.uniform(-0.5, 0.5))
for i, b in enumerate(X["badgePops"]):
place(fx, tock([72, 75, 79][i]), b, 1.0)
for i, b in enumerate(X["logoBlips"]):
place(fx, tock([72, 75, 79][i]), b, 1.1)
place(fx, bell(91, 1.6), X["shimmer"][0], 0.5)
# ---------------------------------------------------------------- mix
duck = np.ones(N)
for kb in kick_times:
s = t_of(kb)
L = t_of(1)
e = 1 - 0.55 * np.exp(-np.arange(L) / (0.1 * SR))
seg = duck[s : s + L]
duck[s : s + L] = np.minimum(seg, e[: len(seg)])
keys *= duck[:, None]
bass *= (duck**0.7)[:, None]
motif = ping_pong(motif, 0.75, 0.42)
motif = reverb(motif, 3.2, 0.45)
keys = reverb(keys, 2.8, 0.3)
fx = reverb(fx, 1.8, 0.2)
drums = reverb(drums, 0.7, 0.07, tone=7000)
# gentle tape hiss bed for air
hiss = lp(hp(rng.standard_normal((N, 2)), 3000), 9000) * 0.004
mix = drums * 1.0 + bass * 0.85 + keys * 1.15 + motif * 0.6 + fx * 0.9 + hiss
mix = hp(mix, 22)
peak = np.max(np.abs(mix))
mix = np.tanh(mix / peak * 1.5) / np.tanh(1.5)
fade_start = t_of(TOTAL_BEATS - 1.5)
mix[fade_start:] *= np.linspace(1, 0, N - fade_start)[:, None] ** 1.5
mix *= 0.89 / np.max(np.abs(mix))
os.makedirs(os.path.join(ROOT, "public", "audio"), exist_ok=True)
out = os.path.join(ROOT, "public", "audio", "reel.wav")
with wave.open(out, "wb") as w:
w.setnchannels(2)
w.setsampwidth(2)
w.setframerate(SR)
w.writeframes((mix * 32767).astype("<i2").tobytes())
print(f"wrote {out} {N / SR:.2f}s")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment