Equal-loudness normalization for MP3 and M4A files using EBU R128 / ITU-R BS.1770.
Normalizes every .mp3 and .m4a file in a directory (optionally recursively) to the same perceived loudness, so tracks play back-to-back without touching the volume knob. By default, output goes to a normalized/ subfolder and originals are left untouched; a recursive in-place mode is also available.
This is not a standalone application. You install a few things first (Python and ffmpeg). All of them are free, open source, and require no account, subscription, or payment. See What you need before installing.
Two ways to set this up:
- Do it yourself. Follow What you need and Installation below, step by step.
- Let Claude Code do it. If you are not technical, or just want it handled, open this project in Claude Code and tell it something like: "Install everything this project needs on my machine and set it up so I can run it." Claude Code will check for Python and ffmpeg, install whatever is missing, install the tool, and get you to a working command. You do not need to understand the steps yourself.
- Why this exists
- What you need
- Installation
- Usage
- Options
- Choosing a loudness target
- How it works
- Notes and limitations
- Project layout
- Testing
- License
Over the years I amassed a large MP3 collection from different sources, each encoded at a different time, by different software, at different loudness levels. For example:
- CDs I ripped through the 90s
- MP3s I bought on Amazon in the 2000s
- music I downloaded from YouTube
The result is a collection where every other track plays back at a noticeably different volume. The point of this tool is simple: be able to play any file at random from the collection without reaching for the volume knob every time. That is the whole goal.
The collection is also large, so the tool is built for unattended bulk work. Recursive replace mode (--replace) walks a folder tree and normalizes everything in place; you point it at a folder containing any number of files and it works through all of them. On a big library it is purely a matter of time until the entire collection is done.
Everything below is free and open source. No accounts, no subscriptions, no purchases.
- Python 3.10 or newer. python.org/downloads
- ffmpeg and ffprobe on your PATH, built with the
loudnormandebur128filters (standard builds include both).
Install ffmpeg:
# macOS (Homebrew)
brew install ffmpeg
# Debian / Ubuntu
sudo apt install ffmpeg
# Fedora
sudo dnf install ffmpeg
# Windows (winget)
winget install Gyan.FFmpegVerify it is reachable:
ffmpeg -version
ffprobe -versionIf those two commands print version info, you are ready. mp3norm checks for both at startup and exits with a clear message if either is missing or lacks the required filters.
pip install -e .This installs the mp3norm command into your environment.
# Normalize all MP3/M4A files in the current directory
# (writes to ./normalized/, originals untouched)
mp3norm
# Point at specific input and output directories
mp3norm --input-dir /path/to/music --out-dir /path/to/output
# See what would be processed without writing anything
mp3norm --dry-run
# Recursive, in-place normalization of an entire library
# (walks subfolders, overwrites originals)
mp3norm --replace --input-dir /path/to/musicRecursive replace mode is the workhorse for large collections: it descends through every subfolder, normalizes each .mp3/.m4a in place (via a temp file that atomically replaces the original on success), and writes an MP3NORM tag so reruns skip files that are already done.
| Option | Default | Description |
|---|---|---|
--input-dir DIR |
. |
Input directory |
--out-dir DIR |
<input-dir>/normalized/ |
Output directory (ignored in --replace mode) |
--target-i LUFS |
-14.0 |
Integrated loudness target |
--true-peak DBTP |
-1.0 |
True-peak ceiling |
--lra LU |
11.0 |
Loudness range target |
--overwrite |
off | Re-process files whose output already exists (default mode only) |
--single-pass |
off | Faster, less accurate (skip the measurement pass) |
--radio |
off | Compress and limit before loudnorm for radio-style loudness (destructive) |
--dry-run |
off | Report what would be processed; write nothing |
--report PATH |
none | Write a JSON report to PATH |
--verbose |
off | Print full ffmpeg command details |
--replace |
off | Recursive and in-place: normalize all files under --input-dir, overwriting originals |
--date DATE |
none | Only process files modified before this date. Applies in --replace mode only |
--reprocess |
off | Ignore the MP3NORM tag and reprocess already-normalized files |
--date accepts any of these formats: M/D/YYYY (e.g. 6/1/2026), YYYY-MM-DD, MM-DD-YYYY, or DD.MM.YYYY.
Two independent skip mechanisms exist; do not confuse them:
--overwritecontrols whether an output file that already exists is regenerated. It applies only in default (non-replace) mode.--reprocesscontrols whether a file carrying a matchingMP3NORMtag is processed again. Note that changing--target-ialready forces reprocessing automatically, because the skip only fires when the tagged target equals the requested target.
-14.0 LUFS is the default because it matches the loudness most streaming platforms normalize to, which makes a personal library feel consistent with the rest of what you listen to. Common alternatives:
-16.0LUFS: typical for podcasts and spoken-word.-23.0LUFS: the EBU R128 broadcast reference.
Lower (more negative) values mean quieter overall loudness and more headroom.
- Two-pass loudnorm. Pass 1 measures integrated loudness with ffmpeg's
loudnormfilter in analysis mode. Pass 2 feeds the measured values back withlinear=true, so the gain is applied as a single linear adjustment rather than dynamic compression. - Sample-rate preservation. Output sample rate is forced to match the source, because
loudnormresamples to 192 kHz internally. - Format preservation.
.mp3is encoded with libmp3lame;.m4ais encoded with AAC (libfdk_aac when available, otherwise the nativeaacencoder). Source bitrate is matched. - Metadata and cover art. All metadata is preserved (
-map_metadata 0); embedded cover art is detected and copied through. - MP3NORM tag. A custom tag (
MP3NORM=I=-14.0;TP=-1.0;LRA=11.0) records the target each file was normalized to, enabling skip-on-rerun.
For --radio mode, a compressor (threshold -20 dB, ratio 4:1, attack 5 ms, release 50 ms, makeup 2 dB) and limiter (ceiling 0.95, about -0.45 dBFS) are inserted before loudnorm. This deliberately reduces dynamic range and is destructive; use it only when you want radio-style consistent loudness rather than transparent normalization.
- Files are re-encoded, not gain-edited in place. Every processed file is decoded and re-encoded (libmp3lame or AAC), so the output is not bit-identical to the source and incurs one generation of lossy re-encoding. The
linear=truemode keeps the loudness adjustment itself transparent, but the codec round-trip is still lossy. For an already-lossy library assembled from mixed sources this is normally inaudible, but it is not lossless. - Two-pass is the default for a reason.
--single-passskips measurement and is faster but less accurate; it cannot apply the precise per-file gain that the measured pass computes. --replaceoverwrites originals. It uses a temp file and only swaps on a successful encode, but there is no undo. Keep a backup of anything irreplaceable, or use--dry-runfirst.
src/mp3norm/
cli.py argparse front end, date parsing, dispatch
core.py orchestration loop, environment checks, per-file pipeline
discovery.py file enumeration (flat and recursive), date/existing filters
ffprobe.py source metadata probe (codec, sample rate, bitrate, cover art, tag)
loudnorm.py pass-1 measurement and loudnorm JSON parsing
command.py ffmpeg filter and command-string builders, MP3NORM tag read/write
tests/ discovery, JSON parsing, command building, ffmpeg-fixture integration
The package uses only the Python standard library (argparse, subprocess, json, re, pathlib, os, sys, datetime, tempfile) and has zero third-party runtime dependencies.
pip install -e ".[dev]"
pytestUnit tests cover file discovery, loudnorm JSON parsing, and ffmpeg command building. Integration tests generate audio fixtures with ffmpeg and verify output loudness lands within 1.0 LU of the target.
MIT