Александр Иванов – Python для творческих. Звук на твоей стороне. (страница 7)
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):