commit bf7e74954d5c4384c61a6495cf28dcf581e96141
parent 48d297306c446967c86d590790df0ddbc58c1c3f
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Thu, 1 Oct 2026 14:46:41 -0400
umtool: the bench's audio-clock playhead paints at 30 Hz, and the exact-stop spec checks the start against the schedule
The playhead read off the audio clock re-rendered the bench every frame;
it now updates at about 30 Hz. `m` reads the playhead through a ref, so the
keyboard is not re-bound on every update.
The exact-stop spec no longer infers the played length from the first
non-zero frame: the selection starts inside the fixture's silence at
2.9-3.1 s. It checks that nothing sounds before the scheduled start; the
stop is still checked to within one render quantum.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
2 files changed, 22 insertions(+), 7 deletions(-)
diff --git a/umtool/components/projects/ClipBench.tsx b/umtool/components/projects/ClipBench.tsx
@@ -399,6 +399,10 @@ export default function ClipBench({ data }: { data: ClipBenchData }) {
const [cutScore, setCutScore] = useState<number | null>(null);
const [peekPad, setPeekPad] = useState(PEEK_STEP);
const [playhead, setPlayhead] = useState<number | null>(null);
+ const playheadRef = useRef<number | null>(null);
+ useEffect(() => {
+ playheadRef.current = playhead;
+ }, [playhead]);
const [note, setNote] = useState<string | null>(null);
const [busy, setBusy] = useState<string | null>(null);
const [dirty, setDirty] = useState(false);
@@ -928,9 +932,16 @@ export default function ClipBench({ data }: { data: ClipBenchData }) {
setPlayInfo((pi) => (pi ? { ...pi, state: "stopped", endedAt: s.to + Math.max(0, late) * rate } : pi));
};
+ // The playhead at ~30 Hz, not every frame: each update re-renders the
+ // bench, and the line moving smoothly is not worth that at 60.
+ let painted = -1;
const tick = () => {
if (session.current !== s) return;
- setPlayhead(playheadAt(from, t0, ctx.currentTime, rate, sch.duration));
+ const now = ctx.currentTime;
+ if (now - painted >= 1 / 30) {
+ painted = now;
+ setPlayhead(playheadAt(from, t0, now, rate, sch.duration));
+ }
s.raf = requestAnimationFrame(tick);
};
s.raf = requestAnimationFrame(tick);
@@ -1298,12 +1309,15 @@ export default function ClipBench({ data }: { data: ClipBenchData }) {
/** `m`: where you are listening. Anywhere outside the selection is refused. */
const muteAtPlayhead = useCallback(() => {
- if (playhead == null || playhead < sel.from - 0.02 || playhead > sel.to + 0.02) {
+ // Through a ref: the playhead moves every frame while something plays,
+ // and a callback keyed on it would re-bind the keyboard every frame.
+ const at = playheadRef.current;
+ if (at == null || at < sel.from - 0.02 || at > sel.to + 0.02) {
setNote("the playhead is not inside the selection — play to the spot, or pick it on the waveform");
return;
}
- placeMute(playhead, false);
- }, [playhead, sel.from, sel.to, placeMute]);
+ placeMute(at, false);
+ }, [sel.from, sel.to, placeMute]);
// ---- keyboard -----------------------------------------------------------
useEffect(() => {
diff --git a/umtool/e2e/clip-bench.spec.ts b/umtool/e2e/clip-bench.spec.ts
@@ -971,9 +971,10 @@ test("a play-selection stops within one audio render quantum of its end, at any
// The STOP: the last sound is the scheduled stop, to within a quantum.
const stopFrame = Math.round(play.ctxStop * sr);
expect(Math.abs(last + 1 - stopFrame), `stopped ${last + 1 - stopFrame} frames from the end at ${rate}x`).toBeLessThanOrEqual(quantum);
- // And what played was the selection: two seconds of source, at any speed.
- const playedSource = ((last + 1 - first) / sr) * Number(rate);
- expect(Math.abs(playedSource - 2) * sr).toBeLessThanOrEqual(quantum * Number(rate) + 1);
+ // And nothing sounded before the scheduled start. (Not "the first sound
+ // IS the start": 3.00 is inside the silence at 2.9-3.1, so the first
+ // non-zero frame is the tone coming back.)
+ expect(first).toBeGreaterThanOrEqual(Math.round(play.ctxStart * sr) - quantum);
}
});