Created
September 26, 2026 22:59
-
-
Save ndbroadbent/363c72ca950bed4d2ce28f82e5b297c8 to your computer and use it in GitHub Desktop.
Track composed by opus 5.5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """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