Skip to content

feat(studio): normalize loudness and duck under voice, with a limiter report (4/8) - #4816

Merged
vanceingalls merged 10 commits into
mainfrom
aov/04-loudness-duck
Oct 2, 2026
Merged

vanceingalls merged 10 commits into
mainfrom
aov/04-loudness-duck

Conversation

@vanceingalls

@vanceingalls vanceingalls commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Loudness in Studio: Normalize loudness and Duck under voice in the clip menu, backed by a shared loudness module that also measures videos. The export now reports how far the true-peak limiter turned the mix down.

Changes

  • studio-server: @hyperframes/studio-server/loudness (measurement with an absolute target and video support), a measure-only normalize endpoint, and the subpaths registered in the package descriptors.
  • cli: normalize-audio uses the shared module and accepts video.
  • studio: Normalize and Duck under voice clip-menu items (clipMenuNormalize, clipMenuDuck).
  • producer: audioLoweredDb on the render job, the SSE complete payload and the /renders record (omitted when nothing was lowered).

Testing

Static gates on the layer tip: bun install, bun run build, tsc --noEmit for every touched package (0 errors), oxlint and oxfmt --check on files changed vs main (clean), gen:skills-manifest --check (in sync), scripts/comment-ratchet.mjs (ok).

  • core 3789, studio 6270, studio-server 987. All pass.
  • producer audioPadTrim 37 (bun test), assembleStage 16, extractVideosStage 21. All pass.
  • cli 3956 pass; 1 cloud/download stream test is load-flaky (passes alone, 12/12).

Notes

  • Much of the normalize-audio.ts churn is a move into the shared loudness module.
  • Part of the audio-on-video stack. See the bottom PR for the overview.

🤖 Generated with Claude Code

Before

before

After

after

Clip menu: Normalize loudness added. Duck under voice shows only when a voice overlaps.

@vanceingalls
vanceingalls added this pull request to stack #4822 October 1, 2026 01:24
Comment thread packages/studio-server/src/helpers/loudness.ts Fixed

@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 incremental #4816 at a244373ae644e6cf8085f7a7804cfc4a91eb08ef against 720871da192bce80bc119e2109165f28079bc460.

The measure-only endpoint correctly leaves persistence to Studio (packages/studio-server/src/routes/loudness.ts:82), and the limiter attenuation is propagated through both CLI and Vite adapters instead of existing only in a log (packages/cli/src/server/studioServer.ts:624, packages/studio/vite.adapter.ts:369).

Blocker: packages/studio/src/player/components/clipMenuAudioItems.tsx:38 — Normalize always writes data-volume, even when a volume automation lane owns the clip's gain. syncRuntimeMedia takes that lane before the static volume (packages/core/src/runtime/media.ts:402); render likewise uses the lane envelope (packages/engine/src/services/audioMixer.ts:1321). An independent runtime witness changed data-volume from 1 to 2 on a constant 0.25 lane and the applied gain remained 0.25, while the command claims normalization. Normalize the effective envelope, or explicitly refuse this unsupported case rather than announce success.

Important: packages/studio/src/player/components/clipMenuAudioItems.tsx:38 and :58 — The quiet-save callback returns a TimelineEditOutcome, including non-throwing failed/refused outcomes. Both new handlers discard that result. The browser menu witness returned {status:"failed",reason:"disk full"} and still displayed “Normalized to −16 LUFS (+6.0 dB)”. Duck can similarly continue after an individual write fails and report success for a partial chain. Check each save outcome and stop on failure.

CI: The existing @github-advanced-security bot already reports the polynomial regex at packages/studio-server/src/helpers/loudness.ts:153; this review does not duplicate that finding. The exact-head CodeQL result is failure (“1 new alert including 1 high severity security vulnerability”). Other observed checks pass.

Audited: loudness helper/route, CLI normalization move, normalize/duck menu handlers, carve output extraction, both render adapters, producer attenuation and assemble propagation; traced existing volume automation playback/render and persistence/undo consumers.
Trusting: parent PRs #4813–4815; full production rendering, complete package suites, unrelated existing runtime/UI behavior.

Verification: 93 existing targeted tests (40 loudness/menu, 37 Bun audioPadTrim, 16 assembleStage) and 2 independent witnesses passed at this exact head (cached Vitest 3.2.4, not declared 4.1.11; parser/core aliases resolve this worktree's source). The browser menu witness ran at #4818 with byte-identical clipMenuAudioItems.tsx; client-only local adapter, not a packaged/full-Studio/backend E2E claim. No customer assets or paid renders.

— Magi

Verdict: REQUEST CHANGES
Reasoning: The new command can claim normalization without changing effective loudness, and the existing CodeQL failure remains unresolved.

@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Edit accuracy: 951 passing here, 951 on the base branch

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 (2)

Unstable (1)

  • seqnudge-none-pct-r0-nested-z100: tracking 0.03, pressJump 0, drop 0, reload 0, render 0.01, undo false, teleport true / tracking 0.03, pressJump 0, drop 0, reload 0, render 0.01, undo true, teleport true / tracking 0.03, pressJump 0, drop 0, reload 0, render 0.01, undo true, teleport true

@vanceingalls
vanceingalls force-pushed the aov/04-loudness-duck branch 2 times, most recently from 1b983f3 to 2f5e250 Compare October 1, 2026 08:20

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

Fix-delta review at dbcbca85d2e088cd05cb28320015aa3109aec27d, against stacked parent c16614b676aa74fdb0f83ff2c4ccb99f3439efd3; compared with my previous review at a244373a.

The prior code findings are repaired: Normalize refuses a nonempty volume lane before measurement (packages/studio/src/player/components/clipMenuAudioItems.tsx:44); Normalize/Duck inspect tagged save outcomes and stop rather than toast success (:49, :73). The hostile-markup parser uses a character scan and exact-head CodeQL is green.

Verification: 24 loudness/parser and 5 menu tests pass on stack head 88255f82e; these implementation and test files are byte-identical to this PR head. Cached Vitest 3.2.4, source aliases, happy-dom, boundary persistence fakes; no full Studio/backend, live audio or render acceptance. No new code blocker found in the repair delta. Parent layers and unrelated rebased-main changes were not re-reviewed.

The required Studio and player captures check still fails for missing Before/After evidence. This COMMENT closes the code findings in the narrative; it does not supersede my earlier changes-requested state or approve landing while that gate remains red.

— Magi

Verdict: COMMENT
Reasoning: The automation, save-result and CodeQL repairs hold in the reviewed paths. Required capture evidence remains missing.

@jerrai-bot-heygen jerrai-bot-heygen left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Reviewed #4816 at b8b2bb09 against #4815 bf04ab97. Normalize still refuses an automated-volume lane; Normalize and Duck check each quiet-save result before reporting success. The hostile-attribute parsing repair remains in the loudness helper. I found no new blocker in those code paths, but the required Studio/player Before/After captures check remains FAILED without evidence or waiver; prior changes-requested review still governs, so no approval. Edit-accuracy and Comments checks pass. No local tests or Studio run. — Jerrai

@jerrai-bot-heygen jerrai-bot-heygen left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code review of this exact #4815-based layer: Normalize refuses automated-volume lanes before claiming a change; Normalize and Duck inspect quiet-save outcomes, and the hostile-markup parser/limiter reporting repairs remain intact. Its 34-file layer patch is unchanged through the rebase and the capture check passes. The red shard-8 visual comparison (sub-comp-t0, 33 frames; adjacent #4815/#4817 pass) needs a separate CI disposition; this approval does not bypass it or Miguel's older changes-requested review. — Jerrai

@jerrai-bot-heygen jerrai-bot-heygen left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Exact 4f68e172 rebase review: old and new parent→head comparisons retain the same 34 paths and per-file patches. The only concurrently changed layer file, packages/studio/src/index.ts, adds WebMCP exports on the new parent while this layer adds a separate audio-meter export; both additions survive in the new head. This preserves the previously reviewed automation/save-outcome code. This approves this layer's code, not the currently held #4815 viewport regression or any pending/cancelled required check. Miguel's older review is also a separate gate. No merge or release action. — Jerrai

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

Re-reviewed #4816 at 4f68e172022742086fded6208f12a72a00dcb990 against #4815 bff5d190e; this supersedes my changes-requested review 5374292381.

The original findings are repaired: automated-volume lanes are refused before measurement; Normalize and Duck inspect each save outcome before reporting success; the hostile-markup parser is the linear scan already checked in my dbcbca85 follow-up. The menu and loudness source remain byte-identical to that reviewed repair. I read the current handlers and reran 17 menu/normalize/duck cases using the current layer's implementations in the source export, including failed/refused saves. The later parent-dependent bed-eligibility change is not counted as part of this layer.

Current required checks, including Studio/player captures, pass. The check-run history contains a canceled file-size run; it is not a failure in the fetched required-check list. No new layer-specific code blocker found. This is a code review with cached Vitest 3.2.4/source aliases and component-boundary persistence fakes, not full Studio/backend, live audio, playback or export acceptance.

Verdict: APPROVE
Reasoning: the effective-gain, save-result and parser findings are fixed and regression-covered at the reviewed layer. The former capture-only hold is resolved.

— Magi

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

Re-approved at eaaf6e1b6d9e243490b0f6dc1d4e5fe366462794 against stacked parent a88f4fe56fa10293a121443999cfe78cca56a5b0, following my review 5387236315.

The parent-to-head patch has identical paths and hunks to the layer I previously approved, ignoring only object indexes and hunk locations. The restack preserves the repaired Normalize automation refusal and failed-save handling. This is a rebase verification; I did not repeat the earlier exact-source tests or the full package suite.

Current checks have no failure, but CI remains in progress; approval does not establish merge readiness. Prior evidence limits (cached tooling, no live Studio/audio acceptance) remain. No merge, release or protection bypass performed.

Verdict: APPROVE
Reasoning: The inspected layer is the same reviewed patch on its new stacked parent, with no additional code change or new layer-specific blocker.

— Magi

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

Re-approved at 9849ba697393839b2134f5a82b79a694163bcb5c against stacked parent 9c6f0a3ec954afc3da7591f14874f89af43aa74a, following my approval 5387641103.

I independently compared the complete old/new parent-to-head patches: same 34-path set and identical hunks, ignoring only object indexes and hunk positions. Every changed-layer source and test blob is also identical between the approved head and this head; the only differing layer file is package.json with the inherited 0.8.106 → 0.8.107 version bump. The prior repairs, test cases and save/decode behavior, survive unchanged.

This is a rebase verification, not a new local test run. The prior focused tests and their stated cached-tooling limits remain the evidence. No live Studio/playback, full package acceptance or merge/release performed. No task worktree, server or browser was created.

Current-head checks have no failure but are still in progress. Approval does not waive any required check or establish merge readiness.

Verdict: APPROVE
Reasoning: The entire inspected layer patch and its executable sources/tests match the previously approved layer on the new stacked parent.

— Magi

Base automatically changed from aov/03-clip-tools to main October 2, 2026 03:22
…et and video support

normalize-audio moves its measure/plan code into @hyperframes/studio-server/loudness,
gains an absolute mode (--lufs, default -16, clamped at +12 dB and -1.5 dBTP) and
accepts <video> clips with sound. Adds the @hyperframes/core/audible-video subpath.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vanceingalls and others added 9 commits October 2, 2026 03:22
POST /projects/:id/loudness/normalize measures a clip's played window (audio or
video) and answers the -16 LUFS data-volume plan; Studio writes the attribute.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…riptors

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… menu

Normalize measures the clip on studio-server and writes data-volume as one undoable
edit; Duck toggles a carve against every overlapping voice (audio or video with
sound) at the default strength. Both live in clipMenuAudioItems with one hook-in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The AAC true-peak pass now returns its attenuation; the render logs "Audio lowered
by X.X dB to stay under -1 dBTP" and sets RenderJob.audioLoweredDb, which Studio
servers carry in the complete SSE event and the /renders record. Also exports
useAudioMetersVisible from @hyperframes/studio for hosts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The start-tag regex backtracked quadratically on '<' followed by many spaces
(CodeQL js/polynomial-redos). A linear character scan reads the same name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ave outcomes

A volume lane owns the clip's gain in preview and render, so writing
data-volume changed nothing while the toast claimed success; Normalize now
refuses with a clear message. Normalize and Duck also check each quiet-save
outcome, stop at the first failed or refused write and report it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vanceingalls
vanceingalls added this pull request to the merge queue Oct 2, 2026
Merged via the queue into main with commit 9244c47 Oct 2, 2026
84 of 95 checks passed
@vanceingalls
vanceingalls deleted the aov/04-loudness-duck branch October 2, 2026 04:16
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.

4 participants