Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

mp3-norm

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:

  1. Do it yourself. Follow What you need and Installation below, step by step.
  2. 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.

Contents

Why this exists

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.

What you need

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 loudnorm and ebur128 filters (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.FFmpeg

Verify it is reachable:

ffmpeg -version
ffprobe -version

If 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.

Installation

pip install -e .

This installs the mp3norm command into your environment.

Usage

# 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/music

Recursive 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.

Options

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:

  • --overwrite controls whether an output file that already exists is regenerated. It applies only in default (non-replace) mode.
  • --reprocess controls whether a file carrying a matching MP3NORM tag is processed again. Note that changing --target-i already forces reprocessing automatically, because the skip only fires when the tagged target equals the requested target.

Choosing a loudness 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.0 LUFS: typical for podcasts and spoken-word.
  • -23.0 LUFS: the EBU R128 broadcast reference.

Lower (more negative) values mean quieter overall loudness and more headroom.

How it works

  1. Two-pass loudnorm. Pass 1 measures integrated loudness with ffmpeg's loudnorm filter in analysis mode. Pass 2 feeds the measured values back with linear=true, so the gain is applied as a single linear adjustment rather than dynamic compression.
  2. Sample-rate preservation. Output sample rate is forced to match the source, because loudnorm resamples to 192 kHz internally.
  3. Format preservation. .mp3 is encoded with libmp3lame; .m4a is encoded with AAC (libfdk_aac when available, otherwise the native aac encoder). Source bitrate is matched.
  4. Metadata and cover art. All metadata is preserved (-map_metadata 0); embedded cover art is detected and copied through.
  5. 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.

Notes and limitations

  • 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=true mode 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-pass skips measurement and is faster but less accurate; it cannot apply the precise per-file gain that the measured pass computes.
  • --replace overwrites 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-run first.

Project layout

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.

Testing

pip install -e ".[dev]"
pytest

Unit 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.

License

MIT

About

Equal-loudness normalization for MP3/M4A files using EBU R128 / ITU-R BS.1770. Normalizes every .mp3/.m4a file in a given folder to the same perceived loudness, so tracks play back-to-back without touching the volume knob.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages