docs(spec): Audio source dropdown (None/Soundtrack/Music) + Volume rename
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
# Audio "Music" option + "Volume" rename — design
|
||||
|
||||
*Date: 2026-07-01 · Status: approved design, awaiting spec review*
|
||||
|
||||
## Goal
|
||||
|
||||
Give the listener a choice of what to hear. Today the Output panel has a single
|
||||
0–10 slider labeled **Audio** that acts as the master gain for the per-altitude
|
||||
ambience crossfade. We split that into two controls:
|
||||
|
||||
- **Audio** — a dropdown selecting the audio *source*: **None · Soundtrack · Music**
|
||||
- **Volume** — the existing 0–10 slider (relabeled), master gain for whatever
|
||||
source is active
|
||||
|
||||
**Music** is one ethereal, relaxing ambient loop (the innerflo.me vibe) that
|
||||
plays continuously and does **not** change with altitude.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- No per-altitude music set (Music is a single track, not a musical Soundtrack).
|
||||
- No change to the Soundtrack behavior itself (per-altitude crossfade stays exactly as-is).
|
||||
- No new audio elements — the existing `#aud` / `#aud-b` pair is reused.
|
||||
|
||||
## Control surface
|
||||
|
||||
The Output `<fieldset>` (`simulator/static/index.html`) gains a dropdown above the
|
||||
renamed slider:
|
||||
|
||||
```
|
||||
Output
|
||||
[ Video ] (toggle)
|
||||
Audio [ Soundtrack ▾ ] ← NEW: <select id="audio-source">
|
||||
Volume ●──────── 2/10 ← existing #audio slider, label → "Volume"
|
||||
Speed ●──────── 1.00×
|
||||
🌐 [ Language ▾ ]
|
||||
```
|
||||
|
||||
- The dropdown is a `<select id="audio-source">` with options `none` / `soundtrack`
|
||||
/ `music`, default **`soundtrack`** (preserves today's launch experience).
|
||||
- The slider keeps `id="audio"` (avoids touching every reference) but its visible
|
||||
label becomes **Volume** (new i18n key `output.volume`). The `output.audio` key
|
||||
("Audio") now labels the dropdown.
|
||||
|
||||
## Behavior by mode
|
||||
|
||||
| Mode | Behavior |
|
||||
|---|---|
|
||||
| **None** | Both `#aud`/`#aud-b` fade to silence — the existing `audioLevel()===0` path. The Volume slider is disabled (greyed). |
|
||||
| **Soundtrack** | Unchanged: per-altitude ambience crossfading across the two elements as the dial moves (`blendAudio` / `restAudio`). |
|
||||
| **Music** | The single music loop plays on `#aud` at master volume; `#aud-b` fades out. Altitude changes do **not** crossfade or reload audio. |
|
||||
|
||||
Volume = 0 remains a hard mute in every mode.
|
||||
|
||||
## Code shape
|
||||
|
||||
All in `simulator/static/app.js`, following the existing audio-layer seams
|
||||
(`restAudio`, `blendAudio`, `ensurePlaying`, `scaleAudioUrl`):
|
||||
|
||||
- **`audioSource()`** — new helper reading `#audio-source` → `"none" | "soundtrack" | "music"`.
|
||||
- **`musicUrl()`** — returns the resolved music loop url via `mediaBase()`
|
||||
(mirrors `scaleAudioUrl`'s R2/local routing), e.g. `mediaBase()+"audio/music/<name>.loop.mp3"`.
|
||||
- **`restAudio(index)`** branches:
|
||||
- `none` (or `audioLevel()===0`) → existing fade-to-silence.
|
||||
- `music` → `ensurePlaying(aud, musicUrl(), audioVol())`; `fadeVolume(audB, 0, …)`.
|
||||
- `soundtrack` → unchanged per-altitude logic.
|
||||
- **`blendAudio(lo, frac)`** returns early (no crossfade) when source is `music` or
|
||||
`none`; only Soundtrack blends across the altitude boundary.
|
||||
- **Dropdown wiring**: `#audio-source` `change` → reconcile synchronously in the
|
||||
gesture (`applyAudio()`), same Safari-unlock discipline the slider already uses;
|
||||
toggles the Volume slider's `disabled` when None.
|
||||
- **`beginExperience()`**: unchanged except it snaps **Volume** (`#audio`) to the
|
||||
start level 2; default source stays Soundtrack.
|
||||
- **Diagnostic readout** (`audioStatusText`): prefix with the current source
|
||||
(`none`/`soundtrack`/`music`) so a "no sound" report stays legible.
|
||||
|
||||
No server/engine change — audio source is a pure client concern.
|
||||
|
||||
## Music asset & licensing
|
||||
|
||||
- One **CC0 / public-domain** ethereal ambient loop (innerflo.me-style: soft,
|
||||
evolving, no percussion, seamless loop). Sourced by the implementer; reviewed by
|
||||
ear before ship.
|
||||
- Stored at `simulator/.../media/audio/music/<name>.loop.mp3` alongside the
|
||||
per-scale soundtracks, and registered in the credits manifest exactly like every
|
||||
other audio asset (this project credits all CC/PD audio — see `credits.html`).
|
||||
- Fallback path constant mirrors the `SCALE_AUDIO_FALLBACK` pattern so Music works
|
||||
even against an older server process.
|
||||
|
||||
## i18n + a11y
|
||||
|
||||
- New key `output.volume` (en: "Volume", es: "Volumen", fr: "Volume", ja: "音量").
|
||||
- Dropdown option labels localized in en/es/fr/ja:
|
||||
- None (Ninguno / Aucun / なし)
|
||||
- Soundtrack (Banda sonora / Bande-son / サウンドトラック)
|
||||
- Music (Música / Musique / 音楽)
|
||||
- `<select id="audio-source">` gets an `aria-label` tied to the Audio label.
|
||||
- Reduced-motion behavior is unaffected (audio is orthogonal to motion).
|
||||
|
||||
## Tests
|
||||
|
||||
- **Node unit** (`simulator/unit/…`): `audioSource()` routing and the
|
||||
`blendAudio`/`restAudio` branch selection — none→silent, music→single-element +
|
||||
no crossfade on altitude change, soundtrack→unchanged.
|
||||
- **E2E** (`simulator/e2e/…`): dropdown switches modes; Music produces a single
|
||||
playing element that persists across an altitude change; None disables the Volume
|
||||
slider and silences audio; the slider label reads "Volume".
|
||||
|
||||
## Decisions (confirmed with operator)
|
||||
|
||||
1. **Music = one continuous, altitude-independent loop** (not per-altitude, not ducked).
|
||||
2. Default Audio source = **Soundtrack**; Music is opt-in.
|
||||
3. **None** disables the Volume slider.
|
||||
4. Music track is **operator-approved CC0** sourced by the implementer.
|
||||
Reference in New Issue
Block a user