Введение в OpenAL, и проигрывание музыкального формата OGG/Vorbis (2 стр)
Автор: Роман Марченко
Часть 2: Формат 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
Демонстрационное приложение (читайте readme.txt): Пример к статье про OpenAL: OGG/Vorbis
Надеюсь, статья вам понравилась. Все отзывы, вопросы, предложения можете присылать мне на почту или оставляйте в комментариях к статье.
31 октября 2003 (Обновление: 29 апр 2011)