понедельник, 13 июня 2011 г.

Формат OGG/Vorbis, и проигрывание его с помощью OpenAL

Ogg/Vorbis является бесплатным, открытым, не требующим лицензирования форматом для хранения цифровой аудио информации. Название состоит из 2-х имен: Ogg – имя контейнера для хранения метаданных, и vorbis – имя кодека созданного для применения в составе Ogg. Разработчики формата утверждают, что качество звучания у них лучше, чем в mp3. Я не проверял, так что этот аспект, вам предстоит проверить самостоятельно. :) Для успешного применения кодека в своих проектах, вам необходимо скачать Ogg/vorbis SDK, ссылка будет ниже.
Интеграция в музыкальный класс.
В состав SDK входит как сам кодек, так и очень облегчающая работу небольшая обёрточка называемая vorbisfile. Вы, конечно, можете использовать кодек напрямую, благо примеры есть в поставке, но это достаточно хлопотное дело, так как количество структур и функций там очень велико, и неподготовленному разуму будет довольно трудно. Так что мы пойдем по более легкому пути, и воспользуемся средством  предоставляемом разработчиком для начинающих/не хотящих лезть в дебри. :) Кстати, то, что я видел во множестве фрисурцевых проигрывателях, доказывает, что все предпочитают пользоваться именно этим средством.
Для начала подключите необходимые библиотеки и заголовочные файлы:
#include <vorbis/codec.h>
#include <vorbis/vorbisfile.h>

#pragma comment(lib, "ogg.lib")
#pragma comment(lib, "vorbisfile.lib")
Принцип работы библиотеки очень прост: открываем файл, если надо, получаем необходимую информацию о файле схожую на ID3 Tag в mp3, и данные о звуковых потоках (формат, частота и т.д.). Читаем файл или целиком, или по частям, пока не достигнем конца, и закрываем файл. Но, давайте все по порядку.
В приватный раздел нашего класса добавим несколько описаний, после чего он примет такой вид:
//…
private:
// Идентификатор источника
  ALuint      mSourceID;
  // Переменные библиотеки vorbisfile
  // Главная структура описания файла
  OggVorbis_File  *mVF;
  // Структура комментариев к файлу
  vorbis_comment  *mComment;
  // Информация о файле
  vorbis_info    *mInfo;

  // Файловый поток содержащий наш ogg файл
  std::ifstream    OggFile;
  bool      mStreamed;

  // Functions
  // Функция чтения блока из файла в буфер
  bool ReadOggBlock(ALuint BufID, size_t Size);
  // Функция открытия и инициализации OGG файла
  bool LoadOggFile (const std::string &Filename, bool Streamed);
  bool LoadWavFile (const std::string &Filename); 
Добавилось, как вы видите 4 новых переменных.
Структура OggVorbis_File содержит большое количество информации о файле (состояния, свойства) которые мы использовать напрямую не будем. Просто знайте, что эта главная структура при работе с ogg/vorbis, и её экземпляр должен передаваться во все функции библиотеки vorbisfile.
vorbis_comment может содержать любую текстовую информацию. Работа с этой структурой проста. Она содержит: массив строк - комментариев, массив длин этих комментариев, и количество комментариев. Так же, содержится отдельно строка о создателе файла. Я не показывал в коде пример работы с этой структурой, это вы запросто можете сделать самостоятельно.
vorbis_info – содержит несколько полей. Основные из них: channels – количество каналов в файле (1 – моно, 2 – стерео), и rate – частота дискретизации потока.
ifstream – поток вывода из файла с помощью которого мы будем работать с нашим контейнером музыки.
Добавилось так же 2 функции:
ReadOggBlock – функция чтения из файла Size байт данных. Если Size равно размеру файла, то происходит чтение всего файла. Считанные данные записываются в буфер OpenAL с идентификатором BufID.
LoadOggFile – открываем, и инициализируем Ogg файл, в зависимости от входных параметров. Как вы видите, эта функция уже поддерживает потоковое проигрывание.
Раскомментируем блок чтения Ogg файла в процедуре Open:
  if (Ext == "OGG") 
  {
    mStreamed = Streamed;
    return LoadOggFile(Filename, Streamed);
  }
Теперь подробно разберем функцию инициализации.
bool remSnd::LoadOggFile(const string &Filename, bool Streamed)
{
  int        i, DynBuffs = 1, BlockSize;
  // OAL specific
  SndInfo      buffer;
  ALuint      BufID = 0;
  // Структура с функциями обратного вызова.
  ov_callbacks  cb;

  // Заполняем структуру cb
  cb.close_func  = CloseOgg;
  cb.read_func  = ReadOgg;
  cb.seek_func  = SeekOgg;
  cb.tell_func  = TellOgg;

  // Создаем структуру OggVorbis_File
  mVF = new OggVorbis_File;

  // Открываем OGG файл как бинарный
  OggFile.open(Filename.c_str(), ios_base::in | ios_base::binary);

  // Инициализируем файл средствами vorbisfile
  if (ov_open_callbacks(&OggFile, mVF, NULL, -1, cb) < 0)
  {
    // Если ошибка, то открываемый файл не является OGG
    return false;
  }

// Начальные установки в зависимости от того потоковое ли проигрывание 
// затребовано
  if (!Streamed)
  {
    for (TBuf::iterator i = Buffers.begin(); i != Buffers.end(); i++)
    {
      if (i->second.Filename == Filename) BufID = i->first;
    }
    // Размер блока – весь файл
    BlockSize = ov_pcm_total(mVF, -1) * 4;
  }
  else
  {
    // Размер блока задан
    BlockSize  = DYNBUF_SIZE;
    // Количество буферов в очереди задано
    DynBuffs  = NUM_OF_DYNBUF;
    alSourcei(mSourceID, AL_LOOPING, AL_FALSE);
  }
  
  // Получаем комментарии и информацию о файле
  mComment    = ov_comment(mVF, -1);
  mInfo      = ov_info(mVF, -1);

  // Заполняем SndInfo структуру данными
  buffer.Rate    = mInfo->rate;
  buffer.Filename  = Filename;
  buffer.Format = (mInfo->channels == 1) ? AL_FORMAT_MONO16 
               : AL_FORMAT_STEREO16;

  // Если потоковое проигрывание, или буфер со звуком не найден то
  if (Streamed || !BufID)
  {
    for (i = 0; i < DynBuffs; i++)
    {
      // Создаем буфер
      alGenBuffers(1, &buffer.ID);
      if (!CheckALError()) return false;
      Buffers[buffer.ID] = buffer;
      // Считываем блок данных
      ReadOggBlock(buffer.ID, BlockSize);
      if (!CheckALError()) return false;

      if (Streamed) // Помещаем буфер в очередь.
      {
        alSourceQueueBuffers(mSourceID, 1, &buffer.ID);
        if (!CheckALError()) return false;
      }
      else 
        alSourcei(mSourceID, AL_BUFFER, buffer.ID);
    }
  }
  else
  {
    alSourcei(mSourceID, AL_BUFFER, Buffers[BufID].ID);
  }

  return true;
}
Сначала инициализируется структура ov_callbacks. Эта структура содержит указатели на 4 функции работы с источником данных: чтение, поиск, закрытие, и сообщение о текущем месте положения читающего указателя. Вся эта суета от того, что функция ov_open(), библиотеки vorbisfile, работает только с stdin, stdout. Это далеко не всегда удобно и приемлемо. Поэтому, разработчики предложили средство для работы с любыми источниками данных (будь то поток, как в нашем случае, или ваша собственная структура). Единственное неудобство при этом – вы самостоятельно должны будете реализовать вышеназванные 4 функции для работы с вашим контейнером данных. В их реализации нет ничего сложного, в этом вы сами можете убедиться, посмотрев код:
size_t ReadOgg(void *ptr, size_t size, size_t nmemb, void *datasource)
{
  istream *File = reinterpret_cast<istream*>(datasource);
  File->read((char *)ptr, size * nmemb);
  return File->gcount();
}

int SeekOgg(void *datasource, ogg_int64_t offset, int whence)
{
  istream *File = reinterpret_cast<istream*>(datasource);
  ios_base::seekdir Dir;
  File->clear();
  switch (whence) 
  {
    case SEEK_SET: Dir = ios::beg;  break;
    case SEEK_CUR: Dir = ios::cur;  break;
    case SEEK_END: Dir = ios::end;  break;
    default: return -1;
  }
  File->seekg((streamoff)offset, Dir);
  return (File->fail() ? -1 : 0);
}

long TellOgg(void *datasource)
{
  istream *File = reinterpret_cast<istream*>(datasource);
  return File->tellg();
}

int CloseOgg(void *datasource)
{
  return 0;
}
Всем функциям передается в качестве параметра datasource - указатель на объект-хранилище данных. Так как у нас это ifstream, то мы сразу же приводим указатель к этому типу. Самая интересная функция в этом квартете – это SeekOgg(). Вас может смутить строчка File->clear(). Но это не очищение файла, а сброс флагов состояния объекта ifstream. Необходимо сказать, что функция Seek должна уметь реагировать на указатели позиции файла SEEK_SET(начало), SEEK_СUR(текущая позиция), SEEK_END(конец).
Давайте, вернемся к нашему барану – функцииLoadOggFile().
Как можно заметить, при открытии файла используется функция ov_open_callbacks(), которой передается: адрес на контейнер с данными, адрес структуры OggVorbis_File и структура ov_callbacks с адресами функций.
Далее в нашей инициализирующей процедуре происходит подготовка данных к дальнейшей работе в зависимости от затребованного режима воспроизведения звука – потоковый, или нет. Если проигрывание не потоковое, то, так же как и в случае с wav файлами, мы ищем в массиве Buffers уже существующий буфер с заданным звуком и устанавливаем размер данных для чтения из OGG файла равной длине всего файла. Это достигается путём произведения количества семплов несжатого файла (вызовом функции ov_pcm_total()), на длину семпла. Если же наш файл должен проигрываться в потоковом режиме, то мы устанавливаем количество динамических буферов, и длину каждого буфера. Далее идет сохранение данных о звуке.
И вот мы добрались до, собственно, реализации технологии потокового проигрывания в OpenAL. Эта библиотека предоставляет нам механизм поочередного проигрывания буферов. Источнику можно, вместо единственного буфера, проассоциировать очередь буферов, которые будут проигрываться последовательно. Алгоритм обновления данных в буферах мы рассмотрим чуть ниже в функции Update.
Таким образом, в инициализации, мы заполняем динамические буфера данными, и добавляем их в очередь источника, посредством вызова функции alSourceQueueBuffers(), которой передаем: идентификатор источника, количество буферов для добавления, и их идентификаторы.
Теперь давайте рассмотрим функцию ReadOggBlock().
bool remSnd::ReadOggBlock(ALuint BufID, size_t Size)
{
  // Переменные
  char    eof = 0;
  int    current_section;
  long    TotalRet = 0, ret;
  // Буфер данных
  char    *PCM;

  if (Size < 1) return false;
  PCM = new char[Size];

  // Цикл чтения
  while (TotalRet < Size) 
  {
    ret = ov_read(mVF, PCM + TotalRet, Size - TotalRet, 0, 2, 1, 
             & current_section);

    // Если достигнут конец файла
    if (ret == 0) break;
    else if (ret < 0)     // Ошибка в потоке
    {
      //
    }
    else
    {
      TotalRet += ret;
    }
  }
  if (TotalRet > 0)
  {
    alBufferData(BufID, Buffers[BufID].Format, (void *)PCM, 
           TotalRet, Buffers[BufID].Rate);
    CheckALError();
  }
  delete [] PCM;
  return (ret > 0);
}
Вся «соль» функции находится в процедуре ov_read(), которая считывает данные порциями, и возвращает количество прочитанных данных. Если возвращает 0, то достигнут конец файла. Затем мы записываем данные в буфер.
Смена буферов по мере проигрывания звука очень важная задача. Давайте, посмотрим сначала на код.
void remSnd::Update()
{
  if (!mStreamed) return;
  
  int      Processed = 0;
  ALuint      BufID;

  // Получаем количество отработанных буферов
  alGetSourcei(mSourceID, AL_BUFFERS_PROCESSED, &Processed);

  // Если таковые существуют то
  while (Processed--)
  {
    // Исключаем их из очереди
    alSourceUnqueueBuffers(mSourceID, 1, &BufID);
    if (!CheckALError()) return;
    // Читаем очередную порцию данных и включаем буфер обратно в очередь
    if (ReadOggBlock(BufID, DYNBUF_SIZE) != 0)
    {
      alSourceQueueBuffers(mSourceID, 1, &BufID);
      if (!CheckALError()) return;
    }
    else // Если конец файла достигнут
    {
      // «перематываем» на начало
      ov_pcm_seek(mVF, 0);
      // Добавляем в очередь
      alSourceQueueBuffers(mSourceID, 1, &BufID);
      if (!CheckALError()) return;
      
      // Если не зацикленное проигрывание то стоп
      if (!mLooped) Stop();
    }
  }
}
Весь алгоритм построен на анализе состояний буферов в очереди. Буфер может находится в 3-х состояниях: UNUSED (не использует ни одним источником), PROCESSED (уже проигран), PENDING (проассоциирован к источнику, но еще не проигран). Так вот, функция alGetSourcei(mSourceID, AL_BUFFERS_PROCESSED, &Processed) в переменную Processed возвращает количество буферов очереди в состоянии PROCESSED. Далее мы пробегаем по всем этим буферам. Каждого исключаем из буфера, заполняем новой порцией данных, и снова добавляем в конец очереди. Реализуется нечто, наподобие конвейера.
Так же, не забудьте немного изменить метод Close() нашего класса:
void remSnd::Close()
{
  alSourceStop(mSourceID);
  if (alIsSource(mSourceID)) alDeleteSources(1, &mSourceID);
  if (!mVF)
  {  
    ov_clear(mVF);
    delete mVF;
  }
}
Здесь мы деинициализируем структуру OggVorbis_File, и освобождаем память.
Вот и всё. Использование для потоковых OGG файлов такое же, как и для wav, с единственным отличием – необходимо периодически вызвать функцию Update. Хочется отметить, что варьируя количество динамических буферов в очереди и их размер, можно регулировать потребляемые ресурсы процессора и памяти для работы с потоковым звуком.
Заметьте, что класс получился расширяемый, и вы сами можете попробовать добавить свои форматы аудио файлов.
Полезные ссылки.
1)  Сайт Vorbis консорциума. Там можно найти FAQ, примеры и статьи: http://www.vorbis.com/
2)  Сайт для разработчиков: http://www.xiph.org/ogg/vorbis/, там же вы найдёте Ogg/Vorbis SDK

Комментариев нет:

Отправить комментарий