Skip to content

fix(runtime): make audio/media sync boundary inclusive to match visibility fix#1173

Open
miguel-heygen wants to merge 1 commit into
mainfrom
fix/runtime-boundary-audio-sync
Open

fix(runtime): make audio/media sync boundary inclusive to match visibility fix#1173
miguel-heygen wants to merge 1 commit into
mainfrom
fix/runtime-boundary-audio-sync

Conversation

@miguel-heygen
Copy link
Copy Markdown
Collaborator

@miguel-heygen miguel-heygen commented Jun 3, 2026

Summary

Follows up on #1166 which changed element visibility to `<= computedEnd`. The audio clock and `syncRuntimeMedia` still used `< end`, creating a 1-frame asymmetry: the host was visible at exactly `t = duration` but audio was silent.

Changed:

  • `init.ts:1908` audio clock: `state.currentTime < end` → `state.currentTime <= end`
  • `media.ts:163` `syncRuntimeMedia`: `params.timeSeconds < clip.end` → `params.timeSeconds <= clip.end`

Why `<=` is safe for audio at clip boundaries:

For `init.ts` (audio clock): the loop does `break` on the first active clip (line 1918), so at `t = boundary` only the outgoing clip is attached — incoming is never reached.

For `media.ts` (`syncRuntimeMedia`): the loop has no `break` — it iterates all clips. At `t = boundary` both clips satisfy `<= end / >= start`, so both are `isActive = true`. However, the outgoing clip's `el.paused` is `false` (mid-play), so the `el.paused` gate skips its `play()` call. Only the incoming clip's `play()` fires. The result is a ~1-frame audio overlap (~33ms at 30fps) — imperceptible for typical content. The `el.paused` gate, not a loop break, is what prevents dual-activation here.

Effect:

  • Seeking to `t = duration` on a composition: audio and visuals both hold their final state (symmetric)
  • Adjacent clip boundaries: unchanged in practice — outgoing audio plays through `t = boundary`, incoming starts at next tick

Test plan

  • `bun run --cwd packages/core test` — 1163/1163 pass (after build)
  • Manual: seek to exact `t = duration` on a composition with audio — verify audio audible at that frame
  • Manual: play through a slide transition — verify audio doesn't gap at boundary

…ility fix

`init.ts` (#1166) changed visibility to `<= computedEnd` so elements
stay visible at exactly t=duration. Audio clock (`init.ts:1908`) and
`syncRuntimeMedia` (`media.ts:163`) still used `< end`, leaving a 1-frame
desync where the host was visible but audio was silent at the boundary.

Change both to `<=` for symmetry:
- At clip end (seeking to t=duration): audio plays through the final frame
- At adjacent boundaries: the `break` in syncRuntimeMedia ensures only
  the outgoing clip's audio is attached — no simultaneous dual activation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Collaborator

@vanceingalls vanceingalls left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly the right fix — init.ts audio clock and media.ts syncRuntimeMedia both updated from < end to <= end, symmetric with the visibility change from #1166. At t = duration, audio holds the final state alongside visible elements. The syncRuntimeMedia dual-activation concern doesn't apply here because it breaks on the first active clip. CI in progress, no failures. LGTM.

— Vai

Copy link
Copy Markdown
Collaborator

@jrusso1020 jrusso1020 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at d701c08c. The 2-line diff is right; the PR description's reasoning is partially wrong — works out fine in practice but the audible-safety argument needs correction for the next reader.

The fix shape is right (verified at HEAD)

Both sites change <<= symmetrically with #1166's visibility fix:

  • init.ts:1908 (audio clock): ✓ — and this site DOES have a break (line 1918) inside the active branch. The audio clock attaches the first matching element and exits. At t = boundary, the outgoing clip matches <= end → attached → break → incoming never reached. Magi's "break on first active" claim holds for this site.
  • media.ts:163 (syncRuntimeMedia): ✓ for the boundary semantics, but the "no dual-activation because the loop breaks" claim is false for this site. Grep confirms syncRuntimeMedia's for-loop has zero break statements (only two continues: one for disconnected elements, one at the end of the active branch). The loop iterates EVERY clip every tick.

Why the fix still works, restated correctly

At t = boundary where outgoing.end === incoming.start:

Clip isActive after fix el.paused? play() fires?
outgoing (A) true (now <= end) false (mid-play) no — gated on el.paused at line ~9700 (if (params.playing && el.paused && !playRequested.has(el)))
incoming (B) true (>= start) true (not yet started) yes

So both clips run through the active-branch of the loop, but only B actually calls play(). A's volume/sync code re-runs (cheap, idempotent). At t = boundary + 1 frame, A's isActive flips to false → cleanup branch → if (!el.paused) el.pause() → A pauses.

Net: 1-frame audio overlap at every clip boundary (~33ms at 30fps, ~17ms at 60fps). For typical content this is below the audibility threshold for a brief crossfade. For sample-perfect transitions (sharp transients exactly at the boundary), it could land as a barely-perceptible blend. Not destructive, but worth knowing the actual mechanism.

Recommendation

Two equally-valid paths:

  1. Fix the PR description: replace "syncRuntimeMedia iterates clips and breaks on the first active one" with the actual mechanism: "the play() gate is el.paused, so the outgoing clip — already playing — doesn't re-trigger; only the incoming clip starts. 1-frame audio overlap at boundaries is imperceptible." That's an accurate description of why the fix is safe. (Cheap; preserves the current diff.)
  2. Add a break to media.ts to match init.ts: after the active branch in syncRuntimeMedia, add break (or a sentinel + early-exit). Genuinely prevents the dual-activation Magi claimed already didn't happen. Symmetric with init.ts's pattern. (More defensive; touches 1 more line.)

I'd lean toward (1) — the existing fix works; the description just over-claims a defense that isn't there. (2) would be a separate "tighten the pattern" PR, not gating this one.

Severity walk

Without this fix: at t = duration, the host is visible (per #1166's <= end) but audio is silent. A user seeking to the exact end frame hears nothing while seeing the final state. Asymmetric and surprising.

With this fix: symmetry restored. The 1-frame audio overlap at boundaries is the cost; the visible+audible final-frame is the benefit. Worth it.

Verdict

Materially LGTM on the 2-line change. The PR description's reasoning for syncRuntimeMedia is incorrect (no break exists) but the fix itself is sound — the el.paused gate is what actually prevents dual play() calls, not a loop break. Updating the PR body would close the documentation gap; adding an actual break would close the pattern asymmetry.

Stamp held — same protocol. James gates.

— Rames Jusso (hyperframes)

@miguel-heygen
Copy link
Copy Markdown
Collaborator Author

Producer regression test results (post-merge)

Ran the full suite inside Dockerfile.test (correct Chrome + FFmpeg build):

The <= boundary fix has zero impact on rendered output. Render captures frames at frame_index / fps timestamps (e.g. t=4.9667s for the last frame of a 5s @30fps clip), never at exactly t=duration. So the final-frame visibility change only affects seeks/scrubs to the exact endpoint — not the MP4 output.

Stale baseline PR: #1178

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.

3 participants