mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-07 01:56:04 +00:00
feat(cli): add normalize-audio to match one clip's loudness to another (#3306)
* feat(cli): add normalize-audio to match one clip's loudness to another 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. * fix(cli): validate --tolerance before paying for the measurement Each EBU R128 pass is bounded at 120s and normalize-audio runs two, so parsing the argument afterwards made a typo'd --tolerance cost both of them before failing on something that was wrong from the start. Not pinned by a test: the ordering is internal to the command and neither it nor the parser is exported, so covering it would mean restructuring for a spy rather than asserting the behaviour. * docs(cli): restore the blank line between the preview and normalize-audio sections Lost when I resolved the rebase conflict against the background-preview docs by hand instead of letting the formatter near it. oxfmt --check failed on the one file, which fails Preflight — and because preview-parity needs Preflight it skipped, and the preview-regression gate fails closed on a skip, so a missing newline read as a preview defect. The quieter half: the same needs chain meant the required Test context was never created at that head. Not failing — absent, so there was no test signal at all on the PR.
This commit is contained in:
@@ -135,6 +135,41 @@ the ambiguity instead.
|
||||
|
||||
## Recipes
|
||||
|
||||
### Compare loudness from the bytes the listener actually hears
|
||||
|
||||
Do not call two clips equally loud because their Studio faders, waveform peaks,
|
||||
or cached asset metadata match. Those are controls and proxies, not a loudness
|
||||
measurement. Resolve the exact URLs used by preview/render, download or inspect
|
||||
those exact served bytes, and measure each decoded stream with FFmpeg's
|
||||
`ebur128` filter. Compare the integrated LUFS values.
|
||||
|
||||
For a target loudness, the required move is:
|
||||
|
||||
```text
|
||||
gain_db = target_lufs - measured_lufs
|
||||
linear_gain = 10 ** (gain_db / 20)
|
||||
```
|
||||
|
||||
When both clips are local authored `<audio>` elements with stable ids, use the
|
||||
CLI instead of transcribing that arithmetic by hand:
|
||||
|
||||
```bash
|
||||
npx hyperframes normalize-audio --reference target-audio --target user-audio
|
||||
npx hyperframes normalize-audio --reference target-audio --target user-audio --write
|
||||
```
|
||||
|
||||
The first command is a dry run. The second writes only the target's
|
||||
`data-volume`, after accounting for both existing gains and refusing a boost
|
||||
that would clip or exceed Studio's ceiling. Always choose the reference from the
|
||||
author's stated intent; the command does not guess which clip should define the
|
||||
mix.
|
||||
|
||||
Studio's clip-gain fader uses `0 dB` / linear gain `1` at its physical midpoint
|
||||
and provides up to `+12 dB` on the upper half. After changing gain, measure the
|
||||
served preview/render bytes again. If a listener still hears a mismatch, trust
|
||||
the report and first verify the asset URL and bytes are current; do not explain
|
||||
it away with matching peaks or a stale proxy measurement.
|
||||
|
||||
All verified with ffmpeg 8.1.1. `-hide_banner` keeps the output readable;
|
||||
`volumedetect` prints to stderr, so do not silence it with `-v error`.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user