diff --git a/simulator/static/app.js b/simulator/static/app.js index 28bd42b..e08f09b 100644 --- a/simulator/static/app.js +++ b/simulator/static/app.js @@ -96,7 +96,10 @@ let mediaVersions = {}; // file -> content-hash token (from /api/media-version // changes with its bytes (permanent cache-bust). The blob cache, when present, // wins — those bytes are already the current ones for this session. function mediaNetUrl(file) { const v = mediaVersions[file]; return "/media/" + file + (v ? "?v=" + v : ""); } -function mediaUrl(file) { return mediaBlobs[file] || mediaNetUrl(file); } +function mediaUrl(file) { + if (file.startsWith("/media/")) return file; // already a resolved absolute url (audio layer) + return mediaBlobs[file] || mediaNetUrl(file); +} // Every media file the ring can show: each scale-pool clip's base footage plus the // per-edge transition clips. This is the full preload set (~two dozen files). @@ -492,12 +495,14 @@ function renderScaleReadout() { } function controls() { - // One bipolar Mood dial (-4 dark .. 0 neutral .. +4 light) maps onto the engine's - // two poles. The calibration gains are pinned to their locked 1.0 (full dial = full - // effect), so the simulator sends no calibration — the server uses DEFAULT_CALIBRATION. + // Visual (on/off) and Audio (off/soundtrack/white_noise) are orthogonal dials + // (audio spec §2). One bipolar Mood dial (-4 dark .. +4 light) maps onto the + // engine's two poles. Calibration gains are pinned to 1.0, so the simulator + // sends no calibration — the server uses DEFAULT_CALIBRATION. const mood = +$("mood").value; return { - content: $("content").value, + visual: $("visual").checked ? "on" : "off", + audio: $("audio").value, left: +$("left").value, right: +$("right").value, dark: mood < 0 ? -mood : 0, light: mood > 0 ? mood : 0, volume: 2, brightness: 2, @@ -509,12 +514,13 @@ async function update() { if (busy) return; const resp = await fetch("/api/alteration", { method: "POST", headers: { "content-type": "application/json" }, - body: JSON.stringify({ controls: controls() }), + body: JSON.stringify({ controls: controls(), altitude_index: ringIndex }), }); if (!resp.ok) { readout.textContent = "invalid: " + resp.status; return; } const data = await resp.json(); readout.textContent = JSON.stringify(data, null, 2); - if (!data.content.video) { black.style.opacity = "1"; black.classList.remove("hidden"); return; } + applyAudio(data.render.audio); // reconcile the audio layer + if (!data.render.video.shown) { black.style.opacity = "1"; black.classList.remove("hidden"); return; } black.classList.add("hidden"); try { ensureClipMedia(); @@ -894,6 +900,66 @@ function devLiveReload() { }, 1000); } +// --- Audio layer: two