Lane C: round-11 audio contract — room.audio on buildInterior
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>
This commit is contained in:
parent
be9f967d19
commit
89f278b603
@ -3,7 +3,25 @@
|
||||
*Status: **v1 complete & verified**. Standalone interiors library + test page. Every shop door opens
|
||||
into a unique, seeded, themed interior, generated on demand in ~4ms, byte-identical every revisit.*
|
||||
|
||||
Last updated: 2026-07-15 (round 9) · owner: PROCITY-C · reviewer: Fable
|
||||
Last updated: 2026-07-15 (round 11) · owner: PROCITY-C · reviewer: Fable
|
||||
|
||||
---
|
||||
|
||||
## Update 2026-07-15 (round 11, audio) — `room.audio` contract
|
||||
|
||||
R11 §Lane C ("one contract answer"). **Confirmed** Fable's proposal: `buildInterior` now returns
|
||||
`room.audio = { musicKey, toneKey }`, seeded per shop — F's interior_mode plays it, B's audio.js
|
||||
resolves the keys. Full contract + F wiring recipe: [docs/LANES/LANE_C_AUDIO.md](docs/LANES/LANE_C_AUDIO.md).
|
||||
|
||||
- `toneKey` → a `manifest.audio.ambience` interior room-tone (always set); `musicKey` → a
|
||||
`manifest.audio.music` bed, or **null** (no music). Keys, not files — buildInterior stays synchronous
|
||||
and asset-free; silence on missing/`?mute=1`/`?noassets=1` (house audio law).
|
||||
- Maps mirror the manifest's per-type `types` arrays (one source of truth): record/milkbar/video/dept/
|
||||
arcade always play their bed; opshop/book/toy/pawn/stall are room-tone-only, except a seeded ~1-in-3
|
||||
keeps a general radio on (the `general`-tagged milk-bar bed).
|
||||
- Verified (9 types × 8 seeds): 0 bad keys, 0 nondeterminism, every key resolves + matches manifest
|
||||
`types` membership; deterministic per shop. `qa.sh --strict` green.
|
||||
- Optional visual-source prop (`userData.audioEmitter`) deferred to v2.2 (in the note) — out of round scope.
|
||||
|
||||
---
|
||||
|
||||
|
||||
50
docs/LANES/LANE_C_AUDIO.md
Normal file
50
docs/LANES/LANE_C_AUDIO.md
Normal file
@ -0,0 +1,50 @@
|
||||
# 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
|
||||
```js
|
||||
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.** `buildInterior` stays synchronous and asset-free — it only *names* what should
|
||||
play. Resolve `musicKey` → `manifest.audio.music[musicKey]`, `toneKey` →
|
||||
`manifest.audio.ambience[toneKey]` (the `.ogg` + `.m4a` fallback + `gain`/`loop` are on those entries).
|
||||
- **Silence is legal** (house audio law): missing key / `?mute=1` / `?noassets=1` → play nothing, no error.
|
||||
`musicKey` is frequently `null` — treat "no music, room-tone only" as the common case.
|
||||
- **Seeded per shop, stable per revisit.** Derived from `shop.seed` on 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.
|
||||
@ -19,6 +19,8 @@
|
||||
// // stands, in room-local space, facing the customer.
|
||||
// browsePoints: [ {x,z,ry,atKind,slotIndex} ], // 0..3 seeded floor poses (R9) where Lane D stands
|
||||
// // browser rigs, facing the goods (same frame + ry as counter.stand)
|
||||
// audio: { musicKey, toneKey }, // R11 — keys into Lane E manifest.audio (interior music bed +
|
||||
// // room-tone), seeded per shop. musicKey may be null (no music).
|
||||
// dims: { W, D, H, archetype, type }, // room metrics
|
||||
// placement, // deterministic placement summary (deep-equal per seed)
|
||||
// pathOK, // door→counter connectivity held (always true; carved if needed)
|
||||
@ -70,6 +72,32 @@ function normalizeShop(shop) {
|
||||
};
|
||||
}
|
||||
|
||||
// ── interior audio contract (round 11) ───────────────────────────────────────────────
|
||||
// buildInterior returns room.audio = { musicKey, toneKey } — keys into Lane E's manifest.audio (a music
|
||||
// bed + an interior room-tone). Lane F's interior_mode plays them on enter / fades on exit; Lane B's
|
||||
// audio.js resolves key → file. Missing key / ?mute=1 / ?noassets=1 → silence (house audio law), so this
|
||||
// is pure data: buildInterior stays synchronous and asset-free, it only names what SHOULD play.
|
||||
// The maps mirror the manifest's per-type `types` arrays so the type→bed grouping has one source of truth.
|
||||
const ROOMTONE_BY_TYPE = { // manifest.audio.ambience entries with scope:'interior'
|
||||
milkbar: 'roomtone-milkbar',
|
||||
video: 'roomtone-video', dept: 'roomtone-video', arcade: 'roomtone-video',
|
||||
}; // default → 'roomtone-retail'
|
||||
const MUSIC_BY_TYPE = { // manifest.audio.music dedicated beds
|
||||
record: 'record-shop', // "record shop plays music" — always on
|
||||
milkbar: 'milkbar', video: 'video-synth',
|
||||
dept: 'arcade', arcade: 'arcade',
|
||||
}; // default → null (no dedicated bed)
|
||||
|
||||
function audioFor(type, ctx) {
|
||||
const toneKey = ROOMTONE_BY_TYPE[type] || 'roomtone-retail';
|
||||
let musicKey = MUSIC_BY_TYPE[type] || null;
|
||||
// Shops with no dedicated bed: a seeded ~1-in-3 keeps a general radio on (the milk-bar bed carries the
|
||||
// manifest "general" tag). Seeded on its own sub-stream so the same shop is silent-or-playing every
|
||||
// revisit and adding it never shifts another subsystem's seeded picks.
|
||||
if (!musicKey && ctx.stream('audio')() < 0.34) musicKey = 'milkbar';
|
||||
return { musicKey, toneKey };
|
||||
}
|
||||
|
||||
export function buildInterior(shop, THREE, opts) {
|
||||
opts = opts || {}; // tolerate undefined AND explicit null
|
||||
const t0 = (typeof performance !== 'undefined' ? performance.now() : 0);
|
||||
@ -122,6 +150,7 @@ export function buildInterior(shop, THREE, opts) {
|
||||
places: lay.places,
|
||||
counter: lay.counter, // { mesh, pose:{x,z,ry}, stand:{x,z,ry} } — Lane D keeper spawn pose
|
||||
browsePoints: lay.browsePoints, // [ {x,z,ry,atKind,slotIndex} ] 0..3 — Lane D browser rig poses (R9)
|
||||
audio: audioFor(recipe.key, ctx), // { musicKey, toneKey } — Lane E manifest.audio keys (R11), seeded per shop
|
||||
dims: { ...dims, archetype, type: recipe.key },
|
||||
recipe: { key: recipe.key, label: recipe.label, counterPos: recipe.counterPos, clutter: recipe.clutter },
|
||||
placement: lay.placement,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user