diff --git a/docs/audio-candidate-pool.md b/docs/audio-candidate-pool.md index 140c3b4..6073fa1 100644 --- a/docs/audio-candidate-pool.md +++ b/docs/audio-candidate-pool.md @@ -50,7 +50,13 @@ 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) +## Music (the Audio-dropdown "Music" option) + +> **SUPERSEDED 2026-07-02:** the single ethereal loop below was replaced by a +> **rotating crossfading playlist of mellow drumless synth** (Kevin MacLeod, CC BY) — +> see `docs/music-candidate-pool.md`. The ethereal-loop record is kept for history. + +### (history) ethereal loop — shipped 2026-07-01, then replaced **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 diff --git a/docs/music-candidate-pool.md b/docs/music-candidate-pool.md index cd44014..fc42edc 100644 --- a/docs/music-candidate-pool.md +++ b/docs/music-candidate-pool.md @@ -12,13 +12,22 @@ URL). Local review gallery (gitignored, like the other review pages): **Honesty flag:** licenses + URLs are verified; the "harmony" notes are inferred from genre/reputation, not a by-ear check — audition before final selection. -## Operator picks & refined brief (2026-07-02) +## FINAL selection — WIRED 2026-07-02 -Operator auditioned and **kept `dreamcult` (Dream Culture) + `silverblue` (Silver Blue Light)** -— both Kevin MacLeod, CC BY, drumless cinematic-calm synth. **Rejected everything else** as -"boring OR too much snare drum." So the refined target is **drumless, evolving-chord ambient -synth** in that exact lane — the HoliznaCC0 lo-fi (beat-driven) is OUT. A Round-4 search is -sourcing ~8–12 more like the two keepers (Kevin MacLeod's Calming/Ambient catalog first). +After a Round-4 drumless pass, the operator's **final Music set = 5 Kevin MacLeod tracks** +(CC BY 4.0), now **shipped as a rotating crossfading playlist**: + +**Music for Manatees · Dewdrop Fantasy · Eastminster · Overheat · Concentration** + +Wiring: `app.js` `MUSIC_PLAYLIST` — the Music source plays one track and every +`MUSIC_ROTATE_MS` (5 min) crossfades to a random OTHER track (`pickNextIndex`, never an +immediate repeat), altitude-independent, on the two `#aud`/`#aud-b` elements. Files +loudness-normalized + transcoded under the 25MB LFS limit by `build_audio_media.py` +(`MUSIC`), committed via LFS; the single ethereal loop was **retired**. Credited on +`credits.html` (CC BY attribution). Earlier candidates below kept for record. + +*(History: the operator first kept Dream Culture + Silver Blue Light, then — after the +drumless Round-4 pass — chose the 5 long-form/floaty tracks above instead.)* ## Two license families diff --git a/simulator/build_audio_media.py b/simulator/build_audio_media.py index fafc287..0d8ba75 100644 --- a/simulator/build_audio_media.py +++ b/simulator/build_audio_media.py @@ -1,42 +1,62 @@ -"""Production pass for the 5 per-altitude soundtracks + the single Music loop -(audio spec §5.3; Music layer per docs/audio-candidate-pool.md). The audio -analogue of build_pool_manifest.py --media: read the sourced ambience clips -(re-downloadable per docs/audio-candidate-pool.md), and make each a seamless -loudness-normalized loop. +"""Production pass for the 5 per-altitude soundtracks + the Music playlist +(audio spec §5.3; Music per docs/music-candidate-pool.md). The audio analogue of +build_pool_manifest.py --media: read the sourced clips (re-downloadable per the +candidate-pool docs) and make each web-ready. -Media is gitignored; this script is the reproducible record. Outputs land under -simulator/sample_media/audio//.loop.mp3, served at /media/audio/... -Run: python simulator/build_audio_media.py +- Soundtracks: seamless loudness-normalized loops -> audio//.loop.mp3. +- Music: the mellow drumless synth playlist (Kevin MacLeod, CC BY 4.0) — NOT looped + (the client crossfades between tracks). Each is loudness-normalized and transcoded + to a bitrate that keeps it under the ~25MB git-LFS/nginx limit (ambient tolerates + it) -> audio/music/.mp3. Put the downloaded original at audio/music/.src.mp3 + (URLs below) first; this transcodes it. -(White-noise is deferred — the live Audio control is a simple on/off soundtrack -toggle. `tools.pipeline.audio_run.generate_white_noise` remains for when it's -wanted, but no noise bed is built here.) +Media is committed via git-LFS; this script is the reproducible record. +Run: python -m simulator.build_audio_media """ from __future__ import annotations +import subprocess from pathlib import Path from tools.pipeline.audio_run import process_soundtrack +from tools.pipeline.run import resolve_ffmpeg AUDIO = Path(__file__).parent / "sample_media" / "audio" # scale -> (sourced raw file, production output file), both under audio//. # Sources per docs/audio-candidate-pool.md (the "✓ KEEP" picks). -# -# The `music` entry is the Audio-dropdown "Music" layer (docs/audio-candidate-pool.md -# § Music): ONE ethereal ambient loop played the same at every altitude — not a -# per-altitude soundtrack. It lands at audio/music/ethereal.loop.mp3, served at -# /media/audio/music/ethereal.loop.mp3 (client fallback: app.js MUSIC_FALLBACK). SOURCES: dict[str, tuple[str, str]] = { "cosmos": ("pillars.mp3", "pillars.loop.mp3"), "orbit": ("spaceamb.mp3", "spaceamb.loop.mp3"), "coast": ("waves.mp3", "waves.loop.mp3"), "reef": ("soundscape.mp3", "soundscape.loop.mp3"), "abyss": ("whale.wav", "whale.loop.mp3"), - "music": ("ethereal.mp3", "ethereal.loop.mp3"), } +# Music playlist (docs/music-candidate-pool.md § Round 4): name -> (incompetech source +# URL, target bitrate). Kevin MacLeod, CC BY 4.0 — credited on credits.html. Drumless, +# chord-moving ambient; the operator's picks. Longer tracks get a lower bitrate to stay +# under the LFS size limit. Client playlist: app.js MUSIC_PLAYLIST. +_KM = "https://incompetech.com/music/royalty-free/mp3-royaltyfree/" +MUSIC: dict[str, tuple[str, str]] = { + "manatees": (_KM + "Music%20for%20Manatees.mp3", "96k"), + "dewdrop": (_KM + "Dewdrop%20Fantasy.mp3", "80k"), + "eastminster": (_KM + "Eastminster.mp3", "128k"), + "overheat": (_KM + "Overheat.mp3", "128k"), + "concentration": (_KM + "Concentration.mp3", "80k"), +} + + +def _transcode(src: Path, dst: Path, bitrate: str, ff: str) -> Path: + """Loudness-normalize + transcode to mp3 at `bitrate` (no looping).""" + subprocess.run( + [ff, "-y", "-hide_banner", "-loglevel", "error", "-i", str(src), + "-af", "loudnorm", "-c:a", "libmp3lame", "-b:a", bitrate, "-ar", "44100", str(dst)], + check=True, + ) + return dst + def main() -> None: for scale, (raw, out) in SOURCES.items(): @@ -47,6 +67,15 @@ def main() -> None: dst = process_soundtrack(src, AUDIO / scale / out) print(f" produced {dst.relative_to(AUDIO.parent)}") + ff = resolve_ffmpeg() + for name, (url, bitrate) in MUSIC.items(): + src = AUDIO / "music" / f"{name}.src.mp3" + if not src.exists(): + print(f" SKIP music/{name}: source missing ({src}) — download {url}") + continue + dst = _transcode(src, AUDIO / "music" / f"{name}.mp3", bitrate, ff) + print(f" produced {dst.relative_to(AUDIO.parent)} @ {bitrate}") + if __name__ == "__main__": main() diff --git a/simulator/e2e/tests/audio-source.spec.ts b/simulator/e2e/tests/audio-source.spec.ts index 9df942b..631f188 100644 --- a/simulator/e2e/tests/audio-source.spec.ts +++ b/simulator/e2e/tests/audio-source.spec.ts @@ -2,7 +2,7 @@ import { test, expect, Page } from "@playwright/test"; // The Audio-source feature (docs/superpowers/specs/2026-07-01-audio-music-option-design.md): // the old 0–10 "Audio" slider becomes "Volume", and a new Audio