TURNCRAFT/docs/briefs/LANE_E_AUDIO_FX.md
jing 5a39e3a947 TURNCRAFT: contracts, docs, and all five lane deliverables (pre-integration)
Lanes A (engine), B (player), C (worldgen), D (machines/quest),
E (audio/fx/ui) as landed, each with HANDOFF.md + update docs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 20:50:56 +10:00

6.5 KiB
Raw Blame History

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) (05 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).
  • FxSystemupdate(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 PannerNodes 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 05, 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.