Объединение двух аудиофайлов (WAV) в один (Python3)

Задача объединения двух wav-файлов кажется элементарной и для yнее существует большое количество утилит (ffmpeg например) и библиотек, особенно для языка Python (например pydub, pyaudio и даже wav встроенный в сам язык), но тем удивительнее результат.  Несмотря на то, что библиотеки должны лишь объединить два файла не производя для них никаких конверсий, попробовав некоторые, типа pydub, pyaudio и др, я  явно слышал ухудшение звучания. Возможно все же какие-то конверсии производились в недрах библиотек. Это совершенно неподходит для разрабатываемой программы Vinyl2CD, конвертирующей винил-рипы в CD-Audio формат, которой порой нужно слить две стороны пластинки в один файл, и поэтому было принято решение написать функцию объединения wav самостоятельно, минуя по возможности разные библиотеки, в которых могут быть подвохи. Для этого понадобилось описание формата WAV.

В сети множество описаний заголовка WAV, но  они, по большей части, оказались неверны, указывали неправильные адреса, что я выяснил экспериментальным путем, начав писать программу и получая совсем не те данные, что ожидались. Так же выяснился факт, что в мире есть огромное количество популяррных программ, которые создают или создавали испорченные WAV-файлы, и другие программисты вынуждены писать код, чтобы корректно читать некорректное.

Кроме того, несмотря на то что считается, что WAV – это формат без сжатия, но это несовсем так. В Windows WAV работает через встроенный в ОС ACM менеджер, который может принимать и воспроизводить кодированные файлы, и в силу этого аудио в WAV может быть закондировано даже в MP3  и тд.

Так же WAV файл ограничен размером в 4 Гб, из за типа Unsigned Int (32 битного целого беззнакового числа),  это примерно 6,8 часов в формате CD-Audio. 

Ниже привожу верный заголовок WAV:

Позиции Значение Описание
1-4 “RIFF” Помечает файл как riff-файл. Каждый символ имеет длину 1 байт.
5-8 Размер файла (целое число) Размер всего файла минус 8 байт, в байтах (32-битное целое число). Как правило, вы заполняете это после создания.
9-12 “WAVE” Заголовок типа файла. Для наших целей это всегда равно «WAVE».
13-16 “fmt” Формат маркера фрагмента. Включает конечный нуль
17-20 16 Длина данных формата, как указано выше
21-22 1 Тип формата (1 — PCM) — 2-байтовое целое
23-24 2 Количество каналов — 2-байтовое целое число
25-28 44100 Частота дискретизации — 32-байтовое целое число. Общие значения: 44100 (CD), 48000 (DAT). Частота дискретизации = количество выборок в секунду или герц.
29-32 176400 (Частота дискретизации * Биты на выборку * Каналы) / 8.
33-34 4 (BitsPerSample * Channels) / 8. 1 — 8 бит моно, 2 — 8 бит стерео/16 бит моно, 4 — 16 бит стерео
35-36 16 Бит на выборку
37-40 “данные” заголовок фрагмента “данные”. Отмечает начало раздела данных.
41-44 Размер файла (данные) Размер раздела аудиоданных.
44 и далее   само аудио

В ходе создания функции для Vinyl2CD объединения было несколько сложностей, поэтому разберу этот фрагмент кода подробно.

Но прежде всего  идея объединения двух файлов в один в моем представлении выглядела так.

В WAV файле есть заголовок с разной информацией, включая метаданные, после заголовка идут чистые аудиоданные.  Соответственно необходимо узнать размер аудиоданных в первом  и втором файле, а потом сложить их для получения размера аудиоданных совмещенного файла.

В итоговый файл имеет смысл полностью поместить первый файл,  а затем из второго файла взять только выкушенный кусок аудиоданных и поместить его после аудиоданных из первого файла.

После этого в заголовок нового файла нужно внести изменения, а именно в поле по адрусу 41 внести 4 байта содержащих размер соединенных из двух файлов аудиоданных.

Если  сделать только это, файлы сольются в один, но воспроизводится будет только аудиочасть первого файла. Потому что в загаловке есть еще одно поле, которое высчитывается по особой формуле: размер аудиодананных + 44 байта заголовка и минус 8 первых байт в которых лежит слово RIFF и то самое поле с адреса 5 , которое и нужно изменить.

Так как создавалась функция под программу Vinyl2CD, то было заранее известно, что будут объединяться файлы в формате WAV 44100 Hz 16 bit.

Теперь рассмотрим код:

Создаем функцию принимающую три аргумента на вход - путь к первому WAV файлу,
путь ко второму WAV файлу и путь с названием выходного итогового файла.
def comb(wav1, wav2, outwav):

Открываем файл для чтения как битовый (rb - read byte), так как это WAV.
    with open(wav1,'rb') as f1, open(wav2,'rb') as f2:

перейдем в заголовке к адресу хранящему размер аудиочасти файла, 
у 1 и 2 файлов
        f1.seek(40)
        f2.seek(40)

Считаем размеры из обоих файлов, в инт из байтов
        sz1 = int.from_bytes(f1.read(4), byteorder='little')
        sz2 = int.from_bytes(f2.read(4), byteorder='little')

Посчитаем сумму аудиоданных
        sz12 = sz1+sz2

прибавим к размеру объединенных аудиоданных заголовок за исключением первых 8 байт
 44 (заголовок)-8 (которые пока не  меняем) = 36
        chunk = sz12+36 

Сохраним в новый файл
        with open(wav1,'rb') as f1, open(wav2,'rb') as f2, open(outwav, 'wb') as out:#записать все 44 байта WAV заголовка из первого файла в выходной новый файл

Считываем первый файл полностью
            out.write(f1.read()) #записал все из первого

Переходим к 4 байту и записываем размер объединенных аудиоданных с +36
            out.seek(4)
            out.write(chunk.to_bytes(4, byteorder='little'))
            
переписать размер аудиоданных на новый - сумма двух
для этого перейдем на начало этого раздела , на 40 байт, где начинаются данные о размере
            out.seek(40)

и запишем туда новый размер в виде байтов, из инт в байты
            out.write(sz12.to_bytes(4, byteorder='little')) #поменял размер на сумму размеров

Теперь пропустим первые 44 байта заголовка и считаем до конца все аудиоданные
            f2.seek(44)
            data2 = f2.read() # из второго считал аудио без заголовка

Перейдем с конец файла исохраним туда аудиоданные из второго файла
            out.seek(0,2)
            out.write(data2)

Пример использования:
    comb('1.wav', '2.wav', '3.wav')
    print("Выполнено слияние")


 

Оставьте первый комментарий

Отправить ответ