There is the ffmpeg-normalize python package.
Also Chatgpt came up with this bash script:
#!/usr/bin/env bash
#
# loudnorm.sh — two-pass EBU R128 loudness normalization with ffmpeg
#
# Usage: ./loudnorm.sh input.wav output.wav
#
# Defaults: I=-16 LUFS, TP=-1.5 dBTP, LRA=11 LU
set -e
INPUT="$1"
OUTPUT="$2"
if [[ -z "$INPUT" || -z "$OUTPUT" ]]; then
echo "Usage: $0 input.wav output.wav"
exit 1
fi
echo "Starting $INPUT -> $OUTPUT"
# Target values (change if you want broadcast settings like I=-23, TP=-2)
I=-16
TP=-1.5
LRA=11
# First pass: analyze
JSON=$(ffmpeg -i "$INPUT" -af loudnorm=I=$I:TP=$TP:LRA=$LRA:print_format=json -f null - 2>&1 | grep -A 12 "^\{" )
echo "Got JSON"
# Extract measured values from JSON using jq (if installed)
if command -v jq >/dev/null 2>&1; then
MI=$(echo "$JSON" | jq -r .input_i)
MTP=$(echo "$JSON" | jq -r .input_tp)
MLRA=$(echo "$JSON" | jq -r .input_lra)
MTHRESH=$(echo "$JSON" | jq -r .input_thresh)
OFFSET=$(echo "$JSON" | jq -r .target_offset)
else
# crude grep/awk fallback if jq isn’t available
MI=$(echo "$JSON" | grep input_i | awk '{print $3}' | tr -d '",')
MTP=$(echo "$JSON" | grep input_tp | awk '{print $3}' | tr -d '",')
MLRA=$(echo "$JSON" | grep input_lra | awk '{print $3}' | tr -d '",')
MTHRESH=$(echo "$JSON" | grep input_thresh | awk '{print $3}' | tr -d '",')
OFFSET=$(echo "$JSON" | grep target_offset | awk '{print $3}' | tr -d '",')
fi
echo "First pass results:"
echo " input_i = $MI"
echo " input_tp = $MTP"
echo " input_lra = $MLRA"
echo " input_thresh = $MTHRESH"
echo " offset = $OFFSET"
# Second pass: apply normalization
echo "Second pass $INPUT"
ffmpeg -y -i "$INPUT" -af loudnorm=I=$I:TP=$TP:LRA=$LRA:measured_I=$MI:measured_TP=$MTP:measured_LRA=$MLRA:measured_thresh=$MTHRESH:offset=$OFFSET:linear=true:print_format=summary "$OUTPUT"