import sys, wave, numpy as np sys.path.insert(0, '/home/claude/afterlight/film') from edl import EDL SR = 44100 def L(s): return s['dur'] / s.get('slow', 1.0) if s['kind'] == 'clip' else s['dur'] total = sum(L(s) for s in EDL) buf = np.zeros(int(SR * (total + 3))) rng = np.random.default_rng(5) def add(sig, t): i = int(t * SR); n = min(len(sig), len(buf) - i) if n > 0: buf[i:i + n] += sig[:n] def tone(f, d, g, kind='tri', att=0.01, dec=None): n = int(d * SR); tt = np.arange(n) / SR if kind == 'sin': w = np.sin(2 * np.pi * f * tt) else: ph = (tt * f) % 1.0; w = 4 * np.abs(ph - 0.5) - 1 env = np.minimum(1, tt / att) * np.exp(-tt * (5.0 / (dec or d))) return np.convolve(w * env, np.ones(5) / 5, 'same') * g PENTA = [146.8, 164.8, 196.0, 220.0, 246.9, 293.7, 329.6, 392.0, 440.0, 587.3] # chapter keys: each chapter gets its own drone root t = 0.0; chapter = 0; roots = [73.4, 82.4, 98.0, 110.0, 73.4, 87.3, 98.0, 65.4, 82.4, 73.4, 61.7, 73.4] segstart = [] for s in EDL: segstart.append(t); t += L(s) # drone bed, crossfading per chapter t = 0.0 for i, s in enumerate(EDL): d = L(s) if s['kind'] == 'card': chapter += 1 add(tone(roots[chapter % len(roots)] * 2, 3.5, 0.10, 'sin', 0.02, 2.5), t) # low bell for the chapter add(tone(roots[chapter % len(roots)] * 3, 3.0, 0.05, 'sin', 0.02, 2.0), t + 0.02) r = roots[chapter % len(roots)] n = int(d * SR); tt = np.arange(n) / SR bg = s.get('bed', 1.0) bed = bg * (np.sin(2 * np.pi * r * tt) * 0.035 + np.sin(2 * np.pi * r * 1.5 * tt + 0.3) * 0.02) * (0.7 + 0.3 * np.sin(2 * np.pi * 0.11 * tt)) fade = np.minimum(1, np.minimum(tt / 0.4, (d - tt) / 0.4)); add(bed * fade, t) # slip ticks: two soft keystrokes when a slip lands for (_, a, e) in s.get('slips', []): for k in range(2): nz = rng.standard_normal(int(0.025 * SR)) * np.exp(-np.arange(int(0.025 * SR)) / (0.004 * SR)) * 0.05 add(nz, t + a + 0.05 + k * 0.09) add(tone(PENTA[rng.integers(3, 9)], 1.4, 0.05, 'tri', 0.01, 1.4), t + a + 0.1) name = s.get('name', '') if name == 'fireflies': for k in range(28): add(tone(1400 + rng.random() * 900, 0.18, 0.02, 'sin', 0.005, 0.18), t + rng.random() * d) if name == 'daimonji': for k in range(140): m = int(0.03 * SR); nz = rng.standard_normal(m) * np.exp(-np.arange(m) / (0.006 * SR)) * 0.035 add(np.convolve(nz, np.ones(8) / 8, 'same'), t + 0.4 + (k / 140) * (d - 0.6) + rng.random() * 0.05) if name == 'makyo': add(tone(392.0, 4.0, 0.08, 'sin', 0.3, 3.5), t + 3.2); add(tone(587.3, 4.0, 0.05, 'sin', 0.3, 3.5), t + 5.0) if s['kind'] == 'grab': gi = sum(1 for q in EDL[:i] if q['kind'] == 'grab') add(tone(PENTA[min(9, 2 + gi // 2)] * (1 + (gi % 2) * 0.5), 1.6, 0.07, 'tri', 0.005, 1.4), t + 0.05) if name == 'yangsui': m = int(1.5 * SR); add(np.convolve(rng.standard_normal(m), np.ones(30) / 30, 'same') * np.linspace(0, 1, m) * 0.12, t + 4.5) if name == 'mirage': add(tone(110.0, 8.0, 0.06, 'sin', 2.0, 9.0), t + 0.5); add(tone(116.5, 8.0, 0.05, 'sin', 2.0, 9.0), t + 0.5) if name == 'daimonji': add(tone(98.0, 9.0, 0.12, 'sin', 3.0, 10.0), t + 2.0) if name == 'title': for k, f in enumerate([146.8, 220.0, 293.7, 329.6]): add(tone(f, 3.0, 0.08, 'tri', 0.01, 2.6), t + 1.2 + k * 0.35) if name == 'end': for k, f in enumerate([329.6, 293.7, 220.0, 146.8]): add(tone(f, 3.5, 0.07, 'tri', 0.01, 3.0), t + 0.5 + k * 0.6) t += d out = buf[:int(SR * total)] out = out / max(1e-9, np.abs(out).max()) * 0.85 with wave.open('/home/claude/afterlight/film/build/score.wav', 'wb') as w: w.setnchannels(1); w.setsampwidth(2); w.setframerate(SR); w.writeframes((out * 32767).astype(np.int16).tobytes()) print(f'{total:.1f}s')