diff --git a/docs/audio-candidate-pool.md b/docs/audio-candidate-pool.md index 77cd033..140c3b4 100644 --- a/docs/audio-candidate-pool.md +++ b/docs/audio-candidate-pool.md @@ -9,8 +9,10 @@ model — random pick per landing, or pairable per-clip). The "audio + video" setting layers **two** kinds of audio per altitude: 1. **Ambience / audio track** — the soundscape (waves, reef crackle, chorus, etc.). **This is what we're sourcing now.** -2. **Music track** — a separate composed musical layer. **Deferred** until the - ambience tracks are settled across all altitudes. +2. **Music track** — a separate composed musical layer. **Shipped** (2026-07-01) + as the Audio-dropdown **"Music"** option: unlike the earlier per-altitude idea, + it is now **one single ethereal loop played the same at every altitude** (see + § Music below). Pools below are the **ambience** layer unless marked `music`. **Distinction (operator):** the ambience/soundtrack layer is an *environmental, real-world soundscape* (waves, reef, @@ -48,6 +50,22 @@ recordings (NASA/NOAA) keep the "machines reshaping perception" thesis literal. | 🏖 coast | `coast/waves` | Sea waves + tern calls | BigSoundBank #0267 — https://bigsoundbank.com/sea-waves-and-seagulls-s0267.html (`/UPLOAD/mp3/0267.mp3`) | 57s | CC0 | | 🕳 abyss | `abyss/whale` | NE Pacific blue-whale "AB call" | NOAA PMEL — https://www.pmel.noaa.gov/acoustics/multimedia/NETS_bluWhale.wav | 13s | PD (NOAA) | +## Music (the Audio-dropdown "Music" option, shipped 2026-07-01) + +**One** ethereal ambient loop, played the same at every altitude (altitude-independent — +NOT the per-altitude idea the "cosmos/music" / "Frozen Star" rows above reserved; those +stay as historical candidates). The operator's brief: *ethereal, relaxing — like +innerflo.me.* CC0 (operator's sourcing choice), so attribution is courtesy, not required. + +| id | clip | source | dur | license | +|---|---|---|---|---| +| `music/ethereal` | **✓ KEEP** — "Ambient F Sharp Minor Ethereal Choir Pad 1": blurry, paulstretched layered choir — "peaceful, magical, soothing" | deadrobotmusic — https://freesound.org/people/deadrobotmusic/sounds/808032/ (hq preview: `cdn.freesound.org/previews/808/808032_*-hq.mp3`) | 3:38 | CC0 | + +Production: `simulator/build_audio_media.py` `SOURCES["music"]` → seamless loudness-normalized +loop at `audio/music/ethereal.loop.mp3`, served `/media/audio/music/ethereal.loop.mp3` +(client fallback: `app.js` `MUSIC_FALLBACK`). Download the sound page's WAV/MP3 to +`simulator/sample_media/audio/music/ethereal.mp3`, then `python simulator/build_audio_media.py`. + ## Sourcing rule (learned 2026-06-26) **Source only from a browsable page the operator can open and whose license is stated.** @@ -67,7 +85,10 @@ hubs: NASA SVS / Chandra, NOAA PMEL/SanctSound (PD), BigSoundBank (CC0), Pixabay - **reef** — ✅ round-4 6-layer bake (`reef/soundscape`): NOAA PD biological layers + a CC0 submerged underwater "you're under" bed (DCSFX freesound #366159, hq preview). License clean. SanctSound clips: `files/SanctSound_*.mp4` at `https://sanctsound.ioos.us/files/` (extract audio). -- **music layer** — separate composed music track per altitude, deferred (cosmos slot reserved). +- **music layer** — ✅ SHIPPED 2026-07-01 as the Audio-dropdown "Music" option, but as + ONE altitude-independent ethereal loop (`music/ethereal`, CC0), not the per-altitude + track once envisioned. See § Music above. (The `cosmos/music` / "Frozen Star" rows are + now historical per-altitude candidates, not planned work.) - **Production (after picks):** loop seams, length normalization, cross-altitude crossfade (mirror video); cosmos/abyss clips are short → loop. diff --git a/docs/superpowers/specs/2026-07-01-audio-music-option-design.md b/docs/superpowers/specs/2026-07-01-audio-music-option-design.md new file mode 100644 index 0000000..a02f9df --- /dev/null +++ b/docs/superpowers/specs/2026-07-01-audio-music-option-design.md @@ -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 `
` (`simulator/static/index.html`) gains a dropdown above the +renamed slider: + +``` +Output + [ Video ] (toggle) + Audio [ Soundtrack ▾ ] ← NEW: ` 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/.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/.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 / 音楽) +- ` chooses the +// source — None / Soundtrack / Music. Music is ONE ethereal loop played the same at +// every altitude (no per-altitude crossfade). + +async function enter(page: Page) { + await page.goto("/"); + await page.waitForFunction(() => (window as any).__hefReady === true, null, { timeout: 45_000 }); + await page.locator("#welcome-launch").click(); + await page.waitForSelector("#welcome", { state: "detached", timeout: 10_000 }); +} + +async function selectSource(page: Page, v: string) { + await page.selectOption("#audio-source", v); +} + +// The url the active