# AFTERLIGHT trailer: score and sound design, cued from the trailer's own shot list import sys, wave, numpy as np from scipy import signal sys.path.insert(0, '/home/claude/afterlight/trailer') import trailer as T SR = 44100; END = T.END; N = int(SR * (END + 3)) rng = np.random.default_rng(31) mus = np.zeros(N); fx = np.zeros(N); amb = np.zeros(N) def put(buf, sig, t, g=1.0): i = int(t * SR); n = min(len(sig), N - i) if n > 0 and i >= 0: buf[i:i + n] += sig[:n] * g def env(n, a, r): e = np.ones(n); A = max(1, int(a * SR)); R = max(1, int(r * SR)); e[:A] = np.linspace(0, 1, A); e[-R:] *= np.linspace(1, 0, R); return e def mtof(m): return 440 * 2 ** ((m - 69) / 12) def bp(x, lo, hi): b, a = signal.butter(2, [lo / (SR / 2), hi / (SR / 2)], 'band'); return signal.lfilter(b, a, x) def lp(x, f): b, a = signal.butter(2, f / (SR / 2), 'low'); return signal.lfilter(b, a, x) def noise(n): return rng.standard_normal(n) def kal(m, t, g=0.15, d=2.4, buf=None): f = mtof(m); n = int(d * SR); tt = np.arange(n) / SR s = (np.sin(2 * np.pi * f * tt) + 0.35 * np.sin(2 * np.pi * f * 2.01 * tt) * np.exp(-tt * 6) + 0.12 * np.sin(2 * np.pi * f * 5.4 * tt) * np.exp(-tt * 18)) * np.exp(-tt * 2.2) * np.minimum(1, tt / 0.003) put(mus if buf is None else buf, s, t, g) def bell(f, t, g=0.06, d=4.0): n = int(d * SR); tt = np.arange(n) / SR put(mus, sum(a * np.sin(2 * np.pi * f * r * tt) * np.exp(-tt * k) for r, a, k in [(1, 1, 1.0), (2.76, 0.45, 2.2), (5.4, 0.25, 4), (8.93, 0.12, 6)]) * np.minimum(1, tt / 0.002), t, g) def pad(ms, t, d, g=0.05, bright=1600, a=None, r=None): n = int(d * SR); tt = np.arange(n) / SR; s = np.zeros(n) for m in ms: for det in (-0.1, 0, 0.09): s += signal.sawtooth(2 * np.pi * mtof(m) * 2 ** (det / 12) * tt + rng.random() * 6.28) * 0.33 put(mus, lp(s, bright) * env(n, a or min(2.5, d * 0.4), r or min(2.5, d * 0.4)) / len(ms), t, g) def flute(m, t, d, g=0.08): f = mtof(m); n = int(d * SR); tt = np.arange(n) / SR ph = 2 * np.pi * np.cumsum(f * (1 + 0.006 * np.sin(2 * np.pi * 5 * tt) * np.minimum(1, tt / 0.4))) / SR put(mus, (np.sin(ph) + 0.18 * np.sin(2 * ph) + bp(noise(n), f * 0.8, min(f * 3, 15000)) * 0.08) * env(n, 0.15, min(0.6, d * 0.4)), t, g) def boom(t, g=0.5): n = int(2.2 * SR); tt = np.arange(n) / SR; f = 46 * np.exp(-tt * 1.2) + 28 put(fx, np.sin(2 * np.pi * np.cumsum(f) / SR) * np.exp(-tt * 1.6) + lp(noise(n), 180) * np.exp(-tt * 5) * 0.6, t, g) def swell(t, d=1.1, g=0.08): # reversed noise swell into a cut n = int(d * SR); tt = np.arange(n) / SR; put(fx, bp(noise(n), 300, 5000) * (tt / d) ** 3, t - d, g) def whoosh(t, d=0.7, g=0.06): n = int(d * SR); tt = np.arange(n) / SR; s = noise(n); f = np.linspace(400, 3000, n) out = np.zeros(n) for k in range(0, n, 2048): out[k:k + 2048] = bp(s[k:k + 2048], f[k] * 0.6, min(f[k] * 1.6, 18000)) put(fx, out * np.sin(np.pi * tt / d) ** 2, t, g) def tick(t, g=0.08, f=3000): n = int(0.03 * SR); tt = np.arange(n) / SR; put(fx, bp(noise(n), f * 0.7, f * 1.4) * np.exp(-tt * 200), t, g) def heart(t, g=0.22): for dt, gg in ((0, 1), (0.26, 0.6)): n = int(0.25 * SR); tt = np.arange(n) / SR; put(fx, np.sin(2 * np.pi * 52 * tt) * np.exp(-tt * 18), t + dt, g * gg) # --- the room of dust: film crackle and air under everything n = int(END * SR); air = lp(noise(n), 500) * 0.25 + bp(noise(n), 2000, 9000) * 0.04 put(amb, air * env(n, 3, 3), 0, 0.12) for k in range(int(END * 9)): tick(rng.random() * END, 0.02 + 0.02 * rng.random(), 2500 + 4000 * rng.random()) SH = [s for s in T.SHOTS if not s[5].get('hard')] MV = [c[0] - 0.05 for c in T.CARDS[1:]] # cold open: the light gathers pad([38, 50, 57], 0.0, MV[0] + 0.4, 0.05, 700, a=5) bell(mtof(81), 1.2, 0.035); bell(mtof(86), 3.4, 0.03) LONG = [(69, 1), (74, 1), (76, 1), (77, 2), (76, 1), (74, 2)]; B = 60 / 66 t = 7.2 for m, b in LONG: kal(m, t, 0.11); t += b * B # cuts within the run of shots: a whoosh as the dust leaves, a swell into each movement for s in SH[1:]: whoosh(s[1] - 0.25, 0.8, 0.05) for i, t0 in enumerate(MV): swell(t0, 1.0, 0.07 + 0.01 * i); boom(t0, 0.35 + 0.05 * i) bell(mtof([81, 79, 84, 86, 88, 93][i]), t0 + 0.05, 0.05) # the pulse: a heartbeat from the first movement, faster as it builds t = MV[0]; bpm = 58 while t < T.MONT_END - 0.1: heart(t, 0.16 + 0.12 * (t - MV[0]) / 38); bpm = 58 + (t - MV[0]) * 1.4; t += 60 / bpm # harmony per movement (D, then F, then G, then B-flat, then D, then the warm return) CH = [[50, 57, 62, 65], [53, 60, 65, 69], [55, 62, 67, 70], [46, 53, 58, 62], [50, 57, 62, 66], [50, 57, 62, 66, 69]] for i, t0 in enumerate(MV): pad(CH[i], t0 - 0.2, (MV[i + 1] if i < 5 else T.MONT_END) - t0 + 0.8, 0.04 + 0.006 * i, 1200 + 250 * i) # a kalimba ostinato enters at REVEAL and thickens t = MV[2] while t < T.MONT_END - 0.2: i = sum(1 for m in MV if t >= m) - 1; root = CH[i][0] + 12 for m in (root, root + 7, root + 12, root + 7): kal(m, t, 0.035 + 0.01 * i, 1.2); t += B / 2 # REMAIN: the theme again, over the warm room t = MV[5] + 0.2 for m, b in [(69, 1), (74, 1), (76, 1), (78, 2), (76, 1), (74, 2)]: flute(m, t, b * B * 1.05, 0.07); t += b * B # the montage: one tick per cut, a riser, then nothing ts = [s[1] for s in T.SHOTS if s[5].get('hard')] for k, t0 in enumerate(ts): tick(t0, 0.12, 2200 + k * 150); kal(74 + (k % 5) * 2, t0, 0.05, 0.8) R0 = min(ts) - 0.5; n = int((T.MONT_END - R0) * SR); tt = np.arange(n) / SR; dur = n / SR rise = bp(noise(n), 500, 9000) * (tt / dur) ** 2 + np.sin(2 * np.pi * np.cumsum(80 * 2 ** (tt / dur * 3)) / SR) * (tt / dur) ** 2 * 0.5 put(fx, rise, R0, 0.12) # the mote, then the word bell(mtof(93), T.MONT_END + 0.5, 0.05, 5) boom(T.TITLE0 + 0.3, 0.55) pad([38, 50, 57, 62, 66, 69], T.TITLE0, END - T.TITLE0 + 0.5, 0.07, 2200, a=1.5, r=4) t = T.TITLE0 + 2.2 for m, b in LONG[:4] + [(74, 4)]: kal(m, t, 0.12); t += b * B def reverb(x, dur=3.2, mix=0.32): n = int(dur * SR); tt = np.arange(n) / SR; ir = rng.standard_normal(n) * np.exp(-tt * 2.6); ir = lp(ir, 6000); ir /= np.sqrt((ir ** 2).sum()) return x * (1 - mix) + signal.fftconvolve(x, ir)[:len(x)] * mix * 1.7 # silence before the title: duck everything for the held breath duck = np.ones(N); a, b = int((T.MONT_END) * SR), int((T.MONT_END + 0.45) * SR); duck[a:b] = 0.0; duck[b:b + int(0.4 * SR)] = np.linspace(0, 1, int(0.4 * SR)) mix = (reverb(mus) + reverb(fx, 1.6, 0.18) + amb) * duck mix = mix[:int(END * SR)]; fo = int(2 * SR); mix[-fo:] *= np.linspace(1, 0, fo) mix = np.tanh(mix / (np.abs(mix).max() * 0.75)) * 0.9 st = np.stack([mix, np.roll(mix, int(0.0006 * SR))], 1) with wave.open('/home/claude/afterlight/trailer/score.wav', 'wb') as w: w.setnchannels(2); w.setsampwidth(2); w.setframerate(SR); w.writeframes((st * 32767).astype(np.int16).tobytes()) print('ok')