/* the envelope at the width the strip draws: 64 columns, each the loudest frame in its slice — the shape of the singing, not the wave */ const cols = $derived.by(() => { const f = tapSync.activity?.frames; if (!f?.length) return []; const per = f.length / 64; return Array.from({ length: 64 }, (_, i) => { let m = 0; for (let j = Math.floor(i * per); j < Math.floor((i + 1) * per) && j < f.length; j++) m = Math.max(m, f[j]); return m; }); }); const spanMs = $derived((tapSync.activity?.frames?.length ?? 0) * (tapSync.activity?.frameMs ?? 100)); const pct = (ms) => (spanMs ? Math.max(0, Math.min(100, (ms / spanMs) * 100)) : 0); /* one button, four states: reading the file, nothing worth reading, the estimate, and the way back out of it */ const estLabel = $derived( tapSync.reading === "ready" ? (tapSync.estimated ? "Clear the estimate" : "Estimate the timing") : tapSync.reading === "none" ? "No estimate for this one" : "Reading the song…", );