| name | normalize-loudness |
| description | Two-pass ffmpeg loudnorm. Brings any video or audio file to a target integrated LUFS with true-peak ceiling and LRA cap. Default is -14 LUFS (social). |
normalize-loudness
Purpose
Normalize the loudness of a video or audio file using ffmpeg's two-pass loudnorm filter (EBU R128). Most social platforms target around -14 LUFS integrated with a -1 dB true-peak ceiling and an LRA cap around 11. This atom is the canonical way to hit those targets — replaces ad-hoc alimiter chains in per-ad polish scripts.
Two-pass is required (not single-pass) because the second pass uses the measured statistics from the first to apply linear normalization with no dynamic distortion.
Inputs
--input <path> — mp4 / mov / wav / mp3 (required)
--output <path> — destination file (required)
--target-i <lufs> — integrated LUFS (default -14)
--target-tp <dbtp> — true-peak ceiling in dBTP (default -1)
--target-lra <lu> — loudness range cap (default 11)
--platform <name> — preset that overrides the three above. Supported: social (default, -14/-1/11), broadcast (-23/-2/7), podcast (-16/-1/11)
Workflow
- Verify
ffmpeg and ffprobe are on PATH.
- Pass 1 — measure: run
ffmpeg -i <input> -af loudnorm=I=<I>:TP=<TP>:LRA=<LRA>:print_format=json -f null - and parse the JSON tail of stderr for input_i, input_tp, input_lra, input_thresh, target_offset.
- Pass 2 — apply: run
ffmpeg -i <input> -af loudnorm=I=<I>:TP=<TP>:LRA=<LRA>:measured_I=...:measured_TP=...:measured_LRA=...:measured_thresh=...:offset=...:linear=true:print_format=summary -c:v copy <output> (video stream is copied untouched).
- Parse the second-pass summary for the measured-after values.
- Write
manifest.json and verification.md alongside the output.
Output
<output> — normalized file (video copy + remuxed audio).
manifest.json:
{
"input": "...",
"output": "...",
"target": { "I": -14, "TP": -1, "LRA": 11 },
"measured_before": { "I": ..., "TP": ..., "LRA": ..., "thresh": ... },
"measured_after": { "I": ..., "TP": ..., "LRA": ..., "thresh": ... },
"platform": "social",
"status": "ok"
}
verification.md — short pass/fail report: integrated LUFS within ±0.5 of target, TP under ceiling.
Quality checks
- Output integrated LUFS within ±0.5 of target.
- Output true-peak strictly below ceiling.
- Video stream byte-identical to input (verify via
ffprobe stream md5).
- Pass-1 JSON parsed correctly (no NaN values in the measured stats).
Failure modes
- Single-pass loudnorm — produces dynamic-range distortion. Always two-pass.
- ffmpeg not on PATH or wrong build —
loudnorm needs ffmpeg ≥ 3.1.
- Input audio is silent / DC — measured_I returns
-inf; script must short-circuit and emit status: skipped_silent.
- Very short clips (< 3 s) — loudnorm window is unreliable; emit a warning in manifest.
Example
scripts/loudnorm.sh \
--input peloton/ads/video-03-ironman-comeback/edits/master-final.mp4 \
--output peloton/ads/video-03-ironman-comeback/edits/master-normalized.mp4 \
--platform social
Consumed by: /polish (audio-loudness branch), auto-refine (audio sub-loop), edit-video (Phase 9 final mix).