TURNCRAFT/src/audio/HANDOFF.md
jing 9c668d340c Headshell Workshop + Glow-Up phase: assembly minigame, art pass, review fixes
- 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>
2026-07-14 21:32:07 +10:00

6.3 KiB
Raw Blame History

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 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 faderIds (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.tsplayTestSignal(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).