Two-pass EBU R128 loudness normalization

ffmpeg guide · audio · loudnorm

loudnorm can run in a single pass, but a single pass is a guess: it estimates gain from the first chunk of audio it sees and applies it blind. The correct sequence measures the whole file first, then re-runs with those exact measured values.

Pass 1 — measure

Run loudnorm in analysis mode against a null output and capture the JSON it prints at the end. Nothing is written yet.


ffmpeg -hide_banner -i talk.wav -af "loudnorm=I=-14:TP=-1.0:LRA=11:print_format=json" -f null - 2>&1

The JSON block includes input_i, input_tp, input_lra, input_thresh, and target_offset — the five values pass 2 needs.

Pass 2 — apply the measured values

Feed those five values back into a second loudnorm call with linear=true, which applies a single linear gain instead of re-estimating. The video stream (if any) is copied untouched; only audio is re-encoded, resampled to 48kHz.


ffmpeg -hide_banner -y -i talk.wav -c:v copy \
  -af "loudnorm=I=-14:TP=-1.0:LRA=11:measured_I=$MI:measured_TP=$MTP:measured_LRA=$MLRA:measured_thresh=$MTH:offset=$OFF:linear=true" \
  -ar 48000 talk_norm.wav

$MI, $MTP, $MLRA, $MTH, and $OFF are the input_i / input_tp / input_lra / input_thresh / target_offset fields parsed out of pass 1's JSON. The toolkit's loudnorm_2pass.sh script parses them with a small grep/sed helper; see the script for the exact one-liner.

Defaults: -14 LUFS integrated, -1 dBTP, 11 LU range

I=-14 (integrated loudness), TP=-1.0 (true peak ceiling), and LRA=11 (loudness range) are the toolkit's defaults — broadcast-consistent numbers that land close to what YouTube and most social platforms already target internally, so a clip normalized to them doesn't get turned down (or up) hard on playback. They're parameterized in the script; override them per delivery spec if a platform asks for something different.

Why single-pass is a guess

Single-pass loudnorm (just -af loudnorm=I=-14:TP=-1.0:LRA=11 with no measure step) uses a rolling estimate as it streams through the file and can drift, especially on material with big dynamic swings — a quiet intro followed by a loud section can throw the running estimate off enough that the export lands noticeably off-target. The two-pass sequence removes that guess: pass 1 measures the actual whole-file loudness, pass 2 applies exactly that.

Verify with a re-measure

Don't trust the pass-2 log line — measure the exported file itself, the same way you'd measure any other input:


ffmpeg -i talk_norm.wav -af loudnorm=I=-14:TP=-1.0:LRA=11:print_format=summary -f null -

A normalized file measured this way against a −14 LUFS target should read close to it — the toolkit's own test run measured a normalized sine back at −13.95 LUFS on ffmpeg 8.1. If the re-measure is off by more than a fraction of a LU, something upstream (wrong measured values passed to pass 2, or a lossy intermediate re-encode) is worth checking.