#!/usr/bin/env python3
"""Sample-accurate SFX track for the Fable explainer.
Synthesizes pops/ticks/whooshes/thumps/chimes procedurally and places them at
the exact times the deterministic engine animates — one 54s stereo WAV out.
"""
import math, wave, struct, random

SR = 48000
TOTAL = 54.0
random.seed(7)

def env_exp(n, k):  return [math.exp(-k * i / SR) for i in range(n)]

def pop(gain=1.0):
    n = int(0.10 * SR); out = []
    for i in range(n):
        t = i / SR
        f = 320 - 170 * (i / n)
        out.append(gain * math.sin(2 * math.pi * f * t) * math.exp(-t * 42))
    return out

def tick(gain=1.0):
    n = int(0.03 * SR); out = []
    for i in range(n):
        t = i / SR
        s = math.sin(2 * math.pi * 1900 * t) + 0.3 * (random.random() * 2 - 1)
        out.append(gain * 0.7 * s * math.exp(-t * 220))
    return out

def whoosh(gain=1.0, dur=0.35):
    n = int(dur * SR); raw = [(random.random() * 2 - 1) for _ in range(n)]
    # cheap lowpass: moving average
    out = []; acc = 0.0; K = 9
    for i in range(n):
        acc += raw[i]
        if i >= K: acc -= raw[i - K]
        e = math.sin(math.pi * i / n) ** 2
        out.append(gain * 0.5 * (acc / K) * e)
    return out

def thump(gain=1.0):
    n = int(0.16 * SR); out = []
    for i in range(n):
        t = i / SR
        s = math.sin(2 * math.pi * 88 * t) + 0.2 * math.sin(2 * math.pi * 176 * t)
        out.append(gain * s * math.exp(-t * 28))
    return out

def chime(gain=1.0):
    n = int(0.6 * SR); out = []
    for i in range(n):
        t = i / SR
        s = 0.6 * math.sin(2 * math.pi * 660 * t) + 0.4 * math.sin(2 * math.pi * 990 * t)
        out.append(gain * 0.5 * s * math.exp(-t * 7))
    return out

GEN = {"pop": pop, "tick": tick, "whoosh": whoosh, "thump": thump, "chime": chime}

INTRO = 2.6; BEAT = 6.0
def B(i): return INTRO + i * BEAT  # beat i start

events = []
def ev(t, kind, g=1.0): events.append((t, kind, g))

# intro
ev(0.15, "pop", 0.8); ev(0.55, "tick", 0.7); ev(0.85, "whoosh", 0.5)
# beat boundaries: wipe whooshes
for i in range(9):
    ev(B(i) - 0.14, "whoosh", 0.45)
# b1 FABLE
ev(B(0) + 0.20, "pop", 0.9); ev(B(0) + 0.55, "thump", 0.5); ev(B(0) + 1.20, "tick", 0.8); ev(B(0) + 1.75, "tick", 0.4)
# b2 RUNS LONG (dots tick along the line)
ev(B(1) + 0.20, "pop", 0.9); ev(B(1) + 0.60, "thump", 0.6); ev(B(1) + 1.10, "tick", 0.7)
for k in range(5):
    ev(B(1) + 1.45 + k * 0.55, "tick", 0.45)
# b3 SAY LESS -> GET MORE
ev(B(2) + 0.35, "pop", 0.9); ev(B(2) + 0.90, "tick", 0.7)
ev(B(2) + 1.10, "whoosh", 0.6); ev(B(2) + 1.80, "thump", 0.8); ev(B(2) + 2.55, "tick", 0.4)
# b4 REMOVE SCAFFOLDING (guides vanish)
ev(B(3) + 0.45, "pop", 0.9); ev(B(3) + 0.80, "tick", 0.7)
for k in range(4):
    ev(B(3) + 1.55 + k * 0.5, "tick", 0.5)
ev(B(3) + 2.45, "tick", 0.4)
# b5 IN PARALLEL
ev(B(4) + 0.30, "pop", 0.9); ev(B(4) + 0.75, "thump", 0.6); ev(B(4) + 1.20, "whoosh", 0.7)
ev(B(4) + 1.50, "tick", 0.7)
for k in range(3):
    ev(B(4) + 2.25 + k * 0.18, "tick", 0.5)
# b6 MORE OUTPUT (bars land)
ev(B(5) + 0.45, "pop", 0.9); ev(B(5) + 1.05, "tick", 0.7); ev(B(5) + 1.40, "whoosh", 0.6)
for k in range(3):
    ev(B(5) + 2.10 + k * 0.35, "thump", 0.55)
# b7 DONT MICROMANAGE (knob ticks fade, plane whoosh)
ev(B(6) + 0.35, "pop", 0.9); ev(B(6) + 0.95, "tick", 0.7)
for k in range(3):
    ev(B(6) + 1.30 + k * 0.4, "tick", 0.4)
ev(B(6) + 2.80, "whoosh", 0.7)
# b8 TRUST THE BRIEF
ev(B(7) + 0.35, "pop", 0.9); ev(B(7) + 0.85, "thump", 0.7); ev(B(7) + 1.75, "chime", 0.8)
# outro
ev(B(8) + 0.15, "pop", 0.7); ev(B(8) + 0.55, "tick", 0.6); ev(B(8) + 1.15, "chime", 0.6)

# ---- mixdown ----
N = int(TOTAL * SR)
buf = [0.0] * N
for t, kind, g in events:
    smp = GEN[kind](g)
    i0 = int(t * SR)
    for j, v in enumerate(smp):
        idx = i0 + j
        if 0 <= idx < N: buf[idx] += v

# master gain + soft clip
peak = max(abs(v) for v in buf) or 1.0
norm = min(1.0, 0.85 / peak)
OUT = "/Users/davendrapatel/videoclaw/projects/fable-explainer/artifacts/audio/sfx-54s.wav"
with wave.open(OUT, "wb") as w:
    w.setnchannels(2); w.setsampwidth(2); w.setframerate(SR)
    frames = bytearray()
    for v in buf:
        s = int(max(-1.0, min(1.0, v * norm)) * 32767)
        frames += struct.pack("<hh", s, s)
    w.writeframes(bytes(frames))
print(f"sfx track: {OUT} ({len(events)} events, peak-normed {norm:.2f})")
