- Workshop (WORKSHOP_CARTRIDGE): five-stage cartridge assembly at Deck A — seat/square, crimp four tag-wires, torque screws, ride-the-arm counterweight balance, needle-drop diagnostic with per-fault audio + scope. Relay-synced (per-field co-op merge: held screw + carried wire survive remote state). - Glow-Up (G1-G5): 32px atlas with per-voxel variants, selective LED bloom (quality-gated), screen-print decal system + party flyers, mixer/PCB worldgen density pass, record groove-sheen side texture. - 10 confirmed multi-agent review fixes, incl. co-op screw-stomp soft-lock, ride-snap collider-identity (magnet feet / eaten record-fling), workshop SFX exact-match map (rca_seated hijack), beam ride colliders to the head, double-crimp guard, WIRING INCOMPLETE diagnosis mode, skate-abort timer, completedState wiring, per-tick material churn, trackingHeavy platter drag. - Crossfader playtest fix: slew-limited sled (3.4 v/s) + ribbed grip caps with amber index — the sled reads as a heavy handle, not a teleporting wall. - Demo harnesses: machinesDemo hold-key wiring, playerDemo seesaw phase continuity, audioDemo incomplete fault button. - Workshop sync: exact 1.0 screw endpoint gets its own emit signature. Verified: typecheck + vite build clean, live solo quest smoke, two-client co-op relay smoke (simultaneous torque, no rewind, exact convergence). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
6.3 KiB
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)
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 onAudioContext.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 aratethat 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.updateListenerfollows the player eye/orientation. - Beat/bar events:
audio:beat {energy}(energy = kick gain) andaudio:barare 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 worldatis 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
fader:movemapping: I mapfaderIdcontainingpitch→ pitch bend, else the first digitN→ stem channelN-1. If Lane D uses otherfaderIds (crossfader, named channels), confirm/extend the map inAudioEngine.wireBus().- Channel↔stem mapping: 5 stems vs the mixer's 4 channels + crossfader.
setChannelGain(ch, v)treatschas a stem index 0..4. Reconcile with Lane D's channel model if needed. player:landedhas no surface category (andplayer:stepno position); I use the last stepped surface and play footsteps listener-centered. Fine unless you want spatialized steps.setEmissiveBoostsemantics: I drive it as an absolute intensity — dead-booth base0.35, stepping toward1.0over the 5 repairs, pulsing to ~1.8on beats. If Lane A's hook is a multiplier with a different baseline, tune the constants inFxSystem(boostBase,pulseTau).- VU tower positions are derived from
LAYOUT.mixer(no explicit VU coords in constants). If Lane C placed physical LED-block towers elsewhere, alignsrc/fx/layout.tsVU_TOWERSto them. - Hotbar model: no core type exists, so
HuddefinesHotbarModel/HotbarSlotand reads it via an injectedgetHotbar(). Adapt Lane D's hotbar to this shape (or wrap it). game:winforces 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'smachines/workshop/types.tsexactly (incl.skate).src/audio/sfx.ts— 9 workshop one-shots:crimp, wireOn, wireOff, screwTick, screwClunk, tiltCreak, weightDetent, bubbleLock, skate.AudioEngineself-wires these bus reactions:workshop:test {errors, clean}→playTestSignal(errorsFromKeys(errors)).workshop:torque {value}→ throttledscrewTickratchet (valuenull= no-op).machine:interact {action}→ workshop action keywords (crimp/wire/screw/tilt/detent/weight/bubble/skate/seat) → the matching one-shot. PublicplayTestSignal(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 earliersrc/fx/scope.tswas removed to avoid duplication). - Screw confetti + torque gauge + diagnostic subtitle: see
fx/uiHANDOFFs.
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).