rms-scan
rms-scan is a small offline CLI that analyzes a media file with FFmpeg astats and reports:
- Overall/average RMS:
lavfi.astats.Overall.RMS_level(dBFS) - Highest RMS window:
lavfi.astats.Overall.RMS_peak(dBFS) - Pass/fail against a configurable RMS window (default:
[-23, -18]dBFS)
It also prints per-channel RMS details when available.
Why / use cases
- Broadcast/podcast loudness spot-checks (quick RMS window compliance checks)
- Batch validation of large media libraries
- CI gating (fail builds when audio is consistently too quiet/loud)
Requirements
- Python 3.10+
- FFmpeg + FFprobe in
PATH(or pass explicit paths via--ffmpeg/--ffprobe)
Install FFmpeg:
- macOS (Homebrew):
brew install ffmpeg - Debian/Ubuntu:
sudo apt install ffmpeg
Install
From project root:
pip install .
After install, the console script is available as:
rms-scan --help
You can also run directly:
python3 rms_scan.py --help
Usage
rms-scan <path-to-audio-or-video-file>
Options:
--minminimum RMS threshold in dBFS (default:-23)--maxmaximum RMS threshold in dBFS (default:-18)--jsonmachine-readable output--verboseprint raw astats lines being parsed--ffmpeg <path>override ffmpeg binary--ffprobe <path>override ffprobe binary
Example (human output)
$ rms-scan program.wav
File: program.wav
Duration: 3728.41 s
Audio: 48000 Hz, 2 ch, stereo
Overall RMS_level: -21.34 dBFS
Overall RMS_peak: -18.62 dBFS
Spec window: [-23.0, -18.0] dBFS
Result: PASS
Details:
Channel 1 RMS_level: -21.20 dBFS
Channel 1 RMS_peak: -18.55 dBFS
Channel 2 RMS_level: -21.48 dBFS
Channel 2 RMS_peak: -18.70 dBFS
Example (FAIL + suggested gain)
$ rms-scan quiet_mix.mp3
File: quiet_mix.mp3
Duration: 912.03 s
Audio: 44100 Hz, 2 ch, stereo
Overall RMS_level: -24.10 dBFS
Overall RMS_peak: -20.80 dBFS
Spec window: [-23.0, -18.0] dBFS
Result: FAIL
Suggested gain change: +3.6 dB
Details:
Channel 1 RMS_level: -24.05 dBFS
Channel 1 RMS_peak: -20.77 dBFS
Channel 2 RMS_level: -24.15 dBFS
Channel 2 RMS_peak: -20.84 dBFS
Suggested gain change targets midpoint -20.5 dBFS using:
target - measured_rms_level
Example (--json)
{
"channel_layout": "stereo",
"channels": 2,
"details": {
"per_channel": {
"1": {
"RMS_level_dbfs": -21.2,
"RMS_peak_dbfs": -18.55
},
"2": {
"RMS_level_dbfs": -21.48,
"RMS_peak_dbfs": -18.7
}
}
},
"duration_seconds": 3728.41,
"file": "program.wav",
"overall": {
"RMS_level": {
"last_dbfs": -21.34,
"max_observed_dbfs": -21.34,
"selected_dbfs": -21.34
},
"RMS_peak": {
"last_dbfs": -18.62,
"max_observed_dbfs": -18.62,
"selected_dbfs": -18.62
}
},
"pass": true,
"range": {
"max_dbfs": -18.0,
"min_dbfs": -23.0
},
"sample_rate_hz": 48000,
"suggested_gain_change_db": -0.84,
"target_midpoint_dbfs": -20.5
}
Exit Codes
0: PASS (Overall RMS_level in range)1: FAIL (outside range)2: ffmpeg/ffprobe missing3: astats parse failure4: invalid input file
FFmpeg Analysis Method
The tool uses FFprobe first for metadata and then analyzes audio without rendering output:
- disables non-audio streams:
-vn -sn -dn - null muxer output:
-f null - - astats filter with metadata keys:
- preferred:
astats=metadata=1:reset=0:measure_overall=RMS_level+RMS_peak:measure_perchannel=RMS_level+RMS_peak,ametadata=print - fallback for older builds:
astats=metadata=1:reset=0,ametadata=print
- preferred:
This processes as fast as decode/filter speed allows (typically faster-than-real-time on modern machines).
Troubleshooting
Error: Unable to find ffmpeg/ffprobe
- Install FFmpeg package (
brew install ffmpegorsudo apt install ffmpeg) - Or pass binary paths with
--ffmpegand--ffprobe
Parse failure (exit code 3)
If lavfi.astats.Overall.RMS_level / RMS_peak are not found:
- Re-run with
--verboseto inspect parsed lines - Check filter availability:
ffmpeg -filters | grep astatsffmpeg -filters | grep ametadata
- Try a newer FFmpeg build (some builds vary in filter/metadata behavior)
Input fails validation (exit code 4)
- Verify file exists and contains an audio stream
- Confirm ffprobe can read it:
ffprobe -v error -show_streams -show_format <file>
Development
python -m venv .venv
source .venv/bin/activate
python -m pip install -U pip
python -m pip install -e ".[dev]"
python -m unittest discover -s tests -p "test_*.py"
python -m build
python -m twine check dist/*
Release
- Update
pyproject.tomlversion andCHANGELOG.md. - Build and check:
python -m buildpython -m twine check dist/*
- Create and push a tag like
v0.1.0. The GitHub Actionsrelease.ymlworkflow publishes to PyPI via Trusted Publishing (OIDC).
Metadata
Release files for rms-scan 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| rms_scan-0.1.0.tar.gz | 10.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| rms_scan-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 19.9 kB
Release files / rms_scan-0.1.0.tar.gz
| Download URL | rms_scan-0.1.0.tar.gz |
|---|---|
| Size | 10.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b7c1b191379932ff541b522ac634971d77cbb0ecf6f0aa5d02f6499325f0a50d
|
|
BLAKE2b-256 checksum How to use checksums |
43b964fd26ba91ab89bd022f034678031276aace1fe7ec3f6b81951b73770bf5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.8
|
Release files / rms_scan-0.1.0-py3-none-any.whl
| Download URL | rms_scan-0.1.0-py3-none-any.whl |
|---|---|
| Size | 9.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8cbd69b971c7227b7c44ad1f27c4704a336cc859a011c48573e91010302686b4
|
|
BLAKE2b-256 checksum How to use checksums |
3ae8521cdf9da2c8964a989f61bbb7de54521513e650a1c0a19ff44a1a897d82
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.8
|