Keep the shifter check in e2e 65 from losing the clip's start (#415)

The check that a track plays through one shifter on its bus asked for
the start of the clip only after the note was planned. The clip ends
where the note starts, and the note is planned just 0.126 s ahead. In
a slow browser the clip's voice was already gone by then, and
`__gm.sound.startedAt` gave `null`.

- Each reading takes the plan, both starts and the audio clock in one
  `evaluate`. The clock goes into the detail of the check.
- The start of the clip is kept from the first reading that has it.
  Readings come every 150 ms plus the round trip, which is longer than
  the 0.126 s window, so one reading alone would not be enough.
- The note's start is still read where the note is planned.

Tests:
- Prettier, eslint on 65-song.mjs: clean
- e2e 65 on melektaus-atx: ALL PASSED, 59 checks
- Bent check (wait until the clip is over, read the start as before):
  red as the only one of 59, `clipStart: null` at clock 21.943 after
  the clip ended at 21.828

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-14 15:33:17 +02:00
1 parent 39c509a8e5
commit 8a49392d7f
1 file changed
+16 -4
+16 -4
View File
@@ -905,8 +905,21 @@ export async function song(t) {
p = await json('__gm.song.rulerAt(0)');
await click(p.x, p.y);
await press('Space');
// The clip ends where the note starts, and the note is planned only the lookahead and a frame
// before: by then the clip's voice may be gone, and with it its start (#415). So the start of the
// clip is kept from the first reading that has it, each reading in one go with the clock.
let clipStart = null;
const throughFx = await waitFor(
() => json('{ planned: __gm.song.planned(), clips: __gm.song.clips() }'),
async () => {
const seen = await json(`(() => {
const planned = __gm.song.planned();
const clips = __gm.song.clips();
const startOf = (list) => (list[0] ? __gm.sound.startedAt(list[0].voice) : null);
return { planned, clips, noteStart: startOf(planned), clipStart: startOf(clips), clock: __gm.sound.clock() };
})()`);
clipStart ??= seen.clipStart;
return seen;
},
(s) => s.planned.length >= 1 && s.clips.length >= 1,
60,
);
@@ -915,8 +928,7 @@ export async function song(t) {
const fxBus = (await json('__gm.song.buses()')).find((b) => b.name === busOf(tracks[1]));
const [fxNote] = throughFx.planned;
const [plainClip] = throughFx.clips;
const noteStart = await json(`__gm.sound.startedAt(${fxNote?.voice ?? 0})`);
const clipStart = await json(`__gm.sound.startedAt(${plainClip?.voice ?? 0})`);
const { noteStart, clock } = throughFx;
check(
'the track plays through one shifter on its bus: its notes start a frame early, the clip of the other on time',
fxBus?.fx === true &&
@@ -926,7 +938,7 @@ export async function song(t) {
Math.abs(fxNote.at - plainClip.at - 2) < 1e-6 &&
Math.abs(noteStart - (fxNote.at - frame)) < 1e-6 &&
Math.abs(clipStart - plainClip.at) < 1e-6,
JSON.stringify({ rate, fxBus, fxNote, plainClip, noteStart, clipStart }),
JSON.stringify({ rate, fxBus, fxNote, plainClip, noteStart, clipStart, clock }),
);
await press('Space');
const released = await waitFor(