"""Launch trailer score + timeline. Reads renderer/public/launch-vo/durations.json, lays scenes out on bar lines at 120 bpm, writes renderer/src/compositions/launchTimeline.ts and renderer/public/launch-vo/music.wav. Shape: uncharted intro (pads, heartbeat) -> launch impact -> driving groove through the feature galaxies (four on the floor, sidechained pads, plucked arp, bass) -> two bar build -> huge hit on "Coming soon" and a long shimmering tail. Pure synthesis, no noise sources. """ import json import math import wave from pathlib import Path import numpy as np ROOT = Path(__file__).resolve().parents[1] DUR = json.loads((ROOT / "public/launch-vo/durations.json").read_text()) BPM = 120 BEAT = 60 / BPM BAR = BEAT * 4 VO_START = {"intro": 1.0, "outro": 1.2} MIN_BARS = {"intro": 4, "outro": 5} order = ["intro", "launch", "sources", "research", "map", "voice", "audio", "video", "launchkit", "launchpad", "resurface", "montage", "outro"] scenes = [] cursor = 0.0 for sid in order: if sid == "montage": bars, vo, vo_len = 2, None, None else: vo = VO_START.get(sid, 0.35) vo_len = DUR[sid] bars = max(MIN_BARS.get(sid, 2), math.ceil((vo + vo_len + 0.45) / BAR)) scenes.append({"id": sid, "start": cursor, "len": bars * BAR, "vo": vo, "voLen": vo_len}) cursor += bars * BAR TOTAL = cursor bar_of = {s["id"]: round(s["start"] / BAR) for s in scenes} print("total", TOTAL, bar_of) ts = ["// Generated by make_launch_music.py from public/launch-vo/durations.json. Do not edit by hand.", f"// {BPM} bpm; every scene starts on a bar line so cuts land on the beat.", f"export const LAUNCH_BPM = {BPM};", f"export const LAUNCH_SECONDS = {TOTAL};", "export const LAUNCH_SCENES = ["] for s in scenes: vo = "null" if s["vo"] is None else s["vo"] vl = "null" if s["voLen"] is None else s["voLen"] ts.append(f' {{ id: "{s["id"]}", start: {s["start"]}, len: {s["len"]}, vo: {vo}, voLen: {vl} }},') ts.append("] as const;") ts.append("export type LaunchSceneId = (typeof LAUNCH_SCENES)[number][\"id\"];") (ROOT / "src/compositions/launchTimeline.ts").write_text("\n".join(ts) + "\n") # ---------------------------------------------------------------- synthesis SR = 44100 n = int(SR * (TOTAL + 0.2)) t = np.arange(n) / SR L = {k: np.zeros(n) for k in ("pad", "bass", "arp", "drums", "fx", "bell")} R = {k: np.zeros(n) for k in L} rng = np.random.default_rng(11) def hz(m: float) -> float: return 440.0 * 2 ** ((m - 69) / 12) def add(bus: str, i0: int, sig: np.ndarray, pan: float = 0.0) -> None: if i0 >= n: return sig = sig[: n - i0] L[bus][i0:i0 + len(sig)] += sig * (1 - pan) R[bus][i0:i0 + len(sig)] += sig * (1 + pan) def at(seconds: float) -> int: return int(seconds * SR) def bar_t(b: float) -> float: return b * BAR INTRO_END = bar_of["launch"] GROOVE_START = bar_of["sources"] BUILD = bar_of["montage"] OUTRO = bar_of["outro"] END_BAR = TOTAL / BAR # I vi IV V in D, two bars each; outro resolves on a wide Dadd9. PROG = [[50, 57, 62, 66, 69], [47, 54, 59, 62, 66], [43, 50, 55, 59, 62], [45, 52, 57, 61, 64]] INTRO_PROG = [[50, 57, 64, 66, 69], [47, 54, 61, 62, 66]] OUTRO_CHORD = [38, 50, 57, 62, 64, 66, 69, 74] def chord_at(b: float) -> list[int]: if b < INTRO_END: return INTRO_PROG[int(b // 2) % 2] if b >= OUTRO: return OUTRO_CHORD[1:6] return PROG[int((b - INTRO_END) // 2) % 4] # Pads: detuned soft saws per two bar chord, slow attack. def pad(notes, start, dur, gain, bright): i0 = at(start) length = at(dur + 2.5) tt = np.arange(length) / SR a = np.clip(tt / 1.2, 0, 1) r = np.clip((dur + 2.5 - tt) / 2.5, 0, 1) e = (0.5 - 0.5 * np.cos(np.pi * a)) * (0.5 - 0.5 * np.cos(np.pi * r)) for k, m in enumerate(notes): tone = np.zeros(length) for cents, ph in ((-8, 0.0), (8, 1.7)): f = hz(m) * 2 ** (cents / 1200) for h in range(1, 8): tone += np.sin(2 * np.pi * f * h * tt + ph * h) * bright ** (h - 1) / h ** 1.3 add("pad", i0, tone * e * gain, pan=(k - 2) * 0.25) b = 0.0 while b < END_BAR: notes = chord_at(b) if b >= OUTRO: pad(OUTRO_CHORD, bar_t(b), END_BAR * BAR - bar_t(b) - 3.0, 0.05, 0.5) break bright = 0.3 if b < INTRO_END else 0.42 + 0.1 * (b - GROOVE_START) / max(1, BUILD - GROOVE_START) pad(notes, bar_t(b), BAR * 2, 0.05 if b < INTRO_END else 0.042, bright) b += 2 # Kick: pitched sine sweep + tiny click. def kick(start, gain=1.0): length = at(0.45) tt = np.arange(length) / SR f = 44 + 90 * np.exp(-tt * 28) body = np.sin(2 * np.pi * np.cumsum(f) / SR) * np.exp(-tt * 7.5) click = np.sin(2 * np.pi * 1800 * tt) * np.exp(-tt * 400) * 0.25 add("drums", at(start), (body + click) * 0.5 * gain) # Hat: inharmonic metallic partials, very short. def hat(start, gain=1.0, pan=0.25): length = at(0.07) tt = np.arange(length) / SR sig = sum(np.sin(2 * np.pi * f * tt + p) for f, p in ((6230, 0.1), (7310, 1.2), (8420, 2.3), (9870, 0.7), (11200, 1.9))) add("drums", at(start), sig * np.exp(-tt * 70) * 0.018 * gain, pan) # Snare/tom tick for the build: short tone burst with a pitch drop. def tom(start, pitch, gain): length = at(0.16) tt = np.arange(length) / SR f = pitch * (1 + 0.6 * np.exp(-tt * 40)) sig = np.sin(2 * np.pi * np.cumsum(f) / SR) + 0.4 * np.sin(2 * np.pi * np.cumsum(f * 2.3) / SR) add("drums", at(start), sig * np.exp(-tt * 22) * gain * 0.2, pan=rng.uniform(-0.3, 0.3)) kicks: list[float] = [] # Heartbeat in the intro's second half. for bb in range(2, INTRO_END): for off in (0, BEAT * 0.75): kick(bar_t(bb) + off, 0.35 if off else 0.5) kicks.append(bar_t(bb) + off) # Launch: kick on beat one of bar 1, quarters on bar 2. kick(bar_t(INTRO_END), 1.2) kicks.append(bar_t(INTRO_END)) for q in range(4): kick(bar_t(INTRO_END + 1) + q * BEAT, 0.7) kicks.append(bar_t(INTRO_END + 1) + q * BEAT) # Groove: four on the floor with offbeat hats and 16th ghost hats. for bb in range(GROOVE_START, BUILD): for q in range(4): s = bar_t(bb) + q * BEAT kick(s, 0.85) kicks.append(s) hat(s + BEAT / 2, 1.0) hat(s + BEAT / 4, 0.35, -0.25) hat(s + BEAT * 3 / 4, 0.35, -0.25) # Build: toms accelerating and rising, no kick. bs = bar_t(BUILD) steps = [(0, 8, 8), (1, 16, 16)] for bar_i, per_bar, _ in steps: for k in range(per_bar): s = bs + bar_i * BAR + k * BAR / per_bar prog = (s - bs) / (2 * BAR) tom(s, 140 + 160 * prog, 0.4 + 0.8 * prog) # Outro hit. kick(bar_t(OUTRO), 1.5) kicks.append(bar_t(OUTRO)) # Sidechain envelope: pads, bass and arp duck briefly after each kick. side = np.ones(n) for k in kicks: i0 = at(k) length = min(n - i0, at(0.4)) if length <= 0: continue tt = np.arange(length) / SR side[i0:i0 + length] = np.minimum(side[i0:i0 + length], 1 - 0.55 * np.exp(-tt * 9)) # Bass: offbeat 8ths on the chord root through the groove, long root in the launch bars. def bass_note(m, start, dur, gain): length = at(dur + 0.05) tt = np.arange(length) / SR f = hz(m) sig = np.sin(2 * np.pi * f * tt) + 0.35 * np.sin(2 * np.pi * 2 * f * tt) + 0.12 * np.sin(2 * np.pi * 3 * f * tt) e = np.clip(tt / 0.008, 0, 1) * np.clip((dur + 0.05 - tt) / 0.05, 0, 1) * np.exp(-tt * 1.5) add("bass", at(start), sig * e * gain) bass_note(chord_at(INTRO_END)[0] - 12, bar_t(INTRO_END), BAR * 2 - 0.1, 0.22) for bb in range(GROOVE_START, BUILD): root = chord_at(bb)[0] - 12 for e8 in range(8): if e8 % 2 == 1 or e8 == 0: bass_note(root + (12 if e8 == 7 else 0), bar_t(bb) + e8 * BEAT / 2, BEAT / 2 - 0.03, 0.2) # Build: sustained root, rising an octave by sliding in steps. for k in range(8): bass_note(chord_at(BUILD)[0] - 12 + k, bs + k * BAR / 4, BAR / 4 - 0.02, 0.12 + 0.02 * k) # Outro: deep sustained sub. bass_note(38 - 12, bar_t(OUTRO), 6.0, 0.3) # Plucked arp: 16ths from the launch on, pattern up the chord, brightness grows. def pluck(m, start, gain, bright, pan): length = at(0.6) tt = np.arange(length) / SR f = hz(m) sig = np.zeros(length) for h in range(1, 7): sig += np.sin(2 * np.pi * f * h * tt) * np.exp(-tt * (6 + h * h * 2.5 / bright)) / h sig *= np.clip(tt / 0.003, 0, 1) add("arp", at(start), sig * gain, pan) k = 0 s = bar_t(INTRO_END) end_arp = bar_t(OUTRO) + BAR * 2 while s < end_arp: b_now = s / BAR notes = chord_at(b_now) pattern = [notes[1], notes[2], notes[3], notes[4], notes[3] + 12, notes[4], notes[2] + 12, notes[3]] m = pattern[k % len(pattern)] + 12 if b_now < GROOVE_START: gain, bright, every = 0.05, 0.7, 2 elif b_now < BUILD: gain, bright, every = 0.06, 0.8 + 0.6 * (b_now - GROOVE_START) / max(1, BUILD - GROOVE_START), 1 elif b_now < OUTRO: gain, bright, every = 0.07, 1.6 + (b_now - BUILD), 1 else: gain, bright, every = 0.05 * max(0, 1 - (b_now - OUTRO) / 2), 1.4, 2 if k % every == 0: pluck(m, s, gain * (1.0 if k % 4 == 0 else 0.75), bright, 0.45 * math.sin(k * 0.8)) s += BEAT / 4 k += 1 # Bells: sparse in the intro, a cascade on the outro hit. def bell(m, start, gain, pan=0.0, decay=1.6): length = at(3.0) tt = np.arange(length) / SR f = hz(m) sig = np.sin(2 * np.pi * f * tt) + 0.35 * np.sin(2 * np.pi * f * 2.76 * tt) * np.exp(-tt * 4) sig += 0.12 * np.sin(2 * np.pi * f * 5.4 * tt) * np.exp(-tt * 8) add("bell", at(start), sig * np.exp(-tt * decay) * np.clip(tt / 0.004, 0, 1) * gain, pan) for i, m in enumerate([81, 76, 78, 74, 81, 83]): bell(m, 0.8 + i * 1.2, 0.05, 0.4 * math.sin(i * 1.3)) for i, m in enumerate([74, 78, 81, 86, 90, 93, 98]): bell(m, bar_t(OUTRO) + i * BEAT / 2, 0.06, 0.5 * math.sin(i), decay=1.1) # FX: risers into the launch and the outro, soft swells into each feature scene, impacts. def riser(end, dur, gain, top=2400): length = at(dur) tt = np.arange(length) / SR p = tt / dur f = 180 * (top / 180) ** p sig = np.zeros(length) for d in (-5, 0, 7): sig += np.sin(2 * np.pi * np.cumsum(f * 2 ** (d / 1200)) / SR) sig *= p ** 2 * gain add("fx", at(end - dur), sig) def impact(start, gain): length = at(3.5) tt = np.arange(length) / SR boom = np.sin(2 * np.pi * np.cumsum(30 + 60 * np.exp(-tt * 6)) / SR) * np.exp(-tt * 1.4) shimmer = sum(np.sin(2 * np.pi * f * tt) * np.exp(-tt * 2.2) for f in (1174.7, 1480, 1760, 2349)) * 0.05 add("fx", at(start), (boom * 0.6 + shimmer) * gain) riser(bar_t(INTRO_END), BAR * 1.5, 0.06) impact(bar_t(INTRO_END), 0.9) for sc in scenes: sb = round(sc["start"] / BAR) if GROOVE_START < sb < BUILD: riser(sc["start"], BEAT * 2, 0.025, top=1600) riser(bar_t(OUTRO), BAR * 2, 0.09, top=3200) impact(bar_t(OUTRO), 1.3) # ---------------------------------------------------------------- mix for bus in ("pad", "bass", "arp"): L[bus] *= side R[bus] *= side # Ping pong delay on the arp and bells (dotted eighth). delay = at(BEAT * 0.75) for bus in ("arp", "bell"): dl, dr = np.zeros(n), np.zeros(n) for i in range(4): shift = delay * (i + 1) g = 0.35 ** (i + 1) src_l, src_r = (R[bus], L[bus]) if i % 2 == 0 else (L[bus], R[bus]) dl[shift:] += src_l[:-shift] * g dr[shift:] += src_r[:-shift] * g L[bus] += dl * 0.6 R[bus] += dr * 0.6 # Reverb (smooth partial-based IR, no noise). ir_len = at(3.4) it = np.arange(ir_len) / SR irl, irr = np.zeros(ir_len), np.zeros(ir_len) for f in rng.uniform(200, 2600, 100): ph = rng.uniform(0, 2 * np.pi, 2) d = np.exp(-it * rng.uniform(1.0, 2.0)) irl += np.sin(2 * np.pi * f * it + ph[0]) * d irr += np.sin(2 * np.pi * f * it + ph[1]) * d irl /= np.max(np.abs(irl)) irr /= np.max(np.abs(irr)) def convolve(x, ir): size = 1 << int(np.ceil(np.log2(len(x) + len(ir)))) return np.fft.irfft(np.fft.rfft(x, size) * np.fft.rfft(ir, size), size)[: len(x)] def norm(x): return x / (np.max(np.abs(x)) + 1e-9) LEVEL = {"pad": 0.55, "bass": 0.7, "arp": 0.55, "drums": 0.8, "fx": 0.6, "bell": 0.5} SEND = {"pad": 0.5, "arp": 0.35, "bell": 0.7, "fx": 0.5, "drums": 0.05, "bass": 0.0} ml, mr = np.zeros(n), np.zeros(n) send_l, send_r = np.zeros(n), np.zeros(n) peak = max(np.max(np.abs(L[b])) + np.max(np.abs(R[b])) for b in L) / 2 for bus in L: gl, gr = L[bus] / peak * LEVEL[bus], R[bus] / peak * LEVEL[bus] ml += gl mr += gr send_l += gl * SEND[bus] send_r += gr * SEND[bus] wet_l = norm(convolve(send_l, irl)) * np.max(np.abs(send_l)) * 0.9 wet_r = norm(convolve(send_r, irr)) * np.max(np.abs(send_r)) * 0.9 ml += wet_l mr += wet_r master = np.minimum(1, t / 1.5) * np.clip((TOTAL + 0.2 - t) / 3.5, 0, 1) ml *= master mr *= master pk = max(np.max(np.abs(ml)), np.max(np.abs(mr))) ml = np.tanh(ml / pk * 1.4) * 0.85 mr = np.tanh(mr / pk * 1.4) * 0.85 out = ROOT / "public/launch-vo/music.wav" with wave.open(str(out), "wb") as w: w.setnchannels(2) w.setsampwidth(2) w.setframerate(SR) w.writeframes((np.stack([ml, mr], axis=1) * 32767).astype("