# Lane E — HANDOFF (Audio / FX / UI) Lane E owns `src/audio/**`, `src/fx/**`, `src/ui/**`, `src/demo/audioDemo.ts`, `demo-audio.html`. This is the primary handoff; see also `src/fx/HANDOFF.md` and `src/ui/HANDOFF.md`. **Everything is done and typechecks clean; the demo runs with zero console errors and previews the full audio-visual arc.** ## Public API surface (what the integrator constructs) ```ts const audio = new AudioEngine(); // self-wires to the bus await audio.init(); // MUST be a user gesture (autoplay) audio.updateListener(playerView); // call per frame audio.getLevels(); // { low, mid, high } for FX VU // also: setStemCount, setChannelGain, setPitch, setPlaybackState, playSfx const fx = new FxSystem({ scene, setEmissiveBoost, getLevels: () => audio.getLevels() }); fx.update(dt); // call each fixed tick const hud = new Hud({ onStart, onResume, getHotbar }); // onStart(once)=>audio.init()+lock; onResume=>re-lock only ``` ## What's done - **Groove** (`groove.ts` + `synth.ts` + `scheduler.ts`): fully synthesized 118 BPM, 8-bar F-minor deep-house loop in 5 stems (drums/bass/chords/lead/ sweeps), scheduled on a drift-free lookahead transport (25 ms tick, notes on `AudioContext.currentTime`). `setStemCount(0..5)` stacks them musically; muted stems cost nothing (scheduling is gated). Sidechain-ducked bass. - **Spin-up/brake**: `setPlaybackState(deck, playing, rpm)` eases a `rate` that scales tempo AND per-voice detune together (`rpm/rpm33`; 45 = +35% & sharper). `setPitch(±1)` detunes ±2 semitones. Braked to a stop = silence. - **Positional**: mix outputs into two PannerNodes at the deck spindles (`LAYOUT`), plus an always-on low-passed dry bass bed. `updateListener` follows the player eye/orientation. - **Beat/bar events**: `audio:beat {energy}` (energy = kick gain) and `audio:bar` are emitted from the scheduler at the *audible* time (queued and drained on the ctx clock), so LED pulses stay in sync. - **SFX** (`sfx.ts`): footsteps per surface category, land (impact-scaled), break/place, fader zip, button clunk, RCA "clunk-clunk-CLICK", fuse zap, tonearm cue creak, needle drop. Positional when a world `at` is given. - **Win audio timeline**: on `game:win`, ducks to silence, then at +1.8 s snaps the platters to speed, drops the needle, and slams the full mix in behind a lowpass that sweeps open — all scheduled on the ctx clock (no timers). - **FX & HUD**: see the two sibling HANDOFF files. ## Contract friction / assumptions the integrator should confirm 1. **`fader:move` mapping**: I map `faderId` containing `pitch` → pitch bend, else the first digit `N` → stem channel `N-1`. If Lane D uses other `faderId`s (`crossfader`, named channels), confirm/extend the map in `AudioEngine.wireBus()`. 2. **Channel↔stem mapping**: 5 stems vs the mixer's 4 channels + crossfader. `setChannelGain(ch, v)` treats `ch` as a stem index 0..4. Reconcile with Lane D's channel model if needed. 3. **`player:landed` has no surface category** (and `player:step` no position); I use the last stepped surface and play footsteps listener-centered. Fine unless you want spatialized steps. 4. **`setEmissiveBoost` semantics**: I drive it as an absolute intensity — dead-booth base `0.35`, stepping toward `1.0` over the 5 repairs, pulsing to ~`1.8` on beats. If Lane A's hook is a multiplier with a different baseline, tune the constants in `FxSystem` (`boostBase`, `pulseTau`). 5. **VU tower positions** are derived from `LAYOUT.mixer` (no explicit VU coords in constants). If Lane C placed physical LED-block towers elsewhere, align `src/fx/layout.ts` `VU_TOWERS` to them. 6. **Hotbar model**: no core type exists, so `Hud` defines `HotbarModel`/ `HotbarSlot` and reads it via an injected `getHotbar()`. Adapt Lane D's hotbar to this shape (or wrap it). 7. **`game:win` forces deck playback in audio.** Ensure Lane D also spins the real platters visually at the finale so audio and visuals agree. Nothing in `src/core/` was edited. No `setBlock` anywhere in Lane E. No rAF loop outside `src/demo/`. `npm run typecheck` is clean. --- ## Round 2 — Headshell Workshop (Lane E slice) The workshop machine (Lane D, `src/machines/workshop/`) is fully decoupled from Lane E **via the bus** — Lane D emits, Lane E sounds/shows. No direct calls, no shared imports. What Lane E added (all additive): - **`src/audio/testSignal.ts`** — `playTestSignal(errors)` renders the stage-5 needle-drop diagnostic in true stereo (ChannelMerger), one audible chain per fault: `swapLR` (hard L/R ping-pong), `phaseCancel` (channel invert → thin), `azimuthTilt` (one channel −12 dB + waveshaper), `trackingLight` (80 ms gate stutter), `trackingHeavy` (1.2 kHz lowpass), `skate` (abort zip). `errorsFromKeys(string[])` adapts the event payload. Error vocabulary matches Lane D's `machines/workshop/types.ts` exactly (incl. `skate`). - **`src/audio/sfx.ts`** — 9 workshop one-shots: `crimp, wireOn, wireOff, screwTick, screwClunk, tiltCreak, weightDetent, bubbleLock, skate`. - **`AudioEngine`** self-wires these bus reactions: - `workshop:test {errors, clean}` → `playTestSignal(errorsFromKeys(errors))`. - `workshop:torque {value}` → throttled `screwTick` ratchet (value `null` = no-op). - `machine:interact {action}` → workshop action keywords (`crimp/wire/screw/tilt/detent/weight/bubble/skate/seat`) → the matching one-shot. Public `playTestSignal(errors)` also exists for direct use. **Integration contract (for Fable / Lane D):** - `trackingHeavy` — Lane E does the lowpass; **Lane D should also sag the platter target rpm ~15%** (platter.ts) for the full "drag" effect. - The **scope block is Lane D's** (its own `drawScopeCanvas`); Lane E renders no scope (my earlier `src/fx/scope.ts` was removed to avoid duplication). - Screw confetti + torque gauge + diagnostic subtitle: see `fx`/`ui` HANDOFFs. Verified live (demo-audio.html → "Headshell Workshop" panel): every error mode plays without clipping, the subtitle names the fault, the radial torque gauge tracks + hides, screw confetti fires, all 9 SFX distinct. Zero console errors; `npm run typecheck` clean across the whole tree (with Lane D's Workshop.ts present).