Skip to content

fix(core): preview audio fades play as ramps on the audio clock, so cuts no longer click - #6397

Merged
vanceingalls merged 5 commits into
mainfrom
fix/preview-fades-as-ramps
Oct 26, 2026
Merged

vanceingalls merged 5 commits into
mainfrom
fix/preview-fades-as-ramps

Conversation

@vanceingalls

@vanceingalls vanceingalls commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator

What

Preview audio fades (data-fade-in / data-fade-out) now play as gain ramps on the audio clock. Before, they were applied as a step once per 50 ms sync tick, so cuts clicked in the preview.

Why

A Desktop podcast episode is many short audio clips, each with a 20 ms fade at its cut. The export applies the fades (ffmpeg afade) and is clean; the preview clicked at every cut. In runtime/media.ts, fadeGain(...) was multiplied into the volume once per 50 ms tick and written as a step: el.volume on the HTMLMedia fallback, or gainNode.gain.value in webAudioTransport.ts setElementVolume. A 20 ms fade never played, so each clip started and stopped at near full level.

Related work

Refs #6198 (the same preview, video catch-up at cuts).

How

  • webAudioTransport.ts: attachClipStages now books the clip's fades on a gain of its own (bookedFades), between the source and its clip gain, when the clip is scheduled. That covers both routes: decoded buffers (schedulePlayback) and captured media elements (scheduleMediaElementPlayback, which is the primary route in init.ts and now receives clipDuration). The fade is linear, as audioFade.ts defines it, clamped to the clip. It starts from where the clip actually starts sounding: past windowStart and the playhead, at the partial level if the playhead is mid-fade. It ramps up to the end of the fade-in, holds, and ramps to 0 at the clip's end. The node is torn down with the clip's other nodes.
  • No double fade: syncRuntimeMedia now also hands onElementVolume the level without the fade, as a new optional fifth argument sent only where a fade changes the level. setElementVolume uses that unfaded level for a source whose fades are booked. A source without booked fades, and the plain el.volume fallback (which cannot be scheduled), keep the stepped fade as before.
  • Existing four-argument calls and setElementVolume(el, volume, clipGain) behave as before.

Test plan

  • Unit tests added: webAudioTransport.test.ts "clip fades on the audio clock". A decoded clip ramps 0 to 1 over its fade-in from its start and 1 to 0 at its end. A clip started halfway into its fade-in starts at 0.5. A captured media element books the same ramps. A clip without fades books nothing. The per-tick volume leaves out a booked fade and keeps an unbooked one. media.test.ts: the transport also gets the level without the fade.
  • Each new test fails without its part of the fix (checked by mutation, then restored): with no booked fades, the four transport ramp tests fail; with the per-tick volume always faded, the per-tick test fails; with no unfaded level from media.ts, the media test fails.
  • vitest run src/runtime: 2485 tests pass (after rebuilding packages/parsers, whose stale dist failed 5 unrelated validateVariables tests on main too). Core typecheck, the studio tsc --noEmit, oxlint, oxfmt and the fallow audit all exit 0, through the pre-commit hook.
  • Manual testing: not yet heard in the Desktop preview. That needs this in a release and the Desktop bump.
  • Comments say why: bookedFades says what a stepped fade did.

Package size budget. @hyperframes/producer carries the core runtime in several bundles, so the fade booking pushed its unpacked size to 2,868,742 bytes, 1% over the 2,840,135 budget. scripts/package-size-budgets.json raises it to the measured size plus 1% (2,897,429). The packed budget is unchanged.

🤖 Generated with Claude Code

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verdict: REQUEST CHANGES · 2 blocker · 1 important · 0 nits · head 1070a48

The ramp approach is right. But fadeNode now suppresses the stepped fade even where the node can't carry the whole fade, so some fades are lost entirely. Each blocker below was reproduced with a scratch test against this head. ↓ inline

Scope

Read all 5 files end-to-end, plus audioFade.ts, setRate/applyWebAudioRate, scheduleWebAudioFor and the media.ts volume/early-start path. PR tests pass locally (267). CI on this head was still running at review time; the prior head's Comments failure is what 1070a48 fixes. No prior reviews.

— Claude (Genesis)

@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 271 (base branch 271), smooth 153 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (0)

vanceingalls and others added 5 commits October 25, 2026 22:57
…uts no longer click

data-fade-in / data-fade-out were folded into one volume per 50 ms sync tick and applied as
a step (el.volume, or gain.value in the Web Audio transport), so a 20 ms fade never played
and each clip started and stopped at near full level: a click at every cut. The export
applies the fades (ffmpeg afade) and does not click.

The transport now books each clip's fades on a gain of its own when it schedules the
clip, for both decoded buffers and captured media elements: a linear ramp from where the
clip starts sounding up to its fade-in, and down to silence at its end. The sync tick hands
the transport the level without the fade (onElementVolume's new fifth argument, sent only
where a fade applies), so a booked fade is not applied twice. The plain el.volume fallback
keeps its stepped fade.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@vanceingalls
vanceingalls force-pushed the fix/preview-fades-as-ramps branch from 0b83a0c to 518066b Compare October 26, 2026 05:57

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verdict: APPROVE · 0 blocker · 0 important · 0 nits · 3 resolved, 0 open, 0 new · 1070a48 → 518066b

Scope

Re-read the full webAudioTransport.ts fade path at the new head. The rebase onto main leaves the PR's runtime patch unchanged apart from hunk offsets. Runtime tests pass locally (270). CI is green on this head. 518066be only raises the producer unpacked budget, and CI's Build passes with it.

— Claude (Genesis)

@vanceingalls
vanceingalls added this pull request to the merge queue Oct 26, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 26, 2026
@vanceingalls
vanceingalls added this pull request to the merge queue Oct 26, 2026
Merged via the queue into main with commit 07a126d Oct 26, 2026
82 of 83 checks passed
@vanceingalls
vanceingalls deleted the fix/preview-fades-as-ramps branch October 26, 2026 07:00
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