#!/usr/bin/env python3 # ═════════════════════════════════════════════════════════════════════════════ # PLAYER COMPUTER — Courier (19/32) # by Gene Kogan · 2026 · https://genekogan.com/player_computer/courier # # A courier has forty minutes to cross the city and a package that weighs nothing. # # This single file IS the piece: it draws every frame, synthesizes every sound, # and muxes them into the final video with ffmpeg. No other project files are # needed. You (or your agent) are invited to make a VARIATION of it: # # Generate a variation of this music video using only code. # Start from https://genekogan.com/player_computer/code/courier.py.txt # # The original render (for reference, yours should differ): # video: https://genekogan.com/player_computer/media/courier.mp4 # cover: https://genekogan.com/player_computer/media/courier.jpg # # Requirements: python3, numpy, pillow, and ffmpeg on PATH. # pip install numpy "pillow<13" # Speech/vocals (in pieces that have them) use the macOS `say` command; on # other platforms swap in espeak-ng / any TTS at the say_wav()/speak() calls, # or mute those lines. git provenance stamps degrade gracefully outside a repo. # Run: python3 courier.py (writes frames/, audio/, and the final mp4 # next to the script; writes ~8 GB of frames, # takes 5-15 min on a modern machine) # ═════════════════════════════════════════════════════════════════════════════ """ player_computer_final — "COURIER" (final cut) Neurofunk drum & bass, 174bpm, F# minor. 50 bars, instrumental (~71s). Intro(4) -> Drop1(16) -> Break(4) -> Drop3(16) -> Break2(4) -> Out(6) Round 2 is a re-marriage. Round 1 kept these visuals but swapped the score for a dnb_ytp-style roller and cut the arrangement to 42 bars; Gene preferred the music of the ORIGINAL side_quests version, so the whole synthesis half — SECTIONS, build_song(), and the shot PLAN that hangs off the section names — is lifted back wholesale from renders/side_quests/courier/render.py: three different drops with three different amen edit tables, the four-saw neuro reese with its moving formant, the half-time breaks whose chords actually move (F#m9 | Dmaj7 | Amaj7 | Esus4) with a tune over the top, risers into every drop and sub impacts out of every break. FINAL CUT — recomposed from 80 bars (1:53) to 50 bars (1:11). Not a speed-up and not a truncation. The arrangement is SECTIONS-driven end to end — build_song() reads it bar by bar and build_shots() re-cuts the film against it — so the cut is made by rewriting the map and letting both halves re-derive: * one whole drop is GONE. The middle drop (drop2, the least distinct of the three amen edit tables) is dropped entirely, and the piece keeps the first drop and the third — the hardest edit table, and the PLAN with the fastest cuts — so the two drops that remain are the two best ones. * the intro is halved, 8 bars -> 4. The four-bar riser still fills exactly the whole of it, so drop 1 arrives on the same gesture, sooner. * both half-time breaks go 8 bars -> 4: exactly one turn of the chord loop (F#m9 | Dmaj7 | Amaj7 | Esus4) instead of two. Nothing is cut mid-phrase. * the outro is 8 -> 6 bars, still landing on SIGNED FOR. Everything else about the score is untouched: the same reese, the same three edit tables (two of them now), the same risers/crashes/sub-impacts — now placed off the section map rather than at hard-coded bar numbers — and the same punch-pass production chain (per-track sidechain, tight low end, drive 1.20 / ceiling 0.922). The round-1 Strobe fix (guaranteed spoke count, energy-fattened widths, half-step ghost spokes) is kept. Delivered native 1920x1080: the vector-neon engines are authored on a 1280x720 frame and rasterised through S = 1.5 with a ScaledDraw proxy, so every line keeps its weight, the glow and the chromatic tear scale with the frame, and the scanline is derived from the authored row instead of becoming static. The section-name / timecode strip along the bottom is gone (renderer debug); the route graph's REROUTING label, its 40-minute countdown and the package's DO NOT BEND / EMPTY are the fiction and stay. A courier has forty minutes to cross the city. The piece is the crossing: wireframe streets coming at you at speed, a rider flat on the tank, a rooftop someone else is running along, a route re-drawing itself every time the road closes, and the package, which is very light. Look: vector neon on black — perspective wireframe, hard strobe, chromatic tearing on the drops. Nothing here is a soft field; everything is a line. Every engine draws off W/H, so the streets, roofs and route graphs are resolution-independent; nothing is stretched. Composition: engine : audio-first x shot-parallel content: audio-groove (neurofunk kit, reese, amen edits) x ytp (glitch chain, error palette, strobe) x effects-post Run from repo root: python3 renders/player_computer_final/courier/render.py --sheet python3 renders/player_computer_final/courier/render.py """ import argparse, datetime, hashlib, math, os, subprocess, wave from pathlib import Path import numpy as np from PIL import Image, ImageDraw, ImageFont, ImageFilter NAME = "courier" TITLE = "COURIER" SETNUM = "02" # ── delivery scale ─────────────────────────────────────────────────────────── # W/H stay the authored frame — every engine below composes against them and # is resolution-independent. OW/OH are real delivery pixels and S = OH/H is the # one number the *rasterisation* scales by: the ScaledDraw proxy multiplies # geometry and stroke widths, font() scales sizes once, glow/tear/grain/ # scanline all derive from S. Nothing is upscaled after the fact. W, H, FPS = 1280, 720, 30 OW, OH = 1920, 1080 S = OH / H def P(v): return int(round(v*S)) def B(r): return r*S def _sxy(v, sc): if isinstance(v, (list, tuple)): return [_sxy(u, sc) for u in v] return v*sc class ScaledDraw: """ImageDraw proxy: authored frame units in, pixels out. Only the first positional arg (xy) and `width` are touched — arc/chord/pieslice take angles positionally and those must pass through untouched.""" __slots__ = ("_d", "_s") _GEOM = frozenset(("line", "rectangle", "rounded_rectangle", "ellipse", "polygon", "arc", "chord", "pieslice", "point", "text")) def __init__(self, d, sc): self._d, self._s = d, sc def __getattr__(self, name): f = getattr(self._d, name) if name not in self._GEOM: return f sc = self._s def wrapped(xy, *a, **kw): w = kw.get("width") if w is not None: kw["width"] = max(2, int(round(w*sc))) return f(_sxy(xy, sc), *a, **kw) return wrapped def mkdraw(im): d = ImageDraw.Draw(im) return d if S == 1.0 else ScaledDraw(d, S) BPM = 174.0 BEAT = 60.0 / BPM BAR = 4 * BEAT SR = 44100 OUT = Path(__file__).parent FRAMES = OUT / "frames"; FRAMES.mkdir(exist_ok=True) AUD = OUT / "audio"; AUD.mkdir(exist_ok=True) ROOT = Path(__file__).resolve().parent # standalone: was repo root (used for git provenance) FONTS = ROOT / "fonts" # FINAL CUT: 50 bars, down from 80. drop2 is gone entirely; the intro is # halved; both breaks are one turn of the chord loop instead of two; the outro # loses two bars. The two drops that remain are the first and the third — the # hardest edit table and the fastest shot plan — so the piece is all arrival. SECTIONS = [ ("intro", 0, 4), ("drop1", 4, 20), ("break", 20, 24), ("drop3", 24, 40), ("break2",40, 44), ("out", 44, 50), ] SEC_AT = {nm: (b0, b1) for nm, b0, b1 in SECTIONS} DROP_B0 = [b0 for nm, b0, b1 in SECTIONS if nm.startswith("drop")] BREAK_B0 = [b0 for nm, b0, b1 in SECTIONS if nm.startswith("break")] N_BARS = SECTIONS[-1][2] DUR = N_BARS * BAR + 2.5 N_FRAMES = int(DUR * FPS) MUSIC_DESC = f"neurofunk drum & bass, {BPM:.0f}bpm, F# minor, {N_BARS} bars, instrumental" ENGINE_DESC = "rush / rider / roof / route / package / strobe (vector neon)" def mtof(m): return 440.0 * 2.0 ** ((m - 69) / 12.0) _PC = {"C":0,"C#":1,"Db":1,"D":2,"D#":3,"Eb":3,"E":4,"F":5,"F#":6,"Gb":6, "G":7,"G#":8,"Ab":8,"A":9,"A#":10,"Bb":10,"B":11} def nf(name): i = 2 if (len(name) > 2 and name[1] in "#b") else 1 return mtof(12 * (int(name[i:]) + 1) + _PC[name[:i]]) def adsr(n, a, d, s, r): e = np.zeros(n) ai, di, ri = max(1, int(a*SR)), max(1, int(d*SR)), max(1, int(r*SR)) ai = min(ai, n); e[:ai] = np.linspace(0, 1, ai) if ai < n: dd = min(di, n - ai) e[ai:ai+dd] = np.linspace(1, s, dd); e[ai+dd:] = s if ri < n: e[-ri:] *= np.linspace(1, 0, ri) return e def voice(freq, dur, kind="saw", nh=26, c0=5200, c1=700, ck=8.0, res=0.0, detune=(0.0,), a=.005, d=.09, s=.7, r=.10, vib=(0.0, 0.0), seed=0): """Additive voice through a *moving* emulated filter (cutoff array). The cutoff glides c0->c1 at rate ck; `res` bumps harmonics near the cutoff. This is what gives plucks, reeses and stabs their motion. """ n = int(dur * SR) if n <= 0: return np.zeros(0) t = np.arange(n) / SR co = c1 + (c0 - c1) * np.exp(-t * ck) rng = np.random.RandomState(seed) out = np.zeros(n) vd, vr = vib for det in detune: f0 = freq * (1 + det * 0.006) for k in range(1, nh + 1): if kind == "saw": base = 1.0 / k elif kind == "square": base = (1.0 / k) if k % 2 else 0.0 elif kind == "tri": base = (1.0 / (k*k)) if k % 2 else 0.0 elif kind == "sine": base = 1.0 if k == 1 else 0.0 else: base = 1.0 / k if base == 0.0: continue fk = f0 * k if fk > SR * 0.45: break g = base / np.sqrt(1.0 + (fk / co) ** 4) if res: g = g + res * base * np.exp(-((fk - co) / (0.3 * co + 1)) ** 2) ph = rng.uniform(0, 2*np.pi) phase = 2*np.pi*fk*t + ph if vd: phase = phase + vd * np.sin(2*np.pi*vr*t) out += g * np.sin(phase) out /= len(detune) return out * adsr(n, a, d, s, r) def fm(freq, dur, ratio=2.0, index=4.0, idec=6.0, a=.002, d=.4, s=.0, r=.2, seed=0): """2-op FM — rhodes / bells / glassy leads.""" n = int(dur*SR); t = np.arange(n)/SR mod = np.sin(2*np.pi*freq*ratio*t) * index * np.exp(-t*idec) return np.sin(2*np.pi*freq*t + mod) * adsr(n, a, d, s, r) def ks(freq, dur, damp=0.996, seed=0): """Karplus-Strong pluck — guitar / harp.""" n = int(dur*SR); L = max(2, int(SR/freq)) rng = np.random.RandomState(seed) buf = rng.uniform(-1, 1, L) out = np.zeros(n); j = 0 for i in range(n): out[i] = buf[j] buf[j] = damp * 0.5 * (buf[j] + buf[(j+1) % L]) j = (j+1) % L return out * adsr(n, .001, .05, .85, .25) def bandshape(x, lo=0.0, hi=0.0, order=4): """Exact FFT band shaping. Noise sources go through this so nothing in the kit is a raw full-band blast (AESTHETIC 13a).""" n = len(x) if n < 8: return x X = np.fft.rfft(x); fq = np.maximum(np.fft.rfftfreq(n, 1/SR), 1e-6) g = np.ones_like(fq) if lo: g *= 1.0/np.sqrt(1.0 + (lo/fq)**order) if hi: g *= 1.0/np.sqrt(1.0 + (fq/hi)**order) return np.fft.irfft(X*g, n) def kick(dur=.30, f0=155, f1=48, punch=30, click=.5, seed=1): """Round-2 punch pass. Three separately-enveloped transient layers (HF tick, mid beater, broadband snap) sit ON TOP of a saturated body — the body gets the harmonic drive that makes it read on a phone, the click stays clean and peaky so the transient survives the master limiter. The body decay is tightened (10.5 -> 13) and the weight it loses is given back by a separate, slower sub tail at f1, which is a *tail* and so never blunts the attack.""" n = int(dur*SR); t = np.arange(n)/SR f = f1 + (f0-f1)*np.exp(-t*punch) body = np.sin(2*np.pi*np.cumsum(f)/SR) * np.exp(-t*13.0) body = np.tanh(body*2.5)/np.tanh(2.5) tail = np.sin(2*np.pi*f1*t) * np.exp(-t*9.5) rng = np.random.RandomState(seed) tick = bandshape(rng.randn(n), lo=4200, hi=11000) * np.exp(-t*820) beater = bandshape(rng.randn(n), lo= 900, hi= 3800) * np.exp(-t*360) snap = rng.randn(n) * np.exp(-t*300) ck = (snap*.55 + beater*1.5 + tick*2.0) * click return (body*.90 + tail*.26 + ck) * .78 def snare(dur=.22, tone=196, bright=1.0, clap=1.0, seed=2): """Crack = a fast, tightly-enveloped 2.6-9.5k noise burst layered over the original body/rattle, plus a 4-burst clap tail (first burst exactly on the beat, so the hit does not move). Ghosts pass clap=0 and stay tight.""" n = int(dur*SR); t = np.arange(n)/SR rng = np.random.RandomState(seed) nz = bandshape(rng.randn(n), lo= 280, hi=6200) crack = bandshape(rng.randn(n), lo=2600, hi=9500) body = np.sin(2*np.pi*tone*t) + .6*np.sin(2*np.pi*tone*1.58*t) cl = np.zeros(n) if clap: for j, off in enumerate((0.0, .0062, .0121, .0179)): i = int(off*SR) if i >= n: break seg = bandshape(rng.randn(n-i), lo=900, hi=5200) cl[i:] += seg*np.exp(-np.arange(n-i)/SR*(90 if j == 0 else 150))*(1.0 if j == 0 else .5) y = (nz*np.exp(-t*21)*.78*bright + crack*np.exp(-t*110)*1.05*bright + cl*.26*clap*bright + body*np.exp(-t*30)*.46) return np.tanh(y*1.4)/np.tanh(1.4) def hat(dur=.055, openh=False, seed=7): n = int(dur*SR); t = np.arange(n)/SR nz = bandshape(np.random.RandomState(seed).randn(n), lo=5200, hi=9800) return nz * np.exp(-t*(14 if openh else 85)) * .40 def ride(dur=.6, seed=9): n = int(dur*SR); t = np.arange(n)/SR bell = sum(np.sin(2*np.pi*f*t) for f in (522, 831, 1180, 1567, 2103)) nz = bandshape(np.random.RandomState(seed).randn(n), lo=3800, hi=9000) return bell*np.exp(-t*10)*.10 + nz*np.exp(-t*5)*.16 def rim(dur=.09, seed=3): n = int(dur*SR); t = np.arange(n)/SR return (np.sin(2*np.pi*1750*t) + .5*np.sin(2*np.pi*2600*t)) * np.exp(-t*90) * .5 def shaker(dur=.09, seed=5): n = int(dur*SR); t = np.arange(n)/SR nz = bandshape(np.random.RandomState(seed).randn(n), lo=3600, hi=8600) return nz * (np.exp(-t*40) * np.clip(t*260, 0, 1)) * .40 def crash(dur=1.6, seed=13): n = int(dur*SR); t = np.arange(n)/SR nz = bandshape(np.random.RandomState(seed).randn(n), lo=1400, hi=8200) return nz * (np.exp(-t*2.6) + .3*np.exp(-t*.6)) * .55 def riser(dur=2.0, seed=17): n = int(dur*SR); t = np.arange(n)/SR env = (t/dur) ** 1.7 sweep = np.sin(2*np.pi*np.cumsum(140 + 3000*(t/dur)**2)/SR) # noise through a band that *rises with the sweep* — pitched motion, # not a static full-band hiss (AESTHETIC 13a) rng = np.random.RandomState(seed) nz = np.zeros(n); blk = 2048 for i in range(0, n, blk): u = (i/max(1, n)) ** 1.4 fc = 300 + 5200*u seg = rng.randn(min(blk, n-i) + 256) nz[i:i+min(blk, n-i)] = bandshape(seg, lo=fc*.72, hi=fc*1.5)[:min(blk, n-i)] return (nz*env*.55 + sweep*env*.22) * .8 def vinyl(n, seed=23): """Surface noise: filtered hiss + sparse crackle.""" rng = np.random.RandomState(seed) hiss = bandshape(rng.randn(n), lo=140, hi=5200) * .022 cr = np.zeros(n) idx = rng.choice(n, size=max(1, n//2400), replace=False) cr[idx] = rng.uniform(-1, 1, len(idx)) * .10 cr = np.convolve(cr, np.exp(-np.arange(60)/9), "same") return hiss + cr def reverb(x, rt=1.6, mix=.3, seed=29, pre=0.02): """FFT convolution with a synthetic exponentially-decaying noise IR.""" n = int(rt*SR); t = np.arange(n)/SR rng = np.random.RandomState(seed) ir = rng.randn(n) * np.exp(-t*(5.0/rt)) ir[:int(pre*SR)] = 0 ir /= np.abs(ir).sum() / 40.0 + 1e-9 from numpy.fft import rfft, irfft L = 1 << int(np.ceil(np.log2(len(x) + n))) wet = irfft(rfft(x, L) * rfft(ir, L))[:len(x)] wet /= np.max(np.abs(wet)) + 1e-9 return x * (1-mix) + wet * mix * (np.max(np.abs(x)) + 1e-9) def delay(x, time=.25, fb=.38, mix=.25, taps=7): d = int(time*SR); out = x.copy() for i in range(1, taps+1): g = mix * (fb ** i); s = d*i if s >= len(x): break out[s:] += x[:len(x)-s] * g return out def lowpass(x, fc): a = np.exp(-2*np.pi*fc/SR); z = 0.0; y = np.empty_like(x) for i in range(len(x)): z = (1-a)*x[i] + a*z; y[i] = z return y # ── round-2 mix chain ─────────────────────────────────────────────────────── # Whole-track filters. Real gain, zero phase (the response is real and even), # so a 200Hz high-pass does not phase-smear the drum transient the way a # cascade of one-poles would. Padded to a power of two: a 5M-sample rfft on a # mixed-radix length is minutes; padded it is under a second. def _pow2(n): return 1 << int(np.ceil(np.log2(max(2, n*2)))) def hp(x, fc, order=4): n = len(x); L = _pow2(n) fq = np.maximum(np.fft.rfftfreq(L, 1/SR), 1e-6) return np.fft.irfft(np.fft.rfft(x, L)/np.sqrt(1.0+(fc/fq)**order), L)[:n] def lp(x, fc, order=4): n = len(x); L = _pow2(n) fq = np.maximum(np.fft.rfftfreq(L, 1/SR), 1e-6) return np.fft.irfft(np.fft.rfft(x, L)/np.sqrt(1.0+(fq/fc)**order), L)[:n] def _smooth(x, tau): """One-pole release, run as an FFT convolution so it stays vectorised.""" k = max(2, int(tau*SR*5)) h = np.exp(-np.arange(k)/(tau*SR)); h /= h.sum() L = _pow2(len(x)+k) return np.fft.irfft(np.fft.rfft(x, L)*np.fft.rfft(h, L), L)[:len(x)] def compress(x, thresh=.10, ratio=5.0, atk=.004, rel=.11): """Feed-forward peak compressor. The detector is max(fast, slow) rectified envelope, so attack is fast on hits and release is slow between them.""" a = np.abs(x) env = np.maximum(_smooth(a, rel), _smooth(a, atk)) g = np.where(env > thresh, (thresh + (env-thresh)/ratio)/np.maximum(env, 1e-9), 1.0) return x*g def _runmin(x, w): """Sliding minimum over a centred window of width w, chunked for memory.""" from numpy.lib.stride_tricks import sliding_window_view half = w//2 p = np.concatenate([np.full(half, x[0]), x, np.full(half, x[-1])]) out = np.empty(len(x)); step = 1 << 20 for i in range(0, len(x), step): j = min(len(x), i+step) out[i:j] = sliding_window_view(p[i:j+2*half], w).min(-1) return out def _fftconv(x, h): L = _pow2(len(x)+len(h)) return np.fft.irfft(np.fft.rfft(x, L)*np.fft.rfft(h, L), L)[:len(x)+len(h)-1] def tp_env(x, os=4, taps=24): """True-peak envelope: |x| evaluated at 1/os-sample offsets via short windowed-sinc interpolation, maxed together. Sample peaks are not what clips a lossy encoder — inter-sample peaks are, and a click-heavy drum bus overshoots hard between samples. Measured on the first pass of this mix the sample peak was a tidy -0.5 dBFS while the TRUE peak was +2.1 dBFS.""" k = np.arange(-taps, taps+1); w = np.hamming(2*taps+1) m = np.abs(x) for o in range(1, os): h = np.sinc(k - o/os)*w; h /= h.sum() m = np.maximum(m, np.abs(_fftconv(x, h)[taps:taps+len(x)])) return m def limiter(x, ceiling=.944, look=.0025, truepeak=True): """Look-ahead peak limiter, and an exact one: the required per-sample gain r is reduced to its running minimum m over +/-la, then smoothed with a Hann of half-width la. Every term of that weighted average is a min over a window containing i, so the smoothed gain is <= r[i] everywhere — the ceiling is provably held without a clipper, which is the whole point: a tanh master squashes every transient, a limiter only touches the ones that overshoot. The detector runs on the true-peak envelope, so the ceiling is a dBTP one.""" if x.ndim > 1: pk = np.maximum.reduce([tp_env(x[:, c]) if truepeak else np.abs(x[:, c]) for c in range(x.shape[1])]) else: pk = tp_env(x) if truepeak else np.abs(x) r = np.minimum(1.0, ceiling/np.maximum(pk, 1e-9)) la = max(1, int(look*SR)) w = np.hanning(2*la+1); w /= w.sum() g = np.convolve(_runmin(r, 2*la+1), w, "same") return x*(g[:, None] if x.ndim > 1 else g) def drumbus(x, par=.10, drive=1.02, out=1.0): """Parallel compression for weight + a touch of drive for glue. The dry path is untouched, so the transient keeps its full height; the compressed path only fills the gaps between hits. Both are deliberately light — measured, par=.40/drive=1.20 cost 0.4dB of drum-bus crest, which is precisely the punch this pass exists to add.""" y = x + par*compress(x, thresh=.09, ratio=5.5, atk=.004, rel=.12) return np.tanh(y*drive)/np.tanh(drive) * out def subshape(x, core_g=.62, ghost_g=.55): """Psychoacoustic bass exciter. Saturating the sub track wholesale is a trap: tanh over a whole track is an upward compressor that turns every quiet passage into a full-scale square, and measured it drove the non-drum bus from 76% to 91% sub-60Hz energy. Instead take ONLY the distortion products, band-limited to 100-420Hz, and add them beside a *reduced* fundamental — the sub then reads on a phone while owning less headroom.""" core = lp(hp(x, 34.0), 110.0) ghost = lp(hp(np.tanh(x*3.0), 100.0), 420.0) return core*core_g + ghost*ghost_g class Song: """A multitrack canvas placed on an absolute bar/beat grid.""" def __init__(self, dur): self.n = int(dur*SR) self.tr = {} self.kick_t = [] def t(self, bar, step=0, swing=0.0): """absolute seconds of 16th-step `step` inside `bar`.""" sw = swing * (BEAT/4) if (step % 2) else 0.0 return bar*BAR + step*(BEAT/4) + sw def put(self, track, sig, at, g=1.0, pan=0.0): b = self.tr.setdefault(track, np.zeros((self.n, 2))) i = int(at*SR); j = min(self.n, i+len(sig)) if i >= self.n or j <= i: return th = (pan*.5+.5) * (np.pi/2) st = np.stack([sig[:j-i]*np.cos(th), sig[:j-i]*np.sin(th)], 1) * g b[i:j] += st def bus(self, track, fn): if track in self.tr: b = self.tr[track] self.tr[track] = np.stack([fn(b[:,0]), fn(b[:,1])], 1) def sec_env(self, levels, glide=0.35): """Section dynamics: a smooth per-sample gain built from {section_name: level}. Arrangement alone tends to come out flat — this is the macro arc the ear actually follows.""" env = np.ones(self.n) for nm, b0, b1 in SECTIONS: i0, i1 = int(b0*BAR*SR), min(self.n, int(b1*BAR*SR)) if i1 > i0: env[i0:i1] = levels.get(nm, 1.0) env[int(SECTIONS[-1][2]*BAR*SR):] = levels.get(SECTIONS[-1][0], 1.0) k = max(1, int(glide*SR)) return np.convolve(env, np.ones(k)/k, "same") def mixdown(self, gains, pump_depth=.30, pump_rel=.16, levels=None, pump_tracks=None, drive=1.30, ceiling=.944): """Round 2: the sidechain is applied PER TRACK, not to the finished mix. Ducking the whole mix (what round 1 did) drops the kick by exactly as much as it drops the bass, so nothing pumps in *relation* to anything — measured, the mids were +1.5 dB LOUDER right after the kick than between kicks. Restricting the duck to `pump_tracks` lets the kick keep its full height while the reese and sub get out of its way, which is both the pump you can hear and several dB of headroom back.""" pump = None if self.kick_t: env = np.ones(self.n); rl = int(pump_rel*SR) shape = 1 - pump_depth*np.exp(-np.arange(rl)/(pump_rel*SR/4)) for at in self.kick_t: i = int(at*SR); j = min(self.n, i+rl) if i < self.n: env[i:j] = np.minimum(env[i:j], shape[:j-i]) # 128 taps, not 320: enough to de-zipper the duck, short enough that # it does not start audibly before the kick it belongs to pump = np.convolve(env, np.ones(128)/128, "same") mix = np.zeros((self.n, 2)) for k, b in self.tr.items(): g = b * gains.get(k, 1.0) if pump is not None and (pump_tracks is None or k in pump_tracks): g = g * pump[:, None] mix += g if levels: mix *= self.sec_env(levels)[:, None] # DC / sub-28Hz rumble trim — zero-phase, so it costs no transient mix = np.stack([hp(mix[:, 0], 28.0), hp(mix[:, 1], 28.0)], 1) # drive the top 0.1% of samples `drive` above the ceiling and limit, # rather than tanh-ing the whole file: only real overshoots pay mix = mix / (np.percentile(np.abs(mix), 99.9) + 1e-9) * drive return limiter(mix, ceiling=ceiling) def write(self, path, mix): with wave.open(str(path), "w") as w: w.setnchannels(2); w.setsampwidth(2); w.setframerate(SR) w.writeframes((np.clip(mix, -1, 1)*32767).astype(" macOS `say` (the canonical voices) -> espeak-ng / # espeak (Linux; language mapped from the say voice name, rate is wpm in both) # -> Windows SAPI (default voice, rate mapped from wpm) -> timed silence as # the last resort (duration from a chars/wpm heuristic, loud warning, never # cached so a later run with an engine present re-voices). A re-voiced film is # a different performance of the same score; that is by design. # Force a tier with POOP_TTS=say|espeak|sapi|none. def _tts_lang(voice): v = str(voice) if "Spanish" in v: return "es-mx" if "Mexico" in v else "es" if "Portuguese" in v: return "pt-br" if "Brazil" in v else "pt" if "English (UK)" in v: return "en-gb" return "en-us" def _tts_engine(): import shutil, platform want = os.environ.get("POOP_TTS", "").strip().lower() if want: return want if shutil.which("say"): return "say" if shutil.which("espeak-ng") or shutil.which("espeak"): return "espeak" if platform.system() == "Windows": return "sapi" return "none" def _tts_render(text, voice, rate, path): """Synthesize text -> mono 44.1k wav at `path` with the best available engine. Returns False if no engine (caller falls back to timed silence).""" import shutil, sys, base64 eng = _tts_engine() tmp = path.with_suffix(".tts.wav") try: if eng == "say": aiff = path.with_suffix(".aiff") subprocess.run(["say", "-v", voice, "-r", str(rate), "-o", str(aiff), text], check=True) subprocess.run(["ffmpeg", "-y", "-i", str(aiff), "-ar", str(SR), "-ac", "1", str(path)], check=True, capture_output=True) aiff.unlink(missing_ok=True) return True if eng == "espeak": exe = shutil.which("espeak-ng") or shutil.which("espeak") or "espeak-ng" subprocess.run([exe, "-v", _tts_lang(voice), "-s", str(int(rate)), "-w", str(tmp), str(text)], check=True) elif eng == "sapi": r = max(-10, min(10, round((int(rate) - 175) / 25))) esc = str(text).replace("'", "''") ps = ("Add-Type -AssemblyName System.Speech;" "$s=New-Object System.Speech.Synthesis.SpeechSynthesizer;" f"$s.Rate={r};$s.SetOutputToWaveFile('{tmp}');" f"$s.Speak('{esc}');$s.Dispose()") enc = base64.b64encode(ps.encode("utf-16-le")).decode() subprocess.run(["powershell", "-NoProfile", "-EncodedCommand", enc], check=True) else: return False subprocess.run(["ffmpeg", "-y", "-i", str(tmp), "-ar", str(SR), "-ac", "1", str(path)], check=True, capture_output=True) return True except Exception as e: print(f"[tts] {eng} failed ({e}) — falling back to timed silence", file=sys.stderr) return False finally: tmp.unlink(missing_ok=True) def _tts_silence(text, rate): import sys dur = max(0.6, len(str(text)) / (max(60, int(rate)) * 5.0 / 60.0)) print(f'[tts] no speech engine — timed silence ({dur:.2f}s): ' f'"{str(text)[:48]}"', file=sys.stderr) return np.zeros(int(dur * SR)) def say_wav(text, voice, rate, path): """text -> mono 44.1k voice wav (cached on disk; deterministic per engine).""" path = Path(path) if not path.exists(): if not _tts_render(text, voice, rate, path): return _tts_silence(text, rate) return read_wav(path) def fit(x, n): """Resample to exactly n samples. Shifts formants a little; pitch is the carrier's job, so this is free.""" if len(x) < 2: return np.zeros(n) return np.interp(np.linspace(0, len(x)-1, n), np.arange(len(x)), x) def carrier(f_per_sample, nh=30, detune=(0.0, -0.55, 0.62), vib=(0.0, 0.0)): """Band-limited additive carrier with continuous phase across note changes.""" n = len(f_per_sample) t = np.arange(n)/SR out = np.zeros(n) for d in detune: f = f_per_sample*(1 + d*0.005) if vib[0]: f = f*(1 + vib[0]*np.sin(2*np.pi*vib[1]*t)) ph = 2*np.pi*np.cumsum(f)/SR for k in range(1, nh+1): live = (f*k) < SR*0.45 if not live.any(): break out += np.sin(ph*k)/k * live return out/len(detune) def vocode(mod, car, nfft=1024, hop=256, bands=26, lo=110, hi=6500, gmax=12.0, rel=0.55, sib=0.06, tilt=4200.0): """Transfer mod's band envelope onto car. Gains are clamped and the band set is bounded — an unclamped vocoder turns carrier aliasing into hiss.""" n = max(len(mod), len(car)) mod = np.pad(mod, (0, n-len(mod))); car = np.pad(car, (0, n-len(car))) win = np.hanning(nfft); nfr = 1 + max(0, (n-nfft))//hop fr = np.fft.rfftfreq(nfft, 1/SR) edges = np.geomspace(lo, hi, bands+1) idx = [np.where((fr >= edges[b]) & (fr < edges[b+1]))[0] for b in range(bands)] keep = np.zeros(len(fr), bool) for ii in idx: keep[ii] = True out = np.zeros(n); wsum = np.zeros(n)+1e-9 prev = np.zeros(bands) for f in range(nfr): s = f*hop M = np.fft.rfft(mod[s:s+nfft]*win); C = np.fft.rfft(car[s:s+nfft]*win) am = np.abs(M); ac = np.abs(C) g = np.zeros(len(fr)) for b, ii in enumerate(idx): if not len(ii): continue em = np.sqrt((am[ii]**2).mean()); ec = np.sqrt((ac[ii]**2).mean()) gb = np.clip(em/(ec+1e-4), 0, gmax) gb = prev[b]*rel + gb*(1-rel) prev[b] = gb; g[ii] = gb out[s:s+nfft] += np.fft.irfft(C*g*keep)*win wsum[s:s+nfft] += win**2 # floor the window sum: at the ramp-in/out edges it -> 0 and the divide # detonates into a single enormous spike ws = np.maximum(wsum, 0.35*np.median(wsum[nfft:max(nfft+1, n-nfft)])) y = out/ws y[:hop] = 0.0; y[-hop:] = 0.0 y /= (np.max(np.abs(y))+1e-9) hp = np.zeros_like(mod); hp[1:] = mod[1:]-mod[:-1] for _ in range(2): hp = np.convolve(hp, [1, -0.93], "same") hp = np.clip(hp/(np.percentile(np.abs(hp), 99.5)+1e-9), -1, 1) a = math.exp(-2*math.pi*tilt/SR); z = 0.0; lp = np.empty_like(y) for i in range(len(y)): z = (1-a)*y[i] + a*z; lp[i] = z y = 0.55*y + 0.85*lp + sib*hp return y/(np.max(np.abs(y))+1e-9) def sing(text, notes, dur, voice="Moira", rate=170, cache=None, nh=30, detune=(0.0, -0.55, 0.62), vib=(0.012, 5.2), gliss=0.012, **vk): """A sung line. `notes` = [(freq, weight), …] carved across `dur` seconds.""" n = int(dur*SR) key = cache/("say_"+_h(text, voice, rate)+".wav") mod = fit(say_wav(text, voice, rate, key), n) tot = sum(w for _, w in notes) or 1.0 f = np.zeros(n); at = 0 for i, (fq, w) in enumerate(notes): ln = int(n*w/tot) if i < len(notes)-1 else n-at f[at:at+ln] = fq; at += ln if gliss: # portamento: smooth the note edges k = max(3, int(gliss*SR)); f = np.convolve(f, np.ones(k)/k, "same") f[:k] = f[k]; f[-k:] = f[-k-1] car = carrier(f, nh=nh, detune=detune, vib=vib) return vocode(mod, car, **vk) def speak(text, dur=None, voice="Alex", rate=170, cache=None, pitch=1.0): """Plain spoken line (no vocoder) — for verses that shouldn't sing.""" key = cache/("say_"+_h(text, voice, rate)+".wav") x = say_wav(text, voice, rate, key) if pitch != 1.0: x = fit(x, int(len(x)/pitch)) if dur: x = fit(x, int(dur*SR)) if len(x) > int(dur*SR) else \ np.pad(x, (0, int(dur*SR)-len(x))) return x/(np.max(np.abs(x))+1e-9) # ════════════════════════════════════════════════════════════════════════════ # THE SONG # ════════════════════════════════════════════════════════════════════════════ SW = 0.0 ROOT_F = nf("F#1") SCALE = [0, 2, 3, 5, 7, 8, 10] # The dnb_ytp harmonic engine, transposed F -> F#: i | VI | III | VII # (F#m | D | A | E), TWO BARS PER CHORD, cycling through the whole piece — # drops included. dnb_ytp's bass roots climbed F1 / Db2 / Ab2 / Eb2; same # contour here (offsets 0 / +8 / +15 / +10 semitones from F#1). Chord tones # are offsets from F#3 (ROOT_F*4). CHORDS = [ (0, (0, 3, 7)), # F#m (0, (0, 3, 7)), (8, (-4, 0, 3)), # D (8, (-4, 0, 3)), (15, (3, 7, 10)), # A (15, (3, 7, 10)), (10, (-2, 2, 5)), # E (10, (-2, 2, 5)), ] def build_song(): s = Song(DUR) R = np.random.RandomState(1740) def sec_of(bar): for nm, a, b in SECTIONS: if a <= bar < b: return nm return SECTIONS[-1][0] def bar_in(bar): """Bar index *within* its section — the cut changes where every section starts, so nothing may key off an absolute bar number.""" for nm, a, b in SECTIONS: if a <= bar < b: return bar - a return bar - SECTIONS[-1][1] # three drops, three different break edits EDITS = { "drop1": [(0, "k", 1.0), (3, "s", .4), (4, "S", 1.0), (6, "k", .8), (10, "k", .9), (12, "S", 1.0), (14, "s", .5)], "drop2": [(0, "k", 1.0), (2, "s", .35), (5, "k", .7), (8, "S", 1.0), (11, "k", .9), (12, "s", .4), (14, "S", .8)], "drop3": [(0, "k", 1.0), (4, "S", 1.0), (7, "k", .8), (8, "k", .9), (10, "s", .5), (12, "S", 1.0), (15, "s", .6)], } for bar in range(N_BARS): sec = sec_of(bar) droppy = sec.startswith("drop") brk = sec.startswith("break") if droppy: for st, kind, v in EDITS[sec]: at = s.t(bar, st) if kind == "k": s.put("drums", kick(dur=.26, f0=150, f1=48, punch=36, click=.55)*v, at, g=.95) if v > .8: s.kick_t.append(at) elif kind == "S": s.put("drums", snare(dur=.24, tone=210, bright=1.25)*v, at, g=.85, pan=-.04) else: s.put("drums", snare(dur=.10, tone=250, bright=.7, clap=0)*v, at, g=.34, pan=.18) for st in range(0, 16, 2): s.put("drums", hat(dur=.038, openh=(st == 10)), s.t(bar, st+1), g=.20+.07*R.rand(), pan=-.35+.7*R.rand()) if bar % 8 == 7: for j, st in enumerate((12, 13, 14, 15)): s.put("drums", snare(dur=.09, tone=260, bright=1.0, clap=0)*(.5+.16*j), s.t(bar, st), g=.5, pan=-.3+.2*j) # the reese: detuned saw stack with a moving formant for st, ln in ((0, 2.0), (6, 1.0), (10, 1.4)): lfo = 900 + 620*math.sin(bar*0.9 + st*0.3) s.put("bass", voice(ROOT_F*2, BEAT*ln, kind="saw", nh=30, c0=lfo, c1=430, ck=2.0, res=.95, detune=(-1.8, -0.5, 0.6, 1.9), a=.004, d=.2, s=.88, r=.08, seed=bar*3+st), s.t(bar, st), g=.30) s.put("sub", voice(ROOT_F, BEAT*ln, kind="sine", nh=2, c0=200, c1=100, ck=3, a=.005, d=.2, s=.9, r=.08, seed=bar+st), s.t(bar, st), g=.36) # stab if bar % 4 in (0, 2): for k2, iv in enumerate((0, 3, 7)): s.put("stab", voice(ROOT_F*4*2**(iv/12), BEAT*.30, kind="square", nh=16, c0=4200, c1=1500, ck=14, res=.55, a=.002, d=.06, s=.25, r=.06, seed=bar*7+k2), s.t(bar, 12), g=.10, pan=-.3+.3*k2) elif brk: # half-time. The chord MOVES here — F#m9 / Dmaj7 / Amaj7 / Esus4-E — # all diatonic to F# minor, voiced high so nothing rubs the sub. BRK = [(0, (2, 7, 11, 14)), # F#m9 (-4, (4, 7, 11, 14)), # D maj7 (3, (4, 7, 11, 14)), # A maj7 (-2, (5, 7, 12, 14))] # E sus4 -> E rootiv, ivs = BRK[bar_in(bar) % 4] broot = ROOT_F*2**(rootiv/12.0) at = s.t(bar, 0) s.put("drums", kick(dur=.34, f0=160, f1=42, punch=22), at, g=.80) s.kick_t.append(at) s.put("drums", snare(dur=.34, tone=180, bright=1.0), s.t(bar, 8), g=.66) for st in (4, 12): s.put("drums", ride(), s.t(bar, st), g=.14, pan=.3) for k2, iv in enumerate(ivs): s.put("pad", voice(broot*4*2**(iv/12), BAR*1.25, kind="saw", nh=20, c0=1600, c1=760, ck=.8, detune=(-1.4, 0, 1.5), a=.5, d=.6, s=.75, r=.9, seed=bar*11+k2), s.t(bar, 0), g=.095, pan=-.5+.33*k2) # a line over the top so the break has a tune, not just a chord MELB = [14, 12, 11, 12, 14, 16, 14, 11] s.put("brklead", voice(broot*4*2**(MELB[(bar*2) % 8]/12.0), BEAT*2.6, kind="tri", nh=12, c0=2600, c1=1200, ck=2.0, vib=(.012, 4.6), a=.06, d=.4, s=.6, r=.5, seed=bar*23), s.t(bar, 4), g=.13, pan=.20) s.put("brklead", voice(broot*8*2**(MELB[(bar*2+1) % 8]/12.0), BEAT*1.6, kind="tri", nh=10, c0=3200, c1=1500, ck=3.0, a=.05, d=.3, s=.5, r=.4, seed=bar*29), s.t(bar, 12), g=.085, pan=-.24) s.put("sub", voice(broot, BAR*.9, kind="sine", nh=2, c0=180, c1=95, ck=2, a=.02, d=.3, s=.9, r=.3, seed=bar), s.t(bar, 0), g=.28) else: for k2, iv in enumerate((0, 7, 10)): s.put("pad", voice(ROOT_F*4*2**(iv/12), BAR*1.3, kind="saw", nh=18, c0=900+250*bar, c1=500, ck=.7, detune=(-1.2, 0, 1.3), a=.8, d=.8, s=.7, r=1.0, seed=bar*13+k2), s.t(bar, 0), g=.10, pan=-.5+.5*k2) # open hats come in for the back half of the section (the intro's # lift into drop 1, and the outro's tick as it walks away) if bar_in(bar) >= 2: for st in (0, 8): s.put("drums", hat(openh=True), s.t(bar, st), g=.16, pan=.3) # fx splits in two so the reverb-and-high-pass path (risers, crashes) can # be kept off the bottom while the sub impacts stay dry and deep # placed off the section map, not off bar numbers — the four-bar riser # still fills exactly the run-up into each drop after the recut for b in DROP_B0: s.put("fxhi", riser(BAR*4), max(0.0, (b-4))*BAR, g=.30) s.put("fxhi", crash(dur=1.6), b*BAR, g=.40, pan=.1) s.put("fxlow", impactlow(1.4), b*BAR, g=.44) # sub-drop into every drop for b in BREAK_B0: s.put("fxlow", impactlow(1.4), b*BAR, g=.38) # …and into every break # THE ARRIVAL. The recut ends on a six-bar outro instead of an eight-bar # one, and a pad fade alone reads as the track stopping rather than the # courier getting there. One crash and one sub impact on the downbeat of # the outro — the same two gestures the drops are announced with, at half # the weight — put a full stop under SIGNED FOR. _out_b0 = SECTIONS[-1][1] s.put("fxhi", crash(dur=2.6), _out_b0*BAR, g=.34, pan=-.08) s.put("fxlow", impactlow(1.6), _out_b0*BAR, g=.30) # Tight low end: the kick and the sub own everything under ~200Hz and # nothing else is allowed down there. Measured on round 1, 82% of the total # energy was below 160Hz and it was driving the master clipper on its own. s.bus("drums", lambda x: drumbus(hp(x, 42.0))) s.bus("bass", lambda x: hp(x, 78.0)) # reese out of the sub's way s.bus("sub", subshape) s.bus("pad", lambda x: hp(reverb(x, rt=3.6, mix=.52, seed=431), 210.0)) s.bus("brklead", lambda x: hp(reverb(delay(x, BEAT*.75, .38, .28), rt=3.0, mix=.44, seed=439), 260.0)) s.bus("stab", lambda x: hp(delay(x, BEAT*.75, .36, .26), 320.0)) s.bus("fxhi", lambda x: hp(reverb(x, rt=2.6, mix=.34, seed=433), 300.0)) s.bus("fxlow", lambda x: lp(x, 320.0)) mix = s.mixdown(dict(drums=1.15, bass=1.05, sub=0.95, stab=1.0, pad=1.0, brklead=1.0, fxhi=1.0, fxlow=1.0), pump_depth=.55, pump_rel=.13, pump_tracks={"bass", "sub", "pad", "stab", "brklead", "fxhi"}, drive=1.20, ceiling=.922, # -0.7 dBFS -> ~-0.5 dBTP levels=dict(intro=.50, drop1=1.0, **{"break": .56}, break2=.60, drop3=1.0, out=.50)) wav = AUD / "final.wav" s.write(wav, mix) return wav, mix def impactlow(dur=1.4, seed=139): """Sub drop. Round 2 gives it a two-stage body envelope (a fast thump on top of the long tail) and a short 1.2-7k edge, so it lands as an *impact* and not just as weight arriving.""" n = int(dur*SR); t = np.arange(n)/SR f = 34 + 96*np.exp(-t*20) body = np.sin(2*np.pi*np.cumsum(f)/SR)*(np.exp(-t*2.5) + .8*np.exp(-t*16)) rng = np.random.RandomState(seed) nz = bandshape(rng.randn(n), lo=60, hi=2200)*np.exp(-t*19) edge = bandshape(np.random.RandomState(seed+1).randn(n), lo=1200, hi=7000)*np.exp(-t*250) return np.tanh((body + nz*0.35 + edge*0.55)*1.3)*0.82 def analyze(mix): x = mix.mean(1) hop = SR / FPS; win = int(hop*1.7) E = {k: np.zeros(N_FRAMES) for k in ("rms", "low", "mid", "high")} for f in range(N_FRAMES): i = int(f*hop); seg = x[i:i+win] if len(seg) < 16: continue E["rms"][f] = np.sqrt((seg**2).mean()) sp = np.abs(np.fft.rfft(seg*np.hanning(len(seg)))) fr = np.fft.rfftfreq(len(seg), 1/SR) E["low"][f] = sp[fr < 170].sum() E["mid"][f] = sp[(fr >= 170) & (fr < 2400)].sum() E["high"][f] = sp[fr >= 2400].sum() for k in E: p = np.percentile(E[k], 96) + 1e-9 E[k] = np.clip(E[k]/p, 0, 1.25) lo = E["low"] flux = np.maximum(0, lo - np.concatenate([[0], lo[:-1]])) E["kick"] = np.clip(np.convolve(flux, [.25,.5,.25], "same") / (np.percentile(flux, 97)+1e-9), 0, 1) np.savez(AUD/"env.npz", **E) return E _ENV = {} def env(): if not _ENV: z = np.load(AUD/"env.npz") for k in z.files: _ENV[k] = z[k] return _ENV # ════════════════════════════════════════════════════════════════════════════ # ANIMATION HELPERS — motion is the point, so it gets its own primitives # ════════════════════════════════════════════════════════════════════════════ def ease_io(u): return u*u*(3-2*u) def ease_out(u): return 1-(1-u)**3 def ease_in(u): return u**3 def bounce(u, n=3): return abs(math.sin(u*math.pi*n))*(1-u) def lerp(a, b, u): return a + (b-a)*u def walk(t, speed=1.0): """Returns (leg_swing, body_bob, arm_swing) for a walk cycle at time t.""" p = t*speed*math.tau return math.sin(p), abs(math.sin(p))*-1.0, math.sin(p+math.pi) def spring(u, freq=3.0, damp=5.0): """Overshoot-and-settle, for things that arrive.""" if u <= 0: return 0.0 return 1 - math.exp(-damp*u)*math.cos(freq*math.tau*u) def arc(p0, p1, u, h=0.3): """Ballistic arc between two points.""" x = lerp(p0[0], p1[0], u) y = lerp(p0[1], p1[1], u) - math.sin(u*math.pi)*h*abs(p1[0]-p0[0]) return x, y # ════════════════════════════════════════════════════════════════════════════ # VECTOR NEON — everything is a line # ════════════════════════════════════════════════════════════════════════════ NEUTRAL = {"rms": .5, "low": .4, "mid": .4, "high": .3, "kick": .2} BLACK = (6, 6, 10) NEON = [(0, 255, 190), (255, 40, 120), (120, 100, 255), (255, 210, 60), (60, 200, 255), (255, 255, 255)] def _img(bg=BLACK): """Frame in authored units; the raster is S times bigger and the proxy scales every coordinate into it, so the engines below are unchanged.""" im = Image.new("RGB", (OW, OH), bg); return im, mkdraw(im) def glow(im, amount=1.0, radius=8): sm = im.resize((OW//3, OH//3), Image.BILINEAR).filter( ImageFilter.GaussianBlur(B(radius))).resize((OW, OH), Image.BILINEAR) return np.clip(np.asarray(im, np.float32) + np.asarray(sm, np.float32)*amount, 0, 255) class Rush: """First person, flat out. Wireframe streets coming at you.""" def __init__(self, shot, rng): self.rng = rng self.n = 30 self.z = np.linspace(1.0, 60.0, self.n) self.side = rng.integers(0, 2, self.n)*2 - 1 self.hgt = rng.uniform(2.0, 9.0, self.n) self.wid = rng.uniform(1.4, 3.4, self.n) self.col = rng.integers(0, len(NEON), self.n) self.speed = float(rng.uniform(24, 46)) self.lean = float(rng.uniform(-0.5, 0.5)) def frame(self, k, u, e): im, d = _img() spd = self.speed*(0.55 + 0.9*e["rms"]) hz = H*0.52 # a small hot point at the vanishing point, not a glowing egg for q in range(7, 0, -1): r = q*7*(0.7+0.5*e["rms"]) d.ellipse([W*0.5-r*1.5, hz-r, W*0.5+r*1.5, hz+r], fill=(int(6+4*(8-q)), int(12+9*(8-q)), int(14+10*(8-q)))) vx = W*0.5 + self.lean*90*math.sin(k*0.05) self.z = self.z - spd*0.03 self.z[self.z < 0.8] += 60.0 # road for lane in (-1, 1): d.line([vx, hz, vx + lane*W*1.6, H], fill=(40, 44, 60), width=2) for j in range(24): # dashes rushing zz = ((j*2.5 + (k*spd*0.05) % 2.5)) p = 1.0/max(0.35, zz) y = hz + (H-hz)*p*1.6 if y > H or y < hz: continue wdt = max(1, int(p*26)) d.line([vx-wdt, y, vx+wdt, y], fill=(90, 100, 130), width=max(1, int(p*8))) order = np.argsort(-self.z) for i in order: zz = self.z[i] if zz < 0.8: continue p = 1.0/zz x = vx + self.side[i]*p*W*1.15 top = hz - self.hgt[i]*p*H*0.5 bot = hz + p*H*0.65 wpx = self.wid[i]*p*W*0.22 c = NEON[self.col[i]] g = min(1.0, 0.42 + 2.6*p) c = tuple(int(v*g) for v in c) x0, x1 = x - wpx*0.5, x + wpx*0.5 if x1 < -80 or x0 > W+80: continue d.rectangle([x0, top, x1, bot], outline=c, width=max(2, int(p*22))) nf_ = max(2, int(self.hgt[i]*1.4)) for q in range(1, nf_): yy = top + (bot-top)*q/nf_ d.line([x0, yy, x1, yy], fill=tuple(int(v*0.7) for v in c), width=max(1, int(p*9))) # leading edge toward the vanishing point — the sense of rushing d.line([x0, top, vx, hz], fill=tuple(int(v*0.34) for v in c), width=1) d.line([x1, bot, vx, hz], fill=tuple(int(v*0.34) for v in c), width=1) # speed streaks for q in range(int(10 + 60*e["rms"])): a = self.rng.random()*math.tau r0 = 40 + self.rng.random()*80 r1 = r0 + 90 + 260*e["rms"] c = NEON[q % len(NEON)] d.line([vx+math.cos(a)*r0, hz+math.sin(a)*r0*0.6, vx+math.cos(a)*r1, hz+math.sin(a)*r1*0.6], fill=tuple(int(v*0.5) for v in c), width=1) return glow(im, 0.55+0.5*e["high"], 7) class Rider: """Side on. The city goes the other way.""" def __init__(self, shot, rng): self.rng = rng self.layers = [] for li in range(4): n = 14 + li*8 self.layers.append((li, [(rng.random(), rng.uniform(0.2, 1.0)) for _ in range(n)])) self.chase = bool(rng.random() < 0.45) def _bike(self, d, cx, cy, t, sc, col, lean): wr = 34*sc for wx in (-46*sc, 46*sc): d.ellipse([cx+wx-wr, cy-wr, cx+wx+wr, cy+wr], outline=col, width=max(2, int(3*sc))) for sp in range(6): # spokes, spinning a = t*17 + sp*math.pi/3 d.line([cx+wx, cy, cx+wx+math.cos(a)*wr, cy+math.sin(a)*wr], fill=tuple(int(v*0.5) for v in col), width=1) d.line([(cx-46*sc, cy), (cx-6*sc, cy-30*sc), (cx+46*sc, cy)], fill=col, width=max(2, int(3*sc))) d.line([(cx-6*sc, cy-30*sc), (cx+30*sc, cy-40*sc)], fill=col, width=max(2, int(3*sc))) # rider: folded flat over the tank hx, hy = cx+10*sc, cy-64*sc + math.sin(t*9)*1.6*sc d.line([(cx-14*sc, cy-16*sc), (cx+4*sc, cy-52*sc)], fill=col, width=max(3, int(6*sc))) d.line([(cx+4*sc, cy-52*sc), (cx+34*sc, cy-40*sc)], fill=col, width=max(2, int(4*sc))) d.ellipse([hx-13*sc, hy-13*sc, hx+13*sc, hy+13*sc], outline=col, width=max(2, int(3*sc))) d.line([(cx-14*sc, cy-16*sc), (cx-30*sc, cy+4*sc)], fill=col, width=max(2, int(4*sc))) # the bag d.rectangle([cx-34*sc, cy-62*sc, cx-6*sc, cy-34*sc], outline=col, width=max(2, int(3*sc))) def frame(self, k, u, e): im, d = _img() gy = H*0.78 for (li, blocks) in self.layers: sp = (0.25 + li*0.55)*(0.5+1.4*e["rms"]) shade = 0.16 + 0.20*li for (bx, bh) in blocks: x = (bx*W*2 - k*sp*7) % (W*2) - W*0.5 bw = 60 + bh*90 top = gy - bh*(120+li*110) c = tuple(int(v*shade) for v in NEON[(li+int(bx*7)) % len(NEON)]) d.rectangle([x, top, x+bw, gy], outline=c, width=2) for wy in range(int(top)+14, int(gy)-10, 22): if ((int(x)+wy) % 3) == 0: d.line([x+8, wy, x+bw-8, wy], fill=tuple(int(v*0.5) for v in c), width=1) d.line([0, gy, W, gy], fill=(70, 80, 110), width=3) for q in range(0, W, 70): # road dashes x = (q - k*22*(0.6+1.2*e["rms"])) % (W+70) d.line([x, gy+26, x+34, gy+26], fill=(110, 120, 150), width=4) t = k/FPS self._bike(d, W*0.36, gy, t, 1.25, NEON[0], 0) if self.chase: lag = 300 + 120*math.sin(k*0.05) self._bike(d, W*0.36+lag, gy, t*1.02, 1.1, NEON[1], 0) return glow(im, 0.5+0.45*e["high"], 6) class Roof: """Somebody else is up here too.""" def __init__(self, shot, rng): self.rng = rng self.prof = [(rng.random(), rng.uniform(0.3, 1.0), rng.uniform(0.5, 1.6)) for _ in range(11)] self.two = bool(rng.random() < 0.6) def frame(self, k, u, e): im, d = _img((8, 6, 16)) sky = np.linspace(0, 1, H)[:, None] arr = np.zeros((H, W, 3), np.float32) arr += (np.array((40, 20, 70), np.float32)*(1-sky)**2)[..., None].squeeze(-1) if False else 0 for y in range(0, H, 4): v = 1-y/H d.line([0, y, W, y], fill=(int(16+34*v*v), int(8+16*v*v), int(30+58*v*v)), width=1) base = H*0.80 pts = [] for i, (px, ph, pw) in enumerate(self.prof): x = i*(W/10.0) - (k*1.4) % (W/10.0) top = base - ph*150 pts.append((x, top, x+W/10.0*pw*0.9)) d.rectangle([x, top, x+W/10.0*pw*0.9, H], fill=(10, 8, 18), outline=NEON[2], width=2) for wy in range(int(top)+20, H, 34): for wx in range(int(x)+16, int(x+W/10.0*pw*0.9)-10, 26): if ((wx*5+wy*3) % 7) < 3: d.rectangle([wx, wy, wx+9, wy+13], fill=tuple(int(v*(0.4+0.6*e["mid"])) for v in NEON[3])) # runners: silhouettes leaping the gaps t = k/FPS def runner(px, col, ph, sc=2.3): gap = math.sin(t*2.2+ph) x = W*px + math.sin(t*0.7+ph)*110 y = base - 150 - max(0, gap)*135 # the leap sw_, bob, aw_ = walk(t*1.7+ph, 1.0) lean = 0.32 # pitched forward, running hx = x + 26*sc*lean d.line([(x, y), (hx, y-52*sc)], fill=col, width=int(9*sc)) # torso d.ellipse([hx-15*sc, y-52*sc-15*sc, hx+15*sc, y-52*sc+15*sc], fill=col) d.line([(hx, y-40*sc), (hx+aw_*34*sc, y-58*sc)], fill=col, width=int(6*sc)) d.line([(hx, y-40*sc), (hx-aw_*34*sc, y-20*sc)], fill=col, width=int(6*sc)) d.line([(x, y), (x+sw_*40*sc, y+34*sc)], fill=col, width=int(8*sc)) d.line([(x, y), (x-sw_*40*sc, y+30*sc)], fill=col, width=int(8*sc)) runner(0.30, NEON[0], 0.0, 2.4) if self.two: runner(0.68, NEON[1], 1.9, 2.1) return glow(im, 0.45+0.4*e["high"], 6) class Route: """The map keeps changing its mind.""" def __init__(self, shot, rng): self.rng = rng self.gx = int(rng.integers(7, 13)); self.gy = int(rng.integers(5, 9)) self.path = [(0, int(self.gy//2))] x, y = 0, self.gy//2 while x < self.gx-1: if rng.random() < 0.55 or y in (0, self.gy-1): x += 1 else: y += 1 if rng.random() < .5 else -1 y = max(0, min(self.gy-1, y)) self.path.append((x, y)) self.blocks = [(int(rng.integers(1, self.gx)), int(rng.integers(0, self.gy))) for _ in range(int(rng.integers(2, 7)))] def frame(self, k, u, e): im, d = _img((5, 8, 10)) cw, ch = W/(self.gx+1), H/(self.gy+1) for i in range(self.gx+1): d.line([cw*(i+0.5), 0, cw*(i+0.5), H], fill=(22, 46, 46), width=1) for j in range(self.gy+1): d.line([0, ch*(j+0.5), W, ch*(j+0.5)], fill=(22, 46, 46), width=1) pts = [(cw*(x+0.5), ch*(y+0.5)) for (x, y) in self.path] prog = np.clip(u*1.25, 0, 1) m = max(2, int(len(pts)*prog)) d.line(pts[:m], fill=NEON[0], width=5) for (bx, by) in self.blocks: # road closed x, y = cw*(bx+0.5), ch*(by+0.5) d.line([x-16, y-16, x+16, y+16], fill=NEON[1], width=5) d.line([x-16, y+16, x+16, y-16], fill=NEON[1], width=5) px, py = pts[min(len(pts)-1, m-1)] r = 10 + 12*e["kick"] d.ellipse([px-r, py-r, px+r, py+r], outline=NEON[5], width=4) d.ellipse([px-4, py-4, px+4, py+4], fill=NEON[5]) f = font(22) d.text((26, 22), "REROUTING", font=f, fill=NEON[1] if int(k*0.2) % 2 else (60, 20, 34)) d.text((W-260, 22), "%02d:%02d" % (39-int(u*38), int((1-u)*59)), font=f, fill=NEON[0]) return glow(im, 0.4, 5) class Package: """The whole job. It weighs nothing.""" def __init__(self, shot, rng): self.rng = rng self.spin = float(rng.uniform(0.4, 1.5)) self.open_ = bool(rng.random() < 0.4) def frame(self, k, u, e): im, d = _img() t = k/FPS cx, cy = W*0.5, H*0.52 s2 = 150*(1+0.10*math.sin(t*2)) a = t*self.spin; b = t*self.spin*0.6 V = [(-1, -1, -1), (1, -1, -1), (1, 1, -1), (-1, 1, -1), (-1, -1, 1), (1, -1, 1), (1, 1, 1), (-1, 1, 1)] Ed = [(0, 1), (1, 2), (2, 3), (3, 0), (4, 5), (5, 6), (6, 7), (7, 4), (0, 4), (1, 5), (2, 6), (3, 7)] P = [] for (x, y, z) in V: x2 = x*math.cos(a) - z*math.sin(a); z2 = x*math.sin(a) + z*math.cos(a) y2 = y*math.cos(b) - z2*math.sin(b); z3 = y*math.sin(b) + z2*math.cos(b) f = 2.6/(3.4+z3) P.append((cx + x2*s2*f, cy + y2*s2*f)) for (i, j) in Ed: d.line([P[i], P[j]], fill=NEON[0], width=4) for (i, j) in ((0, 2), (4, 6)): d.line([P[i], P[j]], fill=tuple(int(v*0.4) for v in NEON[0]), width=2) f = font(20) d.text((cx-120, cy+s2*0.9), "DO NOT BEND", font=f, fill=NEON[3]) if self.open_ and u > 0.5: d.text((cx-70, cy-14), "EMPTY", font=font(40), fill=NEON[1]) return glow(im, 0.6+0.5*e["high"], 8) class Strobe: """Pure rhythm. Geometry on the beat.""" def __init__(self, shot, rng): self.rng = rng self.mode = int(rng.integers(0, 4)) self.n = int(rng.integers(4, 14)) self.rot = float(rng.uniform(-1, 1)) def frame(self, k, u, e): hit = e["kick"] > 0.4 bg = (250, 250, 250) if (hit and self.mode == 3) else BLACK im, d = _img(bg) t = k/FPS col = NEON[(int(t*4) + self.mode) % len(NEON)] amp = 0.3 + 1.2*e["rms"] if self.mode == 0: for i in range(self.n): r = (i+1)/self.n*H*0.9*amp d.ellipse([W/2-r, H/2-r*0.7, W/2+r, H/2+r*0.7], outline=col, width=5) elif self.mode == 1: # sheet fix: with n<8 this read as a near-empty frame — guarantee # enough spokes and let the energy fatten them n = max(10, self.n) wdt = max(4, int(7*amp)) for i in range(n): a = t*self.rot + i*math.tau/n d.line([W/2, H/2, W/2+math.cos(a)*W, H/2+math.sin(a)*W], fill=col, width=wdt) a2 = a + math.tau/(2*n) d.line([W/2, H/2, W/2+math.cos(a2)*W, H/2+math.sin(a2)*W], fill=tuple(int(v*0.4) for v in col), width=max(2, wdt//2)) elif self.mode == 2: # was nested rotating rhombuses — too simple, and it read as a # screensaver. Chevrons rushing the way the rider is going. n = max(4, self.n) for i in range(n+2): p = ((i/n) + (t*0.9*(0.4+1.6*e["rms"])) % (1.0/n)) x = W*(1.15 - p*1.5) th = H*0.30*amp wdt = max(3, int(9*amp)) d.line([(x, H/2-th), (x+W*0.16, H/2), (x, H/2+th)], fill=col, width=wdt, joint="curve") d.line([(x-W*0.05, H/2-th*0.55), (x+W*0.11, H/2), (x-W*0.05, H/2+th*0.55)], fill=tuple(int(v*0.45) for v in col), width=max(2, wdt//2), joint="curve") else: fg = BLACK if bg[0] > 128 else col for i in range(self.n): x = W*(i+0.5)/self.n hgt = H*0.4*amp*abs(math.sin(t*3+i)) d.rectangle([x-W/self.n*0.35, H/2-hgt, x+W/self.n*0.35, H/2+hgt], fill=fg) return glow(im, 0.5, 6) ENGINES = {"rush": Rush, "rider": Rider, "roof": Roof, "route": Route, "package": Package, "strobe": Strobe} # drop3 keeps the fastest cut menu (2-4 beats), so the surviving second drop # is also the one that escalates. The dropped drop2 plan goes with it. PLAN = { "intro": (["route", "package", "rider"], [8, 12, 8]), "drop1": (["rush", "rider", "strobe", "roof"], [4, 2, 4, 8]), "break": (["route", "package", "roof"], [8, 12, 8]), "break2": (["package", "route", "roof"], [12, 8, 8]), "drop3": (["rush", "strobe", "rider", "roof"], [2, 2, 4, 4]), # the outro drops "rush": SIGNED FOR should land on the package and the # finished route, not on more street. The piece ends on the package. "out": (["package", "route"], [12, 8, 16]), } CARDS = {"intro": "COURIER", "drop1": None, "break": "ROAD CLOSED", "break2": None, "drop3": None, "out": "SIGNED FOR"} SYSTEM_NAMES = ["PICKUP", "LEG 1", "LEG 2", "DETOUR", "DROP", "PROOF"] class Shot: __slots__ = ("idx", "i0", "i1", "n", "engine", "section", "seed", "text", "card") def __init__(self, idx, i0, i1, engine, section, text=None, card=None): self.idx, self.i0, self.i1 = idx, i0, i1 self.n = i1 - i0 self.engine, self.section = engine, section self.seed = 90210 + idx*7919 self.text, self.card = text, card def build_shots(): """Deterministic, but not a cycle: each section draws from its pool with no immediate repeats, and shot lengths come from a menu so the cut rhythm breathes instead of ticking.""" R = np.random.RandomState(5150) shots = []; idx = 0; last = None for nm, b0, b1 in SECTIONS: engs, menu = PLAN[nm] t = b0*BAR; j = 0 while t < b1*BAR - 1e-6: step = menu[R.randint(len(menu))]*BEAT t2 = min(t+step, b1*BAR) if (b1*BAR - t2) < BEAT*1.5: t2 = b1*BAR # no orphan sliver i0, i1 = int(t*FPS), int(t2*FPS) if i1 > i0: pool = [x for x in engs if x != last] or list(engs) eng = pool[R.randint(len(pool))] last = eng txt = [SYSTEM_NAMES[(idx+q) % len(SYSTEM_NAMES)] for q in range(2)] shots.append(Shot(idx, i0, i1, eng, nm, txt, CARDS[nm] if j == 0 else None)) idx += 1; j += 1 t = t2 if shots: shots[-1].i1 = N_FRAMES; shots[-1].n = N_FRAMES - shots[-1].i0 return shots # ── portable font resolution (cross-platform; replaces the repo-only lookup) ── import warnings as _warnings _FONT_ALIASES = { "Menlo.ttc": ["Menlo.ttc", "DejaVuSansMono.ttf", "consola.ttf", "LiberationMono-Regular.ttf"], "Georgia.ttf": ["Georgia.ttf", "georgia.ttf", "DejaVuSerif.ttf", "LiberationSerif-Regular.ttf"], "Georgia Bold.ttf": ["Georgia Bold.ttf", "georgiab.ttf", "DejaVuSerif-Bold.ttf", "LiberationSerif-Bold.ttf"], "Georgia Italic.ttf": ["Georgia Italic.ttf", "georgiai.ttf", "DejaVuSerif-Italic.ttf", "LiberationSerif-Italic.ttf"], "Impact.ttf": ["Impact.ttf", "impact.ttf", "Anton-Regular.ttf", "DejaVuSans-Bold.ttf"], "Helvetica.ttc": ["Helvetica.ttc", "arial.ttf", "Arial.ttf", "DejaVuSans.ttf", "LiberationSans-Regular.ttf"], } def _font_dirs(): here = Path(__file__).resolve() dirs = [here.parent / "fonts"] + [p / "fonts" for p in list(here.parents)[1:4]] try: home = Path.home() except Exception: home = None dirs += [Path("/System/Library/Fonts"), Path("/System/Library/Fonts/Supplemental"), Path("/Library/Fonts"), Path("C:/Windows/Fonts"), Path("/usr/share/fonts"), Path("/usr/local/share/fonts")] if home: dirs += [home / "Library/Fonts", home / ".fonts", home / ".local/share/fonts"] return dirs _FONT_DIRS = _font_dirs() _FF = {} def _find_font(name): """Path of a usable font file for `name`, or None. Cached per name.""" if name in _FF: return _FF[name] found = None for cand in _FONT_ALIASES.get(name, [name]): for d in _FONT_DIRS: if not d.is_dir(): continue p = d / cand if p.is_file(): found = p; break try: found = next(iter(d.rglob(cand)), None) except OSError: found = None if found: break if found: break if found is None: _warnings.warn(f"font {name} not found in fonts/ or system font dirs; " f"using Pillow default (layout will differ)") _FF[name] = found return found def _load_font(p, size): """ImageFont for path `p` (from _find_font) at `size`; Pillow default if p is None.""" if p is None: try: return ImageFont.load_default(size=int(size)) except TypeError: return ImageFont.load_default() return ImageFont.truetype(str(p), size) _FC = {} def font(size, name="Menlo.ttc"): key = (size, name) if key not in _FC: p = _find_font(name) _FC[key] = _load_font(p, max(1, P(size))) return _FC[key] _VIG = {} def vignette(): if "v" not in _VIG: yy, xx = np.mgrid[0:OH, 0:OW] nx = (xx-OW/2)/(OW/2); ny = (yy-OH/2)/(OH/2) r = np.sqrt(nx**2+ny**2)/1.42 _VIG["v"] = np.clip(1.0-0.40*r**2.2, 0, 1)[..., None] return _VIG["v"] def post(arr_small, i, e, shot): a = np.asarray(arr_small, np.float32) if not isinstance(arr_small, np.ndarray) \ else arr_small.astype(np.float32) if a.shape[0] != OH or a.shape[1] != OW: a = np.asarray(Image.fromarray(np.clip(a, 0, 255).astype(np.uint8)) .resize((OW, OH), Image.LANCZOS), np.float32) # hard chromatic tear on the kick — this piece is allowed to be violent. # It is a pixel-count effect, so it scales separately from the geometry. sh = int(round((2 + 16*e["kick"])*S)) if sh > P(2): a[..., 0] = np.roll(a[..., 0], sh, axis=1) a[..., 2] = np.roll(a[..., 2], -sh, axis=1) if e["kick"] > 0.62: # slice displacement rng = np.random.RandomState(i) for _ in range(rng.randint(2, 7)): y0 = rng.randint(0, OH-P(24)); hgt = P(rng.randint(8, 40)) a[y0:y0+hgt] = np.roll(a[y0:y0+hgt], P(rng.randint(-70, 70)), axis=1) a *= vignette() rng = np.random.RandomState(3100 + i) if S == 1.0: a += rng.normal(0, 3.0, a.shape) else: # grain is a look, not a resolution: authored on the 1280x720 grid and # blown up nearest-neighbour so a speck keeps its size on screen gn = rng.normal(0, 3.0, (H, W, 3))*8.0 + 128.0 gi = Image.fromarray(np.clip(gn, 0, 255).astype(np.uint8)) a += (np.asarray(gi.resize((OW, OH), Image.NEAREST), np.float32) - 128.0)/8.0 # scanlines pair off AUTHORED rows, otherwise the CRT line becomes static a *= (0.84 + 0.16*np.cos(np.floor(np.arange(OH)/S)*math.pi))[:, None, None] out = Image.fromarray(np.clip(a, 0, 255).astype(np.uint8)) d = mkdraw(out) # (the section-name / timecode strip that used to run along the bottom was # renderer debug and is gone in the final cut. The route graph's REROUTING # label and its 40-minute countdown are the courier's, and stay.) if shot.card: age = i - shot.i0 if age < FPS*2.0: al = min(1.0, age/4.0)*min(1.0, (FPS*2.0-age)/8.0) col = tuple(int(c*al) for c in (255, 255, 255)) d.text((28, 30), shot.card, font=font(30), fill=col) # the show, struck under the title in the route graph's neon — # title card only, not under ROAD CLOSED / SIGNED FOR if shot.card == TITLE: fw = d.textlength(shot.card, font=font(30))/S d.rectangle([28, 70, 28+fw, 73], fill=tuple(int(c*al) for c in NEON[0])) d.text((28, 82), "PLAYER COMPUTER", font=font(15), fill=tuple(int(c*al) for c in NEON[0])) bh = int(H*0.045) d.rectangle([0, 0, W, bh], fill=(0, 0, 0)); d.rectangle([0, H-bh, W, H], fill=(0, 0, 0)) return out def render_shot(job): shot, force = job E = env() rng = np.random.default_rng(shot.seed) eng = ENGINES[shot.engine](shot, rng) made = 0 for k in range(shot.n): i = shot.i0 + k e = {kk: float(E[kk][min(i, N_FRAMES-1)]) for kk in E} p = FRAMES / f"f{i:05d}.png" u = k/max(1, shot.n-1) arr = eng.frame(k, u, e) # ALWAYS step the engine if p.exists() and not force: continue post(arr, i, e, shot).save(p, compress_level=1) made += 1 return f"shot {shot.idx:02d} {shot.engine:8s} {shot.section:9s} {made}/{shot.n}" def contact_sheet(shots): cols = 6; rows = (len(shots)+cols-1)//cols tw, th = 300, 193 sheet = Image.new("RGB", (cols*tw, rows*(th+24)), (10, 10, 14)) sd = ImageDraw.Draw(sheet) E = env() for n, sh in enumerate(shots): rng = np.random.default_rng(sh.seed) eng = ENGINES[sh.engine](sh, rng) mid = sh.n//2 arr = None for k in range(mid+1): i = sh.i0+k e = {kk: float(E[kk][min(i, N_FRAMES-1)]) for kk in E} arr = eng.frame(k, k/max(1, sh.n-1), e) im = post(arr, sh.i0+mid, e, sh).resize((tw, th), Image.LANCZOS) cx, cy = (n % cols)*tw, (n//cols)*(th+24) sheet.paste(im, (cx, cy)) sd.text((cx+5, cy+th+4), f"{sh.idx:02d} {sh.engine} · {sh.section} · {sh.i0/FPS:.1f}s", font=font(13), fill=(190, 195, 205)) p = OUT/"contact_sheet.png"; sheet.save(p) print(f"contact sheet -> {p} ({len(shots)} shots)") def main(): ap = argparse.ArgumentParser() ap.add_argument("--sheet", action="store_true") ap.add_argument("--shots", default="") ap.add_argument("--force", action="store_true") ap.add_argument("--mux-only", action="store_true") ap.add_argument("--audio-only", action="store_true") ap.add_argument("--jobs", type=int, default=min(14, os.cpu_count())) a = ap.parse_args() wav = AUD/"final.wav" if not wav.exists() or not (AUD/"env.npz").exists() or a.force: print(f"[1/3] song… {N_BARS} bars @ {BPM:.0f}bpm = {DUR:.1f}s") wav, mix = build_song(); analyze(mix) if a.audio_only: print(f"audio -> {wav}"); return shots = build_shots() if a.sheet: contact_sheet(shots); return if not a.mux_only: sel = set(int(x) for x in a.shots.split(",") if x.strip() != "") jobs = [(s, a.force) for s in shots if not sel or s.idx in sel] print(f"[2/3] frames… {len(jobs)} shots / {N_FRAMES} frames on {a.jobs} workers") import multiprocessing as mp with mp.get_context("fork").Pool(a.jobs) as pool: for r in pool.imap_unordered(render_shot, jobs): print(" ", r) print("[3/3] mux…") out = OUT/f"{NAME}.mp4" # -2.5 dB delivery trim before AAC. The wav master sits at -0.6 dBTP, but # AAC coding error on a click-heavy drum bus overshoots by ~3 dB: encoded # flat, 111 decoded samples land above 0 dBFS (the round-1 mp4 shipped with # 11). Trimmed, the encode peaks at -0.44 dBFS with none over, and the mp4 # still lands at -11.9 LUFS — the loudness round 1 had, with the punch. subprocess.run(["ffmpeg", "-y", "-framerate", str(FPS), "-i", str(FRAMES/"f%05d.png"), "-i", str(wav), "-c:v", "libx264", "-preset", "medium", "-crf", "20", "-pix_fmt", "yuv420p", "-af", "volume=-2.5dB", "-c:a", "aac", "-b:a", "320k", "-shortest", "-movflags", "+faststart", "-metadata", f"generator=renders/player_computer_final/{NAME}/render.py", "-metadata", f"title=player_computer_final — {TITLE}", str(out)], check=True, capture_output=True) try: sha = subprocess.check_output(["git", "rev-parse", "--short", "HEAD"], cwd=ROOT).decode().strip() br = subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=ROOT).decode().strip() except Exception: sha = br = "unknown" (OUT/"PROVENANCE.txt").write_text( f"generator: renders/player_computer_final/{NAME}/render.py\n" f"git: {sha} branch: {br}\n" f"timestamp: {datetime.datetime.now().astimezone().isoformat()}\n" f"duration: {DUR:.2f}s fps: {FPS} size: {OW}x{OH} (16:9, native)\n" f"music: {MUSIC_DESC}\n" f"sections: {' '.join(n for n,_,_ in SECTIONS)}\n" f"engines: {ENGINE_DESC} (shot-parallel, stateful per shot)\n") print(f"DONE {out} ({DUR:.1f}s)") if __name__ == "__main__": main()