Skip to content

fix(producer): an export over the ceiling limits its peaks and keeps the mix's level - #6940

Merged
miguel-heygen merged 1 commit into
mainfrom
fix/export-true-peak-delivery
Nov 1, 2026
Merged

miguel-heygen merged 1 commit into
mainfrom
fix/export-true-peak-delivery

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Nov 1, 2026 •

Copy link
Copy Markdown
Collaborator

What

An export whose mix passes the delivery ceiling now comes down peak by peak, so quiet passages keep their level. Before, the whole file was lowered by one flat amount.

Why

Export keeps the delivered file under -1 dBTP. When a peak passed that, constrainAacTruePeak turned the whole mix down with volume=, so one loud moment made an entire film several dB quieter. #6938 made that cut visible; this replaces it for the mixes it can.

Same film, real export Quiet tone before Quiet tone after Whole-mix cut after
One clip, bursts at -0.45 dBFS (tone in the source: -23.01 dB) -24.21 dB -23.01 dB none
Two overlapping clips, a -2 limiter on their group -24.29 dB -16.99 dB none
Two overlapping clips, dry -25.49 dB -16.99 dB none

Related work

Builds on #6938, which prints the audio_lowered warning.

How

  • limitDeliveryPeaks (new, audioDeliveryLimit.ts), called from runAudioStage after the first encode: if the encoded mix is over the ceiling, it runs the kept PCM through a truepeak node at -3 dBTP and encodes again. -3 is the margin audio-effects.mdx already gives for a -1 dBTP limit.
  • A mix under the ceiling takes none of this: one peak measurement, no browser and no second encode.
  • constrainAacTruePeak still runs after it, as the backstop for anything that gets over, and prints the same warning. If the limit cannot run, the log says so and the flat cut applies as it does today. A failed encode of the limited mix fails the render.
  • The offline render already trims the limiter's lookahead, so sound does not move against the picture: the first burst lands at 2.000 s in the source and in every export above.
  • @hyperframes/engine now exports applyAudioFxChain.

Tradeoff: this changes the sound of every export that goes over. A mix far over the ceiling is held hard rather than turned down: the two-clip film needs about 8.6 dB of gain reduction on its bursts (+5.6 dB sum to -3 dBTP), which can be audible as distortion on dense material. Preview has no delivery limit, so an export can differ from its preview on those peaks. The separate check in padOrTrimAudioToVideoFrameCount still uses the flat cut.

Test plan

  • Unit tests added: audioDeliveryLimit.test.ts (under the ceiling does nothing; over it re-encodes through the limit; a limit that cannot run leaves the flat cut and warns; a failed encode fails; no source does nothing), and limits the mix's peaks before the whole-mix cut measures it in audioStage.test.ts.
  • fails without the fix: packages/producer/src/services/render/stages/audioStage.test.ts › runAudioStage › limits the mix's peaks before the whole-mix cut measures it
  • Removing the step from runAudioStage fails that test.
  • packages/producer: both files passed 3 runs in a row; audioPadTrim.test.ts 40 passed under bun test; tsc --noEmit clean.
  • Manual testing performed: real hyperframes render exports on Linux, measured with ffmpeg (astats for the quiet tone, ebur128=peak=true for peaks). Peaks after: -2.8, -2.8, -2.9 dBFS. A film already under the ceiling kept its level and -4.0 dBFS peak, with no warning.
  • Listening check by a person: a maintainer compared two exports of the same 24 s of repo music and said the fix "sounds clean". Two more copies of the music play on top from 0:10 to 0:13 to push it over the ceiling, and the clips were not level matched. The "today" clip is this build with the new step switched off.
    • Today: -18.9 LUFS, -1.5 dBTP peak, music outside the burst -24.0 dB, whole clip lowered 5.7 dB.
    • With this change: -14.4 LUFS, -2.9 dBTP peak, music outside the burst -18.3 dB, no whole-clip cut.
  • Not run on macOS or Windows, or through cloud renders.
  • Comments follow CONTRIBUTING.md "Comments"

Base automatically changed from fix/export-warns-when-audio-lowered to main November 1, 2026 16:51
@mintlify

mintlify Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
hyperframes 🟢 Ready View Preview Nov 1, 2026, 5:06 PM

💡 Tip: Enable Automations to automatically generate PRs for you.

@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 271 (base branch 271), smooth 109 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)

Unstable (1)

  • seqplainkeys-none-center-r0-nested-z100: tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, 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.

Verdict: APPROVE · 0 blocker · 0 important · 2 nits · head 374db34b

Checked the fails without the fix: claim. With audioStage.ts reverted to base, runAudioStage › limits the mix's peaks before the whole-mix cut measures it fails (actual value must be number or bigint, received "undefined"). At head, both touched test files pass (20/20) and producer tsc --noEmit is clean. Also checked: the -3 dBTP / 1.7 dB overshoot reasoning matches the hf-truepeak docstring, and chainLatencySamples trims the lookahead in the offline render, so the limited PCM stays aligned with the picture.

Nits and scope
  • audioDeliveryLimit.ts:52: the code encodes limitedPath and ignores the path that applyChain returns. applyAudioFxChain returns the input path without writing anything when the PCM has zero frames, and then encode would fail on a file that does not exist. A mix that is over the ceiling can't really have zero frames, but encoding the returned path costs nothing.
  • audioDeliveryLimit.ts:54-58: when the limit fails and the render goes on with the flat cut, any partial audio-source-limited.wav stays in workDir. The finally in runAudioStage removes only audio-source.wav. Adding a removeScratch(limitedPath) on that path would clean it up.

Scope: read the full diff, applyAudioFxChain, constrainAacTruePeak, and the runtime latency trim. CI was still running at review time and no check had failed. I did not listen to the audio; the sound of the limited exports is left to the listening clips.

@miguel-heygen
miguel-heygen added this pull request to the merge queue Nov 1, 2026
Merged via the queue into main with commit d1d631f Nov 1, 2026
140 checks passed
@miguel-heygen
miguel-heygen deleted the fix/export-true-peak-delivery branch November 1, 2026 17:34
@heygengenesis heygengenesis Bot mentioned this pull request Nov 1, 2026
SanHsien added a commit to SanHsien/hyperframes that referenced this pull request Nov 6, 2026
…view

Adopts heygen-com#7250 (SVG stroke drawing is not a dead stretch), heygen-com#7248
(contract check reads a timeline registered in a local script), heygen-com#7252
(comma-list selectors hidden by authored CSS), heygen-com#7238 (snapshot and
preview show a clip on the render frame), heygen-com#7237 (stalled cloud upload
retries) and heygen-com#7251 (per-scene SVG id namespacing), with prerequisite
heygen-com#6829. Defers heygen-com#7246 (needs the delivery limiter from heygen-com#6940) and heygen-com#7232
(needs the WebKit decode-first pipeline), with conditions in the
ledger. Watermark advances to b108f85.

Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
staging - docs — 374db34b Deployed Nov 1, 2026 by mintlify[bot]
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