Skip to content

fix: remove hidden audio gain in renders#362

Merged
miguel-heygen merged 2 commits intomainfrom
codex/fix-audio-unity-gain
Apr 21, 2026
Merged

fix: remove hidden audio gain in renders#362
miguel-heygen merged 2 commits intomainfrom
codex/fix-audio-unity-gain

Conversation

@miguel-heygen
Copy link
Copy Markdown
Collaborator

@miguel-heygen miguel-heygen commented Apr 21, 2026

Summary

This fixes a render-time audio correctness bug where Hyperframes applied a hidden post-mix gain to every rendered output, boosting audio by about +2.6 dB and causing clipping on normally leveled sources.

It also fixes a related mute bug where data-volume="0" was treated as falsy and silently converted back to full volume during audio track preparation.

Additionally, this PR fixes the Studio workspace typecheck path for @hyperframes/player, so local pre-commit/typecheck flows no longer depend on the Player package having been built first.

Root Cause

The issue report measured a near-constant gain increase and suspected a hidden normalization step. After tracing the engine audio path, the root cause turned out to be explicit code, not FFmpeg behavior:

  • packages/engine/src/config.ts defaulted audioGain to 1.35
  • packages/engine/src/services/audioMixer.ts always appended a post-mix FFmpeg filter:
    • [mixed]volume=${masterOutputGain}[out]
  • with the default config, that meant every render got multiplied by 1.35

That exactly matches the issue reporter's measured scalar boost.

While investigating the workaround, I also found a second correctness bug:

  • processCompositionAudio() used element.volume || 1.0
  • that coerced 0 to 1.0
  • so data-volume="0" did not actually mute the track in rendered output

Separately, the repo-level Studio typecheck could fail before any build step because:

  • packages/studio/src/player/components/Player.tsx imports @hyperframes/player
  • packages/player/package.json points TypeScript at built dist/* outputs
  • in a fresh workspace, those built outputs may not exist yet
  • Studio therefore failed type resolution for @hyperframes/player during pre-commit/typecheck

What Changed

  1. Set the engine default audioGain back to unity (1)
  2. Preserve explicit zero volumes by changing element.volume || 1.0 to element.volume ?? 1.0
  3. Added regression coverage for both behaviors
  4. Updated the producer-side config fixture to reflect the corrected default
  5. Added a Studio tsconfig path mapping for @hyperframes/player to the local workspace source and widened rootDir so workspace typecheck succeeds without requiring a prior Player build

Why These Changes Are Needed

This is not a UX preference issue; it is a correctness and API contract issue.

  • The docs describe data-volume as a direct 0-1 control.
  • Rendered output should preserve source levels unless the author explicitly changes them.
  • Hidden global gain makes output non-deterministic from the author's perspective.
  • data-volume="0" must mean silence, not full-volume playback.
  • Local workspace typecheck should not require unrelated package build artifacts to exist first.

Leaving the current behavior in place means:

  • voice recordings near normal peak levels can clip during render
  • authors need undocumented manual compensation (0.75-ish scaling) to get unity output
  • mute semantics in docs and code diverge
  • local pre-commit/typecheck can fail for reasons unrelated to the actual diff being committed

Testing

Focused regression tests

Ran:

  • packages/engine/node_modules/.bin/vitest run packages/engine/src/config.test.ts packages/engine/src/services/audioMixer.test.ts

Result:

  • 10 passed

These tests specifically verify:

  • default resolved audioGain is 1
  • a track with volume: 0 stays volume=0 in the FFmpeg filter graph
  • the post-mix output filter stays at unity gain ([mixed]volume=1[out])

Broader package verification

Ran:

  • bun run --filter @hyperframes/engine test
  • bun run --filter @hyperframes/engine build
  • packages/engine/node_modules/.bin/vitest run packages/producer/src/services/renderOrchestrator.test.ts
  • bun run --filter @hyperframes/producer typecheck
  • bun run --filter @hyperframes/studio typecheck
  • bunx oxlint packages/engine/src/config.ts packages/engine/src/config.test.ts packages/engine/src/services/audioMixer.ts packages/engine/src/services/audioMixer.test.ts packages/producer/src/services/renderOrchestrator.test.ts
  • bunx oxfmt packages/engine/src/config.ts packages/engine/src/config.test.ts packages/engine/src/services/audioMixer.ts packages/engine/src/services/audioMixer.test.ts packages/producer/src/services/renderOrchestrator.test.ts packages/studio/tsconfig.json
  • bunx lefthook run pre-commit

Results:

  • full engine test suite passed (309 passed)
  • engine build passed
  • touched producer test file passed (7 passed)
  • producer typecheck passed
  • studio typecheck passed
  • oxlint passed with 0 warnings, 0 errors
  • formatting passed
  • pre-commit hook no longer hits the prior @hyperframes/player module-resolution blocker

Known Verification Limitation

There is no meaningful browser UI flow for this bug: the defect is in the engine/CLI audio render pipeline rather than an interactive browser surface. Because of that, verification was done at the renderer and test level rather than through an agent-browser flow.

User Impact

After this change:

  • rendered audio matches source level by default
  • authors no longer need to compensate for a hidden +2.6 dB boost
  • data-volume="0" correctly mutes rendered audio
  • the documented volume contract matches engine behavior again
  • local workspace typecheck no longer depends on prebuilt @hyperframes/player artifacts

Closes #361.

@miguel-heygen miguel-heygen changed the title [codex] Fix hidden audio gain in renders fix: hidden audio gain in renders Apr 21, 2026
@miguel-heygen miguel-heygen marked this pull request as ready for review April 21, 2026 15:09
@miguel-heygen miguel-heygen changed the title fix: hidden audio gain in renders fix: remove hidden audio gain in renders Apr 21, 2026
@miguel-heygen miguel-heygen force-pushed the codex/fix-audio-unity-gain branch from 8ea22ae to f367a36 Compare April 21, 2026 15:20
@miguel-heygen miguel-heygen force-pushed the codex/fix-audio-unity-gain branch from f367a36 to 2ba7e4f Compare April 21, 2026 16:07
Copy link
Copy Markdown
Collaborator

@jrusso1020 jrusso1020 left a comment

Choose a reason for hiding this comment

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

Approving — the root cause analysis and fix are correct. A few follow-ups to address before/after merge, noted below.

Root cause / fix

Both defects match the issue reporter's measurements exactly:

  • Hidden +2.6 dB gain. DEFAULT_CONFIG.audioGain = 1.35 was applied as a post-mix FFmpeg filter in audioMixer.ts ([mixed]volume=${masterOutputGain}[out]). The reporter measured a scalar 1.3517 with -47 dB residual — pure multiplication, no filter — which lines up with this code path precisely. Dropping the default to 1 is the minimal correct fix, and PRODUCER_AUDIO_GAIN stays as an env override for anyone who wants the old boost.
  • data-volume="0" not muting. element.volume || 1.0 coerces 01.0. Switching to ?? preserves explicit zero. Matches the reporter's "data-volume=0 does not suppress the gain" observation.

I also checked for other gain paths (loudnorm, dynaudnorm, other volume= filters, audioGain references across the engine) — there are none, so this is the only hidden boost in the pipeline.

The new audioMixer.test.ts covers both regressions (volume=0 preserved in filter graph, [mixed]volume=1[out] post-mix) which is the right shape for this change.

Follow-ups

1. Two regression suites fail on the audio comparison (blocking merge):

  • font-variant-numeric — correlation -1 (threshold 0.9)
  • missing-host-comp-id — correlation 0.8776 (threshold 0.9)

Both suites' output/output.mp4 reference fixtures were last updated 2026-03-30, i.e. baselined with the old 1.35× gain. Two likely causes:

  • If the old reference was hard-clipping at ±1.0 (peaks got pushed past 0 dBFS per the issue repro), envelope correlation against the new unclipped render legitimately drops — Pearson correlation on a clipped reference is not scalar-invariant. This probably explains the 0.8776 on missing-host-comp-id.
  • The -1 on font-variant-numeric smells like a harness NaN edge case: computeBestCorrelation initializes best = -1 and never updates if every lagged correlation produces NaN (e.g., a near-constant/silent envelope). Worth sanity-checking whether this suite is supposed to exercise audio at all, or whether the -1 is a pre-existing latent bug in regression-harness.ts that this PR happens to tickle.

Either way: please regenerate those two reference output.mp4 files on this branch before merging. If font-variant-numeric isn't meant to be an audio suite, consider lowering/removing its minAudioCorrelation in meta.json instead.

2. Unrelated Studio tsconfig change bundled in. The rootDir: ".." / @hyperframes/player path mapping is a workspace typecheck ergonomics fix, not part of the audio bug. The PR description calls this out honestly, but ideally it would be a separate PR so the audio fix stays independently revertible.

3. (Optional, non-blocking) Release note. This is a user-visible level change — anyone who'd been relying on the boosted default will see ~2.6 dB quieter output. Env override preserves the escape hatch, but a line in the release notes would help existing users.

Nice writeup in the PR description — the root-cause trace and the 1.35 ↔ +2.62 dB connection made this very easy to verify.

@miguel-heygen miguel-heygen merged commit b98093a into main Apr 21, 2026
25 checks passed
Copy link
Copy Markdown
Collaborator Author

Merge activity

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.

Audio output has fixed ~+2.5 dB gain applied during render, causing clipping on normally-leveled sources

2 participants