Задача объединения двух 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("Выполнено слияние")
Отправить ответ
Для отправки комментария вам необходимо авторизоваться.