Normalize audio loudness across a video library

You have 300 videos in a library and no two of them sit at the same volume. Some clips blow out headphones, others are a whisper, and viewers spend the whole session riding the volume slider. Running every file through the same filter didn't fix it.
Quick answer: To normalize audio loudness across a video library, measure each file with FFmpeg'sloudnormfilter in analysis mode (print_format=json), then run a second pass that feeds those measured values back in with a fixed target such asI=-14:TP=-1:LRA=11. Single-passloudnormis a live estimator and routinely lands a full LU off target; two-pass is what makes files match each other. Batch it with a shell loop for a few dozen files, or send each file as one API call when the library runs into the hundreds.
Conventional wisdom says inconsistent audio means inconsistent levels, so you normalize the peaks and move on. Most tools produce exactly that, and the library still sounds uneven. It's not that your levels are wrong. It's that peak normalization measures the wrong thing, and the fix people reach for next, one pass of loudnorm, doesn't hit the number it prints.
Peak normalization isn't loudness normalization
Peak normalization finds the loudest sample in a file and scales the whole thing so that sample lands at 0 dBFS or -1 dBFS. That's a measurement of one instant. A soft interview with one door slam and a dense music bed with no transients can have identical peaks and a 12 LU difference in perceived loudness.
EBU R128 measures loudness the way ears do: K-weighted, gated, averaged over the program. The unit is LUFS (identical in scale to LKFS, the term ATSC uses). Two gates run during the measurement, an absolute gate at -70 LUFS and a relative gate 10 LU below the ungated average, so silence and room tone don't drag the number down.
That gating is why a library normalized by peak stays uneven and a library normalized by integrated loudness doesn't. If you're already stripping dead air in your pipeline, note that trimming silence changes the integrated measurement, so measure after the trim, not before.
Measure what your library actually sounds like
Before you normalize anything, get the numbers. This decodes audio only and writes nothing:
ffmpeg -nostats -i input.mp4 -af ebur128=peak=true -f null -
The summary at the end gives Integrated loudness (I), Loudness range (LRA), and True peak. Run it across a sample of 20 files and you'll usually find a 10 to 15 LU spread. That spread is the complaint.
The two-pass loudnorm recipe
Two-pass loudnorm is the accurate way to normalize audio loudness in FFmpeg because the first pass measures the whole program and the second applies a known, fixed gain instead of guessing in real time.
Pass 1: measure
ffmpeg -hide_banner -i input.mp4 \
-af loudnorm=I=-14:TP=-1:LRA=11:print_format=json \
-f null - 2>&1 | tail -n 12
You get a JSON block on stderr:
{
"input_i" : "-21.53",
"input_tp" : "-4.31",
"input_lra" : "9.20",
"input_thresh" : "-32.24",
"target_offset" : "-0.14"
}
Pass 2: apply
Feed all five values back in and set linear=true so the gain is applied as a single offset rather than a moving compressor:
ffmpeg -i input.mp4 \
-af loudnorm=I=-14:TP=-1:LRA=11:measured_I=-21.53:measured_LRA=9.20:measured_TP=-4.31:measured_thresh=-32.24:offset=-0.14:linear=true \
-c:v copy -c:a aac -b:a 192k -ar 48000 \
output.mp4
-c:v copy matters. There is no reason to re-encode video to change audio, and skipping it turns a 30-second job into a 6-minute one on a 10-minute 1080p file.
Pick the target for where the video lands
One target for the whole library is only right if the whole library goes to one place. These are the numbers people actually deliver against:
| Destination | Integrated target | True peak ceiling |
|---|---|---|
| YouTube, Spotify (observed playback normalization) | -14 LUFS | -1 dBTP |
| Apple Podcasts (published spec) | -16 LUFS | -1 dBTP |
| EBU R128 broadcast | -23 LUFS | -1 dBTP |
| ATSC A/85 (US broadcast) | -24 LKFS | -2 dBTP |
| Netflix delivery | -27 LKFS, dialog-gated | -2 dBTP |
Two honest caveats. YouTube and Spotify don't publish a spec they commit to; -14 LUFS is what their playback normalization converges on and what mastering engineers target. And Netflix measures with dialog gating, which loudnorm does not do, so a -27 LKFS Netflix deliverable is not something plain loudnorm produces.
For a social library, -14 LUFS with TP=-1 is the safe default. Going louder buys nothing because the platform turns you back down, and you keep the limiting damage.
Batching it, and where the loop falls over
For a few dozen files, a shell loop with jq is fine:
#!/usr/bin/env bash
for f in library/*.mp4; do
json=$(ffmpeg -hide_banner -i "$f" \
-af loudnorm=I=-14:TP=-1:LRA=11:print_format=json \
-f null - 2>&1 | sed -n '/^{/,/^}/p')
I=$(jq -r .input_i <<< "$json")
LRA=$(jq -r .input_lra <<< "$json")
TP=$(jq -r .input_tp <<< "$json")
TH=$(jq -r .input_thresh <<< "$json")
OFF=$(jq -r .target_offset <<< "$json")
ffmpeg -hide_banner -y -i "$f" \
-af "loudnorm=I=-14:TP=-1:LRA=11:measured_I=$I:measured_LRA=$LRA:measured_TP=$TP:measured_thresh=$TH:offset=$OFF:linear=true" \
-c:v copy -c:a aac -b:a 192k -ar 48000 \
"out/$(basename "$f")"
done
Two passes per file, serial. At roughly 25 seconds per 10-minute file, 500 files is about three and a half hours of a laptop you can't close, and one corrupt input kills the run at file 212 with Invalid data found when processing input and no record of what already succeeded.
That's the point where the mechanism should change instead of the loop getting better. FFmpeg Micro runs the same filter chain as a job: submit the file, poll or take a webhook, download the output. No FFmpeg install, no jq parsing, no box that has to stay awake, and the fan-out is however many jobs you submit at once. Because it's just an HTTP call, the queue can be a Google Sheet driving n8n, a Make scenario, a Zapier zap, or an MCP tool call from an agent. The full API reference is in the docs, and you can try a single file in the playground before wiring anything.
Common pitfalls
The 192 kHz surprise. loudnorm resamples internally and will happily hand you a 192 kHz output stream if you don't say otherwise. Always set -ar 48000 (or 44100) on the output. Files that suddenly quadrupled in audio size are this bug.
linear=true silently isn't. If the required gain would push true peak past your ceiling, loudnorm falls back to dynamic mode. Check normalization_type in the pass-2 summary. If it says dynamic when you asked for linear, your target is too loud for that source and you should lower I or raise TP.
Mono files read 3 LU quiet. A mono file played on a stereo system is perceptually louder than its R128 measurement suggests. Add dual_mono=true for mono sources or they'll come out consistently too hot.
Normalizing clips before you concatenate. If you normalize each clip to -14 and then stitch, every quiet clip gets pushed up, room tone included, and the assembled video breathes. Normalize the finished timeline instead. Same rule applies when you swap in a new audio track or concatenate clips programmatically: loudness is a property of the final program, not the parts.
LRA set too tight for music. LRA=11 suits talking-head and podcast content. Force a music-heavy piece into LRA=7 and the limiter flattens it. Leave LRA generous, or leave it out and let the source keep its range.
When not to do this
Skip loudness normalization when you're mastering music for release, since a mastering engineer's limiter decisions beat a filter's. Skip it when your deliverable is already spec-compliant, because normalizing a compliant file just adds a lossy re-encode. And if you need dialog-gated measurement for a Netflix or streaming-service deliverable, loudnorm alone won't get you there; that requires a Dolby-based dialog intelligence measurement, and dedicated broadcast QC tools or AWS Elemental MediaConvert's audio normalization modes are the right call.
FAQ
What LUFS should video be for YouTube?
Target -14 LUFS integrated with a -1 dBTP true peak ceiling. YouTube applies playback normalization, so anything louder gets turned down anyway and you keep whatever limiting damage you did to get there.
Does one pass of loudnorm work?
It works, but it doesn't hit the number. Single-pass loudnorm estimates as it goes and commonly lands within about 1 LU of target, which is enough drift to make files in the same library sound different from each other. Use two passes whenever consistency across files is the goal.
How do I check a video's loudness without changing it?
Run ffmpeg -nostats -i input.mp4 -af ebur128=peak=true -f null -. It decodes and reports integrated loudness, loudness range, and true peak, then discards the output. Nothing is written.
What's the difference between LUFS and LKFS?
Nothing in scale. LUFS is the EBU R128 term and LKFS is the ATSC A/85 term for the same K-weighted loudness measurement. A file at -23 LUFS is at -23 LKFS.
Can I normalize audio across a video library without installing FFmpeg?
Yes. Send each file to a video API as a job and let the service run the two-pass measurement and gain application, then download the normalized output. That's the same filter chain without the binary, the queue, or a machine that has to stay online.
If your library is past the point where a shell loop is worth babysitting, the two-pass recipe above runs as one API call per file on the FFmpeg API, with a free tier to test a handful of files first. Sign up free and normalize the noisiest 10 videos in your library before you commit to anything.
About Javid Jamae
Founder & CEO at FFmpeg Micro
Javid is a software engineer, author, and entrepreneur with over 25 years of professional software development experience across enterprise, startup, and consulting environments. He founded FFmpeg Micro to make video processing accessible to developers through a simple, automation-first REST API.
You might also like

Add an Intro and Outro to Video Automatically (n8n + FFmpeg)
Add an intro and outro to video automatically with FFmpeg or one API call. Concat demuxer vs concat filter, an n8n batch recipe, and mismatch bugs to avoid.

An FFmpeg audiogram is one filter. The rest is what breaks.
Build an ffmpeg audiogram from a podcast MP3 and cover art: showwaves vs showwavespic, copy-paste commands, vertical Reels sizing, and a one-call batch API.

How to Install FFmpeg on Windows
Two ways to install FFmpeg on Windows that actually work in 2026: winget in one command, or the gyan.dev ZIP plus PATH. With the PATH mistakes fixed.
Ready to process videos at scale?
Start using FFmpeg Micro's simple API today. No infrastructure required.
Get Started Free