Представляем вашему вниманию статью с подробным разбором заголовка WAV-файла и его структуры.

Теория

Итак, рассмотрим самый обычный WAV файл (Windows PCM). Он представляет собой две, четко делящиеся, области. Одна из них — заголовок файла, другая — область данных. В заголовке файла хранится информация о:

  • Размере файла.
  • Количестве каналов.
  • Частоте дискретизации.
  • Количестве бит в сэмпле (эту величину ещё называют глубиной звучания).

Но для большего понимания смысла величин в заголовке следует ещё рассказать об области данных и оцифровке звука. Звук состоит из колебаний, которые при оцифровке приобретают ступенчатый вид. Этот вид обусловлен тем, что компьютер может воспроизводить в любой короткий промежуток времени звук определенной амплитуды (громкости) и этот короткий момент далеко не бесконечно короткий. Продолжительность этого промежутка и определяет частота дискретизации. Например, у нас файл с частотой дискретизации 44.1 kHz, это значит, что тот короткий промежуток времени равен 1/44100 секунды (следует из размерности величины Гц = 1/с). Современные звуковые карты поддерживают частоту дискретизации до 192 kHz. Так, со временем разобрались.

Амплитуда и сэмплы

Теперь, что касается амплитуды (громкости звука в коротком промежутке времени). Амплитуда выражается числом, которое занимает в файле 8, 16, 24, 32 бита (теоретически можно и больше). От точности амплитуды, я бы сказал, зависит точность звука. Как известно, 8 бит = 1 байту, следовательно, одно значение амплитуды в какой-то короткий промежуток времени в файле занимает 1, 2, 3, 4 байта соответственно. Таким образом, чем больше число занимает места в файле, тем шире возможный диапазон значений для этого числа, а значит и больше точность амплитуды.

Для PCM-файлов точность (или разрядность) может быть следующей:

  • 1 байт / 8 бит — -128…127
  • 2 байта / 16 бит — -32 760…32 760
  • 3 байта / 24 бита — -1…1 (с плавающей точкой)
  • 4 байта / 32 бита — -1…1 (с плавающей точкой)

Но список возможных разрядностей на самом деле весьма шире, здесь представлены лишь наиболее популярные.

Совокупность амплитуды и короткого промежутка времени носит название сэмпл.

Заголовок

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

МестоположениеПолеОписание
0…3 (4 байта)chunkIdСодержит символы "RIFF" в ASCII кодировке 0x52494646. Является началом RIFF-цепочки.
4…7 (4 байта)chunkSizeЭто оставшийся размер цепочки, начиная с этой позиции. Иначе говоря, это размер файла минус 8, то есть, исключены поля chunkId и chunkSize.
8…11 (4 байта)formatСодержит символы "WAVE" 0x57415645
12…15 (4 байта)subchunk1IdСодержит символы "fmt " 0x666d7420
16…19 (4 байта)subchunk1Size16 для формата PCM. Это оставшийся размер подцепочки, начиная с этой позиции.
20…21 (2 байта)audioFormatАудио формат, список допустипых форматов. Для PCM = 1 (то есть, Линейное квантование). Значения, отличающиеся от 1, обозначают некоторый формат сжатия.
22…23 (2 байта)numChannelsКоличество каналов. Моно = 1, Стерео = 2 и т.д.
24…27 (4 байта)sampleRateЧастота дискретизации. 8000 Гц, 44100 Гц и т.д.
28…31 (4 байта)byteRateКоличество байт, переданных за секунду воспроизведения.
32…33 (2 байта)blockAlignКоличество байт для одного сэмпла, включая все каналы.
34…35 (2 байта)bitsPerSampleКоличество бит в сэмпле. Так называемая "глубина" или точность звучания. 8 бит, 16 бит и т.д.
36…39 (4 байта)subchunk2IdСодержит символы "data" 0x64617461
40…43 (4 байта)subchunk2SizeКоличество байт в области данных.
44…dataНепосредственно WAV-данные.

Вот и весь заголовок, длина которого составляет 44 байта.

Подводные камни

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

  1. В chunkSize лежит заведомо слишком большое значение. Такое происходит, когда вы пытаетесь читать данные в режиме стриминга. Например, декодер LAME при выводе результата декодирования в STDOUT в этом поле возвращает значение 0x7FFFFFFF + 44 - 8, а в subchunk2Size0x7FFFFFFF (что равно максимальному значению 32-разрядного знакового целочисленного значения). Это объясняется тем, что декодер в таком режиме выдаёт результат не целиком, а небольшими наборами данных и не может заранее определить итоговый размер данных.

  2. Подцепочек может быть больше, чем две, например, при попытке декодировать аудио универсальным декодером ffmpeg 4.1.3 ffmpeg -i example.mp3 -f wav example.wav в декодированном файле помимо рассмотренных подцепочек fmt и data будет содержаться ещё одна LIST перед областью данных. Таким образом, когда вам понадобится добраться до данных, вам потребуется пропустить ненужные подцепочки, пока не встретится data. Это будет сделать не слишком сложно, так как можно читать ID подцепочки и её размер, и если она не data, то пропускать данные, основываясь на её размере.

Блок данных

В моно варианте значения амплитуды расположены последовательно. В стерео же, например, сначала идет значение амплитуды для левого канала, затем для правого, затем снова для левого и так далее.

Заметка о типах данных

При чтении заголовка можно применять разные типы данных. Например, в Си (MSVS) вместо массива char[4] можно использовать __int32 или DWORD, но тогда сравнение с какой-либо строковой константой, к примеру может оказаться не очень удобным. Также хотелось бы предостеречь вас на тему 64-битных операционных систем. А именно: всегда стоит помнить, что в языке Си тип переменной int в 64-битной системе будет иметь длину 8 байт, а в 32-битной — 4 байта. В таких случаях можно воспользоваться вышеупомянутым типом переменной __int32 или __int64, в зависимости от того, какой размер переменной в памяти Вам необходим. Существуют типы __int8, __int16, __int32 и __int64, они доступны только для MSVC++ компилятора как минимум 7-й версии (Microsoft Visual Studio 2003.NET), но зато Вы не ошибетесь с выбором размера типа данных.

Примеры реализации

На языке C++

#include <stdio.h>
#include <tchar.h>
#include <conio.h>
#include <math.h>
// Структура, описывающая заголовок WAV файла.
struct WAVHEADER
{
// WAV-формат начинается с RIFF-заголовка:
// Содержит символы "RIFF" в ASCII кодировке
// (0x52494646 в big-endian представлении)
char chunkId[4];
// 36 + subchunk2Size, или более точно:
// 4 + (8 + subchunk1Size) + (8 + subchunk2Size)
// Это оставшийся размер цепочки, начиная с этой позиции.
// Иначе говоря, это размер файла - 8, то есть,
// исключены поля chunkId и chunkSize.
unsigned long chunkSize;
// Содержит символы "WAVE"
// (0x57415645 в big-endian представлении)
char format[4];
// Формат "WAVE" состоит из двух подцепочек: "fmt " и "data":
// Подцепочка "fmt " описывает формат звуковых данных:
// Содержит символы "fmt "
// (0x666d7420 в big-endian представлении)
char subchunk1Id[4];
// 16 для формата PCM.
// Это оставшийся размер подцепочки, начиная с этой позиции.
unsigned long subchunk1Size;
// Аудио формат, полный список можно получить здесь http://audiocoding.ru/wav_formats.txt
// Для PCM = 1 (то есть, Линейное квантование).
// Значения, отличающиеся от 1, обозначают некоторый формат сжатия.
unsigned short audioFormat;
// Количество каналов. Моно = 1, Стерео = 2 и т.д.
unsigned short numChannels;
// Частота дискретизации. 8000 Гц, 44100 Гц и т.д.
unsigned long sampleRate;
// sampleRate * numChannels * bitsPerSample/8
unsigned long byteRate;
// numChannels * bitsPerSample/8
// Количество байт для одного сэмпла, включая все каналы.
unsigned short blockAlign;
// Так называемая "глубиная" или точность звучания. 8 бит, 16 бит и т.д.
unsigned short bitsPerSample;
// Подцепочка "data" содержит аудио-данные и их размер.
// Содержит символы "data"
// (0x64617461 в big-endian представлении)
char subchunk2Id[4];
// numSamples * numChannels * bitsPerSample/8
// Количество байт в области данных.
unsigned long subchunk2Size;
// Далее следуют непосредственно Wav данные.
};
int _tmain(int argc, _TCHAR* argv[])
{
FILE *file;
errno_t err;
err = fopen_s(&file, "Slipknot - Three Nil.wav", "rb");
if (err)
{
printf_s("Failed open file, error %d", err);
return 0;
}
WAVHEADER header;
fread_s(&header, sizeof(WAVHEADER), sizeof(WAVHEADER), 1, file);
// Выводим полученные данные
printf_s("Sample rate: %d\n", header.sampleRate);
printf_s("Channels: %d\n", header.numChannels);
printf_s("Bits per sample: %d\n", header.bitsPerSample);
// Посчитаем длительность воспроизведения в секундах
float fDurationSeconds = 1.f * header.subchunk2Size / (header.bitsPerSample / 8) / header.numChannels / header.sampleRate;
int iDurationMinutes = (int)floor(fDurationSeconds) / 60;
fDurationSeconds = fDurationSeconds - (iDurationMinutes * 60);
printf_s("Duration: %02d:%02.f\n", iDurationMinutes, fDurationSeconds);
fclose(file);
_getch();
return 0;
}
view raw main.cpp hosted with ❤ by GitHub

На языке C#

using System;
using System.IO;
using System.Runtime.InteropServices;
namespace WavFormatCSharp
{
[StructLayout(LayoutKind.Sequential)]
// Структура, описывающая заголовок WAV файла.
internal class WavHeader
{
// WAV-формат начинается с RIFF-заголовка:
// Содержит символы "RIFF" в ASCII кодировке
// (0x52494646 в big-endian представлении)
public UInt32 ChunkId;
// 36 + subchunk2Size, или более точно:
// 4 + (8 + subchunk1Size) + (8 + subchunk2Size)
// Это оставшийся размер цепочки, начиная с этой позиции.
// Иначе говоря, это размер файла - 8, то есть,
// исключены поля chunkId и chunkSize.
public UInt32 ChunkSize;
// Содержит символы "WAVE"
// (0x57415645 в big-endian представлении)
public UInt32 Format;
// Формат "WAVE" состоит из двух подцепочек: "fmt " и "data":
// Подцепочка "fmt " описывает формат звуковых данных:
// Содержит символы "fmt "
// (0x666d7420 в big-endian представлении)
public UInt32 Subchunk1Id;
// 16 для формата PCM.
// Это оставшийся размер подцепочки, начиная с этой позиции.
public UInt32 Subchunk1Size;
// Аудио формат, полный список можно получить здесь http://audiocoding.ru/wav_formats.txt
// Для PCM = 1 (то есть, Линейное квантование).
// Значения, отличающиеся от 1, обозначают некоторый формат сжатия.
public UInt16 AudioFormat;
// Количество каналов. Моно = 1, Стерео = 2 и т.д.
public UInt16 NumChannels;
// Частота дискретизации. 8000 Гц, 44100 Гц и т.д.
public UInt32 SampleRate;
// sampleRate * numChannels * bitsPerSample/8
public UInt32 ByteRate;
// numChannels * bitsPerSample/8
// Количество байт для одного сэмпла, включая все каналы.
public UInt16 BlockAlign;
// Так называемая "глубиная" или точность звучания. 8 бит, 16 бит и т.д.
public UInt16 BitsPerSample;
// Подцепочка "data" содержит аудио-данные и их размер.
// Содержит символы "data"
// (0x64617461 в big-endian представлении)
public UInt32 Subchunk2Id;
// numSamples * numChannels * bitsPerSample/8
// Количество байт в области данных.
public UInt32 Subchunk2Size;
// Далее следуют непосредственно Wav данные.
}
class Program
{
static void Main(string[] args)
{
var header = new WavHeader();
// Размер заголовка
var headerSize = Marshal.SizeOf(header);
var fileStream = new FileStream("Slipknot - Three Nil.wav", FileMode.Open, FileAccess.Read);
var buffer = new byte[headerSize];
fileStream.Read(buffer, 0, headerSize);
// Чтобы не считывать каждое значение заголовка по отдельности,
// воспользуемся выделением unmanaged блока памяти
var headerPtr = Marshal.AllocHGlobal(headerSize);
// Копируем считанные байты из файла в выделенный блок памяти
Marshal.Copy(buffer, 0, headerPtr, headerSize);
// Преобразовываем указатель на блок памяти к нашей структуре
Marshal.PtrToStructure(headerPtr, header);
// Выводим полученные данные
Console.WriteLine("Sample rate: {0}", header.SampleRate);
Console.WriteLine("Channels: {0}", header.NumChannels);
Console.WriteLine("Bits per sample: {0}", header.BitsPerSample);
// Посчитаем длительность воспроизведения в секундах
var durationSeconds = 1.0 * header.Subchunk2Size / (header.BitsPerSample / 8.0) / header.NumChannels / header.SampleRate;
var durationMinutes = (int)Math.Floor(durationSeconds / 60);
durationSeconds = durationSeconds - (durationMinutes * 60);
Console.WriteLine("Duration: {0:00}:{1:00}", durationMinutes, durationSeconds);
Console.ReadKey();
// Освобождаем выделенный блок памяти
Marshal.FreeHGlobal(headerPtr);
}
}
}
view raw main.cs hosted with ❤ by GitHub

На Node.js из STDIN

const inputStream = process.stdin;
function readHeader(stream) {
let buffer = stream.read(44);
const chunkId = buffer.slice(0, 4).toString();
const chunkSize = buffer.readUInt32LE(4);
const format = buffer.slice(8, 12).toString();
const subchunk1Id = buffer.slice(12, 16).toString();
const subchunk1Size = buffer.readUInt32LE(16);
const audioFormat = buffer.readUInt16LE(20);
const numChannels = buffer.readUInt16LE(22);
const sampleRate = buffer.readUInt32LE(24);
const byteRate = buffer.readUInt32LE(28);
const blockAlign = buffer.readUInt16LE(32);
const bitsPerSample = buffer.readUInt16LE(34);
// Skip unknown chunks
let dataSubchunkId = buffer.slice(36, 40).toString();
let dataSubchunkSize = buffer.readUInt32LE(40);
while (dataSubchunkId !== 'data') {
console.log('Skipping unknown subchunk', dataSubchunkId);
buffer = stream.read(dataSubchunkSize + 8);
dataSubchunkId = buffer.slice(dataSubchunkSize, dataSubchunkSize + 4).toString();
dataSubchunkSize = buffer.readUInt32LE(dataSubchunkSize + 4);
}
return {
chunkId,
chunkSize,
format,
subchunk1Id,
subchunk1Size,
audioFormat,
numChannels,
sampleRate,
byteRate,
blockAlign,
bitsPerSample,
dataSubchunkId,
dataSubchunkSize,
};
}
function validateHeader(header) {
if (header.chunkId !== 'RIFF') {
throw new Error('Unsupported format: Chunk ID must be "RIFF"');
}
if (header.format !== 'WAVE') {
throw new Error('Unsupported format: Format must be "WAVE"');
}
if (header.subchunk1Id !== 'fmt ') {
throw new Error('Unsupported format: First subchunk ID must be "fmt "');
}
if (header.dataSubchunkId !== 'data') {
throw new Error('Unsupported format: Subchunk with ID "data" not found');
}
}
inputStream.on('readable', () => {
const header = readHeader(inputStream);
console.log('Header', header);
try {
validateHeader(header);
} catch (error) {
console.error(error);
process.exit(1);
}
});
view raw index.js hosted with ❤ by GitHub