Нормализация громкости подкастов через FFmpeg (двухпроходный loudnorm)
Скрипт на основе 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 прямо из терминала.