Skip to content

Test the playhead while it is running - #33

Merged
ibrahimweng merged 2 commits into
mainfrom
claude/clock-tests
Aug 30, 2026
Merged

Test the playhead while it is running#33
ibrahimweng merged 2 commits into
mainfrom
claude/clock-tests

Conversation

@ibrahimweng

Copy link
Copy Markdown
Owner

The render tests cover what lands in the file: one pass over a project with the whole of it known in advance. This is the other half, and the harder one.

Playing is a loop that wakes every twenty-five milliseconds, reads where the video has got to, works out what audio time that corresponds to now, and queues whatever falls in the next fraction of a second. Nothing about that is a function of its inputs — it is two clocks being kept together, and it goes wrong in three ways: a cue fired twice, a cue never fired, or a playhead that stops when the picture does.

What is checked

Seventeen tests, driven through fake timers with the video and the audio clock advanced together.

Queueing while it runs

  • each cue is queued once, however many ticks pass over it, and at an audio time ahead of the playhead rather than on it — Web Audio is only exact about what is scheduled in advance, and a cue handed over at the moment it is due is already late by however long the tick took
  • a cue the playhead has already passed is not queued
  • they arrive in the order they are placed
  • a cue fires again after seeking back over it, and again when the video itself is scrubbed back
  • a muted layer stays silent while it runs

Where it stops

  • the position is published as it goes, and the transport says once that it started and once that it stopped, not once per tick
  • it stops itself at the end of the piece, landing exactly on it
  • it stops at the end of the piece rather than the end of the clip when the piece is the shorter of the two

Past the end of the clip

  • it keeps running on the audio clock once the picture has ended
  • it queues a tail that falls out there
  • it still stops at the end of the piece
  • it can be seeked into and back out of, with the picture held on the frame it ran out on

How it is testable

The clock takes all three things it depends on, so all three can be stood in for. The video is stubbed at the six members it actually reads; the engine at the three. onCue reports every cue queued and the audio time it was given, which is the claim worth checking.

The context is a real one with currentTime overridden on an object in front of it. The nodes have to be real, because the clock builds a layer's graph as it plays and a stub would not catch it building the wrong one; the passing of time has to be ours. A live AudioContext advances on its own and was the obvious first idea — it opens an audio device, and on a machine with none (a CI runner, the container this was written in) that is a pile of ALSA errors rather than a context.

Fault injection earned its keep twice

Four faults put back — forgetting how far it had queued, dropping the handover past the picture, taking the end of the piece from the clip — failed ten of the tests between them.

But removing the tick's own catch-up for a position that has jumped backwards failed nothing at all. The test I had written for it went through seek, and seek puts that marker back itself, so the guard was never reached. It is there for a position moved without going through the clock — which is exactly what the floating window's scrub bar does, writing currentTime directly (video-window.ts:106). That case now has its own test, and it is the only thing that fails when the guard goes. The original test was renamed to say what it actually covers.

231 unit tests (was 214), 55 browser tests, build clean.

The README's "what is not tested yet" now names the mixer and master chain, which the render tests exercise only as far as "something came out".

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ux1kydvUkLRoMbHp82ofDi


Generated by Claude Code

The render tests cover what lands in the file: one pass over a project
with the whole of it known in advance. This is the other half. Playing
is a loop that wakes every twenty-five milliseconds, reads where the
video has got to, works out what audio time that corresponds to now,
and queues whatever falls in the next fraction of a second. Nothing
about that is a function of its inputs — it is two clocks being kept
together, and it goes wrong in three ways: a cue fired twice, a cue
never fired, or a playhead that stops when the picture does.

Seventeen tests, driven through fake timers with the video and the
audio clock advanced together:

- each cue is queued once, however many ticks pass over it, and ahead
  of the playhead rather than on it
- a cue already behind the playhead is not queued, and they arrive in
  the order they are placed
- a muted layer stays silent while it runs
- the position is published as it goes, and the transport says once
  that it started and once that it stopped
- it stops itself at the end of the piece, landing exactly on it, and
  at the end of the piece rather than the end of the clip when the
  piece is shorter
- past the last frame it keeps running on the audio clock, queues a
  tail that falls out there, still stops at the end of the piece, and
  can be seeked into and back out of with the picture held on the
  frame it ran out on

The clock is testable because it takes all three things it depends on.
The video is stubbed at the six members it reads, the engine at the
three. The context is a real one with currentTime overridden in front
of it: the nodes have to be real, since the clock builds a layer's
graph as it plays, but the passing of time has to be ours. A live
AudioContext advances on its own and was the obvious first idea; it
opens an audio device, and on a machine with none — a CI runner, this
container — that is a pile of ALSA errors rather than a context.

Fault injection earned its keep twice here. Four faults put back —
forgetting how far it had queued, dropping the handover past the
picture, taking the end of the piece from the clip — failed ten of the
tests between them. But removing the tick's own catch-up for a
position that has jumped backwards failed nothing at all: the test I
had written for it went through seek, and seek puts that marker back
itself. The guard is for a position moved without going through the
clock, which is exactly what the floating window's scrub bar does
(video-window.ts writes currentTime directly). That case now has its
own test, and it is the only thing that fails when the guard goes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ux1kydvUkLRoMbHp82ofDi
@vercel

vercel Bot commented Aug 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
beat-studio Ready Ready Preview Aug 30, 2026 12:12pm

CI on Node 20 came back with two renders of the same project differing
by 5e-22 while the same test matched exactly on Node 22 and on the
machine it was written on. That is denormal-sized noise in the
convolver's tail, four hundred decibels below anything that is a
sound.

The assertion was the problem, not the renderer. "Sample for sample the
same" is a claim about a third-party implementation's arithmetic, and
it is not one that implementation makes. What the test is actually for
is our own seeding: that a placed sound is one sound rather than a new
one each time it is heard.

A billionth is about a hundred and eighty decibels down, far below the
smallest step a float can take at any level anybody could hear. The
fault it guards against — the voice drawing fresh noise instead of
drawing it from the cue's id — moves samples by six tenths, checked by
putting it back, so there are nine orders of magnitude between the
threshold and the thing it has to catch.

A test that passes on one machine and fails on another is worse than
no test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ux1kydvUkLRoMbHp82ofDi
@ibrahimweng
ibrahimweng merged commit a8f401a into main Aug 30, 2026
5 checks passed
@ibrahimweng
ibrahimweng deleted the claude/clock-tests branch August 30, 2026 12:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant