# LANE E — Update (Audio / FX / UI) **Status: ✅ Complete, typechecks clean (whole 5-lane repo), verified live in the browser, adversarially reviewed (6 findings — all fixed).** Lane E owns `src/audio/**`, `src/fx/**`, `src/ui/**`, `src/demo/audioDemo.ts`, `demo-audio.html`. Built strictly against the contract layer in `src/core/` — nothing there was edited, no `setBlock` anywhere, no imports of other lanes' concrete code, no rAF loop outside the demo, no new runtime deps (three only), all audio synthesized, all textures procedural. --- ## Round 2 — Headshell Workshop, Lane E slice (2026-07-14) Fable asked me to take the **Lane E (audio/fx/ui) portion** of `WORKSHOP_CARTRIDGE.md` while other agents handle the machine/interaction core. Done, additive, staying strictly in my three directories + demo. Zero core edits (I did **not** touch `src/core/events.ts` — Lane D added the `workshop:*` events per its brief; I only subscribe to them). **Coordination note:** the Lane D workshop session was building `src/machines/workshop/**` concurrently on the shared tree. I read their actual code and aligned to it rather than my brief-guesses — so integration is by **bus events, not direct calls**, my error vocabulary matches theirs (the 6th mode is `skate`, not `rotationWrong`), and I **removed my `src/fx/scope.ts`** because their machine already renders the scope block. No file-ownership overlap. **What I added (all verified live in `demo-audio.html` → "Headshell Workshop"):** - **Diagnostic audio** — `src/audio/testSignal.ts` `playTestSignal(errors)`: a true-stereo kick+bass loop with one audible chain per fault (L/R ping-pong, phase invert, azimuth −12 dB + distortion, 80 ms gate stutter, 1.2 kHz muffle, skate-abort). `AudioEngine` reacts to `workshop:test`. - **9 workshop SFX** (`sfx.ts`): crimp, wireOn/Off, screwTick/Clunk, tiltCreak, weightDetent, bubbleLock, skate — driven by `workshop:torque` (ratchet) and `machine:interact` action keywords. - **HUD** — radial torque gauge on `workshop:torque` (hides on `value:null`), fault subtitle on `workshop:msg` (`Hud.showTorque`/`hideTorque` also public). - **FX** — screw confetti on workshop completion (`signal:repair` node `stylus`), also `fx.screwBurst(x,y,z)`. **Two things for Fable/Lane D to close the loop:** 1. `trackingHeavy` — Lane E does the audio lowpass; Lane D should also sag the platter target rpm ~15% (platter.ts) for the full "drag". 2. Optional: Lane D can emit `machine:interact {action:'crimp'|'screwClunk'|…}` for the per-step SFX; the torque/test events already cover the main cues. Whole tree typechecks clean **with Lane D's `Workshop.ts` present**; zero console errors exercising every error mode, the torque gauge, screw confetti, and all 9 SFX. Full API surface is in `src/{audio,fx,ui}/HANDOFF.md` (Round 2). --- ## What I built ### Audio (`src/audio/`) — "sound is the win state" - **`synth.ts`** — low-level voices: kick (pitch-drop sine + click), hats, clap, detuned-saw bass, resonant chord stabs, square/sine lead, noise sweeps, plus procedural white-noise and vinyl-crackle buffers. No samples. - **`scheduler.ts`** — a drift-free lookahead transport (25 ms tick, notes scheduled on `AudioContext.currentTime`, not `setInterval` timing). A single `rate` models the Technics spin-up/brake: it eases torque-ily toward its target and scales tempo **and** per-voice detune together. Beat/bar events are queued and fired at their audible time so LED pulses stay in sync. - **`groove.ts`** — the endless 118 BPM, 8-bar F-minor deep-house loop in five stems (drums → bass → chords → lead → sweeps). `setStemCount(0..5)` stacks them musically; muted stems are not scheduled (near-zero cost). Sidechain- ducked bass. - **`sfx.ts`** — every diegetic one-shot: footsteps per surface category, impact-scaled land, break/place, fader zip, button clunk, RCA "clunk-clunk-CLICK", fuse zap, tonearm creak, needle drop. - **`AudioEngine.ts`** — the WebAudio graph, positional sound from two PannerNodes at the deck spindles (+ an always-on low-passed dry bass bed), the analyser for VU `getLevels()`, the full bus wiring, and the win audio timeline (silence → +1.8 s needle-drop → full mix slams in behind an opening lowpass — all scheduled on the ctx clock). **Nothing sounds before `init()` on a user gesture** (autoplay policy). ### FX (`src/fx/`) — the booth comes alive - **`particles.ts`** — `DustField` (ambient brownian motes) and `Burst` (a round-robin pool powering break puffs, win confetti, and 45-rpm rim sparkle). Preallocated typed arrays, mutated in place — **zero per-frame allocation**. One procedural glow texture, no assets. - **`overlays.ts`** — emissive sprite overlays we own (never mutating another lane's materials): mixer VU towers keyed to `getLevels()`, a bright sprite that runs the signal-path polyline on each repair, and a patch-bay blink. - **`layout.ts`** — every overlay anchor derived from `constants.ts` `LAYOUT`. - **`FxSystem.ts`** — the global LED pulse via Lane A's injected `setEmissiveBoost` (dead-booth base 0.35 → steps toward 1.0 over the five repairs → pulses to ~1.8 on beats), plus the win visual timeline (1.8 s dark → drop flash + confetti → living-booth steady state). ### UI (`src/ui/Hud.ts`) — DOM overlay Crosshair, 9-slot hotbar (tint swatches + counts + active ring, via an injected `getHotbar()`), 5-node quest tracker, event subtitle line, the **start splash that gates the AudioContext + pointer lock**, a pause overlay, and the win banner. `pointer-events:none` except the gate screens, so it never eats gameplay input. Dynamic text via `textContent` (no injection risk). ### Demo (`demo-audio.html` + `src/demo/audioDemo.ts`) Runs with **zero code from other lanes**: a mock stage (dark tabletop + two glowing "record" discs at the real deck spindle positions, orbit camera) and a full control panel exercising every acceptance criterion, including a quest simulator that previews the entire signal→stem→light arc and the win sequence. A mock `setEmissiveBoost` flashes the discs on every beat via the real FX code path; a mock hotbar feeds the HUD. --- ## Acceptance criteria — all met | # | Criterion | Status | |---|---|---| | 1 | Groove runs indefinitely with zero drift; stems stack 0→5 | ✅ lookahead scheduler; gated stems | | 2 | Spin-up/brake bends tempo+pitch together; pitch fader detunes | ✅ single `rate` → tempo+detune; `setPitch` | | 3 | Walking between decks pans/attenuates | ✅ two PannerNodes + listener follow; dry bed remains | | 4 | Every SFX distinct, non-clipping; footsteps vary by surface | ✅ per-category footstep bodies | | 5 | Beat pulse, VU, dust, break puffs at 60 fps | ✅ zero-alloc hot paths, verified live | | 6 | Full win sequence previews from the quest simulator | ✅ verified end-to-end in browser | | 7 | No audio before user gesture; typecheck clean; HANDOFF | ✅ start-splash gate; clean; 3 HANDOFFs | --- ## Verification - `npm run typecheck` — **clean across the entire repo** (all five lanes are now present; the whole tree compiles under strict mode). - **Browser smoke test** (`/demo-audio.html`): start gate → booth alive (discs, VU towers, dust, hotbar) → all five repairs light the tracker, run the LED signal-path trace, show subtitles, and step the booth brighter → WIN plays the dark→needle-drop→slam sequence with the "THE MIX IS LIVE" banner. **Zero console errors** across the whole session. ## Adversarial review (multi-agent) — 6 findings, all fixed Ran a 5-dimension review (contract / audio / fx / ui / demo) with an adversarial verify pass. 10 raw findings → 4 refuted as false positives → **6 confirmed, all now fixed**: 1. **[med] Hud** — pause "resume" reused `onStart`, re-running first-start side effects. → Added a separate `onResume` callback (re-lock only). 2. **[low] FxSystem** — `game:win` wasn't idempotent (audio was), so a repeated event desynced visuals vs audio. → Guarded with an already-won check. 3. **[low] groove** — `audio:beat.energy` was a bare constant vs the contract's "low-band energy". → Now scaled by the drums channel gain. 4. **[low] Hud** — the win-banner `setTimeout` leaked (not cleared in `dispose`). → Tracked and cleared. 5. **[low] demo** — control panel `z-index` sat above the start splash, defeating the gesture gate. → Panel is hidden until start. 6. **[low] demo** — resume force-restarted Deck A and desynced its button. → `hasStarted` guard + button label sync + pure `onResume`. Fix #1 changed the `Hud` public API (added optional `onResume`) — noted in the HANDOFFs. All fixes re-typechecked and re-verified live. --- ## Notes for integration (friction to confirm — details in `src/audio/HANDOFF.md`) 1. **`fader:move` → channel mapping**: I map `faderId` containing `pitch` → pitch bend, else the first digit `N` → stem channel `N-1`. Confirm Lane D's `faderId`s (crossfader / named channels) and extend if needed. 2. **Channel↔stem mapping**: 5 stems vs 4 mixer channels + crossfader — `setChannelGain(ch, v)` treats `ch` as a stem index 0..4. 3. **`player:landed`/`player:step`** carry no surface category / position; I use the last stepped surface and listener-centered playback. 4. **`setEmissiveBoost` semantics**: I drive it as an absolute intensity (0.35 → 1.0 base, ~1.8 peak). If Lane A's hook is a multiplier, tune `FxSystem` constants. 5. **VU tower positions** are derived from `LAYOUT.mixer`. If Lane C placed physical LED towers elsewhere, align `src/fx/layout.ts` `VU_TOWERS`. 6. **Hotbar model** — no core type; `Hud` exports `HotbarModel`/`HotbarSlot`, consumed via injected `getHotbar()`. Adapt Lane D's hotbar to this shape. 7. **`game:win`** forces deck playback in audio; ensure Lane D also spins the real platters at the finale so audio and visuals agree. Wiring is exactly as `docs/INTEGRATION.md` step 8 describes: `AudioEngine` + `FxSystem({ scene, setEmissiveBoost })` + `Hud`, audio init behind the start-overlay click, `fx.update(dt)` + `audio.updateListener(player)` each frame. ## Repo / git The working tree is **not yet a git repo** locally, and the fresh remote (`ssh://git@100.71.119.27:222/monster/TURNCRAFT.git`) is empty. I did **not** `git init`/commit/push — that's a cross-lane integration decision, so I left it for Fable/the integrator to sequence (init, land all five lanes, run `docs/INTEGRATION.md`, then push). ## Suggested next steps for Fable 1. Integrate per `docs/INTEGRATION.md` (all five lanes' `HANDOFF.md` first, resolve the friction items above — especially #1, #4, #6). 2. Wire `src/main.ts` step 8 and run the smoke tests. 3. Once integrated, do a full-game soak (the demo already proves Lane E holds 60 fps and never drifts in isolation).