О чём эта глава
В предыдущей главе мы смотрели на звук и изучали его свойства. Мы были как учёные, которые исследуют образец под микроскопом. Теперь пришло время стать ремесленниками. Мы возьмём аудиофайлы в руки и начнём с ними работать: отрезать лишнее, соединять нужное, переставлять куски местами. Всё то, что обычно делается мышкой в программах вроде Audacity, мы будем делать кодом. Почему кодом, а не мышкой? Потому что код можно запустить повторно. Потому что код не устаёт и не ошибается от усталости. Потому что код может обработать сто файлов за время, которое вы тратите на ручную обработку одного. В этой главе мы напишем инструменты, которые превратят монтаж аудио из утомительной рутины в быстрое и приятное занятие. Мы научимся вырезать фрагменты из аудио, склеивать несколько файлов в один, добавлять паузы нужной длины, выравнивать громкость разных фрагментов и собирать полноценный эпизод подкаста из заготовок. К концу главы у вас будет скрипт, который делает всю черновую работу по монтажу за вас.
Как резать аудио в коде
Начнём с самого простого действия: отрезать кусок от начала или конца записи. Представьте, что вы записали подкаст. Вы начали запись, проверили микрофон, сказали «раз-раз, меня слышно?», потом выпили воды, прокашлялись и только через три минуты начали говорить по делу. Эти три минуты не должны попасть в финальную версию. Более того, в конце записи вы, скорее всего, скажете что-то вроде «Всё, выключаю запись. Ой, она ещё пишет? Ну всё, пока». Эти последние десять секунд тоже нужно убрать. В графическом редакторе вы бы выделили ненужный фрагмент мышкой и нажали Delete. В коде мы сделаем то же самое, только вместо мышки у нас будут индексы массива.
Вспомним, что наш аудиофайл — это массив чисел y. Каждое число имеет свой порядковый номер, или индекс. Первое число имеет индекс ноль, второе — индекс один, сотое — индекс девяносто девять. В Python мы можем взять любую часть массива, указав начальный и конечный индекс в квадратных скобках через двоеточие. Это называется срез. Если мы хотим убрать первые три минуты, нам нужно выбросить все числа с нулевого индекса до индекса, соответствующего трём минутам. А этот индекс мы вычисляем умножением: три минуты — это сто восемьдесят секунд, умножаем на частоту дискретизации 22050, получаем около четырёх миллионов. Значит, нам нужны все числа начиная с индекса четыре миллиона и до конца массива.
Вот как это выглядит в коде:
python
import librosa
import soundfile as sf
y, sr = librosa.load('long_record.wav')
# Сколько секунд отрезать от начала
cut_seconds = 180
cut_samples = cut_seconds * sr
# Берём всё, начиная с cut_samples и до конца
y_trimmed = y[cut_samples:]
sf.write('record_trimmed.wav', y_trimmed, sr)
print(f"Было: {len(y) / sr:.1f} сек")
print(f"Стало: {len(y_trimmed) / sr:.1f} сек")
Запись y[cut_samples:] — это и есть срез. Квадратные скобки говорят Python, что мы хотим взять часть массива. Число до двоеточия — начальный индекс. Двоеточие без числа после него означает «и до самого конца». Всё, что было до индекса cut_samples, игнорируется. Обратите внимание: мы не удаляем данные из исходного массива y. Мы создаём новый массив y_trimmed, который содержит только нужную нам часть. Исходный массив остаётся нетронутым, и мы всегда можем к нему вернуться.
Теперь обратная задача: обрезать конец. Это делается похоже, только мы указываем конечный индекс, а начальный оставляем пустым:
python
# Сколько секунд отрезать от конца
cut_end_seconds = 10
cut_end_samples = len(y) - (cut_end_seconds * sr)
# Берём всё от начала до cut_end_samples
y_trimmed = y[:cut_end_samples]
Здесь срез y[:cut_end_samples] означает «от начала до индекса cut_end_samples, не включая его». Мы вычисляем этот индекс как общую длину массива минус количество измерений в десяти секундах. Таким образом мы отрезаем ровно последние десять секунд.
Если нужно обрезать и начало, и конец одновременно, оба среза можно объединить в одну строчку:
python
y_trimmed = y[180 * sr : len(y) - 10 * sr]
Читается это так: «Возьми массив y от индекса, соответствующего ста восьмидесяти секундам, до индекса, который находится за десять секунд до конца». Всё просто и понятно.
Склеиваем несколько файлов
С разрезанием разобрались. Теперь обратная операция — склеивание. Предположим, у вас есть три файла: музыкальное вступление, записанный голос и музыкальная концовка. Их нужно объединить в один файл, который пойдёт на публикацию. В видеоредакторе это делается перетаскиванием файлов на дорожку. В коде это делает функция np.concatenate из библиотеки numpy. Она берёт несколько массивов и соединяет их в один длинный массив, как будто склеивает ленту из кусков.
Вот базовый пример:
python
import librosa
import numpy as np
import soundfile as sf
# Загружаем три файла
intro, sr1 = librosa.load('intro.wav')
voice, sr2 = librosa.load('voice.wav')
outro, sr3 = librosa.load('outro.wav')
# Склеиваем
final = np.concatenate([intro, voice, outro])
# Сохраняем
sf.write('final.wav', final, sr1)
Однако здесь есть подводный камень: все три файла должны иметь одинаковую частоту дискретизации. Если вступление записано с частотой 44100, голос — с 22050, а концовка — с 48000, склеенный файл будет звучать неправильно. На стыках скорость воспроизведения будет меняться, и голос может стать то быстрее, то медленнее. Поэтому перед склеиванием нужно убедиться, что все файлы имеют одинаковую частоту, и если нет — привести их к единому стандарту.
Вот улучшенная версия скрипта с проверкой частоты:
python
import librosa
import numpy as np
import soundfile as sf
# Загружаем и проверяем частоту
target_sr = 22050
intro, sr_i = librosa.load('intro.wav', sr=target_sr)
voice, sr_v = librosa.load('voice.wav', sr=target_sr)
outro, sr_o = librosa.load('outro.wav', sr=target_sr)
print(f"Интро: {len(intro) / target_sr:.1f} сек")
print(f"Голос: {len(voice) / target_sr:.1f} сек")
print(f"Аутро: {len(outro) / target_sr:.1f} сек")
# Склеиваем
final = np.concatenate([intro, voice, outro])
print(f"Итого: {len(final) / target_sr:.1f} сек")
sf.write('episode.wav', final, target_sr)
Параметр sr=target_sr в функции librosa.load говорит библиотеке: «Загрузи файл и пересчитай его так, как будто он был записан с частотой target_sr». Если исходный файл был записан с другой частотой, librosa автоматически подгонит его под нужную. Это очень удобно и экономит нам кучу ручной работы.
Добавляем паузы между фрагментами
Когда вы склеиваете вступление и голос вплотную, они могут звучать неестественно. Обычно между музыкальной заставкой и началом речи нужна небольшая пауза — полсекунды или секунда, чтобы слушатель переключился с музыки на голос. В коде пауза — это просто массив из нулей. Ноль в аудио означает абсолютную тишину. Чтобы создать секунду тишины, нам нужно создать массив из нулей длиной в частоту дискретизации. При частоте 22050 это будет 22050 нулей.
python
# Создаём паузу в одну секунду
silence_sec = 1.0
silence = np.zeros(int(target_sr * silence_sec))
# Склеиваем с паузами
final = np.concatenate([intro, silence, voice, silence, outro])
Функция np.zeros(n) создаёт массив из n нулей. Мы передаём ей количество сэмплов, которое равно длительности паузы в секундах, умноженной на частоту дискретизации. Этот массив нулей можно вставлять между любыми фрагментами, и на слух это будет восприниматься как естественная пауза. Длину паузы можно менять по своему вкусу. Для энергичного контента паузы делают короче — четверть секунды или даже меньше. Для спокойного, вдумчивого — длиннее, до полутора секунд.
Выравниваем громкость фрагментов
Ещё одна важная задача при склеивании — выравнивание громкости. Музыкальная заставка часто бывает записана громче, чем голос. Если их склеить без обработки, слушателю придётся убавлять громкость на музыке и прибавлять на голосе. Это раздражает. Профессиональный монтаж подразумевает, что все части звучат на одном уровне.
Выравнивать громкость мы будем через пиковую нормализацию — метод, который мы кратко упоминали в Главе 1, а подробно разберём в Главе 3. Сейчас используем упрощённый вариант:
python
def normalize_volume(y, target_peak=0.9):
"""Подгоняет пиковую громкость к заданному уровню."""
current_peak = np.max(np.abs(y))
if current_peak == 0:
return y
gain = target_peak / current_peak
return y * gain
# Нормализуем все части
intro = normalize_volume(intro, 0.9)
voice = normalize_volume(voice, 0.9)
outro = normalize_volume(outro, 0.9)
После такого выравнивания все три части будут иметь одинаковый пиковый уровень, и слушателю не придётся тянуться к регулятору громкости.
Собираем готовый эпизод подкаста
Теперь давайте соберём всё вместе в один большой скрипт, который принимает набор файлов и выдаёт готовый смонтированный эпизод. Этот скрипт можно будет использовать для каждого выпуска подкаста, меняя только имена файлов.
python
import librosa
import numpy as np
import soundfile as sf
def build_podcast(intro_file, voice_file, outro_file, output_file,
trim_start=180, trim_end=10,
silence_between=1.0,
target_sr=22050,
target_peak=0.9):
"""
Собирает эпизод подкаста из трёх файлов.
trim_start - сколько секунд отрезать от начала голосовой дорожки
trim_end - сколько секунд отрезать от конца голосовой дорожки
silence_between - длина паузы между частями в секундах
"""
print("=== Сборка эпизода ===")
# Загружаем
print("Загружаю файлы...")
intro, _ = librosa.load(intro_file, sr=target_sr)
voice, _ = librosa.load(voice_file, sr=target_sr)
outro, _ = librosa.load(outro_file, sr=target_sr)
# Обрезаем голос
start_sample = int(trim_start * target_sr)
end_sample = len(voice) - int(trim_end * target_sr)
voice = voice[start_sample:end_sample]
print(f"Голос после обрезки: {len(voice) / target_sr:.1f} сек")
# Нормализуем громкость
for arr in [intro, voice, outro]:
peak = np.max(np.abs(arr))
if peak > 0:
arr *= target_peak / peak
# Создаём паузу
silence = np.zeros(int(target_sr * silence_between))
# Склеиваем
final = np.concatenate([intro, silence, voice, silence, outro])
print(f"Финальная длина: {len(final) / target_sr:.1f} сек")
# Сохраняем
sf.write(output_file, final, target_sr)
print(f"Сохранено: {output_file}")
print("Готово!")
# Запускаем
build_podcast(
intro_file='intro.wav',
voice_file='raw_voice.wav',
outro_file='outro.wav',
output_file='episode_ready.wav',
trim_start=180,
trim_end=10,
silence_between=1.0
)
Запустите этот скрипт, указав свои файлы. Если у вас нет готовых джинглов для интро и аутро, не беда — можете пока использовать любые музыкальные фрагменты или даже тишину. Главное — понять процесс. Когда появятся настоящие джинглы, вы просто подставите их в скрипт, и всё будет работать.
Пакетная обработка: монтируем много файлов сразу
Одно из главных преимуществ кода перед ручным монтажом — возможность обрабатывать много файлов одной командой. Предположим, у вас в папке лежит десять записей, и каждую нужно обрезать с начала и с конца, а потом сохранить результат в другую папку. В Audacity это заняло бы час. В Python это делается одним скриптом и занимает минуту.
python
import os
import librosa
import soundfile as sf