Антон Малявский Антон Малявский

Нормализация громкости в FFmpeg (двухпроходный loudnorm)

16 января 2026 г.

Что это?  Ссылка на этот раздел

Скрипт на основе FFmpeg устанавливается в MacOS как набор команд в папке ~/bin: lufs14, lufs16 и lufs19. Он позволяет нормализовать финальные аудиофайлы подкаста или видео без сторонних программ: достаточно вызвать нужную команду с путём к файлу. Внутри выполняются два прохода FFmpeg: первый анализирует громкость и динамику, второй применяет точные параметры для достижения целевых уровней −14 LUFS (громче, под ролики и соцсети), −16 LUFS (стандарт стерео-подкастов) или −19 LUFS (моно). В результате получается готовый по уровню и балансу MP3-файл, оптимизированный под разные сценарии публикации.

Требования  Ссылка на этот раздел

  • MacOS с Homebrew
  • python3 в системе (обычно уже есть на macOS)

Установка FFmpeg  Ссылка на этот раздел

1brew install ffmpeg
2ffmpeg -version

Настройка: команды в ~/bin  Ссылка на этот раздел

Создай папку для личных команд (если её ещё нет):

1mkdir -p ~/bin

Создай скрипт ~/bin/lufs и вставь в него код:

ZSH
  1cat > ~/bin/lufs <<'ZSH'
  2#!/bin/zsh
  3set -euo pipefail
  4
  5# Defaults
  6BITRATE="192k"
  7SR="44100"
  8LRA="11"
  9TP="-1.5"
 10
 11# Command can be invoked as lufs14/lufs16/lufs19 via symlink
 12cmd="$(basename "$0")"
 13
 14target=""
 15force_mono=0
 16
 17case "$cmd" in
 18  lufs14) target="-14"; force_mono=0 ;;
 19  lufs16) target="-16"; force_mono=0 ;;
 20  lufs19) target="-19"; force_mono=1 ;;
 21  lufs)
 22    if [[ "$#" -lt 2 ]]; then
 23      echo "Usage: lufs <-14|-16|-19> <input-file> [--mono]"
 24      exit 1
 25    fi
 26    target="$1"; shift
 27    ;;
 28  *)
 29    echo "Unknown command name: $cmd"
 30    exit 1
 31    ;;
 32esac
 33
 34in="${1:-}"
 35opt="${2:-}"
 36
 37if [[ -z "${in}" ]]; then
 38  echo "No input file."
 39  echo "Example: lufs16 \"episode.wav\""
 40  exit 1
 41fi
 42
 43# Support file:// paths
 44in="${in#file://}"
 45
 46if [[ ! -f "$in" ]]; then
 47  echo "Not a file: $in"
 48  exit 1
 49fi
 50
 51if [[ "${opt:-}" == "--mono" ]]; then
 52  force_mono=1
 53fi
 54
 55command -v ffmpeg >/dev/null 2>&1 || { echo "Need ffmpeg"; exit 1; }
 56command -v python3 >/dev/null 2>&1 || { echo "Need python3"; exit 1; }
 57
 58dir="$(dirname "$in")"
 59base="$(basename "$in")"; base="${base%.*}"
 60
 61suffix="${target#-}LUFS_${BITRATE}"
 62out="${dir}/${base}_${suffix}.mp3"
 63if [[ "$force_mono" -eq 1 ]]; then
 64  out="${dir}/${base}_${suffix}_mono.mp3"
 65fi
 66
 67echo "Pass 1: measure loudness..."
 68if [[ "$force_mono" -eq 1 ]]; then
 69  pass="$(ffmpeg -hide_banner -nostats -y -vn -i "$in" \
 70    -af "aformat=channel_layouts=mono,loudnorm=I=${target}:LRA=${LRA}:TP=${TP}:print_format=json" \
 71    -f null - 2>&1)"
 72else
 73  pass="$(ffmpeg -hide_banner -nostats -y -vn -i "$in" \
 74    -af "loudnorm=I=${target}:LRA=${LRA}:TP=${TP}:print_format=json" \
 75    -f null - 2>&1)"
 76fi
 77
 78json_block="$(echo "$pass" | sed -n '/^{/,$p' | sed -n '1,/^}/p')"
 79
 80read -r I TPm LRAi THRESH OFFSET < <(python3 - <<'PY'
 81import sys, json, re
 82s = sys.stdin.read()
 83m = re.search(r'^\{.*\}\s*$', s, re.S | re.M)
 84if not m:
 85    raise SystemExit(2)
 86d = json.loads(m.group(0))
 87keys = ["input_i","input_tp","input_lra","input_thresh","target_offset"]
 88print(" ".join(str(d.get(k,"")) for k in keys))
 89PY
 90<<< "$json_block")
 91
 92if [[ -z "${I:-}" || -z "${TPm:-}" || -z "${LRAi:-}" || -z "${THRESH:-}" || -z "${OFFSET:-}" ]]; then
 93  echo "Failed to parse loudnorm metrics."
 94  echo "$pass"
 95  exit 1
 96fi
 97
 98echo "Pass 2: normalize to ${target} LUFS..."
 99if [[ "$force_mono" -eq 1 ]]; then
100  ffmpeg -hide_banner -y -vn -i "$in" \
101    -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}" \
102    -ar "${SR}" -ac 1 -c:a libmp3lame -b:a "${BITRATE}" "$out"
103else
104  ffmpeg -hide_banner -y -vn -i "$in" \
105    -af "loudnorm=I=${target}:LRA=${LRA}:TP=${TP}:measured_I=${I}:measured_TP=${TPm}:measured_LRA=${LRAi}:measured_thresh=${THRESH}:offset=${OFFSET}" \
106    -ar "${SR}" -c:a libmp3lame -b:a "${BITRATE}" "$out"
107fi
108
109echo "Done: $out"
110ZSH

Сделай файл исполняемым:

1chmod +x ~/bin/lufs

Создай команды lufs14, lufs16 и lufs19 через симлинки:

1ln -sf ~/bin/lufs ~/bin/lufs14
2ln -sf ~/bin/lufs ~/bin/lufs16
3ln -sf ~/bin/lufs ~/bin/lufs19

PATH: чтобы команды работали без полного пути  Ссылка на этот раздел

Если ~/bin ещё не добавлен в PATH, открой ~/.zshrc:

1nano ~/.zshrc

Добавь в конец:

1export PATH="$HOME/bin:$PATH"

Применить изменения:

1source ~/.zshrc

Проверь, что команды доступны:

1type lufs14
2type lufs16
3type lufs19

Использование  Ссылка на этот раздел

Громкий стерео-микс под ролики / соцсети  Ссылка на этот раздел

1lufs14 /путь/к/файлу.wav

Результат: <имя>_14LUFS_192k.mp3

Финальный стерео-микс выпуска подкаста  Ссылка на этот раздел

1lufs16 /путь/к/файлу.wav

Результат: <имя>_16LUFS_192k.mp3

Моно-запись голоса  Ссылка на этот раздел

1lufs19 /путь/к/файлу.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 прямо из терминала.

Понравилось? Подпишитесь на меня!
RSS Телеграм