mobile: the first tap anywhere unlocks the player (silent clip inside the gesture) so the first track autoplays when it lands; on iOS the cover pulses to the beat instead of the spectrum, so music keeps playing with the screen off and ignores the mute switch
shots / shots (push) Canceled after 28s

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6897PWcUEwHiqZMcVnNF6
This commit is contained in:
teahubxyzandClaude Fable 5.1 committed 2026-10-21 02:33:56 +00:00
1 parent d372da5950
commit 0ffeefe94d
1 file changed
+28 -1
+28 -1
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; connect-src https://api.venice.ai https://inference.yolopreneur.lol; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; media-src blob:; font-src 'self'">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; connect-src https://api.venice.ai https://inference.yolopreneur.lol; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; media-src blob: data:; font-src 'self'">
<title>gandora</title>
<meta name="description" content="Type a seed. Get a station. Every song is made for you as it plays, and every cover is drawn from the song's own settings.">
<link rel="icon" href="favicon.svg">
@@ -595,8 +595,35 @@ function drawRings(canvas, spec, { lit = 1, pulse = null } = {}) {
}
/* ---------- the visualizer: an analyser on the player, driving the cover while music plays ---------- */
/* the first tap unlocks the player: a silent clip played inside the gesture makes the <audio> element "user-started",
so the first real track can start itself when the render lands, however long that takes */
const SILENCE = "data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAIA+AAACABAAZGF0YQAAAAA=";
function prime() {
if (state.primed || (state.audio.src && !state.audio.paused)) return;
try { state.audio.src = SILENCE; const p = state.audio.play(); if (p && p.then) p.then(() => { state.primed = true; }).catch(() => {}); } catch {}
}
for (const ev of ["pointerdown", "touchend", "keydown"]) document.addEventListener(ev, prime, { capture: true, passive: true });
/* iOS routes sound through Web Audio the moment the element is wired to an analyser, and suspends Web Audio when
the screen locks (and obeys the mute switch). So on iOS the cover pulses to the beat from the sheet instead,
and the music stays a plain <audio> element that keeps playing in the background. */
const IOS = /iP(hone|ad|od)/.test(navigator.userAgent) || (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
const viz = { ctx: null, an: null, data: null, raf: 0, smooth: { bass: 0, mid: 0, high: 0 } };
function vizBeat() {
cancelAnimationFrame(viz.raf);
const canvas = $("#cover canvas"), t0 = performance.now(), rnd = prng(hash32(state.current.spec.title || ""));
const frame = () => {
if (!state.playing || !state.current || !state.current.blob) { viz.raf = 0; return; }
const spec = state.current.spec, t = (performance.now() - t0) / 1000, bps = (+spec.bpm || 100) / 60, en = +spec.energy || 0.5;
const beat = Math.pow(Math.max(0, Math.cos(2 * Math.PI * t * bps)), 6); // a kick on every beat
const bar = Math.pow(Math.max(0, Math.cos(2 * Math.PI * t * bps / 4)), 2); // and a swell on the bar
const bass = Math.min(1, (0.25 + 0.75 * beat) * (0.5 + 0.5 * en)), mid = 0.3 + 0.3 * bar * en, high = 0.2 + 0.25 * Math.pow(Math.max(0, Math.sin(2 * Math.PI * t * bps * 2)), 4) * en;
drawCover(canvas, spec, { pulse: { bass, mid, high, t } });
viz.raf = requestAnimationFrame(frame);
};
viz.raf = requestAnimationFrame(frame);
}
function vizStart() {
if (IOS || state.beatViz) return vizBeat();
// the player is only wired through the analyser once the context is actually running: a suspended context
// (iOS before a tap) would mute the music, and the cover can wait for the next gesture
try {