Files
homarr/tools/motion-reel/score.py
T
+12 fcd7c979df feat!: release v2 (#6545)
Co-authored-by: Artur <145776561+arturict@users.noreply.github.com>
Co-authored-by: arturict <arturict@users.noreply.github.com>
Co-authored-by: Thomas <thomas@ajnart.fr>
Co-authored-by: Afonso Jorge Ramos <afonsojorgeramos@gmail.com>
Co-authored-by: Danura30082 <danura30082+github@gmail.com>
Co-authored-by: Kushie <122986913+kushiemoon-dev@users.noreply.github.com>
Co-authored-by: ajnart <thomas@ajnart.fr>
Co-authored-by: ajnart <thomas@ajnart.dev>
Co-authored-by: chenjun <chenjun@foxmail.com>
Co-authored-by: Wikibear <wikibear@users.noreply.github.com>
Co-authored-by: Bergasha <bergasha@gmail.com>
Co-authored-by: leafyelin <43306196+leafyelin@users.noreply.github.com>
Co-authored-by: Lucas Araujo <lucas.lra@gmail.com>
Co-authored-by: GreenMonito <86085570+GreenMonito@users.noreply.github.com>
Co-authored-by: Nerdy-Technician <roffo@nerdy-technician.social>
Co-authored-by: Jun <34545896+jcstrong@users.noreply.github.com>
Co-authored-by: Edmond Abraham <54119952+gitedmond@users.noreply.github.com>
Co-authored-by: thrawnx <tight.bulb9384@fastmail.com>
Co-authored-by: Chris <58918656+nordada@users.noreply.github.com>
Co-authored-by: yi111 <153097222+Yi-111-a@users.noreply.github.com>
Co-authored-by: Yanis <naamaneyanis@gmail.com>
Co-authored-by: Wikibear <me@sej.de>
Co-authored-by: alessiottavo <93441886+alessiottavo@users.noreply.github.com>
2026-10-02 21:00:14 +02:00

401 lines
20 KiB
Python

"""Soundtrack for the time-remapped cuts.
Music is written on the output beat grid; every SFX is authored in reel time (the choreography in
reel.js) and placed through the warp, so it stays glued to its on-screen event.
Usage: python3 score.py global|v2cut -> soundtrack-<version>.wav
The v2 cut starts on a black pre-roll that rises into the "Now in v2" impact.
"""
import json
import sys
import numpy as np
from scipy import signal
from scipy.io import wavfile
from synth import (BAR, BEAT, E8, S16, SR, adsr, bass, bell, clap, click, crash, glide, hat, hp, impact, kick, lp, mtof,
nf, noise, pad, pluck, pop, revcym, riser, snare, thud, tt, tv_biquad, whoosh)
VERSION = sys.argv[1] if len(sys.argv) > 1 else 'global'
W = json.load(open(f'warp-{VERSION}.json'))
PRE = W.get('pre', 0.0)
TT = np.array(W['t'][int(round(PRE / W['du'])):])
UU = PRE + np.arange(len(TT)) * W['du']
DUR = W['duration']
N = int(round(DUR * SR))
SB = {s['name'].split()[0]: int(round((s['u0'] - PRE) / BEAT)) for s in W['shots']} # shot start beats
HAS_A = 'S1' in SB # the global cut opens with the original reel
U = lambda t: float(np.interp(t, TT, UU)) # reel time -> output time
D = lambda t, d: max(0.02, U(t + d) - U(t)) # output length of a reel-time span
bt = lambda b: PRE + b * BEAT # output beat -> seconds
IMP, SLAM = 2 * BEAT, 7 * BAR + 2 * BEAT
R = np.random.default_rng(42)
seg = lambda t, a, b: min(1.0, max(0.0, (t - a) / (b - a)))
out_cubic = lambda x: 1 - (1 - x) ** 3
out_quint = lambda x: 1 - (1 - x) ** 5
music = np.zeros((N, 2))
sfx = np.zeros((N, 2))
verb = np.zeros((N, 2))
kicks = []
def place(dst, sig, u, gain=1.0, pan=0.0, send=0.0):
if sig.ndim == 1:
th = (pan + 1) * np.pi / 4
sig = np.stack([sig * np.cos(th), sig * np.sin(th)], 1) * np.sqrt(2)
st = sig * gain
i = int(round(u * SR))
if i < 0:
st, i = st[-i:], 0
n = min(len(st), N - i)
if n <= 0:
return
dst[i:i + n] += st[:n]
if send:
verb[i:i + n] += st[:n] * send
def fx(sig, t, g=1.0, pan=0.0, send=0.0):
"""SFX at reel time t; events before the cut's first shot are dropped."""
if t >= TT[0] - 1e-9:
place(sfx, sig, U(t), g, pan, send)
def groove(b0, b1, chords, no_kick=(), hats16=False, lead=None):
"""Four-on-the-floor from beat b0 to b1; chords maps a beat to (notes, root), lead maps a beat to 4 notes or None."""
for b in range(b0, b1):
t = bt(b)
if b not in no_kick:
place(music, kick(), t, 0.95)
kicks.append(t)
if (b - b0) % 2 == 1:
place(music, clap(), t, 0.55, send=0.25)
place(music, hat(), t + E8, 0.55, pan=0.25)
if hats16:
place(music, hat(), t + S16, 0.2, pan=-0.2)
place(music, hat(), t + 3 * S16, 0.28, pan=-0.3)
notes, root = chords(b)
if b not in no_kick:
place(music, bass(nf(root), E8 * 0.9), t + E8, 0.6)
place(music, bass(nf(root) * 2, S16 * 0.8), t + E8 + S16, 0.25)
if lead and lead(b):
for i in range(4):
place(music, pluck(nf(lead(b)[i]), 0.3, 1.0), t + i * S16, 0.1 if i == 0 else 0.065, pan=0.4 * np.sin((b * 4 + i) * 1.3), send=0.3)
# pads: one per chord span
b = b0
while b < b1:
notes, root = chords(b)
e = b + 1
while e < b1 and chords(e) == (notes, root):
e += 1
place(music, pad([nf(n) for n in notes], bt(e) - bt(b), 1700), bt(b), 0.5)
b = e
# ─────────────────────────── MUSIC ───────────────────────────
S8, S9, FA, FB, S12, S16_ = SB['S8'], SB['S9'], SB['FDA'], SB['FDB'], SB['S12'], SB['S16']
if HAS_A:
S2, S5, S6 = SB['S2'], SB['S5'], SB['S6']
# intro: drone + stroke sparkles, riser into the logo impact
place(music, (np.sin(2 * np.pi * 55 * tt(U(IMP) + 0.1)) * 0.6 + lp(np.sin(2 * np.pi * 110 * tt(U(IMP) + 0.1)), 260) * 0.3) * adsr(U(IMP) + 0.1, 0.4, 0.08), 0, 0.35)
for i, n in enumerate(['A4', 'C5', 'E5', 'G5', 'A5', 'C6']):
place(music, pluck(nf(n), 0.7, 1.4), U(0.234 + i * S16), 0.16, pan=-0.5 + i * 0.2, send=0.5)
place(music, pad([nf(n) for n in ['A2', 'E3', 'A3', 'C4', 'E4']], bt(S2) - U(IMP), 1400, a=0.3), U(IMP), 0.35, send=0.4)
A_CH = [(['A3', 'C4', 'E4', 'G4'], 'A1'), (['F3', 'A3', 'C4', 'E4'], 'F1'), (['E3', 'G3', 'C4', 'B3'], 'C2'), (['D3', 'G3', 'B3', 'E4'], 'G1')]
chA = lambda b: A_CH[((b - S2) // 4) % 4]
groove(S2, S8, chA, no_kick={S6 - 1, S8 - 1})
for b in range(S5, S8): # 16th hats from the live-data shot on
place(music, hat(), bt(b) + S16, 0.2, pan=-0.2)
place(music, hat(), bt(b) + 3 * S16, 0.28, pan=-0.3)
for i, n in enumerate(['A4', 'C5', 'E5', 'G5', 'A5', 'C6', 'E6', 'G6']): # one pluck per service card
place(music, pluck(nf(n), 0.4, 1.2), bt(S2 + i), 0.3, pan=(-0.4 if i % 2 else 0.4), send=0.3)
for b in range(S6, S8): # sparkle arp over the ring and the features grid
arp = ['A5', 'E5', 'C6', 'E5'] if chA(b)[1] in ('A1', 'C2') else ['A5', 'F5', 'C6', 'F5']
for i in range(4):
place(music, pluck(nf(arp[i]), 0.25, 0.8), bt(b) + i * S16, 0.08, pan=0.5 * np.sin(b * 4 + i), send=0.3)
else:
# pre-roll: sub swell and reversed cymbal into the impact
place(music, np.sin(2 * np.pi * 55 * tt(PRE)) * np.linspace(0, 1, len(tt(PRE))) ** 2, 0, 0.3)
place(sfx, riser(PRE, 180, 2600), 0, 0.5)
place(sfx, revcym(PRE), 0, 0.9)
place(sfx, whoosh(PRE, 300, 4000, 1.6, 2.5, 0, 0, 0.97), 0, 0.5)
# "Now in v2": groove drops out, held chord, slam on the beat
place(music, pad([nf(n) for n in ['A2', 'E3', 'A3', 'B3', 'C4', 'E4', 'G4']], bt(S9) - bt(S8) + 0.1, 2400, a=0.01, r=0.3), bt(S8), 0.6, send=0.5)
place(music, bass(nf('A1'), 1.2), bt(S8), 0.9)
place(music, kick(), U(SLAM), 1.0)
kicks.append(U(SLAM))
place(music, clap(), U(SLAM), 0.7, send=0.4)
place(music, bass(nf('A1'), 1.0), U(SLAM), 0.9)
for i, n in enumerate(['E5', 'A5', 'B5', 'E6']):
place(music, pluck(nf(n), 0.5, 1.2), U(SLAM) + 0.15 + i * S16, 0.14, pan=-0.4 + i * 0.25, send=0.5)
# v2 groove, lined up backwards so the bar before the end card is E7; the endpoint diagram drops the lead
B_CYC = [(['F3', 'A3', 'C4', 'E4'], 'F1'), (['D3', 'G3', 'B3', 'E4'], 'G1'), (['A3', 'C4', 'E4', 'G4'], 'A1'), (['E3', 'G3', 'C4', 'B3'], 'C2'),
(['F3', 'A3', 'C4', 'E4'], 'F1'), (['D3', 'G3', 'B3', 'E4'], 'G1'), (['A3', 'C4', 'E4', 'G4'], 'A1'), (['E3', 'G#3', 'B3', 'D4'], 'E1')]
LEADS = {'F1': ['C6', 'A5', 'F5', 'A5'], 'G1': ['B5', 'G5', 'D5', 'G5'], 'A1': ['C6', 'A5', 'E5', 'A5'], 'C2': ['B5', 'G5', 'E5', 'G5'], 'E1': ['B5', 'G#5', 'E5', 'D6']}
chB = lambda b: B_CYC[(7 - (S16_ - 1 - b) // 4) % 8]
groove(S9, S16_, chB, no_kick={S16_ - 1}, hats16=True, lead=lambda b: None if FA <= b < FB else LEADS[chB(b)[1]])
for i in range(8): # fill into the end card
place(music, snare(), bt(S16_ - 1) + i * BEAT / 8, 0.2 + 0.4 * i / 7, send=0.2)
# the front door gets a bell counter-melody on top
for b in range(FA, S12):
if (b - FA) % 2 == 0:
n = {'F1': 'A6', 'G1': 'B6', 'A1': 'C7', 'C2': 'G6', 'E1': 'G#6'}[chB(b)[1]]
place(music, bell(nf(n), 0.9), bt(b), 0.1, pan=0.3 * np.sin(b), send=0.5)
# end card: resolve to A major
fin_len = DUR - bt(S16_)
place(music, pad([nf(n) for n in ['A2', 'E3', 'A3', 'C#4', 'E4', 'B4']], fin_len, 2800, a=0.01, r=0.8), bt(S16_), 0.8, send=0.5)
place(music, bass(nf('A1'), fin_len) * adsr(fin_len, 0.004, 0.8), bt(S16_), 0.9)
kicks.append(bt(S16_))
place(music, kick(), bt(S16_), 1.0)
for i, n in enumerate(['A5', 'C#6', 'E6', 'A6']):
place(music, bell(nf(n), 1.2), U(19 * BAR + 0.78 + i * 0.07), 0.3, pan=-0.3 + i * 0.2, send=0.6)
# sidechain pump
sc = np.ones(N)
for kt in kicks:
i = int(kt * SR)
n = min(int(0.3 * SR), N - i)
if n > 0:
sc[i:i + n] = np.minimum(sc[i:i + n], 1 - 0.55 * np.exp(-np.arange(n) / SR / 0.09))
kick_bus = np.zeros((N, 2))
for kt in kicks:
place(kick_bus, kick(), kt, 0.95)
music = (music - kick_bus) * sc[:, None] + kick_bus
if HAS_A: # rack-focus low-pass (live-data headline), opening again as a build into the ring
a, b = int(U(8.3) * SR), int(U(5 * BAR) * SR)
fc = np.array([20000 * (650 / 20000) ** (out_cubic(seg(t, 8.36, 8.62)) * (1 - seg(t, 8.9, 9.36) ** 2)) for t in np.interp(np.arange(a, b) / SR, UU, TT)])
for ch in range(2):
music[a:b, ch] = tv_biquad(music[a:b, ch], fc, 0.8, 'lp', 128)
# ─────────────────────────── SFX, original reel (reel-time authored) ───────────────────────────
fx(click(1800, 0.05, 0.01), 0.0, 0.4)
fx(whoosh(D(0, 0.5), 600, 6000, 1.2, 1.4, -0.4, 0.4, 0.55), 0.0, 0.45)
fx(riser(D(0.1, IMP - 0.1), 200, 2000), 0.1, 0.35)
fx(impact(2.4, 1.2), IMP, 1.0, send=0.35)
fx(crash(1.6), IMP, 1.0, send=0.3)
for i in range(6):
fx(click(2400 + i * 260, 0.03, 0.004), 1.0 + i * 0.045, 0.22, pan=-0.5 + i * 0.2)
fx(bell(nf('E6'), 0.8), 1.22, 0.12, send=0.5)
fx(revcym(D(1.52, BAR - 1.52)), 1.52, 0.9)
fx(whoosh(D(1.52, BAR - 1.52), 300, 4000, 1.6, 2.5, 0, 0, 0.97), 1.52, 0.7)
for i in range(8):
fx(whoosh(0.14, 3000, 900, 1.5, 1.0, 0.6 * (-1) ** i, -0.6 * (-1) ** i, 0.15), BAR + i * E8 - 0.012, 0.25)
# one place: burst, tile rain, morph
fx(impact(1.6, 0.8), 2 * BAR, 0.7, send=0.3)
fx(whoosh(0.5, 5000, 400, 1.0, 1.0, 0, 0, 0.08), 2 * BAR, 0.6)
for k in range(87):
fx(click(R.uniform(2600, 5200), 0.02, 0.003), 2 * BAR + 0.56 + R.uniform(0, 0.24) + R.uniform(-0.02, 0.02), 0.07, pan=R.uniform(-0.8, 0.8))
fx(whoosh(D(4.0, 0.35), 800, 3000, 1.4, 1.0, -0.3, 0.3, 0.6), 4.0, 0.25)
fx(revcym(D(4.45, 0.45)), 4.45, 0.5)
for k in range(7):
fx(pop(300 + k * 40, 800 + k * 60, 0.1), 5.0 + k * 0.045, 0.16, pan=-0.6 + k * 0.2)
# drag & drop
fx(click(3000, 0.04, 0.006), 5.64, 0.45)
fx(whoosh(D(5.70, 0.38), 500, 1800, 1.3, 1.2, -0.5, 0.5, 0.5), 5.70, 0.3)
fx(thud(), 6.094, 0.55)
fx(click(2200, 0.04, 0.006), 6.094, 0.35)
fx(click(3000, 0.04, 0.006), 6.50, 0.45, pan=0.3)
fx(glide(900, 500, D(6.563, 0.39)), 6.563, 0.35, pan=0.3)
fx(click(2200, 0.04, 0.006), 6.95, 0.35, pan=0.3)
fx(pop(350, 1100, 0.15), 7.031, 0.5, pan=0.4)
fx(glide(300, 1400, D(7.08, 0.38)), 7.08, 0.28, pan=0.4)
fx(bell(nf('E6'), 0.7), 7.12, 0.22, pan=0.4, send=0.4)
fx(bell(nf('A6'), 0.7), 7.19, 0.22, pan=0.4, send=0.4)
fx(whoosh(D(7.26, 0.24), 400, 5000, 1.2, 2.2, 0, 0, 0.9), 7.26, 0.6)
# live data
for i, p in enumerate((-0.7, 0.7, -0.7, 0.7)):
fx(whoosh(D(7.5 + i * 0.117, 0.22), 4000, 800, 1.4, 1.0, p, p * 0.3, 0.2), 7.5 + i * 0.117, 0.3)
fx(impact(1.0, 0.5) * 0.6, 8.42, 0.45, send=0.4)
fx(impact(1.0, 0.5) * 0.6, 8.62, 0.45, send=0.4)
if HAS_A:
for i in range(12): # snare roll in the last output beat before the ring
place(sfx, snare(), bt(S6 - 1) + i * BEAT / 12, 0.2 + 0.45 * i / 11, send=0.2)
place(sfx, riser(BEAT * 1.5, 400, 3000), bt(S6) - BEAT * 1.5, 0.7)
# ring
fx(impact(1.8, 1.0), 5 * BAR, 0.85, send=0.35)
fx(crash(1.6), 5 * BAR, 0.9, send=0.3)
prev = 0
for i in range(1, 4000):
t = 5 * BAR + i / 4000 * 1.0
v = int(87 * out_quint(seg(t, 5 * BAR + 0.06, 5 * BAR + 0.95)))
if v != prev:
fx(click(3800 + v * 12, 0.02, 0.0025), t, 0.1, pan=0.3 * np.sin(v))
prev = v
fx(bell(nf('A5'), 0.8), 5 * BAR + 1.0, 0.18, send=0.5)
fx(whoosh(D(10.95, 0.5), 300, 5500, 1.1, 1.6, 0.9, -0.9, 0.5), 10.95, 1.0)
# features bento
for k, a0 in enumerate((11.2, 11.32, 11.6, 11.72, 12.0)):
fx(thud(0.2), a0 + 0.02, 0.28, pan=[-0.4, 0.4, 0.1, 0.6, 0][k])
fx(pop(260, 700, 0.1), a0 + 0.02, 0.12, pan=[-0.4, 0.4, 0.1, 0.6, 0][k])
fx(click(5200, 0.03, 0.003), 11.62, 0.4, pan=-0.5)
fx(click(4200, 0.02, 0.003), 11.635, 0.3, pan=-0.5)
for i in range(3):
fx(pop(500, 1300, 0.08), 11.4 + i * 0.07, 0.12, pan=-0.5)
for i in range(2):
fx(click(2600, 0.03, 0.004), 11.85 + i * 0.12, 0.3, pan=-0.5)
prev = 0
for i in range(1, 6000):
t = 11.38 + i / 6000 * 1.37
r = int(round(19 * out_cubic(seg(t, 11.38, 12.75))))
if r != prev:
fx(click(3300, 0.02, 0.003), t, 0.14, pan=0.2)
prev = r
for i in range(5):
fx(click(R.uniform(1800, 2600), 0.02, 0.003), 11.86 + i * 0.07, 0.25, pan=0.6)
for i in range(0, 45, 2):
fx(click(R.uniform(1600, 2800), 0.015, 0.002), 12.12 + i * 0.0105, 0.16, pan=-0.2)
fx(bell(nf('C6'), 0.6), 12.66, 0.2, send=0.4)
fx(bell(nf('G6'), 0.6), 12.72, 0.16, send=0.4)
fx(revcym(D(7 * BAR - 0.5, 0.5)), 7 * BAR - 0.5, 1.0)
fx(whoosh(D(12.88, 0.3), 5000, 300, 1.3, 1.0, 0, 0, 0.8), 12.88, 0.45)
# ─────────────────────────── SFX, v2 ───────────────────────────
fx(impact(2.4, 1.4), 7 * BAR, 1.0, send=0.4)
fx(crash(1.9), 7 * BAR, 1.1, send=0.4)
fx(whoosh(D(7 * BAR, 1.0), 3000, 250, 1.2, 1.2, -0.6, 0.6, 0.25), 7 * BAR, 0.5)
for c in (7 * BAR + 0.55, SLAM, SLAM + 0.5):
clk = click(1500, 0.06, 0.012)
clk[int(0.03 * SR):] += click(1100, 0.06, 0.012)[:len(clk) - int(0.03 * SR)]
fx(clk, c + 0.28, 0.45, pan=-0.3)
for i in range(6):
fx(click(2600 + i * 200, 0.03, 0.004), 13.92 + i * 0.04, 0.14, pan=0.2 + i * 0.1)
fx(impact(1.8, 1.0), SLAM, 0.9, send=0.4)
fx(whoosh(D(SLAM - 0.02, 0.35), 6000, 500, 1.2, 1.0, 0, 0, 0.1), SLAM - 0.02, 0.5)
fx(riser(D(14.35, 0.45), 300, 3000), 14.35, 0.35)
# transitions: vertical whips, slab wipes, horizontal whip
for c in (14.8, 13 * BAR - 0.2):
fx(whoosh(D(c, 0.4), 300, 5000, 1.1, 1.6, 0, 0, 0.5), c, 0.8)
for c, d in ((9 * BAR, 1), (10 * BAR, -1), (15 * BAR, 1), (16 * BAR, -1), (18 * BAR, 1)):
fx(whoosh(D(c - 0.225, 0.45), 700, 5000, 1.2, 1.4, -0.7 * d, 0.7 * d, 0.5), c - 0.225, 0.55)
fx(whoosh(D(11 * BAR - 0.12, 0.42), 300, 5500, 1.1, 1.6, 0.8, -0.8, 0.5), 11 * BAR - 0.12, 0.8)
# custom widgets
T = 8 * BAR
LT = lambda i: T + 0.1 + i * 0.086
for i in range(13):
for j in range(3):
fx(click(R.uniform(1800, 2600), 0.015, 0.002), LT(i) + j * 0.026, 0.09, pan=-0.4)
fx(pop(380, 950, 0.1), LT(2) + 0.06, 0.2, pan=0.4)
fx(pop(500, 1200, 0.08), LT(3) + 0.06, 0.14, pan=0.5)
for i in range(4):
fx(pop(300 + i * 60, 800 + i * 90, 0.09), LT(8) + 0.05 + i * 0.07, 0.15, pan=0.4)
fx(glide(400, 900, D(LT(11) + 0.08, 0.5)), LT(11) + 0.08, 0.15, pan=0.4)
# workshop
T = 9 * BAR
for i in range(21):
fx(click(R.uniform(2800, 4200), 0.02, 0.003), T + 0.14 + i * 0.025, 0.05, pan=0.5)
fx(pop(250, 700, 0.14), T + 0.62, 0.25, pan=0.4)
fx(click(3000, 0.04, 0.006), T + 1.02, 0.45, pan=0.4)
fx(bell(nf('E6'), 0.6), T + 1.07, 0.2, pan=0.4, send=0.4)
fx(bell(nf('B6'), 0.6), T + 1.14, 0.18, pan=0.4, send=0.4)
fx(click(4200, 0.02, 0.003), T + 1.27, 0.2, pan=0.4)
# assistant
T = 10 * BAR
CLICK = T + 1.0
for i in range(0, 33, 2):
fx(click(R.uniform(1800, 2800), 0.015, 0.002), T + 0.08 + i * 0.0125, 0.1, pan=0.5)
for a0, b0 in ((T + 0.5, T + 0.64), (T + 0.62, T + 0.76), (CLICK + 0.1, CLICK + 0.22)):
fx(pop(400, 900, 0.08), a0, 0.12, pan=0.4)
fx(bell(nf('A6'), 0.4), b0, 0.1, pan=0.4, send=0.3)
fx(pop(260, 720, 0.14), T + 0.74, 0.22, pan=0.4)
fx(click(3000, 0.04, 0.006), CLICK, 0.45, pan=0.4)
fx(bell(nf('E6'), 0.7), CLICK + 0.04, 0.2, pan=0.4, send=0.4)
fx(bell(nf('A6'), 0.7), CLICK + 0.11, 0.18, pan=0.4, send=0.4)
fx(pop(300, 850, 0.12), CLICK + 0.33, 0.2, pan=0.3)
# front door, diagram: request in, auth checks, secret from the vault, service hit, response back
T = 11 * BAR
O = lambda x: T + x
fx(pop(300, 800, 0.12), O(0.24), 0.3, pan=0.4)
fx(click(2600, 0.03, 0.004), O(0.24), 0.25, pan=0.4)
for k, a0 in enumerate((0.44, 0.5, 0.52, 0.6)):
fx(thud(0.2), O(a0) + 0.02, 0.22, pan=[-0.6, -0.6, 0, 0.6][k])
for i in range(5):
fx(click(3200 + i * 200, 0.02, 0.003), O(0.7 + i * 0.03), 0.12, pan=[-0.2, 0.2, 0.6, -0.6, -0.6][i])
FLOWS = [ # appear, go, checks, token, go to service, hit, back, gain
(0.95, (1.0, 1.35), (1.42, 1.5, 1.58), (1.6, 1.8), (1.84, 2.12), 2.12, (2.22, 2.64), 1.0),
(2.72, (2.74, 2.96), (2.97,), (2.97, 3.09), (3.1, 3.26), 3.26, (3.3, 3.52), 0.75),
]
for appear, go, checks, tok, go2, hit, back, g in FLOWS:
fx(pop(500, 1300, 0.08), O(appear), 0.22 * g, pan=-0.6)
fx(whoosh(D(O(go[0]), go[1] - go[0]), 900, 2600, 1.2, 1.2, -0.6, 0, 0.5), O(go[0]), 0.3 * g)
for k, c in enumerate(checks):
fx(bell(mtof(81 + [0, 4, 7][k]), 0.4), O(c), 0.12, pan=0.05, send=0.3)
fx(glide(500, 1300, D(O(tok[0]), tok[1] - tok[0])), O(tok[0]), 0.2 * g)
fx(click(1800, 0.05, 0.008), O(tok[1]), 0.4 * g)
fx(whoosh(D(O(go2[0]), go2[1] - go2[0]), 900, 3200, 1.2, 1.2, 0, 0.7, 0.5), O(go2[0]), 0.3 * g)
fx(thud(0.25), O(hit), 0.35 * g, pan=0.7)
fx(pop(300, 900, 0.1), O(hit) + 0.06, 0.2 * g, pan=0.7)
fx(whoosh(D(O(back[0]), back[1] - back[0]), 2600, 800, 1.2, 1.2, 0.7, -0.6, 0.5), O(back[0]), 0.3 * g)
fx(bell(nf('E6'), 0.6), O(back[1]), 0.18 * g, pan=-0.6, send=0.4)
fx(bell(nf('A6'), 0.6), O(back[1]) + 0.05, 0.15 * g, pan=-0.6, send=0.4)
for i, a0 in enumerate((0.95, 1.32, 1.6, 1.84, 2.22)): # numbered steps light up
fx(click(2000 + i * 250, 0.03, 0.004), O(a0), 0.15, pan=-0.8 + i * 0.4)
# front door, live: prompt, tool calls, results table, phone pings
T = 13 * BAR
for i in range(0, 112, 3):
fx(click(R.uniform(1800, 2800), 0.015, 0.002), O(0.3) + i * 0.006, 0.09, pan=0.5)
for i, (a0, b0) in enumerate(((1.05, 1.17), (1.25, 1.37), (1.45, 1.62), (1.7, 1.85))):
fx(pop(400 + i * 40, 900 + i * 80, 0.08), O(a0), 0.13, pan=0.4)
fx(bell(nf('A6'), 0.4), O(b0), 0.1, pan=0.4, send=0.3)
for i in range(7):
fx(click(2400 + i * 160, 0.03, 0.004), O(1.9 + i * 0.13), 0.18, pan=0.5)
fx(bell(mtof(81 + [0, 2, 4, 7, 9, 12, 14][i]), 0.35), O(2.0 + i * 0.13), 0.07, pan=0.5, send=0.3)
for i in range(2): # two ntfy pings
fx(bell(nf('E6'), 0.5), O(2.8 + i * 0.12), 0.24, pan=-0.5, send=0.3)
fx(bell(nf('B6'), 0.5), O(2.8 + i * 0.12) + 0.03, 0.2, pan=-0.5, send=0.3)
fx(pop(220, 520, 0.16), O(3.05), 0.25, pan=0.4)
# board editing
T = 15 * BAR
fx(click(3000, 0.04, 0.006), T + 0.27, 0.4, pan=0.4)
fx(glide(500, 950, D(T + 0.32, 0.3)), T + 0.32, 0.3, pan=0.4)
fx(click(2200, 0.04, 0.006), T + 0.62, 0.35, pan=0.4)
fx(thud(0.25), T + 0.62, 0.3, pan=0.4)
fx(whoosh(D(T + 0.84, 0.64), 2500, 400, 1.3, 1.3, 0.2, 0.6, 0.45), T + 0.84, 0.45)
fx(glide(1100, 380, D(T + 0.84, 0.64)), T + 0.84, 0.25, pan=0.5)
# setup studio
T = 16 * BAR
for i in range(6):
fx(pop(420 + i * 60, 1000 + i * 120, 0.08), T + 0.3 + i * 0.14, 0.16, pan=-0.4)
for i in range(7):
fx(click(2400 + i * 180, 0.03, 0.004), T + 0.62 + i * 0.12, 0.2, pan=0.5)
fx(bell(mtof(81 + [0, 2, 4, 7, 9, 12, 14][i]), 0.35), T + 0.63 + i * 0.12, 0.07, pan=0.5, send=0.3)
# +31
T = 17 * BAR
fx(impact(1.2, 0.6), T, 0.6, send=0.3)
prev = 0
for i in range(1, 3000):
t = T + i / 3000 * 0.9
v = int(31 * out_quint(seg(t, T + 0.05, T + 0.85)))
if v != prev:
fx(click(3600 + v * 20, 0.02, 0.0025), t, 0.12, pan=-0.4)
prev = v
for d in range(11):
fx(click(R.uniform(3000, 4600), 0.02, 0.003), T + 0.18 + d * 0.035, 0.07, pan=0.5)
for i in range(4):
fx(whoosh(0.2, 3500, 900, 1.4, 1.0, -0.6 + i * 0.4, -0.6 + i * 0.4, 0.3), T + 0.7 + i * 0.07, 0.18)
# also in v2
T = 18 * BAR
fx(impact(1.0, 0.5), T + 0.12, 0.45, send=0.3)
for i in range(12):
fx(click(R.uniform(2200, 3400), 0.025, 0.004), T + 0.2 + i * 0.07, 0.13, pan=0.3 + 0.2 * np.sin(i))
fx(revcym(D(19 * BAR - 0.5, 0.5)), 19 * BAR - 0.5, 1.0)
fx(whoosh(D(19 * BAR - 0.25, 0.3), 5000, 300, 1.3, 1.0, 0, 0, 0.8), 19 * BAR - 0.25, 0.45)
fx(impact(2.4, 1.4), 19 * BAR, 1.0, send=0.4)
fx(crash(1.9), 19 * BAR, 1.1, send=0.4)
# ─────────────────────────── reverb + master ───────────────────────────
ir_d = 1.8
it = tt(ir_d)
ir = np.stack([lp(noise(ir_d), 6000), lp(noise(ir_d), 6000)], 1) * np.exp(-it / 0.45)[:, None]
ir /= np.sqrt((ir ** 2).sum(0))
wet = np.stack([signal.fftconvolve(verb[:, c], ir[:, c])[:N] for c in range(2)], 1)
mix = hp(music * 0.8 + sfx * 0.85 + wet * 0.45, 25)
fade = np.ones(N)
fa = int((DUR - 0.25) * SR)
fade[fa:] = (1 - np.linspace(0, 1, N - fa)) ** 2
mix *= fade[:, None]
mix = np.tanh(mix / np.abs(mix).max() * 1.6) / np.tanh(1.6)
mix *= 10 ** (-1 / 20) / np.abs(mix).max()
wavfile.write(f'soundtrack-{VERSION}.wav', SR, (mix * 32767).astype(np.int16))
print(VERSION, f'{DUR:.3f}s', 'peak', round(float(np.abs(mix).max()), 3), 'rms dB', round(20 * np.log10(np.sqrt((mix ** 2).mean())), 1))