Главная » Программирование звука » Чтение файлов формата WAVE

0

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

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

Хотя такой подход оправдан, он имеет серьезный недостаток: необходимо, чтобы сразу был доступен весь файл. B частности, если планируется воспользоваться потоковым распространением файла по сети и проигрыванием его по мере получения  данных,  нельзя  просмотреть  весь  файл  и  локализовать  области,  не  начав проигрывание.

B моей версии класса WaveRead реализован иной подход, частично для обеспечения  использования  потокового  аудио,  частично  из-за  простоты.  Вместо  того чтобы заносить на «карту» расположение каждого блока, я читаю файл от начала до конца и обрабатываю каждый блок по мере его появления. Тем не менее мот существовать  файлы  WAVE,  которые  с  помощью  данной  программы  проигран нельзя.

Контейнеры

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

_currentChunk определяет  текущее  положение  вершины  стека;  -1  означает.что

стек пуст.

Листинг 17.4. Члены класса WaveRead

private:

struct {

// Стек блоков  WAVE.

unsigned long type;          // Тип  блока. unsigned long size;          // Размер блока.

unsigned long remaining;    // Осталось прочесть  байтов. bool isContainer;            // Истина, если это контейнер. unsigned long containerType; // Тип  контейнера.

} _chunk[5];

int _currentChunk;              // Вершина   стека.

void NextChunk(void);

Метод  NextChunk является  основным  для  этого  класса,  притом  достаточно большим по размеру. Данный метод сначала пропускает остаток текущей области и  соответствующим  образом  обновляет  стек.  Затем  читается  заголовок  следующей области и принимается решение, что с ней делать.

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

Листинг 17.5. Реализация класса WaveRead

void WaveRead::NextChunk(void) {

if (_stream.eof()) {

// Считываем  следующий  блок.

_currentChunk = 1;  // Очищаем  стек. return;

}

unsigned long type = ReadIntMsb(_stream,4); unsigned long size = ReadIntLsb(_stream,4); if (_stream.eof()) {

_currentChunk = 1;  // Очищаем  стек.

return;

}

_currentChunk++;

// Помещаем   этот  блок в стек.

_chunk[_currentChunk].type   =   type;

_chunk[_currentChunk].size   =   size;

_chunk[_currentChunk].remaining  =  size;

_chunk[_currentChunk].isContainer   =   false;

_chunk[_currentChunk].containerType   =   0;

char code[5] = "CODE";

code[0] = (type>>24)&255;code[1] = (type>>16)&255;

code[2] = (type>>8 )&255;code[3] = (type )&255;

// Игнорируем блок  неопознанного  типа…

cerr << "Ignoring unrecognized `" << code << "" chunk\n";

}

Один  из  основных  принципов  RIFF  и  схожих  форматов   обязательная  возможность  пропустить  любой  блок,  который  не  удается  распознать.  B  нашем  случае  нераспознанная  область  не  может  быть  прочитана  когда-либо  еще.  Поэтому до  попытки  прочтения  следующего  блока  мы  должны  позаботиться  о  том,  чтобы пропустить  оставшуюся  часть  текущего.  Пропустив  блок,  нужно  также  проверить, корректным  ли  образом  мы  вычислили  размер  контейнера,  включающего  в  себя данный блок.

Есть  одна  важная  небольшая  деталь.  B  файлах  RIFF  (как  и  во  всех  форматах файлов,  являющихсяпроизводными  от  IFF)  любой  блок  занимает  четное  количество  байтов.  Если  реальный  размер  блока  нечетный,  значит  там  присутствует  дополнительный заполняющий байт, который необходимо пропустить.

Листинг 17.6. Пропускаем остальную часть блока

if ((_currentChunk >= 0) && (!_chunk[_currentChunk].isContainer))

{

unsigned long lastChunkSize = _chunk[_currentChunk].size;

if (lastChunkSize & 1) {  // Есть байт-заполнитель?

_chunk[_currentChunk].remaining++;

lastChunkSize++;   // Учитываем заполнение   при   обновлении

// контейнера.

}

SkipBytes(_stream,_chunk[_currentChunk].remaining);

// Отбрасываем  блок.

_currentChunk–;      // Выкидываем блок  из  стека.

// Контрольная  проверка: блок-оболочка

// должен быть  контейнером.

if ((_currentChunk < 0) || (!_chunk[_currentChunk].isContainer)) {

// Блок заключен  не   в контейнере?

cerr << "Chunk contained in non-Container?!?!\n";

exit(1);

}

// Уменьшаем  размер контейнера.

if (_currentChunk >= 0) {

// Санитарная  проверка: убедимся, что

// размер контейнера достаточен.

// Кроме   того, избежим  по-настоящему

// неприятных ситуаций  переполнения.

if ((lastChunkSize+8) > _chunk[_currentChunk].remaining) {

// Ошибка: размеры блока  не   позволяют

// разместить его  в контейнере.

cerr << "Error: Chunk is too large to fit in container!?!?\n";

_chunk[_currentChunk].remaining = 0; // Контейнер  пуст.

} else

_chunk[_currentChunk].remaining = lastChunkSize + 8;

}

}

Блок,  который  мы  только  что  обработали,  возможно,  был  последним  в  своем

контейнере.  Этот  контейнер,  в  свою  очередь,  мог  быть  последним  блоком  в  другом  контейнере  и  т.д.  Заметьте,  что  любой  контейнер,  содержащий  менее  8  байт, должен  быть  завершен,  так  как  для  записи  заголовка  блока  требуется,  по  меньшей мере,  8  байт.  За  исключением  этой  детали,  следующий  фрагмент  кода  программы практически не отличается от предыдущего.

Листинг 17.7. Отбрасываем законченные контейнеры

// Если   есть формы, которые также

// окончены, отбросим  их. while ( (_currentChunk >= 0) // Имеется  блок.

&& (_chunk[_currentChunk].remaining < 8)

)

{

SkipBytes(_stream,_chunk[_currentChunk].remaining);

// Отбрасываем  его.

unsigned long lastChunkSize = _chunk[_currentChunk].size;

_currentChunk–;          // Отбрасываем  контейнерный  блок.

// Санитарная  проверка: блок, который включает

// текущий, должен быть  контейнером.

if (!_chunk[_currentChunk].isContainer) {

// Контейнер блока  не   является контейнером?

cerr << "Chunk contained in non-container?!?!\n";

exit (1) ;

}

// Уменьшаем  размер контейнера.

if (_currentChunk >= 0) {

if ((lastChunkSize+8) > _chunk[_currentChunk].remaining) {

// Ошибка  в WAVE-файле: блок  слишком большой

// и  не   подходит!

cerr << "Error in WAVE file: Chunk is too large to fit!?!?\n";

lastChunkSize = _chunk[_currentChunk].remaining;

}

_chunk[_currentChunk].remaining = lastChunkSize + 8;

}

}

Метод  NextChunk также  отвечает  за  обработку  данных  внутри  распознанной области. Об этом мы подробнее поговорим в следующих разделах.

Koнтeйнep RIFF WAVE

Файл  WAVE  содержит  единственный  контейнер  RIFF,  который,  в  свою  очередь, включает в себя все остальные блоки файла. Я решил считывать наружный блок во время инициализации.

Листинг 17.8. Инициализация объекта WaveRead

_currentChunk = 1; // Очищаем  стек. NextChunk();

// Проверяем,  что  первый блок  -

// это  контейнер RIFF/WAVE.

if ( (_currentChunk != 0)

|| (_chunk[0].type != ChunkName(‘R’,’I’,’F’,’F’))

|| (_chunk[0].isContainer != true)

|| (_chunk[0].containerType != ChunkName(‘W’,’A’,’V’,’E’))

)

{

// Внешний   блок  в  WAVE-файле не   является

// RIFF-блоком!

cerr << "Outermost chunk in WAVE file isn’t RIFF!!";

exit(1);

}

Даже  если  текущий  блок  не  является  блоком  RIFF,  мы  знаем,  что  внешний всегда должен принадлежать к этому типу.

Листинг 17.9. Обработка WAVE-блока конкретного формата и возвращение

if ((_currentChunk >= 0) &&

(_chunk[0].type != ChunkName(‘R’,’I’,’F’,’F’))){

// Наружный   блок не   является RIFF-блоком?

cerr << "Outermost chunk is not RIFF?!?!\n";

_currentChunk = 1;

return;

}

B  рамках  метода  NextChunk RIFF-блок  легко  обработать.  Нужно  только  по-

метить его как контейнер и прочитать в контейнерный тип.

Листинг 17.9. Обработка WAVE-блока конкретного формата и возвращение

(продолжение)

if (type == ChunkName(‘R’,’I’,’F’,’F’)) {

_chunk[_currentChunk].isContainer = true;

// Сначала необходимо проверить  размер  контейнера.

_chunk[_currentChunk].containerType = ReadIntMsb(_stream,4);

_chunk[_currentChunk].remaining = 4;

if (_currentChunk > 0) {

// RIFF-блок замечен   на   внутреннем  уровне?

cerr << "RIFF chunk seen at inner level?!?!\n";

}

return;

}

Блок fmt

Блок  fmt содержит  информацию  о  реально  используемом  звуковом  формате. Точное  содержимое  области  fmt варьируется  в  зависимости  от  метода  компрессии.  B  табл.  17.2  показан  формат,  применяемый  для  чистых  данных  ИКМ.  Другие технологии   сжатия   расширяют  этот   блок,   помещая   в   него   дополнительную   информацию.

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

Таблица 17.2. Содержимое области блока fmt для данных ИКМ

Размер                     Описание

2                                     Код компрессии (см. табл. 17.3)

2                                     Количество каналов

4                                     Отсчетов в секунду

4                                     Среднее количество байтов в секунду

2                                     Выравнивание блока

2                                     Значащих битов на отсчет

2                                     Количество байтов дополнительной информации

n                                     Дополнительная информация, связанная с конкретным компрессором

Листинг 17.9. Обработка WAVE-блок конкретного формата и возвращение

(продолжение)

if (type == ChunkName(‘f’,’m’,’t’,’ ‘)) {

if (_currentChunk != 1) {

// FMT-блок  встретился не   на   том   уровне?

cerr << "FMT chunk seen at wrong level?!?!\n";

}

_formatData = new unsigned char[size+2];

_stream.read(reinterpret_cast<char *>(_formatData),size);

_formatDataLength = _stream.gcount();

_chunk[_currentChunk].remaining = 0;

return;

}

Нам нет необходимости анализировать данные о формате до тех пор, пока ктонибудь  не  запросит  информацию  о  файле.  Например,  когда  запрашивают  частоту дискретизации  или  количество  каналов,  мы  можем  считать  соответствующие  байты непосредственно из сохраненных данных о формате. Аналогичным образом, если кто-то запросит отсчеты, необходимо убедиться, что объект декомпрессора уже проинициализирован.     Метод     InitializeDecompression  гарантирует     прочтение блока fmt и, кроме того, инициализирует декомпрессор.

Листинг 17.4. Члены класса WaveRead (продолжение)

private:

void MinMaxSamplingRate(long *min, long *max, long *preferred);

void MinMaxChannels(int *min, int *max, int *preferred); size_t GetSamples(AudioSample *buffer, size_t numSamples); void InitializeDecompression();

Листинг 17.5. Реализация класса WaveRead (продолжение)

void WaveRead::MinMaxSamplingRate(long *min, long *max, long

*preferred) { InitializeDecompression();

unsigned long samplingRate = BytesToIntLsb(_formatData+4,4);

*max = *min = *preferred = samplingRate;

}

void WaveRead::MmMaxChannels(int *min, int *max, int *preferred)

{

InitializeDecompression();

unsigned long channels = BytesToIntLsb(_formatData+2,2);

*min = *max = *preferred = channels;

}

size_t WaveRead::GetSamples(AudioSample *buffer, size_t numSamples)

{

if (!_decoder) InitializeDecompression();

return _decoder->GetSamples(buffer,numSamples);

}

Создание объекта декомпрессора

Фирмой Microsoft было  зарегистрировано  для использования в  файлах WAVE почти 100 кодов компрессии. Список кодов определен в заголовке файла mmreg.h, включенном  в  текущую  версию  средств  разработки  Microsoft.  B  табл.  17.3  перечисляются некоторые из наиболее важных кодов.

При выборе декодера я первым делом убеждаюсь в том, что блок fmt был прочитан.  Затем,  используя  код  компрессии,  я  пытаюсь  создать  подходящий  объект декомпрессии.

Листинг 17.5. Реализация класса WaveRead (продолжение)

void WaveRead::InitializeDecompression() {

if (_decoder) return;

// Убедимся, что  читаем блок fmt.

while (!_formatData) { NextChunk();

if (_currentChunk < 0) {

// He найден  блок  fmt.

cerr << "No ‘fmt’ chunk found?!?!\n";

exit (1) ;

}

}

// Основываясь  на   типе  компрессии, подбираем

// декомпрессор.

unsigned long type = BytesToIntLsb(_formatData+0, 2);

if (!_decoder) {

// Этот  тип   компрессии   WAVE-файла не   поддерживается.

cerr << "I don’t support WAVE compression type " << type <<

"\n";

Таблица 17.3. Примеры кодов формата WAVE

Код                                             Описание

0                                                  Неизвестен/недопустим

1                                                  ИКМ

2                                                  Microsoft АДИКМ

6                                                  ITU G.711 А-функция

7                                                  ITU G.711 мю-функция

17                                               IMA ADPCM

20                                                ITU G.723 АДИКМ

49                                                GSM 6.10

64                                                ITU G.721 АДИКМ

80                                                MPEG

65535                                          Экспериментальный

exit (1);

}

}

Данные ИКМ

Большинство файлов  WAVE  используют  данные  в  формате чистой ИКМ.  Отсчеты размером 8 бит или менее сохраняются как беззнаковые данные; большие хранятся как знаковые.

Листинг 17.10. Подбор декомпрессора для WAVE-файла исходя из типа компрессии

if (type == 1) {                  // Формат  ИКМ.

unsigned long bitsPerSample = BytesToIntLsb(_formatData+14,

2);

if (bitsPerSample <= 8)        // B формате  WAVE 8-битные

// данные записываются

// в беззнаковом формате.

_decoder = new DecompressPcm8Unsigned(*this);

else if (bitsPerSample <= 16)  // 16-битные данные со  знаком.

_decoder = new DecompressPcml6LsbSigned(*this);

}

Данные IMA ADPCM

Microsoft   использует   собственный   вариант   компрессии   IMA   ADPCM.   При этом для указания длины каждого  пакета звуковых данных  2 байта добавляются кданным, приведенным в табл. 17.2.

Листинг 17.10. Подбор декомпрессора для WAVE-файла исходя из типа компрессии (продолжение)

if (type == 17) {    // Формат   IMA ADPCM.

unsigned long bitsPerSample = BytesToIntLsb(_formatData+14, 2);

if (bitsPerSample != 4) {

// Для  IMA ADPCM требуется 4 бита  на

// отсчет, а не   …

cerr << "IMA ADPCM requires 4 bits per sample, not ";

cerr << bitsPerSample << "\n";

exit (1) ;

}

if (_formatDataLength < 20) {

// Для  IMA ADPCM требуется  дополнительная

// информация  о  декомпрессии.

cerr  <<  "IMA ADPCM requires additional decompression data.\n";

exit(1) ;

}

int packetLength = BytesToIntLsb(_formatData+18,2);

int channels = BytesToIntLsb(_formatData+2,2);

_decoder = new

DecompressImaAdpcmMs(*this,packetLength,channels);

}

Функции ? и А

B  файлах  WAVE  можно  использовать  компрессию  посредством  компрессии мюи А-функций.

Листинг 17.10. Подбор декомпрессора для WAVE-файла исходя из типа компрессии (продолжение)

if (type == 6) {

_decoder = new DecompressG711ALaw(*this);

}

if (type == 7) {

_decoder = new DecompressG711MuLaw(*this);

}

Другие методы сжатия

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

Листинг 17.10. Подбор декомпрессора для WAVE-файла исходя из типа компрессии (продолжение)

if (type == 2) {

// Компрессия  MS ADPCM не   поддерживается.

cerr << "I don’t support MS ADPCM compression.\n";

}

Блок data

Блок  data  хранит  сжатые  звуковые  данные.  Нет  необходимости  делать  с  ним что-либо в рамках метода NextChunk.

Листинг 17.9. Обработка WAVE-блока конкретного формата и возвращение (продолжение)

if (type == ChunkName(‘d’ , ‘а’ , ‘t’ , ‘а’)) {

return;

}

Здесь  важен  метод  ReadBytes.  Когда  кто-либо  запрашивает  звуковые  дан-

ные,  сначала  нужно  убедиться,  что  чтение  осуществляется  из  области  data,  a  за-

тем извлечь соответствующее количество байтов и вернуть управление.

Листинг 17.4. Члены класса WaveRead (npoдoлжeние)

public:

size_t ReadBytes(AudioByte *buffer, size_t numSamples);

Листинг 17.5. Реализация класса WaveRead (npoдoлжeниe)

size_t WaveRead::ReadBytes(AudioByte *buffer, size_t numBytes) {

while (_chunk[_currentChunk].type != ChunkName(‘d’ , ‘а’ , ‘t’ , ‘а’)) {

NextChunk();

if (_currentChunk < 0) {

// Никаких звуковых  данных не   обнаружено.

cerr << "I didn’t find any sound data!?!?\n";

return 0;

}

}

if (numBytes > _chunk[_currentChunk].remaining)

numBytes = _chunk[_currentChunk].remaining;

_stream.read(reinterpret_cast<char *>(buffer), numBytes);

numBytes = _stream.gcount();

_chunk[_currentChunk].remaining = numBytes;

return numBytes;

}

Текстовые блоки

Многие  блоки  содержат  текстовые  аннотации.  Эти  блоки  могут  появляться в любом типе файла RIFF, не только в WAVE. Все их названия начинаются с символа  I  в  верхнем  регистре,  который  показывает,  что  блоки  используются  в  информационных целях.

Листинг 17.11. Обработка WAVE-блока непатентованного формата и возвращение

if ((type & 0xFF000000) == ChunkName("I",0,0,0)) { // Первый

// символ  "I"?

char *text = new char[size+2];

_stream.read(text,size) ;

long length = _stream.gcount();

_chunk[_currentChunk].remaining = length;

text[length] = 0;

if (type == ChunkName(‘I’,’C’,’M’,’T’))        // Комментарий.

cerr << "Comment: ";

else if (type == ChunkName(‘I’,’C’,O’,’P’))    // Авторские

// права.

cerr << "Copyright: ";

else if (type == ChunkName(‘I’,’N’,’A’,’M’))   // Название

// произведения.

cerr << "Title: ";

else if (type == ChunkName(‘I’,’A’,’R’,’T’))   // Исполнитель.

cerr << "Artist: ";

else

cerr << "Text: ";     // Информационные  блоки другого  типа.

cerr << text << "\n";

return;

}

Источник: Кинтцель Т.  Руководство программиста по работе со звуком = A Programmer’s Guide to Sound: Пер. с англ. М.: ДМК Пресс, 2000. 432 с, ил. (Серия «Для программистов»).

По теме:

  • Комментарии