Skip to content

fix(engine): offset clips from a WAV with a wrong byte rate play the right sound to their end - #7541

Merged
miguel-heygen merged 2 commits into
mainfrom
fix/offset-clip-sound
Nov 11, 2026
Merged

miguel-heygen merged 2 commits into
mainfrom
fix/offset-clip-sound

Conversation

@miguel-heygen

Copy link
Copy Markdown
Collaborator

What

A sound clip that starts partway into a WAV (data-media-start greater than zero) now plays the right sound for its whole length, even when the WAV's header states the wrong byte rate.

Why

Some speech WAVs (24 kHz, mono, 16-bit) carry a header whose byte rate is 96,000 bytes a second, twice the real 48,000, as if the samples were stereo. The samples themselves are fine, and a player that decodes from the start (the browser preview) plays them correctly.

The mixer cut each clip with an input seek (-ss <in-point> -i file.wav). ffmpeg turns that time into a byte offset using the header's byte rate, so the cut started at twice the in-point. The clip played later words, then went silent once it ran past the end of the file. A clip starting past the halfway point of its file played nothing. The render still completed with no error.

Example: a clip with an in-point of 38.2 s in a 135.5 s file started at 76.4 s. It played the remaining 59.1 s and then fell silent. A clip with an in-point of 0 was not affected, because seeking to 0 is right whatever the rate.

The bug is not new: the same seek is in 0.9.14, 0.9.29 and 0.9.32. Local and cloud renders are both affected.

Related work

None of the open audio PRs touches this seek.

How

  • Probe (ffprobe.ts): the probe now reads each audio stream's stated bit rate and sample size. It flags a PCM stream whose stated rate is not sample rate × channels × sample size.
  • Mixer (audioMixer.ts): a flagged source is cut by decoded timestamps, using the timestamp trim (atrim) the mixer already uses for streams that start late. It is no longer cut by a byte seek.
  • Every other source keeps the input seek, so its sound does not change.
  • Block align: a header that also states a stereo block align (4 instead of 2) is covered by the same check, and the test includes it. With a correct byte rate, a wrong block align only rounds the seek to whole sample pairs.
  • Other formats: the check covers PCM in WAV, since that header stores the byte rate the seek trusts. MP3 without a seek table also seeks by an estimated rate. That is a separate, approximate case this PR does not touch. AIFF stores no byte rate.
  • Other seeks: I checked every other input seek in the engine and producer. They read video, or audio the renderer wrote itself.

Test plan

  • Unit tests added/updated: audioMixer.wavByteRate.test.ts builds an 8 s WAV that changes pitch every second. It writes two copies with lying headers (byte rate doubled; byte rate doubled plus a stereo block align). A 4 s clip with a 3 s in-point is rendered from each copy and from the true file, and the pitch is read at 0.5, 1.5, 2.5 and 3.5 s.
    • Without the fix, both cases fail with expected [ 2800, 3200, null, null ] to deeply equal [ 1600, 2000, 2400, 2800 ]: the wrong seconds, then silence.
    • With the fix, both pass and match the true file.
    • The engine's audio mixer and probe tests: 276 of 277 pass with the fix. The one failure, reads many clips of one URL at once (expected 16 to be greater than 16), fails the same way on main on the same 32-core box, so this change does not cause it. Typecheck passes.
  • Manual testing performed, rendering from source on Linux, main against this branch:
    • A 150 s film shaped like the report: one lying-header WAV, clips with in-points 0, 38.2 and 129.6 s. On main, the 38.2 s clip goes silent 63.6 s in and the 129.6 s clip is silent. On this branch, the only silences are the gaps between clips, the same as with a correct header.
    • The same film with a correct header and no in-points: the audio stream's framemd5 is identical on main and on this branch.
    • The real project that showed the bug: the reported silences are gone. The silences left are the gaps between clips and pauses longer than 1 s.
  • Documentation updated (if applicable): not applicable.
  • Comments follow CONTRIBUTING.md "Comments".

…right sound to their end

A WAV header that states twice its real byte rate sent the mixer's input seek
to twice the clip's in-point: the clip played later words, then silence. Such a
source is now cut by decoded timestamps instead of by a byte seek.
@github-actions

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 267 (base branch 267), smooth 100 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 (8)

Unstable (2)

  • seqrepeat-none-center-r0-root-z100: tracking 0.05, pressJump 0, drop 0, reload 0, render 0.02, renderKey -, undo true, teleport true / tracking 0.05, pressJump 0, drop 0, reload 0, render 0.02, renderKey -, undo true, teleport true / tracking 0.05, pressJump 0, drop 0, reload 0, render 0.02, renderKey -, undo true, teleport true
  • seqfastkeys-none-px-r0-root-z100: tracking 0.07, pressJump 0.05, drop 0, reload 0, render 0.02, renderKey -, undo false, teleport true / tracking 0.06, pressJump 0.05, drop 0, reload 0, render 0.02, renderKey -, undo true, teleport true / tracking 0.06, pressJump 0.05, drop 0, reload 0, render 0.02, renderKey -, undo true, teleport true

@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.

Approving at 8f8bb8e1.

  • The root cause and the fix match. An input seek (-ss before -i) on a PCM WAV becomes a byte offset computed from the header's byte rate, so a doubled rate lands at twice the in-point. misstatesPcmByteRate compares ffprobe's stream bit_rate with sample_rate × channels × bits_per_sample. The probe already runs -show_streams, so both fields are present. A flagged source falls through to the existing atrim path, which cuts by decoded timestamps. The check fails safe: a false positive only costs a decode from the start, and any non-PCM or incomplete stream keeps the input seek, so other sources are unchanged.
  • The "fails without the fix:" claim holds; I confirmed it locally. Both audioMixer.wavByteRate.test.ts cases pass on this head. With the !metadata?.misstatedByteRate guard removed, they fail with expected [ 2800, 3200, null, null ] to deeply equal [ 1600, 2000, 2400, 2800 ], as the body says. The test builds its own WAVs with ffmpeg and patches the header bytes, so it doesn't depend on a fixture.
  • The rest of the engine audioMixer* and ffprobe* tests: the 6 that fail in my sandbox (HDR PNG fixture, group FX) fail the same way with main's audioMixer.ts and ffprobe.ts, so they come from my environment. CI is green on this head.

Non-blocking: a WAVE_FORMAT_EXTENSIBLE file whose container sample size differs from its valid bits could also be flagged. That direction is safe (it only takes the slower trim), so no change is needed.

@miguel-heygen
miguel-heygen added this pull request to the merge queue Nov 11, 2026
Merged via the queue into main with commit 7ae1ae6 Nov 11, 2026
90 checks passed
@miguel-heygen
miguel-heygen deleted the fix/offset-clip-sound branch November 11, 2026 05:37
meefs pushed a commit to meefs/hyperframes that referenced this pull request Nov 12, 2026
…render plays it (heygen-com#7618)

* fix(cli): check hears a WAV whose header misstates its byte rate, as render plays it

A PCM WAV whose header byte rate is wrong (some speech generators write 2x) sent the silence decode's input seek past the file's end, so every clip past the middle was reported audio_clip_silent. Render already cuts such a file by decoded time (heygen-com#7541); the byte-rate rule now lives in parsers and the CLI decoder seeks after decoding starts for those files.

* test(cli): keep the misstated byte-rate case with the other single-clip checks

* style(cli): format the WAV test helper

* style(parsers): keep the byte-rate rule's comment within the line limit
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