Skip to content

feat(cli): add normalize-audio to match one clip's loudness to another - #3306

Draft
miguel-heygen wants to merge 1 commit into
audio-gain-above-unityfrom
cli-normalize-audio
Draft

feat(cli): add normalize-audio to match one clip's loudness to another#3306
miguel-heygen wants to merge 1 commit into
audio-gain-above-unityfrom
cli-normalize-audio

Conversation

@miguel-heygen

Copy link
Copy Markdown
Collaborator

Measures two authored <audio> clips with FFmpeg's integrated EBU R128 loudness and writes the target's matching data-volume, leaving the reference untouched. Dry run by default; --write persists.

npx hyperframes normalize-audio --reference bed --target vo
npx hyperframes normalize-audio --reference bed --target vo --write
npx hyperframes normalize-audio --reference bed --target vo --json

Measuring the window the composition actually plays

Two ways to get this wrong, both of which produce a plausible-but-wrong gain:

  • data-end bounds a clip's timeline window just as data-duration does. The parser, the runtime and the render mixer all honour it.
  • -ss / -t belong before -i. After it they bound the output, and with -f null there is no output worth bounding — ffmpeg keeps feeding the filter graph and ebur128 integrates audio the clip never plays.

On a fixture whose played window is −61.8 LUFS inside a file that measures −27.9 LUFS whole, either mistake reports the wrong number and --write "corrects" an already-matched clip by tens of dB.

Writing safely

Two EBU R128 passes run between reading the composition and writing it, each bounded only by a two-minute timeout, and the skill docs tell agents to keep Studio open on the project meanwhile. The attribute patch is re-applied to a fresh read and written through a temp file and a rename, so a concurrent Studio edit is not reverted and a crash cannot leave a half-written composition.

Agent contract

Under --json the failures are documents too — an agent doing JSON.parse(stdout) on a bare error line throws. The command is registered in the curated --help listing so an agent enumerating capabilities can discover it.

A pair needing more than the +12 dB authoring ceiling has a source-file problem rather than a mixer one — mixer gain raises the noise floor with the signal — so the refusal names the remedy instead of just declining.

Stack

Based on u1-audio-gain-core, whose shared gain module it imports. Retarget to main before merging. Independent of u2-studio-gain-surface; the two may land in either order.

Measures two authored `<audio>` clips with FFmpeg's integrated EBU R128
loudness and writes the target's matching `data-volume`, leaving the
reference untouched.

The measurement is bounded to the window the composition actually plays.
`data-end` bounds a clip's timeline window just as `data-duration` does, and
`-ss`/`-t` belong before `-i`: after it they bound the OUTPUT, and with
`-f null` there is none, so ebur128 keeps integrating past the clip. On a
fixture whose played window is -61.8 LUFS inside a file that measures -27.9
whole, either mistake reports a loudness the composition never plays and
"corrects" an already-matched clip by tens of dB.

Two EBU R128 passes run between reading the composition and writing it, each
bounded only by a two-minute timeout, and the skill docs tell agents to keep
Studio open meanwhile — so the attribute patch is re-applied to a fresh read
and written through a temp file and a rename.

Under `--json` the failures are documents too: an agent doing
`JSON.parse(stdout)` on a bare error line throws. A pair needing more than the
+12 dB ceiling has a source-file problem rather than a mixer one — mixer gain
raises the noise floor with the signal — so the refusal names the remedy.
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