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

125 lines
6.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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)`.
- `FxSystem``update(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 `PannerNode`s 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.