Skip to content

fix(core): replay ended audio after backward seeks - #3383

Merged
jrusso1020 merged 1 commit into
mainfrom
fix/audio-replay-offline-mix
Aug 21, 2026
Merged

fix(core): replay ended audio after backward seeks#3383
jrusso1020 merged 1 commit into
mainfrom
fix/audio-replay-offline-mix

Conversation

@jrusso1020

Copy link
Copy Markdown
Collaborator

What

Recover ended non-looping audio after a real backward timeline seek or true clip re-entry, including short clips whose native media element is still near EOF.

This PR covers the Studio/player replay half of the Framey audio investigation. The independent offline-render timestamp fix is already in #3380.

Why

After a short audio element reached native EOF, the runtime rejected it in the active gate before it could rewind currentTime and call play(). Seeking the Studio timeline back to the beginning therefore made short lines such as “who?” and “really?” deterministically silent once their elements entered the browser's ended state.

A broad ended-audio recovery would introduce a second bug: native EOF can lead the runtime clock by a few milliseconds, and ordinary play/pause or rate-change force-sync ticks must not replay that tail.

How

  • Track each element's previous desired source time separately from drift state.
  • Admit ended audio only on true first activation or a meaningful backward desired-time transition.
  • Rewind stale forward audio on the first active tick at the normal strict-drift threshold, without forcing cold/buffering audio forward.
  • Preserve the desired-time baseline while native EOF remains inside the authored window so later polling ticks cannot become false re-entries.
  • Keep existing non-looping video held-tail behavior unchanged.

Test plan

  • Unit tests added/updated
  • Manual post-fix Studio testing performed
  • Documentation updated (not applicable; runtime-only bug fix)

Added regressions for:

  • ended audio re-entering at clip start
  • backward seeks within the active clip
  • multiple native-EOF ticks with forceSync
  • desired time beyond source duration
  • stale short audio on re-entry
  • cold/buffering audio that must not be forced forward

Verified locally:

  • Core suite: 121 files, 2,437 tests passed
  • Focused media runtime: 105 tests passed
  • Core typecheck passed
  • Packaged runtime behavior and seek checks passed
  • oxlint, oxfmt, git diff check, and pre-commit hooks passed

Companion renderer fix: #3380.

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed exact head b28de8cc43614d92ccda3967fba1e3e4d90f90cf.

The recovery gate is narrow enough to fix backward transport without turning ordinary EOF into a loop:

  • media.ts:243-256 admits ended audio only when it is non-looping, has a known playable source window, is inside that source, and either has no prior desired-time baseline (true activation) or moves backward by more than the strict 40 ms threshold.
  • media.ts:491-499 preserves only desired source time while native EOF is still inside the authored window, so repeated EOF/force-sync ticks cannot manufacture a fresh activation; a real outside-window transition and evictMediaSyncState still clear every baseline for true re-entry/source swap.
  • media.ts:383-393 rewinds stale short audio only when the native element is ahead on its first active tick, while cold/buffering audio at zero is not forced forward. The non-looping video held-tail/backward-seek behavior remains structurally unchanged.

The regressions distinguish ended re-entry, within-window backward seek, repeated EOF force-sync ticks, past-source time, stale-forward re-entry, and cold audio. All required checks are green at this exact head, including Linux + Windows tests, runtime contract, build, typecheck, Windows render, and regression.

Verdict: APPROVE
Reasoning: The new desired-time baseline separates actual backward transport from EOF and transition ticks, closes the silent short-audio replay failure, and preserves existing video/tail semantics with direct edge-case coverage.

— Magi

@jrusso1020
jrusso1020 merged commit 477e096 into main Aug 21, 2026
57 checks passed
@jrusso1020
jrusso1020 deleted the fix/audio-replay-offline-mix branch August 21, 2026 03:51
vanceingalls added a commit that referenced this pull request Aug 23, 2026
Twelve of the stack's feature commits landed on main as squashes (#3274
through #3292, plus #3401's canary removal); the 96 review-and-fix commits
that followed them here did not, and main moved 64 commits on in the
meantime. This reconciles the two.

58 files conflicted. 44 were audio-only — main's side there is the
squashed form of commits this branch already carries and has since
superseded, so the branch side stands. The rest needed real work, in both
directions:

**Taken from main, absent here.**
- `ensureAudioGroupInertStyle` (#3278's review). An `<hf-audio-group>` is
  an unknown custom element, so it still takes a flex/grid slot and can
  open a line box — adding a group shifted authored layout. The helper and
  its `init.ts` call never came back to the branch, and this branch is
  what emits the element.
- `#3383`'s ended-audio replay: `canSeekEndedMediaBackward` and its five
  siblings in `media.ts`, with all six tests. Not present here in any
  form.
- `#3380`'s `asetpts=N/SR/TB` between `apad` and `atrim`. Also applied to
  `mixGroupMembers`, the group submix, which is new on this branch and so
  had the same bug in a path main's fix could not reach: delayed members
  padded then amix'd, where a group of four or more silently loses one.
- `#3401`'s `displayNumber` thread. The header derives its row from the
  group-aware order and the undo label from ascending element keys, so
  once a group exists the same click said "Hide track 2" and recorded
  "Hide track 1".
- `#3413`/`#3421`'s viewport handling — the popover's height cap and
  `inset()`, and `resolveFloatingPanelPosition` for the grouping dialog,
  which lives in a track header at the bottom of the window.
- Two extractions this branch had inline and at exactly the 600-line cap:
  `useTimelineDeleteOps` and `editingModeSlice`. Bodies were identical.

**Kept from the branch, against main.** Mute and solo are gone by
deliberate breaking change (`remove mute and solo from tracks and groups`,
`remove the group volume slider and level meter`), so eight files main
still carries are deleted again, `PlayerControls` keeps no
`previewIframeRef` (it existed only to feed `SoloBanner`), the
group-levels branch comes out of main's new `previewMessageRouter`, and
`STRIP_H` goes with the bus strip it sized. Main's
`TimelineTrackPlainHeader.test.tsx` is rewritten against the control that
actually exists — the visibility eye, withheld from an audible audio row
and offered back once hidden, which is the only way out of `data-hidden`.

**Unioned.** `TimelineFxPopover` — main's positioning, this branch's
audition telemetry (`auditionPresetChain`, `storedChain`,
`onAuditionTracked`); `SKILL.md` — main's #3416 "keep the carve group a
voice group" beside this branch's bus section, with the canary paragraph
dropped since the canaries no longer exist.

Every port is mutation-checked. core 2508, studio 4460, lint 528, engine
1630, cli 2813, sdk 549, producer green; tsc, oxlint, oxfmt, fallow and
the 600-line cap clean.
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.

2 participants