Last active
January 9, 2026 23:38
-
-
Save gustavomdsantos/491f2efd88212772ae2c411ea70a5b98 to your computer and use it in GitHub Desktop.
A simple file LUFS meter using FFmpeg, and inspired by rsgain and iZotope RX Waveform Statistics.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # ================================================================== | |
| # LUFS Meter | |
| # Analyzes audio files and extracts loudness metrics based on the | |
| # ITU-R BS.1770-4 (EBU R128) standard. | |
| # | |
| # AUTHOR: | |
| # gustavomdsantos@pm.me | |
| # ================================================================== | |
| show_help() { | |
| cat << EOF | |
| Usage: $(basename "$0") [OPTIONS] FILE... | |
| Analyze audio files and report loudness statistics. | |
| Options: | |
| -h, --help Show this help message and exit | |
| Metrics: | |
| Integrated Overall loudness of the file (LUFS). | |
| Short-Term Max Maximum loudness of the file (LUFS). | |
| SIR Short-to-Integrated Ratio, measures the | |
| dynamic lift above the average loudness (LU). | |
| Perceived Loudness A linear scale based on human perception (%). | |
| True-Peak The maximum absolute level of the signal (dBTP). | |
| Peak The maximum sample value (dBFS). | |
| Requires: ffmpeg, ffprobe, bc | |
| EOF | |
| } | |
| # Process options | |
| case "$1" in | |
| -h|--help) | |
| show_help | |
| exit 0 | |
| ;; | |
| "") | |
| show_help | |
| exit 1 | |
| ;; | |
| esac | |
| for cmd in ffmpeg ffprobe bc; do | |
| if ! command -v "$cmd" &> /dev/null; then | |
| echo "Error: Tool '$cmd' not found. Please install it to continue." | |
| exit 1 | |
| fi | |
| done | |
| if [ "$#" -eq 0 ]; then | |
| echo "Usage: $0 file1 [file2 ...]" | |
| exit 1 | |
| fi | |
| for file in "$@"; do | |
| echo -ne "Analyzing: \"$(basename "$file")\"...\r" | |
| output="" | |
| diff="" | |
| sample_peak="" | |
| perceived_loudness="" | |
| channels=$(ffprobe -v error -select_streams a:0 -show_entries stream=channels -of default=noprint_wrappers=1:nokey=1 "$file") | |
| if [ "$channels" -eq 1 ]; then | |
| audio_filter="pan=stereo|c0=c0|c1=c0,ebur128=peak=true,astats=measure_overall=1:reset=0" | |
| else | |
| audio_filter="ebur128=peak=true,astats=measure_overall=1:reset=0" | |
| fi | |
| log=$(ffmpeg -hide_banner -nostats -i "$file" -filter_complex "[0:a]$audio_filter" -f null - 2>&1) | |
| integrated=$(echo "$log" | grep "I:" | tail -n1 | sed -n 's/.*I:\s*\([-0-9.]*\).*/\1/p' | tr -d ' ') | |
| short_term=$(echo "$log" | grep "S:" | sed -n 's/.*S:\s*\([-0-9.]*\).*/\1/p' | sort -n | tail -n1 | tr -d ' ') | |
| true_peak=$(echo "$log" | grep -A2 "True peak:" | grep "Peak:" | sed -n 's/.*Peak:\s*\([-0-9.]*\).*/\1/p' | tr -d ' ') | |
| peak=$(echo "$log" | grep -i "Peak level dB" | head -n1 | sed -E 's/.*: (.*)/\1/' | tr -d ' ') | |
| # SIR (Short-to-Integrated Ratio), inspired by the PAPR (Peak-to-Average Power Ratio). | |
| # Formula: SIR = Short-Term Max - Integrated | |
| if [[ -n "$integrated" && -n "$short_term" ]]; then | |
| sir_raw=$(echo "scale=2; $short_term - $integrated" | LC_ALL=C bc) | |
| sir=$(LC_ALL=C printf "%.1f" "$sir_raw") | |
| fi | |
| # Perceived Loudness (PL), a simple linear interpolation of LUFS-I scale. | |
| # Formula: PL = (IL + 17) * (100 / 11) | |
| # Eg.: -17 LUFS = 0% | -11.5 LUFS = 50% | -6 LUFS = 100% | |
| if [[ -n "$integrated" ]]; then | |
| pl_raw=$(echo "scale=4; ($integrated + 17) * 100 / 11" | LC_ALL=C bc) | |
| pl=$(LC_ALL=C printf "%.1f" "$pl_raw") | |
| fi | |
| if [[ -n "$peak" && "$peak" != "inf" ]]; then | |
| sample_peak=$(LC_ALL=C printf "%.1f" "$peak") | |
| fi | |
| echo -ne "\033[K" | |
| output="Track: \"$(basename "$file")\"\n\n" | |
| output+=" Integrated: ${integrated:-N/A} LUFS\n" | |
| output+=" Short-Term Max: ${short_term:-N/A} LUFS\n" | |
| output+=" SIR: ${sir:-N/A} LU\n" | |
| output+=" Perceived Loudness: ${pl:-N/A} %\n" | |
| output+=" True-Peak: ${true_peak:-N/A} dBTP\n" | |
| output+=" Peak: ${sample_peak:-N/A} dBFS" | |
| echo -e "\n$output\n" | |
| done |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment