Skip to content

Instantly share code, notes, and snippets.

@gustavomdsantos
Last active January 9, 2026 23:38
Show Gist options
  • Select an option

  • Save gustavomdsantos/491f2efd88212772ae2c411ea70a5b98 to your computer and use it in GitHub Desktop.

Select an option

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.
#!/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