To normalize voiceover loudness with ffmpeg, run the loudnorm filter twice: once to measure the file, then again with those measurements fed back in so the filter can apply one clean, linear gain change. Pick a target in LUFS for where the video will play, cap the true peak a little below 0 dBTP, and check the output with a third measurement. The rest of this post is the commands and the reasons behind each number.
Key takeaways
- Loudness is measured in LUFS, which tracks how loud audio sounds, not how high the peaks go.
- Single-pass
loudnormworks, but it adjusts gain on the fly and can pump. Two passes give a steadier result. - Normalize the narration track before mixing music under it, then normalize the final mix.
- Always measure the output. A filter that ran without errors is not proof of the level.
Contents
- Why does voiceover loudness drift between videos?
- What do LUFS, LRA and true peak mean?
- How do you run loudnorm in two passes?
- Which target should you pick?
- How do you check the result automatically?
- FAQ
Why does voiceover loudness drift between videos?
Because every source is different. A text to speech provider returns files at its own level, and a laptop mic recording sits somewhere else. Change the voice or the room and the next video is louder or quieter than the last.
If someone watches two of your demos back to back and has to reach for the volume, that is a small tax on every video. I hit this when I swapped voices partway through a set of feature clips. Each file sounded fine alone. In sequence, one jumped out.
What do LUFS, LRA and true peak mean?
These three numbers come from the EBU R128 and ITU BS.1770 standards, and loudnorm reports all of them.
| Measure | What it describes | Typical use |
|---|---|---|
| Integrated loudness (LUFS) | Average perceived loudness over the whole file | The main target you normalize to |
| Loudness range (LRA, in LU) | How much loudness varies between quiet and loud parts | Keep it low for speech so nothing gets lost |
| True peak (dBTP) | The highest level, including peaks between samples | Cap it below 0 to avoid clipping after encoding |
Peak normalization only looks at the loudest sample, so equal peaks can sound very different. LUFS weights the measurement toward how ears actually hear.
How do you run loudnorm in two passes?
Pass one: measure
ffmpeg -hide_banner -i voice.wav \ -af loudnorm=I=-16:TP=-1.5:LRA=11:print_format=json \ -f null - 2> measure.log Only the log is written. It ends with a JSON block holding input_i, input_tp, input_lra, input_thresh and target_offset.
Pass two: apply
Feed those values back in and set linear=true:
ffmpeg -hide_banner -i voice.wav \ -af loudnorm=I=-16:TP=-1.5:LRA=11:measured_I=-23.4:measured_TP=-4.1:measured_LRA=6.2:measured_thresh=-34.0:offset=0.3:linear=true \ -ar 48000 voice-norm.wav The measured_ numbers are placeholders; use your own. With them, the filter applies one gain change across the file instead of riding the level.
Two notes:
- Add
-ar 48000.loudnormupsamples internally, and without an explicit rate you can end up with a 192 kHz file. - If the file needs more gain than the true peak cap allows, the filter falls back to dynamic mode. The output log will say so. Fixing a very quiet source beats forcing it.
If you script this, parse the JSON with jq and pass the values through, so nobody copies numbers by hand.
Which target should you pick?
Pick by where the video plays, and keep it the same across your library.
- Web embeds, docs and social feeds: around -16 LUFS integrated is a common choice for speech heard on laptops and phones.
- Broadcast style delivery: EBU R128 specifies -23 LUFS. You will rarely need this for product demos.
- Platforms that normalize on playback: some turn loud uploads down. Check their current docs.
For true peak, -1.5 or -1 dBTP leaves headroom for lossy encoders like AAC, which can push peaks slightly higher than the source.
With music, normalize the narration first, mix the music under it, then run the final mix through both passes. I covered the export side, codecs and containers, in choosing product demo video resolution and format.
How do you check the result automatically?
Run pass one again on the output and fail the build if it is off target:
out_i=$(ffmpeg -hide_banner -i voice-norm.wav -af loudnorm=print_format=json -f null - 2>&1 | sed -n '/^{/,/^}/p' | jq -r .input_i) awk -v v="$out_i" 'BEGIN { exit (v < -17 || v > -15) }' || { echo "Loudness $out_i out of range"; exit 1; } A one LU window is tight enough that nobody hears a jump, and loose enough not to fail on rounding.
At DemoVexa the narration comes from a connected voice provider or your own recording, exactly the mix of sources that drifts. The voiceover, script and render features page shows how voiceover fits the rest of the pipeline.
FAQ
Is single-pass loudnorm good enough?
For a preview, yes. For published files, two passes are steadier because the filter knows the whole file first.
Should I normalize before or after adding music?
Both. Voice alone first, then the final mix.
Why is my output file 192 kHz?
loudnorm resamples internally. Set the output rate with -ar 48000.
Does AAC encoding change loudness?
Integrated loudness barely moves, but peaks can rise a little. That is why the true peak cap sits below 0 dBTP.
Baltej Singh is the founder of DemoVexa, a desktop app and CLI that turns a local codebase into narrated product demo videos. He writes from Delaware, USA.

