hold: the level is baked into the sample (iOS ignores element volume); OFF holds on pure silence
shots / shots (push) Successful in 2m21s

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 08:38:29 +00:00
1 parent bc7a16bb34
commit d97628d1ad
1 file changed
+8 -3
+8 -3
View File
@@ -639,7 +639,7 @@ function drawRings(canvas, spec, { lit = 1, pulse = null } = {}) {
/* vinyl crackle under the gaps: a few seconds of soft noise and pops, made once, looped quietly in a second <audio>
while a track cooks or between tracks. Not Web Audio, so it behaves on iOS like the music does. */
const crackle = { el: null, on: false };
function makeCrackle(seconds = 12) {
function makeCrackle(seconds = 12, gain = 1) {
const sr = 22050, n = sr * seconds, mix = new Float32Array(n), out = new Int16Array(n), rnd = prng(0x5eed);
let lp = 0;
for (let i = 0; i < n; i++) {
@@ -647,7 +647,7 @@ function makeCrackle(seconds = 12) {
mix[i] += lp * 1.6;
if (rnd() < 0.0015) { const len = 20 + Math.floor(rnd() * 80), amp = 0.3 + rnd() * 0.5; for (let k = 0; k < len && i + k < n; k++) mix[i + k] += amp * (1 - k / len) * (rnd() * 2 - 1); } // a pop
}
for (let i = 0; i < n; i++) out[i] = Math.max(-1, Math.min(1, mix[i])) * 32767;
for (let i = 0; i < n; i++) out[i] = Math.max(-1, Math.min(1, mix[i] * gain)) * 32767;
const buf = new ArrayBuffer(44 + n * 2), dv = new DataView(buf);
const str = (o, s) => { for (let i = 0; i < s.length; i++) dv.setUint8(o + i, s.charCodeAt(i)); };
str(0, "RIFF"); dv.setUint32(4, 36 + n * 2, true); str(8, "WAVE"); str(12, "fmt "); dv.setUint32(16, 16, true); dv.setUint16(20, 1, true); dv.setUint16(22, 1, true);
@@ -1365,7 +1365,12 @@ function toCooking(job) {
}
/* → holding, inside `ended`: nothing is cooked, so the player loops the crackle in its own element to stay alive */
function toHolding() {
try { crackle.hold ||= makeCrackle(4); crackleOff(); setSource(crackle.hold.src, true); if (state.holdVol == null) { state.holdVol = state.audio.volume; state.audio.volume = bedOff() ? 0 : Math.min(state.audio.volume, crackle.base ?? 0.18); } state.phase = "holding"; const p = state.audio.play(); if (p && p.catch) p.catch(() => {}); } catch {}
try {
// the level is baked into the sample: iOS ignores an element's volume. OFF holds on four seconds of nothing (its own
// URL, not SILENCE, so `ended` still fires and hands over).
const lvl = bedOff() ? 0 : Math.round((crackle.base ?? 0.18) * 100) / 100;
if (!crackle.hold || crackle.hold.lvl !== lvl) { crackle.hold = lvl ? makeCrackle(4, lvl) : Object.assign(new Audio(URL.createObjectURL(wavBlob(new Int16Array(8000 * 4), 8000))), { loop: true }); crackle.hold.lvl = lvl; }
crackleOff(); setSource(crackle.hold.src, true); state.phase = "holding"; const p = state.audio.play(); if (p && p.catch) p.catch(() => {}); } catch {}
}
/* a landed track while the player holds: it queues behind the crackle, the loop is switched off so the pass ends, and
handOver starts it inside that `ended`. Resolves once the track has been handed over (or dropped). */