Структура WAV файла
Представляем вашему вниманию статью с подробным разбором заголовка 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 байта) | subchunk1Size | 16 для формата 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 байта.
Подводные камни
Выше мы рассмотрели простейший случай заголовка с одной подцепочкой перед областью данных. Но на практике встречаются и более сложные или даже непредвиденные сценарии, с которыми можно увязнуть надолго.
В
chunkSize
лежит заведомо слишком большое значение. Такое происходит, когда вы пытаетесь читать данные в режиме стриминга. Например, декодер LAME при выводе результата декодирования вSTDOUT
в этом поле возвращает значение0x7FFFFFFF + 44 - 8
, а вsubchunk2Size
—0x7FFFFFFF
(что равно максимальному значению 32-разрядного знакового целочисленного значения). Это объясняется тем, что декодер в таком режиме выдаёт результат не целиком, а небольшими наборами данных и не может заранее определить итоговый размер данных.Подцепочек может быть больше, чем две, например, при попытке декодировать аудио универсальным декодером 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; | |
} |
На языке 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); | |
} | |
} | |
} |
На 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); | |
} | |
}); |
Комментарии