Skip to content

fix(runtime): restore clip gain after the prewarm probe on the no-graph path - #22

Merged
lightninglu10 merged 2 commits into
codepress-mainfrom
freecut-prewarm-silences-clip
Aug 26, 2026
Merged

fix(runtime): restore clip gain after the prewarm probe on the no-graph path#22
lightninglu10 merged 2 commits into
codepress-mainfrom
freecut-prewarm-silences-clip

Conversation

@lightninglu10

Copy link
Copy Markdown

The bug

pitch-corrected-audio.tsx prewarms an audio element by briefly playing it muted, then restoring the prior gain. The restore read:

const previousGain = graph?.outputGainNode.gain.value ?? 0

On the only path that has no graph, ?? 0 restores zero — permanent silence for that clip.

Two defects, not one:

  1. ?? 0 default. Fixed by branching on graph and falling back to currentAudio.volume, the real prior value.
  2. Restore gated behind !isPlaying. The restore only ran on one branch, so it could be skipped entirely. Now unconditional via .finally(restoreGain).

Why FreeCut's own dev environment never saw it

The buggy branch is unreachable locally. FreeCut dev loads media as blob:/OPFS, which always constructs a graph. CodePress serves media from S3 presigned URLs, which never do. The bug is only reachable from a host embedding, which is why it survived in-repo testing.

Verification

Regression test added at pitch-corrected-audio.test.tsx covering both the no-graph restore value and the unconditional-restore path.

Acceptance

Ships in 0.3.7. Acceptance test is CodePress PR #6092, which goes green once the CodePress pin moves to 0.3.7.

…ip audio

The decoder pre-warm drops the clip's gain to 0, plays briefly to fill the
decode buffer, then restores the level it snapshotted. The snapshot read
`graph?.outputGainNode.gain.value ?? 0`, but clips whose source is
cross-origin deliberately have no Web Audio graph — `isWebAudioSafeMediaSource`
keeps them on `element.volume`, because MediaElementAudioSourceNode outputs
silence for a non-CORS source. For those clips the optional chain yielded
undefined and the `?? 0` default made the restore write `volume = 0`.

Nothing repairs that: the volume effect's deps are `[finalVolume, muted]` and
neither changes when playback starts, so the clip plays picture with no sound
until a remount or a gain change. Snapshot from whichever stage the clip
actually uses instead. Standalone FreeCut never saw this because blob:/OPFS
sources are same-origin and always graph-routed.

Covers both the .then and .catch restores, and the ordered volume writes so a
restore cannot coincidentally land on the pre-mute value.
…-warm

Both the .then and the .catch gated the gain restore behind !isPlaying, not
just the pause. If transport starts while the pre-warm's play() promise is
still pending, neither branch runs and the clip keeps playing at gain 0 —
the same picture-with-no-sound symptom, and unlike the cross-origin default
this one reaches the Web Audio graph path too, so standalone FreeCut is
affected as well. Nothing repairs it: the volume effect's deps are
[finalVolume, muted] and pressing play changes neither, so a clip without
fades stays silent for its full duration.

Only the pause is playback state's business. Restore in `finally` and gate
just the pause, matching the sibling video pre-warm in video-content.tsx.

Restoring while transport runs is correct: previousGain is whatever the
volume effect last committed, so a muted or zero-volume clip restores a
correct 0 and stays silent, and a clip inside a fade is overwritten by the
next effect run within a frame.
@lightninglu10
lightninglu10 merged commit ed8ad3e into codepress-main Aug 26, 2026
3 of 4 checks passed
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