volume and room-tone sliders in the station sheet, remembered
shots / shots (push) Canceled after 0s

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 04:32:36 +00:00
1 parent cf00f15447
commit 3ee513ad34
1 file changed
+16 -4
+16 -4
View File
@@ -88,6 +88,10 @@ input,textarea{font:inherit;color:inherit}
#stationSheet label{display:block;color:var(--dim);font-size:12px;margin:8px 0 4px}
#stationSheet input{width:100%;background:var(--field);border:1px solid var(--line);border-radius:10px;padding:10px 12px;outline:none}
#stationSheet .row{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px}
#stationSheet .sl{display:flex;align-items:center;gap:12px;margin-top:6px}
#stationSheet .sl label{margin:0;width:84px}
#stationSheet input[type=range]{flex:1;accent-color:var(--ink);margin:0}
#stationSheet .sl span{font:11px var(--font);color:var(--dim);width:32px;text-align:right}
#stationSheet .row button{padding:9px 14px;border-radius:999px;border:1px solid var(--line);font:600 11px var(--font);letter-spacing:.14em}
#stationSheet .row button.go{background:var(--acc);color:var(--onacc);border-color:var(--acc)}
#stationSheet .row button.bad{color:hsl(0 60% 70%);border-color:hsl(0 30% 35%)}
@@ -405,7 +409,7 @@ const state = {
history: [], // tracks played this session (specs)
current: null, nextJob: null, playing: false,
recent: [], pos: -1, // ids of tracks played this session, and where the player stands in them
audio: new Audio(), cost: parseFloat(localStorage.getItem(LS.cost) || "0"),
audio: Object.assign(new Audio(), { volume: Math.min(1, Math.max(0, parseFloat(localStorage.getItem("gandora.vol") || "1"))) }), cost: parseFloat(localStorage.getItem(LS.cost) || "0"),
};
state.audio.preload = "auto";
@@ -633,13 +637,13 @@ function makeCrackle() {
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);
dv.setUint32(24, sr, true); dv.setUint32(28, sr * 2, true); dv.setUint16(32, 2, true); dv.setUint16(34, 16, true); str(36, "data"); dv.setUint32(40, n * 2, true);
new Int16Array(buf, 44).set(out);
const el = new Audio(URL.createObjectURL(new Blob([buf], { type: "audio/wav" }))); el.loop = true; el.volume = 0.45; el.preload = "auto";
const el = new Audio(URL.createObjectURL(new Blob([buf], { type: "audio/wav" }))); el.loop = true; el.volume = crackle.base = parseFloat(localStorage.getItem("gandora.bed") || "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); }
function crackleOn() { if (crackle.on || state.holding) return; crackle.el ||= makeCrackle(); crackle.on = true; crackle.el.volume = crackle.base ?? crackle.el.volume; crackle.el.currentTime = 0; crackle.el.play().catch(() => {}); }
function crackleOff() { if (!crackle.on) return; crackle.on = false; const el = crackle.el, t0 = crackle.base ?? el.volume; let v = el.volume; 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 */
function wavBlob(samples, sr) {
const n = samples.length, buf = new ArrayBuffer(44 + n * 2), dv = new DataView(buf);
@@ -921,6 +925,14 @@ async function stationSheet(id) {
palRow.append(b);
}
p.append(palRow);
// volume and room tone, for everything (not just this station)
for (const [label, key, get, set] of [["volume", "gandora.vol", () => state.audio.volume, (v) => { state.audio.volume = v; }], ["room tone", "gandora.bed", () => (crackle.el ||= makeCrackle()).volume, (v) => { (crackle.el ||= makeCrackle()).volume = v; crackle.base = v; }]]) {
const sl = document.createElement("div"); sl.className = "sl";
const r = document.createElement("input"); r.type = "range"; r.min = 0; r.max = 100; r.value = Math.round(get() * 100);
const out = Object.assign(document.createElement("span"), { textContent: r.value + "%" });
r.addEventListener("input", () => { set(r.value / 100); out.textContent = r.value + "%"; localStorage.setItem(key, String(r.value / 100)); });
sl.append(Object.assign(document.createElement("label"), { textContent: label }), r, out); p.append(sl);
}
if (state.stationId === id) {
const more = document.createElement("div"); more.className = "row"; more.style.marginTop = "10px"; more.style.justifyContent = "flex-start";
const fromT = Object.assign(document.createElement("button"), { textContent: "* STATION FROM THIS TRACK", title: "a new station grown from the track playing now" });