Audio Wave: snap splits to group line/frame + 'split @ groups' button

Double-click now snaps the split to the nearest 721 group line when within ~9px (so a
segment ends exactly on frame 721), else to the nearest frame (frame-exact splits). New
'split @ groups' button drops a split on every group boundary at once, so covering the
721 grid is one click.
This commit is contained in:
2026-07-04 23:58:33 +02:00
parent c4d7224118
commit 64e2110835
+18 -6
View File
@@ -64,10 +64,10 @@ function setupWave(node) {
const bar = document.createElement("div");
bar.style.cssText = "display:flex;gap:6px;align-items:center;font-size:10px;color:#bbb;flex-wrap:wrap;";
const mk = (t) => { const b = document.createElement("button"); b.textContent = t; b.style.cssText = "font-size:10px;padding:1px 6px;"; return b; };
const playBtn = mk("▶ play"), upBtn = mk("upload"), clearBtn = mk("clear splits");
const playBtn = mk("▶ play"), upBtn = mk("upload"), grpBtn = mk("split @ groups"), clearBtn = mk("clear splits");
const readout = document.createElement("span"); readout.textContent = "0.00 / 0.00s";
const hint = document.createElement("span"); hint.textContent = "click=seek · dblclick=add split · shift-click a split=remove";
bar.append(playBtn, upBtn, clearBtn, readout, hint);
const hint = document.createElement("span"); hint.textContent = "click=seek · dblclick=add split (snaps to group line/frame) · shift-click a split=remove";
bar.append(playBtn, upBtn, grpBtn, clearBtn, readout, hint);
const canvas = document.createElement("canvas");
canvas.width = 900; canvas.height = 170;
canvas.style.cssText = "width:100%;height:170px;background:#141418;border-radius:4px;cursor:pointer;";
@@ -181,10 +181,16 @@ function setupWave(node) {
}
st.audio.currentTime = Math.max(0, Math.min(st.duration, t)); draw(); // click = seek
});
canvas.addEventListener("dblclick", (e) => { // add a user split
function snap(t) { // snap to a group line if near, else to a frame
const gs = groupFrames() / fps();
const gt = Math.round(t / gs) * gs;
if (gt > 0.02 && gt < st.duration - 0.02 && Math.abs(t2x(gt) - t2x(t)) < 9) return gt;
return Math.round(t * fps()) / fps();
}
canvas.addEventListener("dblclick", (e) => { // add a user split (snapped)
const r = canvas.getBoundingClientRect();
const t = x2t(((e.clientX - r.left) / r.width) * canvas.width);
if (t > 0.05 && t < st.duration - 0.05) { st.splits.push(t); serialize(); syncNotesBox(); draw(); }
const t = snap(x2t(((e.clientX - r.left) / r.width) * canvas.width));
if (t > 0.02 && t < st.duration - 0.02) { st.splits.push(t); serialize(); syncNotesBox(); draw(); }
});
const loop = () => { if (!st.playing) return; draw(); requestAnimationFrame(loop); };
@@ -194,6 +200,12 @@ function setupWave(node) {
};
st.audio.onended = () => { st.playing = false; playBtn.textContent = "▶ play"; draw(); };
clearBtn.onclick = () => { st.splits = []; serialize(); syncNotesBox(); draw(); }; // wipe stale user splits
grpBtn.onclick = () => { // one split on every 721 group line
const set = new Set(st.splits.map((s) => Math.round(s * 100) / 100));
for (const g of groupBounds()) set.add(Math.round(g * 100) / 100);
st.splits = Array.from(set).sort((a, b) => a - b);
serialize(); syncNotesBox(); draw();
};
upBtn.onclick = () => {
const inp = document.createElement("input"); inp.type = "file"; inp.accept = "audio/*";