Spaces:
Runtime error
Runtime error
Download renderer/scripts/make_launch_music.py from firebird-technologies/NoteStack-Test: direct link, hf CLI and curl.
- Browser
- Download file 13.3 kB
-
https://huggingface.co/spaces/firebird-technologies/NoteStack-Test/resolve/main/renderer/scripts/make_launch_music.py
- Command line
-
hf download hf://spaces/firebird-technologies/NoteStack-Test/renderer/scripts/make_launch_music.py
-
curl -L -o make_launch_music.py https://huggingface.co/spaces/firebird-technologies/NoteStack-Test/resolve/main/renderer/scripts/make_launch_music.py
13.3 kB
| """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("<i2").tobytes()) | |
| print("music ok", out, round(TOTAL, 2), "s") | |