Формат WAVE файлов

 

WAVE – это формат хранения оцифрованных аудио данных. Данный формат поддерживает данные различной битности, с различной частотой выборки и числом каналов. Данный формат весьма популярен на платформах стандарта IBM PC (и совместимых с ним).

Этот формат подразумевает хранение аудио данных в особых блоках, которые называют чанками (chunks).

Форматы данных

ID = array[1..4] of Char; - идентификатор чанка.

Организация данных

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

Byte    :  | байт 0 |.

Word  :  | байт 0 | байт 1 |

Integer : | байт 0 | байт 1 | байт 2 | | байт 3 |

Структура файла

Wav файл представляет собой набор из многочисленных чанков разного типа. Важнейшим чанком является чанк формата (Format chunk – «fmt»), который содержит важные параметры звуковых данных, например частоту выборки. Другим важным чанком является чанк данных (Data chunk), который и определяет форму аудио сигнала. Присутствие других дополнительных чанков (optional chunks) не обязательно.

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

Рассмотрим схему простейшего wav файла

__________________________
| RIFF WAVE Chunk          |
|   groupID  = 'RIFF'      |
|   FileSize : Cardinal;   |
|   riffType = 'WAVE'      |
|    __________________    |
|   | Format Chunk     |   |
|   |   ckID = 'fmt '  |   |
|   |__________________|   |
|    __________________    |
|   | Sound Data Chunk |   |
|   |   ckID = 'data'  |   |
|   |__________________|   |
|__________________________|
 

Формат wav файла создавался и модифицировался без всякой координации множеством авторов (каждый из которых вносил свои специфические чанки). В результате он получился отчасти противоречивым, например 8-ми битные данные являются беззнаковыми, а 16-битные – числа со знаком.

Элементы выборок и фреймы

            Важнейшими понятиями при интерпретация wave файлов, являются понятия элементы выборок и фреймы. Элемент выборки представляет собой мгновенное значение амплитуды сигнала. Данные длинной более 8-ми бит хранятся в виде блока размером от 9 до 32 бит состоящего из двух смежных слов (определяется значением поля wBitsPerSample, в чанке формата, для несжатого подстандарта PCM). Например для 16 битного формата (2х8 бит) диапазон значений амплитуд от -32768 ($8000)  до 32767 ($7FFF). Для формата 8бит и меньше (1 беззнаковый байт) диапазон значений амплитуд от   0 до 255.

            В связи с архитектурой современных процессоров было решено, что при сохранении данных размер выборки будет округлятся до величины кратной 8-битам. Для АЦП с разрядностью не более 8-бит данные сохраняются в 8-ми битном формате. Для АЦП с разрядностью от 9 до 16 бит элемент выборки будет 16 битным числом со знаком.

Для АЦП с разрядностью от 17 до 24 бит выборка будет 3-х байтной. Для АЦП с разрядностью от 25 до32 бит выборка будет двойным знаковым 32 битным словом. И т.д.

            Кроме того, биты данных (не кратных 8) должны быть выровнены по левому краю. Например 12 битные данные (записываются 16-ти битным словом) выравниваются следующим образом – данные выборки записываются с 4-го по 15 бит включительно, биты от 0 до 3 устанавливаются в нуль. Т.о. число 101000010111 записывается как

___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
|   |     |   |   |   | |   |   |   |   |   |   |   |   |   |   |
| 1   0   1   0   0   0   0   1   0   1   1   1   0   0   0   0 |
|___|___|___|___|___|___|___|___|___|___|___|___|___|___|___|___|
 <---------------------------------------------> <------------->
    12 битная выборка выровненная по левому краю  крайне правые  
                                                  Байты установленные
                                                  в нуль.

Но в файл данные будут записаны так

___ ___ ___ ___ ___ ___ ___ ___    ___ ___ ___ ___ ___ ___ ___ ___
|   |     |   |   |   | |   |   |  |   |   |   |   |   |   |   |   |
| 0   1   1   1   0   0   0   0 |  | 1   0   1   0   0   0   0   1 |
|___|___|___|___|___|___|___|___|  |___|___|___|___|___|___|___|___|
 <-------------> <------------->    <----------------------------->
   биты 0 до 3     4 нулевых                 биты от 4 до 11

 

При многоканальной записи элементы выборок записываются поочередно. Так для стерео записи в начале пишется одна выборка левого канала, затем правого, затем следующая выборка левого и т.д. Такое расположение выборок упрощает пересылку данных в ЦАП. Блок из нескольких выборок воспроизводимых одновременно называется фреймом. В нашем примере фрейм это блок, состоящий из двух выборок (левого и правого каналов)

 
  фрейм 0      фрейм 1             фрейм N
 _____ _____ _____ _____         _____ _____
| ch1 | ch2 | ch1 | ch2 | . . . | ch1 | ch2 |
|_____|_____|_____|_____|       |_____|_____|
 _____
|     | = одна выборка
|_____|

 

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

 

 

 

 

channels       1         2
             _________ _________ 
            | left    | right   |
  stereo    |         |         |
            |_________|_________|
 
 
                 1         2         3
             _________ _________ _________ 
            | left    | right   | center  |
  3 channel |         |         |         |
            |_________|_________|_________|
 
                 1         2         3         4
             _________ _________ _________ _________ 
            | front   | front   | rear    | rear    |
  quad      | left    | right   | left    | right   |
            |_________|_________|_________|_________|
 
                 1         2         3         4
             _________ _________ _________ _________ 
            | left    | center  | right   | surround|
  4 channel |         |         |         |         |
            |_________|_________|_________|_________|
 
                 1         2         3         4         5         6
             _________ _________ _________ _________ _________ _________
            | left    | left    | center  | right   | right   |surround |
  6 channel | center  |         |         | center  |         |         |

                        |__________|_________|__________|__________|__________|_________|

 

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

 

Чанк формат (Format Chunk)

Чанк Формат («fmt») описывает фундаментальные параметры данных, частоту выборки, разрядность данных и число каналов цифровой звукозаписи.

 

ID = array[1..4] of Char;

ChankFormat = Record

  chunkID          : ID;
  chunkSize        : Integer;
 
  wFormatTag       : SmallInt;
  wChannels        : Word;
  dwSamplesPerSec  : Cardinal;
  dwAvgBytesPerSec : Cardinal;
  wBlockAlign      : Word;
  wBitsPerSample   : Word;

end;

ChankFormat – может иметь и дополнительные поля, поэтому нельзя игнорировать поле

chunkSize – определяющее размер записи  минус 8 бит (поля chunkID и chunkSize),.

Значение chunkedID всегда равно “fmt “. Звуковые данные могут хранится без сжатия, тогда выборки хранятся как было описано в разделе Элементы выборок и фреймы. В случае формата со сжатием размеры выборок могут отличаться от разрядности звуковых данных. На наличие сжатия указывает поле wFormatTag. В этом случае его значение wFormatTag отличается от единицы. Кроме того в ChankFormat появятся несколько полей идентифицирующие метод сжатия.

- Первое дополнительное поле (типа Word) определяет длину добавочного блока(после этого поля).

-Далее следует специальный чанк (Fact chunk) содержащий переменную типа Cardinal, определяющую размер всех аудио данных (в несжатом ввиде). Размер задается в выборках. Существует большое количество методов сжатия. Детальное описание каждого из них можно посмотреть на сайте Microsoft.

            Если никакое сжатие не используется (то есть, wFormatTag = 1), то после ChankFormat нет дополнительных служебных полей.

Поле wChannels содержит число звуковых каналов. Значение 1 указывает на моно звучание, значение 2 стерео ,4 четыре канала звука и т.д. Напомню, что выборки многоканальных записей чередуются, а элементарный блок из таких данных образуют Фрейм. Реальные данные хранятся в чанке данных (Data Chunk), который будет описан позже.

-Поле dwSamplesPerSec хранит частоту выборок в секунду (то есть, Герц). Существуют три стандарта MPC - 11025, 22050, и 44100 ГЦ, хотя допускается использование других частот.

- Поле dwAvgBytesPerSec указывает, сколько байтов проигрываются каждую секунду. dwAvgBytesPerSec может использоваться приложением, чтобы оценить, буфер какого размера необходим, для того чтобы должным образом воспроизводить звук без проблем связанных с прерыванием воспроизведения звука. Его значение должно быть равно результату произведения dwSamplesPerSec * wBlockAlign округленного в большую сторону.

- Поле wBlockAlign должно быть равно значению выражения

wChannels * (wBitsPerSample div 8) округленному в большую сторону. По существу, wBlockAlign - размер фреймов в байтов. (т.о. фрейм для 16-битовой выборки равен 2 байтом, фрейм для 16-битовой стерео выборки 4 байта. И т.д).

            В каждом звуковом файле может быть не более одного чанка формата.

 

Чанк данных (Data Chunk)

Чанк данных содержит фреймы звуковых данных (все каналы звуковых данных)

Рассмотрим структуру

 

DataChunk = Record

  chunkID             : ID;
  chunkSize           : Integer;
 
  waveformData        : array of byte; 
end;
 

-Значение ID всегда равно ‘data’.

- chunkSize - число байтов в чанке минус 8 байт (поля ID и chunkSize) кроме того, не учитываются байты выравнивания структуры. Помните, что описание данных хранится в чанке формата. Все вышеперечисленное относится только к несжатому формату.

-Массив waveformData содержит собственно выборки, порядок их следования и выравнивание описано в разделе выборки и фреймы. Число фреймов можно вычислить, разделив значение chunkSize на wBlockAlign чанка формата.

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

 

В заключение хочется напомнить, что существуют множество других чанков таких как например Cue Chunk(вроде описания данного аудио файла), Playlist chunk, Associated Data List, Label Chunk, Note Chunk, Labeled Text Chunk, Sampler Chunk, Instrument Chunk Format. Для воспроизведения аудио их надо просто игнорировать.

----------------------------------------------------------------------------

 

Пример чтения wav 

Продолжение чтение wav файлов (мой пример).........

 

А теперь попробуем правельно прочитать прочитать wav файл как этого требует формат.

Const

  noError            = 0;

  ReadError          = 1;

  HeaderError        = 2;

  DataError          = 3;

  FileCorrupt        = 4;

  IncorectFileFormat = 5;

 

type

  TForm1 = class(TForm)

    Memo1: TMemo;

    Button1: TButton;

    OpenDialog1: TOpenDialog;

    procedure Button1Click(Sender: TObject);

  private

    { Private declarations }

  public

    { Public declarations }

  end;

 

  TWaveHeaderChank = record     //Заголвок (чанк) формата

    wFormatTag     : Smallint;

    wChannels      : WORD;

    wSamplesPerSec : Cardinal;

    wAvgBytesPerSec: Cardinal;

    wBlockAlign    : WORD;

    wBitsPerSample : WORD;

    wcbSize        : WORD;

  end;

 

  TWaveResult = record          //Минимальная структура

    ERROR          : WORD;      //Используеися для возвращения результата

    wAvgBytesPerSec: Cardinal;  //разбора Wav файла

    wBitsPerSample : WORD;

    wChannels      : WORD;

    Data           : TMemoryStream;

  end;

 

var

  Form1: TForm1;

 

implementation

 

{$R *.dfm}

 

uses math;

 

Function ReadWave(FileName : AnsiString) : TWaveResult;

var

  f             : TFileStream;

  wFileSize     : Cardinal;

  wChankSize    : Cardinal;

  ID            : array[0..3] of Char;

  Header        : TWaveHeaderChank;

  RealFileSize  : Cardinal;

 

Begin

  FillChar(Result, SizeOf(Result), 0);

 

  Try

    f := TFileStream.Create(FileName, fmOpenRead);

    f.Seek(0, soFromBeginning);

    f.ReadBuffer(ID[0], 4);       //читаем тип файла

 

    if String(ID) <> 'RIFF'       //Определяем тип файла

    then

      Begin

        Result.ERROR := IncorectFileFormat;

        f.Free;

        exit;

      end;

      //////////////////////////

      Form1.memo1.Lines.Add(String(ID));

 

    f.ReadBuffer(wFileSize, 4);   //читаем размер файла

      //////////////////////////

      Form1.memo1.Lines.Add('FileSize ' + intToStr(wFileSize));

 

    if f.size <> (wFileSize + 8)  //Определяем соответствие указанного размера

    then                          //и размера файла(на случай если был поврежден при

      Begin                       //передаче)

        Result.ERROR := FileCorrupt;

        f.Free;

        exit;

      end;

     

    f.ReadBuffer(ID[0], 4);

      //////////////////////////

      Form1.memo1.Lines.Add(String(ID));

 

    if String(ID) <> 'WAVE'             //Определяем формат файла

    then

      Begin

        Result.ERROR := IncorectFileFormat;

        f.Free;

        exit;

      end;

 

    wChankSize := 0;

    repeat                              //Ищем чанк формата

      f.Seek(wChankSize, soFromCurrent);//Пропускаем все дополнительные чанки

      f.ReadBuffer(ID[0], 4);           //Читаем идентификатор чанка

 

        //////////////////////////

        Form1.memo1.Lines.Add(String(ID));

      f.ReadBuffer(wChankSize, 4);   //Читаем размер чанка

 

      if wChankSize > High(integer)  //Проверяем размер загловка на разумность

      then                           //размера вероятно можно установить чило и

        Begin                        //меньше например 100

          Result.ERROR := DataError;

          f.Free;

          exit;

        end;

        //////////////////////////

        Form1.memo1.Lines.Add('chankSize ' + intToStr(wChankSize));

    until  (String(ID)='fmt ') or (String(ID)='data');

 

    if String(ID)='data'             //Проверяем найден ли заголовок формата

    then

      Begin

        Result.ERROR := HeaderError;

        f.Free;

        exit;

      end;

 

    f.ReadBuffer(Header, Min(wChankSize, SizeOf(TWaveHeaderChank))); //Читаем заголовок

      //////////////////////////               //меньше нашей структуры

      Form1.memo1.Lines.Add('wFormatTag '     + intToStr(Header.wFormatTag));

      Form1.memo1.Lines.Add('wChannels '      + intToStr(Header.wChannels));

      Form1.memo1.Lines.Add('wSamplesPerSec ' + intToStr(Header.wSamplesPerSec));

      Form1.memo1.Lines.Add('wBlockAlign '    + intToStr(Header.wBlockAlign));

      Form1.memo1.Lines.Add('wBitsPerSample ' + intToStr(Header.wBitsPerSample));

 

    if wChankSize > SizeOf(TWaveHeaderChank)  //Смещаем указатель чтения в конец блока

    then                                      //нужно только для больших заголовков

      f.Seek(wChankSize - SizeOf(TWaveHeaderChank), soFromCurrent);

 

    if wChankSize >= SizeOf(TWaveHeaderChank) //определяем расширенный ли заголовок или нет

    then

      //////////////////////////     

        Form1.memo1.Lines.Add('wcbSize '        + intToStr(Header.wcbSize));

 

    wChankSize := 0;

    repeat                              //Ищем чанк данных

      f.Seek(wChankSize, soFromCurrent);//Пропускаем все дополнительные чанки

      f.ReadBuffer(ID[0], 4);           //Читаем идентификатор чанка

        //////////////////////////

        Form1.memo1.Lines.Add(String(ID));

      f.ReadBuffer(wChankSize, 4);      //Читаем  размер чанка

        //////////////////////////

        Form1.memo1.Lines.Add('chankSize '    + intToStr(wChankSize));

    until  String(ID)='data';

 

    Result.ERROR           := noError;               //Заполняем структуру результата

    Result.wAvgBytesPerSec := Header.wAvgBytesPerSec;

    Result.wBitsPerSample  := Header.wBitsPerSample;

    Result.wChannels       := Header.wChannels;

                                                    //Копируем данные в память

    Result.Data := TMemoryStream.Create;

    Result.Data.Seek(0, soFromBeginning);

    Result.Data.Size := wChankSize;                 //Выделяем память под данные

    f.ReadBuffer(Result.Data.Memory^, wChankSize);  //Копируем данные в память

 

  Except

    Result.ERROR := ReadError;

  end;

  f.Free;

end;

 

procedure TForm1.Button1Click(Sender: TObject);

var

  r : TWaveResult;

begin

  if opendialog1.Execute

  then

    r := ReadWave(opendialog1.FileName);

end;

 

end.

-------------------------------------------

Строки Form1.memo1.Lines.Add пошагово выводят информацию в memo и могут быть удалены.

Также можно (но очень не желательно) удалить все проверки на ошибки чтения и т.д.

Данный код читает PCM не сжатый формат и возвращает дастаточный минимум информации, однако этот метод позволяет правельно прочитать и любой сжатый формат(однако возвращаемой информации для интерпретации таких данных не достаточно)

Данные возвращаются ввиде потока TMemoryStream откуда их можно извлекать в соответствии с их форматом.

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

 

Пример записи wav

    Наконец дошли руки написал процедуру записи wav файлов.
    Отмечаю следующие особенности: Запись TWaveHeaderChank следует обявлять с директивой packed, в связи с тем, что формат wav подразумевает выравнивание на границу 2 байт, а delphi по умалчанию выравнивает на границу 4 байт. Т.о. размер структуры TWaveHeaderChank без диретивы packed будет занимать 20 байт(структура в примере программ) вместо положенных 18.
    Следующая программа записывает одноканальный wav файл длительностью 10 сек, 16 бит выборка. Звук будет напоминать свисты коротковолнового радиоприемника.

 TWaveHeaderChank = packed record  //Заголвок (чанк) формата

    wFormatTag     : Smallint;

    wChannels      : WORD;

    wSamplesPerSec : Cardinal;

    wAvgBytesPerSec: Cardinal;

    wBlockAlign    : WORD;

    wBitsPerSample : WORD;

    wcbSize        : WORD;

  end;

-------------------------------------------------------------------

Const

  noError            = 0;

  ReadError          = 1;

  HeaderError        = 2;

  DataError          = 3;

  FileCorrupt        = 4;

  IncorectFileFormat = 5;

  HeaderWriteError   = 6;

  StreamError        = 7;

-------------------------------------------------------------------

Function WriteWave(FileName : AnsiString; data : TWaveResult) : WORD;

var

  f             : TFileStream;

  wFileSize     : Cardinal;

  wChankSize    : Cardinal;

  ID            : array[0..3] of Char;

  Header        : TWaveHeaderChank;

 

Begin

   Result := noError;

   Try

    f := TFileStream.Create(FileName, fmCreate);

    f.Seek(0, soFromBeginning);

 

    Header.wFormatTag     := 1;

    Header.wChannels      := data.wChannels;

    Header.wSamplesPerSec := data.wSamplesPerSec;

    Header.wBlockAlign    := data.wChannels * (data.wBitsPerSample div 8);

    Header.wAvgBytesPerSec:= data.wSamplesPerSec * Header.wBlockAlign;

    Header.wBitsPerSample := data.wBitsPerSample;

    Header.wcbSize        := 0; //нет дополнительного блока

 

    ID := 'RIFF';

    f.WriteBuffer(ID, 4);

    wFileSize := 0;       //пока не известен

    f.WriteBuffer(wFileSize, 4);

    ID := 'WAVE';

    f.WriteBuffer(ID, 4); //Запись идентификатора формата

 

    ID := 'fmt ';        

    f.WriteBuffer(ID, 4);                  //Запись идентификатора чанка формата

    wChankSize := SizeOf(Header);

    f.WriteBuffer(wChankSize, 4);              //Запись размера чанка

    f.WriteBuffer(Header, SizeOf(Header)); //Запись чанка формата

 

   except

    Result := HeaderWriteError;

   end;

 

   Try

    ID := 'data';

    f.WriteBuffer(ID, 4);                  //Запись чанка данных

    wChankSize := data.Data.Size;          //Запись размера чанка

    f.WriteBuffer(wChankSize, 4);          //Запись данных

    data.Data.Seek(0, soFromBeginning);

    f.CopyFrom(data.Data, data.Data.Size);

 

   except

    Result := StreamError;

   end;

   f.Seek(SizeOf(ID), soFromBeginning);     //Поиск записи размера файла

   wFileSize := f.Size - SizeOf(ID) - SizeOf(wFileSize);

   f.Write(wFileSize, 4);                   //Запись размера файла - заголовок

   f.Free;

end;

 

procedure TForm1.Button2Click(Sender: TObject);

var

  r : TWaveResult;

  i : Integer;

  d : SmallInt;

 

begin

  r.ERROR := noError;

  r.wSamplesPerSec := 44100;

  r.wBitsPerSample := 16;

  r.wChannels      := 1;

  r.Data           := TMemoryStream.Create;

  r.Data.Seek(0, soFromBeginning);

 

  For i := 1 to 10 * r.wSamplesPerSec

  do

    Begin

      d := Round(High(SmallInt) * Sin(2 * Pi * (5 * i / r.wSamplesPerSec) * (500 * i / r.wSamplesPerSec)));

      r.Data.WriteBuffer(d, 2);

    end;

  WriteWave('Sample.wav', r);

  r.Data.Free;

end;

 

 

 

Hosted by uCoz