Нормализация громкости в FFmpeg (двухпроходный loudnorm)
Скрипт на основе FFmpeg
устанавливается в MacOS
как набор команд в папке ~/bin: lufs14, lufs16 и lufs19. Он позволяет нормализовать финальные аудиофайлы подкаста или видео без сторонних программ: достаточно вызвать нужную команду с путём к файлу. Внутри выполняются два прохода FFmpeg: первый анализирует громкость и динамику, второй применяет точные параметры для достижения целевых уровней −14 LUFS (громче, под ролики и соцсети), −16 LUFS (стандарт стерео-подкастов) или −19 LUFS (моно). В результате получается готовый по уровню и балансу MP3-файл, оптимизированный под разные сценарии публикации.
Требования
- MacOS с Homebrew
- python3 в системе (обычно уже есть на macOS)
Установка FFmpeg
brew install ffmpeg
ffmpeg -versionНастройка: команды в ~/bin
Создай папку для личных команд (если её ещё нет):
mkdir -p ~/binСоздай скрипт ~/bin/lufs и вставь в него код:
cat > ~/bin/lufs <<'ZSH'
#!/bin/zsh
set -euo pipefail
# Defaults
BITRATE="192k"
SR="44100"
LRA="11"
TP="-1.5"
# Command can be invoked as lufs14/lufs16/lufs19 via symlink
cmd="$(basename "$0")"
target=""
force_mono=0
case "$cmd" in
lufs14) target="-14"; force_mono=0 ;;
lufs16) target="-16"; force_mono=0 ;;
lufs19) target="-19"; force_mono=1 ;;
lufs)
if [[ "$#" -lt 2 ]]; then
echo "Usage: lufs <-14|-16|-19> <input-file> [--mono]"
exit 1
fi
target="$1"; shift
;;
*)
echo "Unknown command name: $cmd"
exit 1
;;
esac
in="${1:-}"
opt="${2:-}"
if [[ -z "${in}" ]]; then
echo "No input file."
echo "Example: lufs16 \"episode.wav\""
exit 1
fi
# Support file:// paths
in="${in#file://}"
if [[ ! -f "$in" ]]; then
echo "Not a file: $in"
exit 1
fi
if [[ "${opt:-}" == "--mono" ]]; then
force_mono=1
fi
command -v ffmpeg >/dev/null 2>&1 || { echo "Need ffmpeg"; exit 1; }
command -v python3 >/dev/null 2>&1 || { echo "Need python3"; exit 1; }
dir="$(dirname "$in")"
base="$(basename "$in")"; base="${base%.*}"
suffix="${target#-}LUFS_${BITRATE}"
out="${dir}/${base}_${suffix}.mp3"
if [[ "$force_mono" -eq 1 ]]; then
out="${dir}/${base}_${suffix}_mono.mp3"
fi
echo "Pass 1: measure loudness..."
if [[ "$force_mono" -eq 1 ]]; then
pass="$(ffmpeg -hide_banner -nostats -y -vn -i "$in" \
-af "aformat=channel_layouts=mono,loudnorm=I=${target}:LRA=${LRA}:TP=${TP}:print_format=json" \
-f null - 2>&1)"
else
pass="$(ffmpeg -hide_banner -nostats -y -vn -i "$in" \
-af "loudnorm=I=${target}:LRA=${LRA}:TP=${TP}:print_format=json" \
-f null - 2>&1)"
fi
json_block="$(echo "$pass" | sed -n '/^{/,$p' | sed -n '1,/^}/p')"
read -r I TPm LRAi THRESH OFFSET < <(python3 - <<'PY'
import sys, json, re
s = sys.stdin.read()
m = re.search(r'^\{.*\}\s*$', s, re.S | re.M)
if not m:
raise SystemExit(2)
d = json.loads(m.group(0))
keys = ["input_i","input_tp","input_lra","input_thresh","target_offset"]
print(" ".join(str(d.get(k,"")) for k in keys))
PY
<<< "$json_block")
if [[ -z "${I:-}" || -z "${TPm:-}" || -z "${LRAi:-}" || -z "${THRESH:-}" || -z "${OFFSET:-}" ]]; then
echo "Failed to parse loudnorm metrics."
echo "$pass"
exit 1
fi
echo "Pass 2: normalize to ${target} LUFS..."
if [[ "$force_mono" -eq 1 ]]; then
ffmpeg -hide_banner -y -vn -i "$in" \
-af "aformat=channel_layouts=mono,loudnorm=I=${target}:LRA=${LRA}:TP=${TP}:measured_I=${I}:measured_TP=${TPm}:measured_LRA=${LRAi}:measured_thresh=${THRESH}:offset=${OFFSET}" \
-ar "${SR}" -ac 1 -c:a libmp3lame -b:a "${BITRATE}" "$out"
else
ffmpeg -hide_banner -y -vn -i "$in" \
-af "loudnorm=I=${target}:LRA=${LRA}:TP=${TP}:measured_I=${I}:measured_TP=${TPm}:measured_LRA=${LRAi}:measured_thresh=${THRESH}:offset=${OFFSET}" \
-ar "${SR}" -c:a libmp3lame -b:a "${BITRATE}" "$out"
fi
echo "Done: $out"
ZSHСделай файл исполняемым:
chmod +x ~/bin/lufsСоздай команды lufs14, lufs16 и lufs19 через симлинки:
ln -sf ~/bin/lufs ~/bin/lufs14
ln -sf ~/bin/lufs ~/bin/lufs16
ln -sf ~/bin/lufs ~/bin/lufs19PATH: чтобы команды работали без полного пути
Если ~/bin ещё не добавлен в PATH, открой ~/.zshrc:
nano ~/.zshrcДобавь в конец:
export PATH="$HOME/bin:$PATH"Применить изменения:
source ~/.zshrcПроверь, что команды доступны:
type lufs14
type lufs16
type lufs19Использование
Громкий стерео-микс под ролики / соцсети
lufs14 /путь/к/файлу.wavРезультат: <имя>_14LUFS_192k.mp3
Финальный стерео-микс выпуска подкаста
lufs16 /путь/к/файлу.wavРезультат: <имя>_16LUFS_192k.mp3
Моно-запись голоса
lufs19 /путь/к/файлу.wavРезультат: <имя>_19LUFS_192k_mono.mp3
Примечания
- -14 LUFS: чуть громче, удобно для шортсов, соцсетей и более плотного звука.
- -16 LUFS: стандартный уровень для стерео-подкастов и плееров.
- -19 LUFS: эквивалентная целевая громкость для моно.
- Двухпроходный режим (сначала измерение, потом применение) даёт более точную нормализацию.
- Битрейт фиксирован: CBR 192 kbps MP3 (libmp3lame). При необходимости можно поменять параметр
BITRATE="192k"в скрипте.
Диагностика
- «Failed to parse loudnorm metrics.»: проверь, что вывод первого прохода содержит JSON-блок (в логе будут поля
input_i,target_offsetи т.п.). - Если команда «не находится», проверь
export PATH="$HOME/bin:$PATH"и выполниsource ~/.zshrc. - Если скрипт запускается только как
~/bin/lufs16, значит~/binещё не в PATH.
Этого достаточно, чтобы стабильно приводить выпуски к целевым LUFS прямо из терминала.