Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 625d32c0f3 | |||
| 4ac39c9034 | |||
| 3d46b96ecb | |||
| 3b19d3eb92 |
@@ -0,0 +1,88 @@
|
||||
# Session 0021.0 — Transcript
|
||||
|
||||
> App: human-experience-filter-art
|
||||
> Start: 2026-06-26T08-12 (PST)
|
||||
> End: 2026-06-26T08-20 (PST)
|
||||
> Type: executing-plans (operator-driven hotfix/revision of session 0020's audio feature)
|
||||
> Posture: yolo
|
||||
> Claude-Session: e0cc053a-41fa-416d-a4f5-493f14984e75
|
||||
> Status: FINALIZED
|
||||
|
||||
## Launch prompt
|
||||
|
||||
> Show video unchecked doesn't turn off the video. Make "show video" just "Video"
|
||||
> and have a toggle like "Dev Mode" has. I can't hear any audio when "soundtrack
|
||||
> (follows altitude)" is selected. Let's just have Audio as off/on like Video for
|
||||
> now and get rid of white noise.
|
||||
|
||||
## Pre-state
|
||||
|
||||
- Session 0020 had just merged the audio feature to `main` (Visual×Audio split,
|
||||
per-altitude soundtracks, pink white-noise, A/B crossfade). The operator tested it
|
||||
live and reported three issues.
|
||||
|
||||
## Session arc
|
||||
|
||||
1. **Diagnosis (the key work).** Both reported bugs were reproduced *only* in the
|
||||
operator's real browser — neither appeared in headless Chromium **or** WebKit
|
||||
(installed `playwright install webkit` to test Safari's engine). Root causes:
|
||||
- **Video-off doesn't blank:** `#vid` (`<video>`) and `#paint` (WebGL `<canvas>`)
|
||||
are GPU-composited layers; the `#black` cover had *auto* z-index, so on a real
|
||||
GPU (Safari/Chrome) the video layer can composite *above* it despite DOM order.
|
||||
Headless software-raster hides this.
|
||||
- **No soundtrack audio:** real Safari/iOS unlocks an `<audio>` element only when
|
||||
`play()` is called *synchronously inside* the user gesture; the 0020 gesture
|
||||
played in an async continuation (after `await fetch`), which Chrome allows but
|
||||
Safari blocks. Headless WebKit relaxes autoplay, so it passed.
|
||||
This is the session's main lesson: **Playwright headless relaxes both GPU
|
||||
compositing and autoplay**, so the 0020 E2E couldn't have caught either — real
|
||||
device / by-eye review is required.
|
||||
2. **Fixes (branch `session-0021`):**
|
||||
- **Video** + **Audio** are now Dev-Mode-style on/off toggles (`.dev-switch`),
|
||||
wired on `change`. Audio on → soundtrack; **white-noise deferred** (removed from
|
||||
the live control; `AUDIO_SOURCES={off,soundtrack}`; the pure pink-noise
|
||||
machinery + tests stay for later).
|
||||
- Video-off: `.black` gets `z-index:50` + `translateZ(0)` (own layer) **and** the
|
||||
video layers are set `opacity:0` on video-off (belt-and-suspenders blanking).
|
||||
- Safari audio: the tap-to-start gesture now primes both A/B `<audio>` elements
|
||||
**synchronously in-gesture** (set src, vol 0, `play().then(pause)`), unlocking
|
||||
the later async crossfades.
|
||||
- Server: `player/audio.py` resolver + `controls.py`/`state.py` + `/api/alteration`
|
||||
drop white-noise; `build_audio_media.py` stops emitting the noise bed.
|
||||
3. **Verify + ship.** Updated unit/contract/E2E tests; rewrote the 3 E2E for the new
|
||||
toggles (click the visible `.dev-switch-track`; wait for the opacity transition).
|
||||
**288 passed, 2 skipped.** Re-verified in **both Chromium and WebKit**: Audio toggle
|
||||
plays `cosmos/pillars.loop.mp3`, video-off sets vid+paint opacity 0 with `#black`
|
||||
shown, zero JS errors; screenshotted the new toggle UI. Merged `session-0021` →
|
||||
`main` and pushed. (Also untracked a stray `docs/.threads/*.json` editor artifact
|
||||
swept in by `git add -A`, and gitignored `.threads/`.)
|
||||
|
||||
## Deferred decisions
|
||||
|
||||
_No low-confidence calls. All three changes were explicit operator requests; the
|
||||
root-cause fixes (GPU z-index/layer-hide; Safari in-gesture unlock) are the standard
|
||||
remedies for these well-known browser behaviors._
|
||||
|
||||
## Cut state
|
||||
|
||||
- `main` @ `fddb6d6` (merge of session-0021), clean, pushed.
|
||||
- **288 tests + 3 Playwright E2E pass** (2 skipped: opt-in media integration).
|
||||
- White-noise is deferred, not deleted: `tools/pipeline/audio_ops.white_noise_args` +
|
||||
`audio_run.generate_white_noise` + their tests remain; only the live control + the
|
||||
build-script emission were removed.
|
||||
- **Unvalidated by this session:** real-device audio (Safari autoplay) and real-GPU
|
||||
video-off — headless can't exercise either; needs the operator's by-ear/by-eye pass.
|
||||
|
||||
## Operator plate
|
||||
|
||||
- Video + Audio are simple on/off toggles (Dev-Mode style). Audio on = the current
|
||||
altitude's soundtrack. The two real-browser bugs should now be fixed — but please
|
||||
confirm on a real device (especially iPad/Safari).
|
||||
- Deferred: white-noise (easy to re-add as a 3rd Audio position), the per-altitude
|
||||
music layer, the networked-control-surface build, Pi renderer.
|
||||
|
||||
## Next /goal
|
||||
|
||||
```
|
||||
/goal by-ear/by-eye review the audio + Video/Audio toggles on a REAL device (esp. iPad/Safari — headless can't validate Safari autoplay or GPU compositing), then source/compose the deferred per-altitude music layer per docs/audio-candidate-pool.md (or re-enable white-noise as a 3rd Audio position if wanted)
|
||||
```
|
||||
@@ -58,5 +58,8 @@
|
||||
},
|
||||
"0020": {
|
||||
"title": ""
|
||||
},
|
||||
"0021": {
|
||||
"title": ""
|
||||
}
|
||||
}
|
||||
|
||||
+26
-1
@@ -920,8 +920,29 @@ function devLiveReload() {
|
||||
// (e.g. on an altitude change). audio on = the current scale's soundtrack.
|
||||
const aud = $("aud");
|
||||
let audUrl = null; // soundtrack url currently loaded (null = silent)
|
||||
let audLastErr = ""; // last play() rejection / element error (for the readout)
|
||||
const FADE_MS = 500;
|
||||
|
||||
// --- Live audio status readout (diagnostic) — so a "no sound" report is legible ---
|
||||
const AUD_ERR = ["", "aborted", "network", "decode", "format-not-supported"];
|
||||
function audioStatusText() {
|
||||
if (!$("audio").checked) return "audio: off";
|
||||
if (aud.error) return `audio: MEDIA ERROR ${aud.error.code} (${AUD_ERR[aud.error.code] || "?"})`;
|
||||
if (audLastErr) return "audio: " + audLastErr;
|
||||
if (aud.paused) return `audio: on, PAUSED (readyState ${aud.readyState})`;
|
||||
return `audio: ▶ playing ${aud.currentTime.toFixed(1)}s · vol ${Math.round(aud.volume * 100)}% · rs ${aud.readyState}`;
|
||||
}
|
||||
function updateAudioStatus() {
|
||||
const el = $("audio-status");
|
||||
if (!el) return;
|
||||
el.textContent = audioStatusText();
|
||||
const playing = $("audio").checked && !aud.paused && !aud.error && !audLastErr;
|
||||
el.classList.toggle("playing", playing);
|
||||
el.classList.toggle("blocked", !!aud.error || !!audLastErr);
|
||||
}
|
||||
aud.addEventListener("error", () => { audLastErr = ""; updateAudioStatus(); });
|
||||
setInterval(updateAudioStatus, 400);
|
||||
|
||||
function fadeVolume(el, to, ms, done) {
|
||||
// setInterval (not requestAnimationFrame): rAF throttles/pauses when the tab is
|
||||
// backgrounded, which would stall the gain ramp; a timer fires regardless.
|
||||
@@ -947,8 +968,12 @@ function soundtrackUrl() {
|
||||
function playUrl(url) {
|
||||
aud.src = mediaUrl(url);
|
||||
aud.volume = 0;
|
||||
audLastErr = "";
|
||||
const pr = aud.play();
|
||||
if (pr) pr.catch(() => {});
|
||||
if (pr) {
|
||||
pr.then(() => { audLastErr = ""; updateAudioStatus(); })
|
||||
.catch((e) => { audLastErr = "play BLOCKED: " + ((e && e.name) || e); updateAudioStatus(); });
|
||||
}
|
||||
fadeVolume(aud, 1, FADE_MS);
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,15 @@
|
||||
<span class="dev-switch-track"><span class="dev-switch-thumb"></span></span>
|
||||
<span class="dev-switch-label">Audio</span>
|
||||
</label>
|
||||
<div id="audio-status" class="audio-status">audio: off</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Audio check (diagnostic)</legend>
|
||||
<p class="hint">Press play below to test the soundtrack file directly in the
|
||||
browser's own player — this bypasses all app code. If you hear it here but
|
||||
not from the Audio toggle, the file is fine and it's a code/autoplay issue.</p>
|
||||
<audio id="aud-test" controls preload="auto" src="/media/audio/cosmos/pillars.loop.mp3" style="width:100%"></audio>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
|
||||
@@ -49,6 +49,10 @@ main { display: flex; gap: 1rem; padding: 1rem; flex-wrap: wrap; align-items: fl
|
||||
.hidden { display: none; }
|
||||
/* Stack the two Output toggles (Video / Audio) with a little breathing room. */
|
||||
.dev-switch + .dev-switch { margin-top: 0.45rem; }
|
||||
/* Live audio status readout (diagnostic) — turns green when actually playing. */
|
||||
.audio-status { margin-top: 0.5rem; font: 11px/1.4 monospace; color: #9ab; }
|
||||
.audio-status.playing { color: #4e9; }
|
||||
.audio-status.blocked { color: #f97; }
|
||||
.panel { flex: 0 0 280px; display: flex; flex-direction: column; gap: 0.8rem;
|
||||
max-height: calc(100vh - 2rem); overflow-y: auto;
|
||||
position: sticky; top: 1rem; }
|
||||
|
||||
Reference in New Issue
Block a user