the crackle is the room tone: it runs whenever music isn't playing (seed screen, cooking, paused) from the first tap on, and fades out the moment a track starts
shots / shots (push) Canceled after 34s
shots / shots (push) Canceled after 34s
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K6897PWcUEwHiqZMcVnNF6
This commit is contained in:
1 parent
4f7d0999e3
commit
cf00f15447
1 file changed
+8
-6
+8
-6
@@ -636,6 +636,8 @@ function makeCrackle() {
|
||||
const el = new Audio(URL.createObjectURL(new Blob([buf], { type: "audio/wav" }))); el.loop = true; el.volume = 0.45; el.preload = "auto";
|
||||
return el;
|
||||
}
|
||||
/* the bed: crackle whenever no music is playing. Decided from state, called whenever state changes. */
|
||||
function bed() { const want = state.primed && !state.playing && !state.holding; if (want) crackleOn(); else crackleOff(); }
|
||||
function crackleOn() { if (crackle.on || state.holding) return; crackle.el ||= makeCrackle(); crackle.on = true; crackle.el.currentTime = 0; crackle.el.play().catch(() => {}); }
|
||||
function crackleOff() { if (!crackle.on) return; crackle.on = false; const el = crackle.el, t0 = el.volume; let v = t0; const fade = setInterval(() => { v -= 0.04; if (v <= 0 || crackle.on) { clearInterval(fade); if (!crackle.on) el.pause(); el.volume = t0; } else el.volume = v; }, 40); }
|
||||
/* 50 ms of silence as a wav, built here rather than pasted */
|
||||
@@ -651,8 +653,8 @@ const SILENCE = URL.createObjectURL(wavBlob(new Int16Array(400), 8000));
|
||||
const SILENCE_UNUSED = "data:audio/wav;base64,UklGRkQDAABXQVZFZm10IBAAAAABAAEAQB8AAIA+AAACABAAZGF0YSADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==";
|
||||
function prime() {
|
||||
if (state.primed || state.current || (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 {}
|
||||
try { crackle.el ||= makeCrackle(); if (!crackle.on) { const v = crackle.el.volume; crackle.el.volume = 0; crackle.el.play().then(() => { if (!crackle.on) crackle.el.pause(); crackle.el.volume = v; }).catch(() => { crackle.el.volume = v; }); } } catch {}
|
||||
try { state.audio.src = SILENCE; const p = state.audio.play(); if (p && p.then) p.then(() => { state.primed = true; bed(); }).catch(() => {}); } catch {}
|
||||
try { crackle.el ||= makeCrackle(); if (!crackle.on) crackleOn(); } 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
|
||||
@@ -1093,11 +1095,10 @@ async function playJob(job) {
|
||||
drawCover($("#cover canvas"), spec);
|
||||
if (!$("#nowRow").classList.contains("hidden")) { drawCover($("#now canvas"), spec); $("#nowTitle").textContent = `${spec.title} · ${state.station || spec.seed}`; }
|
||||
$("#cover").classList.add("cooking"); $("#bar i").style.width = "0";
|
||||
if (!job.blob && state.primed) crackleOn(); // the needle is down while it cooks
|
||||
bed(); // the needle is down while it cooks
|
||||
await job.done;
|
||||
if (state.current !== job) return; // skipped while cooking
|
||||
$("#cover").classList.remove("cooking");
|
||||
crackleOff();
|
||||
if (job.err) { crackleOff(); toast(job.err.message, 6000); $("#lyric").textContent = job.err.message + (/limit|budget/i.test(job.err.message) ? "" : " · skip to try the next"); return; }
|
||||
if (state.holding && !state.audio.paused) { // the crackle is on: queue behind it, it hands over at the end of its loop
|
||||
job.url = job.url || URL.createObjectURL(job.blob);
|
||||
@@ -1183,7 +1184,7 @@ async function advance() {
|
||||
const t = await shuffled();
|
||||
if (t) { state.audio.pause(); await playJob({ id: ++seq, spec: t.spec, blob: t.blob, err: null, done: Promise.resolve() }); return; }
|
||||
}
|
||||
if (!(state.nextJob && state.nextJob.blob) && state.primed) crackleOn(); // nothing ready: needle down while the DJ writes and the oven runs
|
||||
bed(); // nothing ready: the room tone carries while the DJ writes and the oven runs
|
||||
const job = state.nextJob || await cookNext();
|
||||
state.nextJob = null; $("#next").textContent = "";
|
||||
if (!job) { crackleOff(); return; }
|
||||
@@ -1223,7 +1224,8 @@ state.audio.addEventListener("ended", () => {
|
||||
if (state.holding) return; // crackle loop ran out with nothing waiting (loop is on, so this is rare)
|
||||
state.playing = false; setPlayIcon(); vizStop(); swapNow(); advance();
|
||||
});
|
||||
state.audio.addEventListener("pause", () => { if (!state.audio.ended) { state.playing = false; setPlayIcon(); vizStop(); } });
|
||||
state.audio.addEventListener("pause", () => { if (!state.audio.ended) { state.playing = false; setPlayIcon(); vizStop(); } bed(); });
|
||||
state.audio.addEventListener("playing", () => { if (state.audio.src !== SILENCE && !state.holding) { state.playing = true; crackleOff(); } });
|
||||
state.audio.addEventListener("play", () => { state.playing = true; setPlayIcon(); vizStart(); });
|
||||
state.audio.addEventListener("timeupdate", () => { if (state.audio.duration && !$("#bar").classList.contains("drag")) $("#bar i").style.width = (state.audio.currentTime / state.audio.duration * 100) + "%"; });
|
||||
/* scrubbing: tap or drag along the bar; the readout shows where you'd land */
|
||||
|
||||
Reference in new issue
Block a user