# LANE E — Audio, FX & UI **Read first:** `docs/DESIGN.md` (§5–§7), `docs/CONTRACTS.md`, everything in `src/core/`. **You own:** `src/audio/**`, `src/fx/**`, `src/ui/**`, `src/demo/audioDemo.ts`, `demo-audio.html`. **You may not touch anything else.** ## Mission Make the booth alive: a fully synthesized house groove in stems that unmute as the quest progresses, positional audio from the decks, beat events driving LED pulses/VU meters/particles, all diegetic SFX, and the HUD. "Sound is the win state" — you own the payoff of the whole game. ## Provides - `AudioEngine` — WebAudio graph, `init()` on first user gesture, `setStemCount(n)` (0–5 stems audible), `setChannelGain(ch, v)` (live fader ducking), `setPlaybackState(deck, playing, rpm)` (pitch-bend spin-up/brake), `playSfx(name, at?: Vec3)`, positional listener sync `updateListener(view: IPlayerView)`. - `FxSystem` — `update(dt)`, needs `{ scene, setEmissiveBoost }` injected (Lane A's hook), owns particles + LED pulse logic + VU animation hooks. - `Hud` — DOM overlay: crosshair, hotbar (renders Lane D's hotbar model via events/injected getter), quest tracker (5 nodes), subtitles line ("SIGNAL RESTORED: CROSSFADER — 3/5"), win screen, start/pause overlay (pointer-lock gate + "click to drop the needle" splash). ## Consumes `bus` events (`signal:repair`, `game:win`, `platter:state`, `fader:move`, `audio:*` are yours to emit, `block:break/place`, `player:step/landed`, `machine:interact`), `IPlayerView` interface, `blockDef().sound` categories. ## Tasks ### E1. The groove (all synthesized, no samples) - 118 BPM, 8-bar loop, five stems built from oscillators/noise through a shared WebAudio clock (lookahead scheduler, ~25 ms tick — do not use `setInterval` naively for note timing; schedule on `AudioContext.currentTime`): 1. **drums** — kick (sine drop + click), hats (filtered noise, offbeat), clap on 2/4 2. **bass** — detuned saw through lowpass, one-bar riff, sidechain-ducked from the kick (a simple gain envelope is fine) 3. **chords** — filtered saw stabs, minor 7th vamp, ping-pong-ish delay 4. **lead** — square/sine hook, sparse (every 4th bar), long release 5. **sweeps** — noise riser + downlifter each 8 bars, vinyl crackle bed - Stems map to quest progress (`setStemCount`): silence → drums → +bass → +chords → +lead → +sweeps/full. Keep musical when partial. - Vinyl-truth: master through a gentle highshelf + crackle when playing off the blue record; `setPlaybackState` bends a master playbackRate-equivalent (detune all stems' pitch AND tempo via a global rate param — simplest: scale scheduler tempo and per-voice detune together) for spin-up/brake, and pitch-fader moves detune ±. - Positional: stems output into two `PannerNode`s at the deck spindle world positions (constants LAYOUT), plus a dry low bed so it never fully disappears; `updateListener` follows the player eye/orientation. ### E2. Analysis & beat events - Drive `audio:beat { energy }` from the scheduler itself (you know when the kick fires — no FFT needed; energy = current kick gain), `audio:bar`. Additionally an `AnalyserNode` on master for VU levels (expose `getLevels(): { low, mid, high }` for FX). ### E3. SFX (synthesized, short) - Footsteps by `blockDef.sound` category (metal/wood/plastic/soft/glass — filtered noise bursts with different bodies), land thump scaled by impactSpeed, block break/place clicks per category, fader zip, button clunk, RCA "clunk-clunk-CLICK" (rising), fuse zap, tonearm cue lever creak, win: needle-drop crackle → the full mix slams in (brief lowpass sweep open). ### E4. FX systems - **LED pulse**: on `audio:beat`, pulse Lane A's `setEmissiveBoost` (1.0 → ~1.8 decay ~150 ms) — but only after ≥1 repair (dead booth = dim: start boost at 0.35, step toward 1.0 per repair). - **VU towers**: Lane C built LED columns at known LAYOUT-derived positions; animate them by swapping emissive boost per-level is Lane-A-internal, so instead: overlay thin emissive sprite quads you own, positioned over the tower faces, height-keyed to `getLevels()`. Same trick for the patch-bay blink and signal-path trace on repairs (a moving bright sprite running the cartridge→tonearm→cable→mixer path — hardcode the polyline from LAYOUT). - **Particles**: drifting dust motes in light shafts (a few hundred point sprites, gentle brownian), block-break puff at break position, record-rim sparkle when a platter spins at 45. - **Win sequence**: on `game:win` — 2 s: kill boost, silence… then needle drop, full mix, boost pulse ×2 for 8 bars, every LED sprite strobing to levels, confetti of tiny vinyl-black quads over the decks. Then settle to living-booth steady state. ### E5. HUD/UI - DOM (not canvas): crosshair; hotbar strip (9 slots, block tint swatches + counts, active ring); quest tracker top-right (5 icons, lit as repaired); event subtitle line; start overlay (title "TURNCRAFT", "click to drop the needle") gating AudioContext + pointer lock; pause on lock-loss; win banner. Styling: dark, minimal, DJM-font-ish (system mono is fine), amber/green LED accent colors from `blocks.ts` tints. ## Demo (`demo-audio.html`) No other lanes: a mock stage (dark plane, two glowing discs at deck positions, a camera you orbit) + a control panel: stem count slider 0–5, play/stop per deck with spin-up bend, pitch slider, channel-fader sliders (live ducking), every SFX as a button, beat-pulse visual (the discs flash via the same code path as `setEmissiveBoost` — inject a mock), quest simulator buttons firing `signal:repair` 1→5 then `game:win` to preview the entire audio-visual arc, HUD rendered live with a mock hotbar. ## Acceptance - [ ] Groove runs indefinitely with zero timing drift (scheduler, not setInterval); stems musically stack 0→5 - [ ] Spin-up/brake audibly bends tempo+pitch together; pitch fader detunes - [ ] Walking between decks audibly pans/attenuates (test with camera move) - [ ] Every SFX distinct and non-clipping; footsteps vary by surface category - [ ] Beat pulse, VU sprites, dust motes, break puffs all run at 60 fps - [ ] Full win sequence previews correctly from the quest simulator - [ ] No audio starts before user gesture; `npm run typecheck` clean; `HANDOFF.md` ## Out of scope Gameplay logic (D), world/meshes (A/C), player (B). You react to events and render overlays/sprites you own — you never `setBlock` and never mutate another lane's materials beyond the provided `setEmissiveBoost` hook.