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 (default11)--platform <name>— preset that overrides the three above. Supported:social(default, -14/-1/11),broadcast(-23/-2/7),podcast(-16/-1/11)
Workflow
- Verify
ffmpegandffprobeare 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 forinput_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.jsonandverification.mdalongside 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
ffprobestream 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 —
loudnormneeds ffmpeg ≥ 3.1. - Input audio is silent / DC — measured_I returns
-inf; script must short-circuit and emitstatus: 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).