buildInterior returns room.audio = { musicKey, toneKey }, seeded per shop: keys into Lane E's manifest.audio (interior music bed + room-tone). Lane F's interior_mode plays it, B's audio.js resolves keys->files. Keys not files: buildInterior stays synchronous + asset-free; silence on missing/mute/noassets (house audio law).
Maps mirror the manifest per-type types arrays: record/milkbar/video/dept/arcade always play their bed; other types room-tone-only except a seeded ~1-in-3 general radio (the 'general'-tagged milk-bar bed).
Verified 9 types x 8 seeds: 0 bad keys, 0 nondeterminism, all keys resolve + match manifest type membership, deterministic per shop. Contract + F wiring: docs/LANES/LANE_C_AUDIO.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3.1 KiB
LANE C — audio contract (round 11) → for Lane F + Lane B
PROCITY-C, 2026-07-15. Answers the R11 §Lane C question: "where should the interior bed + room-tone
attach?" Confirmed — buildInterior now returns room.audio = { musicKey, toneKey }, seeded per
shop. Lane F's interior_mode just plays it; Lane B's audio.js resolves the keys → files.
The contract
room.audio = {
musicKey, // string | null — an interior MUSIC bed key in manifest.audio.music, or null (no music)
toneKey, // string — an interior ROOM-TONE key in manifest.audio.ambience (scope:'interior')
}
- Keys, not files.
buildInteriorstays synchronous and asset-free — it only names what should play. ResolvemusicKey→manifest.audio.music[musicKey],toneKey→manifest.audio.ambience[toneKey](the.ogg+.m4afallback +gain/loopare on those entries). - Silence is legal (house audio law): missing key /
?mute=1/?noassets=1→ play nothing, no error.musicKeyis frequentlynull— treat "no music, room-tone only" as the common case. - Seeded per shop, stable per revisit. Derived from
shop.seedon its own sub-stream, so the same shop is silent-or-playing identically every visit (no per-frame or per-enter randomness).
What each type resolves to (mirrors manifest.audio types arrays — one source of truth)
| type | toneKey | musicKey |
|---|---|---|
| record | roomtone-retail | record-shop (always — "record shop plays music") |
| milkbar | roomtone-milkbar | milkbar (radio, always) |
| video | roomtone-video | video-synth (always) |
| dept, arcade | roomtone-video | arcade (always) |
| opshop, book, toy, pawn, stall | roomtone-retail | null, or milkbar for a seeded ~1-in-3 (a general radio playing — the milk-bar bed is types:[…,'general']) |
Verified (9 types × 8 seeds): 0 bad keys, 0 nondeterminism, every key resolves + matches its manifest
types membership; dedicated-music types are stable, non-music types vary on/off by seed.
Lane F wiring (interior_mode)
on enter(room):
if room.audio.toneKey: playLoop(ambience[room.audio.toneKey], fadeIn) // room-tone always
if room.audio.musicKey: playLoop(music[room.audio.musicKey], fadeIn) // may be null → skip
on exit / dispose(room):
fadeOut + stop both, release AudioNodes // your enter/exit leak smoke checks this
Nothing before the first gesture; ?mute=1/?noassets=1 short-circuit before any fetch. room.audio
is plain data on the return — no new lifecycle, no dispose hook needed from Lane C's side.
Optional (deferred to v2.2, per the round scope)
A places-tagged visual source ("the music comes from there") — e.g. a radio/hifi mesh with
userData.audioEmitter = musicKey so Lane B can position/spatialise the bed. Only the record shop has a
fitting for it today (listeningCorner); milk-bar/video would need a new radio prop. Not in this round —
proposing the userData.audioEmitter hook + the two props as a tidy v2.2 item. v2.1 plays the bed
non-spatially (room-filling), which is the right default for an interior anyway.