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

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

12 ноября 2025 г.

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

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

  • macOS с Homebrew.
  • FFmpeg установлен в системе.

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

brew install ffmpeg
ffmpeg -version

Для работы используется утилита jq, которая позволяет парсить JSON-вывод FFmpeg при анализе громкости. Если её нет в системе, установи командой:

brew install jq

Настройка: функции в** ~/.zshrc  Ссылка на этот раздел

Открой файл конфигурации:

nano ~/.zshrc

Вставь в конец файла этот код и сохрани.

# --- FFmpeg loudnorm: -16 LUFS (стерео, CBR 192 kbps MP3) ---
lufs16() {
  local in="$1"
  if [ -z "$in" ]; then echo "Использование: lufs16 <входной файл>"; return 1; fi
  command -v ffmpeg >/dev/null 2>&1 || { echo "Нужен ffmpeg"; return 1; }

  local dir base out pass I TP LRA THRESH OFFSET
  dir="$(dirname "$in")"
  base="$(basename "$in")"; base="${base%.*}"
  out="${dir}/${base}_16LUFS_192k.mp3"

  echo "Проход 1: измерение громкости..."
  pass="$(ffmpeg -hide_banner -nostats -y -i "$in" \
    -af loudnorm=I=-16:LRA=11:TP=-1.5:print_format=json -f null - 2>&1)"

  I="$(echo "$pass"      | grep -o '"input_i"[[:space:]]*:[[:space:]]*"-\?[0-9.]*"'        | grep -o '"-*[0-9.]*"' | tr -d '"')"
  TP="$(echo "$pass"     | grep -o '"input_tp"[[:space:]]*:[[:space:]]*"-\?[0-9.]*"'       | grep -o '"-*[0-9.]*"' | tr -d '"')"
  LRA="$(echo "$pass"    | grep -o '"input_lra"[[:space:]]*:[[:space:]]*"-\?[0-9.]*"'      | grep -o '"-*[0-9.]*"' | tr -d '"')"
  THRESH="$(echo "$pass" | grep -o '"input_thresh"[[:space:]]*:[[:space:]]*"-\?[0-9.]*"'   | grep -o '"-*[0-9.]*"' | tr -d '"')"
  OFFSET="$(echo "$pass" | grep -o '"target_offset"[[:space:]]*:[[:space:]]*"-\?[0-9.]*"'  | grep -o '"-*[0-9.]*"' | tr -d '"')"

  if [ -z "$I" ] || [ -z "$TP" ] || [ -z "$LRA" ] || [ -z "$THRESH" ] || [ -z "$OFFSET" ]; then
    echo "Не удалось распарсить метрики loudnorm."
    echo "$pass"
    return 1
  fi

  echo "Проход 2: нормализация до -16 LUFS (стерео)..."
  ffmpeg -hide_banner -y -i "$in" \
    -af loudnorm=I=-16:LRA=11:TP=-1.5:measured_I=$I:measured_TP=$TP:measured_LRA=$LRA:measured_thresh=$THRESH:offset=$OFFSET \
    -c:a libmp3lame -b:a 192k "$out" || { echo "Ошибка на втором проходе ffmpeg"; return 1; }

  echo "Готово: $out"
}

# --- FFmpeg loudnorm: -19 LUFS (моно, CBR 192 kbps MP3) ---
lufs19() {
  local in="$1"
  if [ -z "$in" ]; then echo "Использование: lufs19 <входной файл>"; return 1; fi
  command -v ffmpeg >/dev/null 2>&1 || { echo "Нужен ffmpeg"; return 1; }

  local dir base out pass I TP LRA THRESH OFFSET
  dir="$(dirname "$in")"
  base="$(basename "$in")"; base="${base%.*}"
  out="${dir}/${base}_19LUFS_mono_192k.mp3"

  echo "Проход 1: измерение громкости (моно)..."
  pass="$(ffmpeg -hide_banner -nostats -y -i "$in" \
    -af "aformat=channel_layouts=mono,loudnorm=I=-19:LRA=11:TP=-1.5:print_format=json" \
    -f null - 2>&1)"

  I="$(echo "$pass"      | grep -o '"input_i"[[:space:]]*:[[:space:]]*"-\?[0-9.]*"'        | grep -o '"-*[0-9.]*"' | tr -d '"')"
  TP="$(echo "$pass"     | grep -o '"input_tp"[[:space:]]*:[[:space:]]*"-\?[0-9.]*"'       | grep -o '"-*[0-9.]*"' | tr -d '"')"
  LRA="$(echo "$pass"    | grep -o '"input_lra"[[:space:]]*:[[:space:]]*"-\?[0-9.]*"'      | grep -o '"-*[0-9.]*"' | tr -d '"')"
  THRESH="$(echo "$pass" | grep -o '"input_thresh"[[:space:]]*:[[:space:]]*"-\?[0-9.]*"'   | grep -o '"-*[0-9.]*"' | tr -d '"')"
  OFFSET="$(echo "$pass" | grep -o '"target_offset"[[:space:]]*:[[:space:]]*"-\?[0-9.]*"'  | grep -o '"-*[0-9.]*"' | tr -d '"')"

  if [ -z "$I" ] || [ -z "$TP" ] || [ -z "$LRA" ] || [ -z "$THRESH" ] || [ -z "$OFFSET" ]; then
    echo "Не удалось распарсить метрики loudnorm (моно)."
    echo "$pass"
    return 1
  fi

  echo "Проход 2: нормализация до -19 LUFS (моно)..."
  ffmpeg -hide_banner -y -i "$in" \
    -af "aformat=channel_layouts=mono,loudnorm=I=-19:LRA=11:TP=-1.5:measured_I=$I:measured_TP=$TP:measured_LRA=$LRA:measured_thresh=$THRESH:offset=$OFFSET" \
    -ac 1 -c:a libmp3lame -b:a 192k "$out" || { echo "Ошибка на втором проходе ffmpeg (моно)"; return 1; }

  echo "Готово: $out"
}

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

source ~/.zshrc

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

  • Финальный стерео-микс выпуска:
lufs16 /путь/к/файлу.wav

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

  • Моно-запись голоса:
lufs19 /путь/к/файлу.wav

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

Примечания  Ссылка на этот раздел

  • -16 LUFS — стандарт для стерео подкастов/плееров.
  • -19 LUFS — эквивалентная целевая громкость для моно.
  • Двухпроходный режим (измерение → применение) даёт точную нормализацию.
  • Битрейт фиксирован: CBR 192 kbps MP3 (libmp3lame). При необходимости можно поменять -b:a.

Диагностика  Ссылка на этот раздел

  • «Не удалось распарсить метрики loudnorm»: проверь, что вывод первого прохода содержит JSON (в логе показаны поля input_i, target_offset и т.п.).
  • Если функция «не находится», проверь source ~/.zshrc.

Этого достаточно, чтобы стабильно приводить выпуски к целевым LUFS прямо из терминала.

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