Self-hosted service
slhck/ffmpeg-normalize avatar
slhck/ffmpeg-normalize

ffmpeg-normalize for controlled audio loudness and batch processing

Audio Normalization for Python/ffmpeg

1,538 stars131 forksHTMLNOASSERTION

At a glance

What is it?
ffmpeg-normalize wraps FFmpeg with EBU R128, RMS, peak, album, and selective stream normalization. It suits Python users who need repeatable audio processing from a CLI, Python API, or Docker image.
Who is it for?
Adopt ffmpeg-normalize when you need reproducible FFmpeg loudness processing through Python, a command line, or Docker. Do not choose it if you require a fully independent audio engine or undocumented automatic mastering decisions.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository last received commits 9 days ago.
What is it written in?
Mainly HTML, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 16, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Where ffmpeg-normalize fits in an audio workflow

ffmpeg-normalize addresses a specific gap between invoking FFmpeg manually and controlling loudness across many media files. A single FFmpeg command can apply an audio filter, but determining the required gain, encoding the result, handling containers, and processing a collection consistently requires additional decisions. This project packages those decisions into a command named `ffmpeg-normalize`, a Python package, and a Docker entry point.

Its primary audience is developers and media operators who already work with FFmpeg but do not want to assemble measurement and encoding pipelines for every batch. EBU R128 normalization is the default path and targets perceived loudness rather than only the tallest sample. For material that follows an RMS reference, the package can instead raise or lower the mean level. Peak normalization addresses a different constraint by setting the highest sample level.

The project also covers cases where loudness is only one part of the requested operation. It can preserve video streams, select particular audio streams, copy unselected audio without normalizing it, skip files already near the target, normalize an album as one unit, and write processing details into a Matroska or WebM metadata tag. That makes it more than a loudness filter wrapper, although FFmpeg remains the underlying media processor.

The repository classifies version 1.43.0 as `Development Status :: 5 - Production/Stable`. Its metadata declares Python 3.10 through Python 3.15 support, with `requires-python = ">=3.10"`. The last push was on 2026-10-14, and releases v1.42.1 and v1.43.0 were published that week. This indicates recent project activity, but it does not by itself guarantee compatibility with every FFmpeg build or media format.

Normalization passes, filters, and stream handling

The README states that EBU R128 normalization uses two passes by default. This matches the need to measure a source before deciding how much adjustment its encoder input requires. The package can also use one-pass dynamic normalization when a caller chooses that mode. In contrast, RMS normalization changes the source toward a configured mean RMS level, while peak normalization aims at a configured sample peak.

The Python package uses an `src` layout, and `pyproject.toml` maps the `ffmpeg-normalize` console script to `ffmpeg_normalize.__main__:main`. Its runtime dependencies include `tqdm`, `ffmpeg-progress-yield`, `colorlog`, `mutagen`, and, on Windows, `colorama`. Based on that dependency set and the project description, progress reporting, logging, and media metadata handling are explicit parts of the package. The repository also includes `tests/`, `pytest.ini`, and linting and type-checking tools in its development dependency group.

Stream selection is more granular than the common single-audio-file case. `-as/--audio-streams` accepts stream numbers such as `1,2`; `--audio-default-only` limits processing to default audio streams; and `--keep-other-audio` copies non-selected streams instead of normalizing them. This distinction matters for multilingual or alternate-audio Matroska files because normalizing every track could erase deliberate differences between languages or commentary.

Album processing changes the analysis boundary. With `--batch`, files are processed jointly so their relative loudness is preserved while the group receives a common adjustment. That is a different goal from normalizing each track independently to the same endpoint, where a quiet mastered track and a loud mastered track could both be moved to the target.

Since v1.38.0, output is written directly to the destination rather than through temporary files. Since v1.39.0, input bit depth is preserved by default for formats such as FLAC, with `--no-keep-bit-depth` available to opt out. These are processing choices with real storage and quality implications, so callers should verify them against their catalog policy rather than assume every file is transformed identically.

Installing ffmpeg-normalize and normalizing the first file

The README requires a recent FFmpeg installation and Python 3.10 or higher. FFmpeg is external to the Python package, so installing the package alone does not supply the media engine on a normal local installation. The shortest documented installation path uses `pip3`:

bash
pip3 install ffmpeg-normalize
ffmpeg-normalize /path/to/your/file.mp4

The second command performs the first normalization. According to the Quick Start, the resulting file is written as `normalized/file.mkv`. The `.mkv` extension reflects the documented default destination and should be checked if a workflow requires another container.

The README also documents an `uv` route. After installing `uv`, the command below runs the package without requiring a persistent manual installation in the current environment:

bash
uvx ffmpeg-normalize /path/to/your/file.mp4

The reader should first confirm that `ffmpeg` and `ffprobe` are available because the Dockerfile installs both before installing the package. FFprobe is not listed as a Python dependency. A local installation therefore needs to account for the FFmpeg distribution separately.

For a repeatable podcast target, the README gives a preset command. The `podcast` preset is documented as EBU R128 at -16 LUFS:

bash
ffmpeg-normalize input.mp3 --preset podcast

Presets reduce repeated command construction, but the documentation excerpt does not enumerate every field in each built-in preset. Users who need to audit the exact audio codec, bitrate, channels, filter settings, or metadata behavior should inspect the effective invocation and output rather than rely only on the preset name.

Batch gain, peak control, thresholds, and summaries

Batch normalization has two distinct interpretations in this project. Ordinary multi-file processing can normalize each input independently, while `--batch` treats the supplied files as a collection and applies a common adjustment to preserve relative loudness. The README's album example uses RMS normalization and sets the batch target to -20 dB:

bash
ffmpeg-normalize album/*.flac --batch -nt rms -t -20

Here, `-nt rms` selects RMS normalization and `-t -20` sets the target. The shell expands `album/*.flac` before the program receives the arguments. This example is intended to move the album as a group rather than force every FLAC file to an identical RMS value.

Independent files can be skipped when they already fall within a configured distance from the target. The feature is disabled by default and is enabled with `--threshold`. The README's example also requests per-file statistics:

bash
ffmpeg-normalize input.flac -nt peak -t 0 -c:a flac --print-stats -o output.flac

A skipped file is copied through without re-encoding. Status can be `normalized`, `skipped`, or `error`, and the documented exit code is non-zero if any file fails. This is useful in automation because successful invocation alone does not mean every input succeeded.

For review workflows, `--print-summary` reports input level, output level, and applied gain across files. The optional `--write-encoder-settings` tag records an FFmpeg-command-equivalent string containing audio codec, bitrate, sample rate, channels, and the normalization filter with measured loudnorm values. The README identifies this as a Matroska or WebM convention and states that it has no effect with `--metadata-disable`.

One limitation is visible in the metadata behavior itself. The encoder settings tag is an audit aid, not a complete mastering history. It records effective encoder and normalization settings, but it does not claim to reconstruct every earlier processing event.

Docker deployment and the boundary around FFmpeg

The repository includes a multi-stage `Dockerfile` whose final stage exposes `ffmpeg-normalize` as the container entry point. The builder starts from `python:3.13-slim`, installs `wget` and `xz-utils`, detects the machine architecture, and downloads an FFmpeg build. The final image copies `ffmpeg` and `ffprobe`, installs `ffmpeg-normalize` from Python packages, and makes the binaries executable.

Only `x86_64` and `aarch64` are recognized by the Dockerfile's architecture branch. Any other architecture causes the image build to stop with an unsupported-architecture error. This is a concrete deployment boundary. A developer on another architecture can still use a suitable host installation of FFmpeg and the Python package, but this Dockerfile does not define a build path for that machine type.

The Docker route packages both layers rather than requiring the operator to manage FFmpeg separately. That is useful for repeatable batch jobs, yet it also means the container carries a downloaded FFmpeg build and the published Python dependency set. The Dockerfile does not pin that FFmpeg archive to a dated release because it references a URL ending in `ffmpeg-master-latest`. Container users should therefore account for the image's FFmpeg update behavior separately from the Python package release.

The `ENCODER_SETTINGS` feature provides a second form of reproducibility, but it does not freeze the media engine. If exact byte-for-byte reproduction matters, the FFmpeg build, codecs, filters, package version, and input files all form part of the processing environment. The documented tag records encoder settings and measured normalization values, which is useful evidence, but it is not a complete execution container specification.

Operational costs, licence metadata, and upgrade checks

The Python distribution declares `license = "MIT"` and points to `LICENSE.md` through `license-files = ["LICENSE.md"]`. The repository listing classifies the top-level licence as another custom category, so the precise classification should be checked against the repository's `LICENSE.md` before redistribution or compliance work. This article does not interpret the legal terms.

An upgrade can affect more than the Python wrapper. The package version, FFmpeg version, selected codec, output container, bit depth behavior, defaults, and Docker base image all participate in processing. The changelog excerpt shows that recent releases changed codec selection, encoder metadata, skip behavior, bit-depth preservation, destination writing, and preset support. Those are functional changes rather than cosmetic version increments.

Direct output since v1.38.0 removes temporary files from the documented workflow, but it does not mean processing is transactional. The README does not claim that an interrupted encode restores a prior destination. Scripts should therefore choose output paths deliberately and inspect the documented error status and exit code.

The Python API is listed as a supported interface, but the README does not provide its import names, function signatures, or a code example. That omission matters for adoption decisions. A team intending to embed normalization directly should consult the API documentation rather than infer a stable Python interface from the console entry point.

Editorial conclusion

Adopt ffmpeg-normalize when you need reproducible FFmpeg loudness processing through Python, a command line, or Docker. Do not choose it if you require a fully independent audio engine or undocumented automatic mastering decisions. Verify first that your local FFmpeg installation is suitable, then confirm the target level, normalization mode, output codec, container, and treatment of multiple audio streams for your files.

Frequently asked questions

Does ffmpeg-normalize require FFmpeg to be installed?

Yes. The README's local Quick Start says to install a recent version of FFmpeg and Python 3.10 or higher before installing the Python package. The Dockerfile bundles `ffmpeg` and `ffprobe` for container use.

Which normalization methods does ffmpeg-normalize support?

It supports EBU R128 loudness, RMS-based normalization, and peak normalization. EBU R128 uses two passes by default and can also use one-pass dynamic normalization.

How does ffmpeg-normalize process an album without flattening its relative loudness?

Use `--batch` so the supplied files are processed jointly and shifted toward a common target. The documented album example selects RMS normalization and uses `-nt rms -t -20`.

Official sources

  1. Issues
  2. Project website
  3. README
  4. Releases
  5. slhck/ffmpeg-normalize on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/slhck-ffmpeg-normalize.svg)](https://hysenlabs.com/projects/slhck-ffmpeg-normalize)

Was this useful?