Normalize loudness
Give it an assembled track (a dialogue track, a narration, a final mix) and you get the
same audio at -16 LUFS integrated with true peaks under -1.5 dBTP, the usual web and
streaming level. Two passes: the first measures, the second applies one linear gain
from those measurements, so nothing is compressed and every sample stays where it was
(track.json line times remain true). Run it once, after the last edit to the track.
Inputs
- The track (
track.wav, ideally lossless: pcm_48000 from ElevenLabs). - Its sample rate, from
ffprobe(loudnorm resamples to 192 kHz internally, so the second pass resamples back to it).
Run it
Keep the unnormalized file beside the result: rename track.wav to track_raw.wav
first, and normalize from it.
- Sample rate:
exec ffprobe -v error -select_streams a:0 -show_entries stream=sample_rate -of csv=p=0 track_raw.wav
- Measure (pass 1). The JSON block is at the end of stderr; read
input_i,input_tp,input_lra,input_threshandtarget_offsetfrom it.
exec ffmpeg -hide_banner -nostats -i track_raw.wav \
-af loudnorm=I=-16:TP=-1.5:LRA=11:print_format=json -f null -
- Apply (pass 2), with the measured values and the source rate:
exec ffmpeg -v error -y -i track_raw.wav \
-af "loudnorm=I=-16:TP=-1.5:LRA=11:linear=true:measured_I=<input_i>:measured_TP=<input_tp>:measured_LRA=<input_lra>:measured_thresh=<input_thresh>:offset=<target_offset>,aresample=<rate>" \
track.wav
- Re-encode the MP3 from the normalized WAV:
exec ffmpeg -v error -y -i track.wav -b:a 192k track.mp3
Record the change where the track's metadata lives, e.g. "loudness": "-16 LUFS (from -22.4)" in track.json.
What you get
track.wav at -16 LUFS, track.mp3 from it, and track_raw.wav untouched. Durations
and line times are unchanged.
Tips and traps
- Normalize the assembled track, not each line or passage. Per-line normalization pumps the level from line to line and has to be redone after every edit.
linear=truefalls back to dynamic (compressing) mode when one gain can't reach the target without passing the true-peak ceiling. Add:print_format=jsonto the pass 2 filter to seenormalization_type; if it saysdynamic, the track has peaks far above its speech. Accept it for voice, or lower the target (I=-18) to stay linear.- Replacing one piece inside a normalized track (a re-voiced chunk or line): don't
normalize the piece on its own. Match its speech level to the piece it replaces,
limit it with
alimiter=limit=0.84:attack=2:release=50:level=false(-1.5 dBFS), and put it through the same post chain (EQ) as the rest of the track. - Loudness doesn't fix a thin voice. A voice that sounds like a telephone is usually
band-limited at the source; see
elevenlabs-tts-v4.
Check it
Run pass 1 on track.wav: input_i within 0.5 of -16 and input_tp at or under
-1.5. ffprobe gives the same duration and sample rate as track_raw.wav.


