Your viewers keep reaching for the volume knob. Here’s the audio bug nobody owns.
Every video team I have worked on obsesses over the picture. Bitrate ladders, VMAF scores, per-title encoding, the great AV1 debate. And…
Your viewers keep reaching for the volume knob. Here’s the audio bug nobody owns.
Every video team I have worked on obsesses over the picture. Bitrate ladders, VMAF scores, per-title encoding, the great AV1 debate. And almost every one of them ships audio that makes viewers reach for the volume knob on every third clip. One video is whisper quiet, the next one blasts, and the person watching learns to keep a thumb on the slider. That is not a codec problem. It is a loudness problem, and it is one of the cheapest quality wins available to anyone running an encoding pipeline.
The reason it goes unfixed is that nobody owns it. The encoder team thinks of audio as a passthrough. Product thinks the player handles it. It doesn’t. So let’s talk about what loudness actually is, why peak normalization is the wrong tool, and how to fix it in the place you already control: FFmpeg.
Peak normalization is not loudness
The intuitive fix, the one most people try first, is to normalize every file so its loudest sample hits the ceiling. Bring the peak up to 0 dBFS and call it even. This does almost nothing for the problem you actually have.
Peak measures the single loudest instant in the file. Perceived loudness is about the whole thing: how energetic the audio feels across its duration, weighted toward the frequencies human ears are most sensitive to. A sparse acoustic track and a dense, compressed pop mix can have identical peaks and sound wildly different in volume. So you can peak-normalize an entire library to the same ceiling and your viewers will still be riding the volume knob, because you measured the wrong thing.
What you want is a loudness measurement that models perception. That standard exists, it has existed for years, and the broadcast world has been legally required to use it for over a decade.
What EBU R128 actually measures
EBU R128, and its close American cousin ATSC A/85, defines loudness in units called LUFS, Loudness Units relative to Full Scale. The measurement applies a frequency weighting that approximates human hearing, then integrates energy over the program with a gating step that ignores silence so a quiet intro doesn’t drag the number down. The result is a single integrated loudness figure that lines up with how loud a clip actually feels.
Broadcasters target -23 LUFS integrated, with a true peak ceiling of -1 dBTP and a loudness range around 7 LU. That target exists so that flipping channels never assaults you. The streaming world settled on something louder. Most large platforms normalize playback to somewhere around -14 LUFS, give or take; the exact figure differs per platform and they adjust it over time, so treat -14 as a ballpark rather than a law. The point is not the specific number. The point is that there is a target, perceptual rather than peak-based, and once you pick one and apply it consistently, the volume-knob problem disappears.
Peak normalization measures the loudest instant. Loudness normalization measures what your ears actually feel. Your viewers are complaining about the second thing.
The filter you already have
FFmpeg ships a filter called loudnorm that implements R128 directly. You do not need a new tool in the stack, a paid service, or an audio engineer on retainer. You need to use the filter correctly, and the way most people first reach for it is the way that sounds worst.
The tempting one-liner runs loudnorm in a single pass: point it at a target and let it go. In single-pass mode the filter has to make loudness decisions on the fly, without knowing what is coming, so it applies dynamic processing. On music and dialogue that pumps audibly. The track breathes in a way that sounds cheap, and ironically you have traded one audio complaint for another.
The correct approach for any file you are processing ahead of time, which is to say all video-on-demand, is two passes. The first pass measures: you run loudnorm purely to analyze the input and emit the measured integrated loudness, true peak, loudness range, and threshold, conveniently as JSON. The second pass corrects: you feed those measured values back into the filter and ask it to apply a single, linear gain toward your target. Linear mode means one consistent adjustment across the whole file rather than moment-to-moment compression. The audio keeps its dynamics; it just sits at the right level. It is the difference between turning the whole track up by the right amount and squashing it.
A short code sketch, just to make the shape concrete:
# pass 1: measure, print JSON, throw the audio away
ffmpeg -i input.mp4 -af loudnorm=I=-16:TP=-1.5:LRA=11:print_format=json -f null -
# pass 2: apply the measured values back, linear gain toward target
ffmpeg -i input.mp4 -af loudnorm=I=-16:TP=-1.5:LRA=11:measured_I=...:measured_TP=...:measured_LRA=...:measured_thresh=...:linear=true -ar 48000 output.mp4
The values in pass two come straight out of the JSON from pass one. If you are batching a whole library, the ffmpeg-normalize Python wrapper does exactly this two-pass dance with sensible defaults and saves you the JSON plumbing.
The gotchas worth knowing before you ship
A few sharp edges have caught me and everyone I have handed this to.
The loudnorm filter resamples internally to 192 kHz, and if you do not specify an output sample rate it will happily hand you a 192 kHz file you did not want. Always set the output rate explicitly, usually -ar 48000, which is why it is in the sketch above.
Integrated loudness is gated, which means very short or very quiet inputs report numbers down near the floor, around -70 LUFS. If you are normalizing user uploads and someone sends you ten seconds of near silence, handle that case rather than trying to yank it up to target and amplifying nothing but hiss.
And match the target to where the audio lives. Slamming a dialogue-heavy podcast to -14 because that is what a music platform uses can crush the quiet passages a conversation depends on. Broadcast-style content wants the -23 or -16 neighborhood; loud, dense entertainment can sit higher. Pick the target for the context, write it down, and apply it to everything.
There is one more place this pays off that people miss. If you build an adaptive bitrate ladder, your viewer’s player switches between renditions as their bandwidth changes, sometimes several times in a single sitting. If each rendition carries its own separately processed audio, those switches can nudge the perceived volume up and down, a subtle wrongness that is hard to name but easy to feel. The fix is to normalize the audio exactly once and mux that single track into every rung of the ladder. The audio is then bit-for-bit identical across renditions, so a mid-stream quality switch never touches the volume. It is a small discipline that removes a whole category of “something feels off” complaints you would otherwise never trace back to their source.
That last part is the whole game. The technology here is not new and it is not hard; the filter has been stable in FFmpeg since the version most of us were running years ago, and any recent build, the 8.0 line or anything reasonably current, handles it fine. What is rare is teams treating audio loudness as a pipeline setting they decide once and enforce, the same way they enforce a resolution ladder. Do that, and you will have fixed a complaint your viewers have probably never articulated but absolutely feel, every time they reach for the volume knob and you are the reason their thumb is already there.
메타데이터
- post_id
- 4fa0bd3d10dd
- slug
- your-viewers-keep-reaching-for-the-volume-knob-heres-the-audio-bug-nobody-owns-4fa0bd3d10dd
- url
- https://medium.com/@nikodev1/your-viewers-keep-reaching-for-the-volume-knob-heres-the-audio-bug-nobody-owns-4fa0bd3d10dd
- canonical_url
- https://medium.com/@nikodev1/your-viewers-keep-reaching-for-the-volume-knob-heres-the-audio-bug-nobody-owns-4fa0bd3d10dd
- author_url
- https://medium.com/@nikodev1
- status
- ok
- fetched_at
- 2026-06-09 15:37:30