Ранее писал уже про OpenAL. В Кубике мы использовали именно OpenAL (игра была изначально написана под iOS). Дабы не переписывать весь код по работе со звуком, мы не стали переписать всё на OpenSL ES. В игре использовалось не так много звуков, поэтому проблем с ними не было. Некоторые, правда, жаловались на задержки при воспроизведении, но в целом всё было довольно неплохо. В Снежке же у нас использовалось много звуков (специфика игры обязывает), вот тут-то мы и столкнулись с большой проблемой. Было решено переписать всё на OpenSL ES. Для этого я написал парочку враперов, которыми решил и поделиться. Так же решил небольшой экскурс провести, так сказать Quick Start в OpenSL ES (:
- Краткое описание OpenSL ES.
- Аудио контент.
- Немного про обёртки.
- Принцип работы с объектами.
- Инициализация библиотеки (контекста).
- Работа со звуками.
- Проигрывание PCM.
- Проигрывание сжатых форматов.
- Заключение.
- Доп. информация.
Краткое описание OpenSL ES
Доступно сие дело с Android API 9 (Android 2.3) и выше. Некоторые возможности доступны лишь в Android API 14 (Android 4.0) и выше. OpenSL ES предоставляет интерфейс на языке С, который также можно вызывать из C++, предоставляющий те же возможности, что и части Android Java API по работе со звуками:
Примечание: хотя оно и основано на OpenSL ES, это API не является полной реализацией любого профиля из OpenSL ES 1.0.1.
Либа, как вы могли догадаться, написана на чистом C. По-сему полноценного ООП там нет. Используются специальные структуры (назовём их псевдообъектно-ориентированными структуры (: ), которые представляют собой обычные структуры языка C, содержащие указатели на функции, получающие первым аргументом указатели на саму структуру. Что-то вроде this
в С++, но явно. В OpenSL ES два вида таких структур:
- Объект (
SLObjectItf
) – абстракция набора ресурсов, предназначенная для выполнения определенного круга задач и хранения информации об этих ресурсах. При создании объекта определяется его тип, определяющий круг задач, которые можно решать с его помощью. - Интерфейс (
SLEngineItf
,SLSeekItf
и тд) – абстракция набора взаимосвязанных функциональных возможностей, предоставляемых конкретным объектом. Интерфейс включает в себя множество методов, используемых для выполнения действий над объектом. Интерфейс имеет тип, определяющий точный перечень методов, поддерживаемых данным интерфейсом. Интерфейс определяется его идентификатором, который можно использовать в коде для ссылки на тип интерфейса (напримерSL_IID_VOLUME
,SL_IID_SEEK
). Все константы и названия интерфейсов довольно очевидные, так что проблем особых возникнуть не должно.
Если обобщить: объекты используются для выделения ресурсов и получения интерфейсов. А уже потом с помощью этих интерфейсов происходит работа с объектом. Один объект может иметь несколько интерфейсов (для изменения громкости, для изменения позиции т.п.). В зависимости от устройства (или типа объекта), некоторые интерфейсы могут быть недоступны. Скажу наперёд, вы можете стримить аудио из вашей директории assets, используя SLDataLocator_AndroidFD
, который поддерживает интерфейс для перемещения позиции по треку. В тоже время, вы можете загрузить файл целиком в буфер (используя SLDataLocator_AndroidFD
), и проигрывать уже оттуда. Но этот объект не поддерживает интерфейс SL_IID_SEEK
, посему переместиться по треку не получится =/
Аудио контент
Есть много способов, чтобы упаковать аудио-контент в приложение:
- Resources. Размещая аудио файлы в res/raw/ директории, можно легко получить к ним доступ с помощью API для Resources. Однако нет прямого нативного доступа к этим ресурсам, поэтому вам придётся их скопировать из Java кода.
- Assets. Размещая аудио файлы в директории assets/, вы сможете получить к ним доступ из C++ с помощаью нативного менеджера. См. хэдеры android/asset_manager.h и android/asset_manager_jni.h для дополнительной информации.
- Сеть. Можно использовать URI data locator для проигрывания аудио непосредственно из сети. Не забываем про необходимые пермишены для этого (:
- Локальная файловая система. The URI data locator поддерживает схему file: для доступа к локальным файлам, при условии, что файлы доступны приложению (ну, то есть, прочитать файлы из внутреннего хранилища другого приложения не получится). Обратите внимание, что в Android доступ к файлам ограничивается с помощью механизмов Linux user ID и group ID.
- Запись. Ваше приложение может записывать аудио с микрофона, сохранить контент, а позже проиграть.
- Compiled and linked inline. Вы можете непосредственно запихать аудио контент в библиотеку, а затем проиграть с помощью buffer queue data locator. Это очень хорошо подходит для коротких композиций в PCM формате. PCM данные конвертируются в hex строку с использование bin2c tool.
- Генерация в реальном времени. Приложение может генерировать (синтезировать) данные PCM на лету, а затем воспроизводить с помощью buffer queue data locator.
Немного про мои обёртки
Я вообще поклонник ООП, поэтому стараюсь как-то сгруппировать определённый функционал Си-методов и обернуть своими классами, чтобы в дальнейшем было удобно работать. По аналогии с тем, как я это делал для OpenAL, появились классы:
OSLContext
. Он ответственен за инициализацию библиотеки и создание экземпляров нужных буферов.OSLSound
. Базовый класс для работы со звуками.OSLWav
. Класс для работы с WAV. Наследуется отOSLSound
, чтобы сохранить общий интерфейс для работы. Для работы с ogg можно потом создать классOSLOgg
, как я в OpenAL делал. Такое разграничение сделал, так как у этих форматов кардинально отличается процесс загрузки. WAV — чистый формат, там достаточно просто прочитать байты, ogg же надо ещё декомпрессить с помощью Vorbis, про mp3 вообще молчу (:OSLMp3
. Класс для работы с Mp3. Наследуется отOSLSound
, чтобы сохранить общий интерфейс для работы. Класс вообще ничего почти не реализует у меня, потому что mp3 стримлю. Но если захотите декодировать mp3 с помощью какого-нибудь lame или ещё чего-нить, то в методеload(char* filename)
можете реализовать декодирование и использовать BufferPlayer.OSLPlayer
. Собственно, основной класс по работе со звуком. Дело в том, что механизм работы в OpenSL ES не такой как в OpenAL. В OpenAL есть специальная структура для буфера и источника звука (на который мы навешиваем буфер). В OpenSL ES же всё крутится вокруг плейеров, которые бывают разные.OSLBufferPlayer
. Используем этот плейер, когда хотим загрузить файл целиком в память. Как правило, используется для коротеньких звуковых эффектов (выстрел, взрыв и т.п.). Как уже говорил, не поддерживает интерфейсSL_IID_SEEK
, посему переместиться по треку не получится.OSLAssetPlayer
, позволяет стримить из директории assets (то есть, не грузить весь файл в память). Использовать для проигрывания длинных треков (музыки фоновой, например).
Принцип работы с объектами
Весь цикл работы с объектами примерно такой:
- Получить объект, указав желаемые интерфейсы.
- Реализовать его, вызвав
(*obj)->Realize(obj, async)
. - Получить необходимые интерфейсы вызвав (*obj)-> GetInterface (obj, ID, &itf);
- Работать через интерфейсы.
- Удалить объект и очистить используемые ресурсы, вызвав
(*obj)->Destroy(obj)
.
Инициализация библиотеки (контекста)
Для начала необходимо добавить в секцию LOCAL_LDLIBS файла Android.mk в jni директории флаг lOpenSLES: LOCAL_LDLIBS += -lOpenSLES
и два заголовочных файла подключить:
#include <SLES/OpenSLES.h> #include <SLES/OpenSLES_Android.h>
Теперь необходимо создать объект, через который будем работать с библиотекой, (что-то аналогичное контексту в OpenAL) с помощь метода slCreateEngine
. Полученный объект становится центральным объектом для доступа к OpenSL ES API. Далее инициализируем объект с помощью метода Realize
.
result = slCreateEngine(&engineObj, //pointer to object 0, // count of elements is array of additional options NULL, // array of additional options lEngineMixIIDCount, // interface count lEngineMixIIDs, // array of interface ids lEngineMixReqs); if (result != SL_RESULT_SUCCESS ) { LOGE("Error after slCreateEngine"); return; } result = (*engineObj)->Realize(engineObj, SL_BOOLEAN_FALSE ); if (result != SL_RESULT_SUCCESS ) { LOGE("Error after Realize"); return; }
Теперь необходимо получить интерфейс SL_IID_ENGINE
, через который будет осуществляться доступ к динамикам, проигрыванию звуков и тд.
result = (*engineObj)->GetInterface(engineObj, SL_IID_ENGINE, &engine); if (result != SL_RESULT_SUCCESS ) { LOGE("Error after GetInterface"); return; }
Теперь остаётся получить и инициализировать объект OutputMix
для работы с динамиками с помощью метода CreateOutputMix
:
result = (*engine)->CreateOutputMix(engine, &outputMixObj, lOutputMixIIDCount, lOutputMixIIDs, lOutputMixReqs); if(result != SL_RESULT_SUCCESS){ LOGE("Error after CreateOutputMix"); return; } result = (*outputMixObj)->Realize(outputMixObj, SL_BOOLEAN_FALSE); if(result != SL_RESULT_SUCCESS){ LOGE("Error after Realize"); return; }
Помимо инициализации основных объектов в конструкторе моего врапера OSLContext
происходит инициализация всех необходимых плееров. Максимально возможно число плееров ограничено. Рекомендую создавать не более 20.
void OSLContext::initPlayers(){ for(int i = 0; i< MAX_ASSET_PLAYERS_COUNT; ++i) assetPlayers[i] = new OSLAssetPlayer(this); for(int i = 0; i< MAX_BUF_PLAYERS_COUNT; ++i) bufPlayers[i] = new OSLBufferPlayer(this); }
Работа со звуками
По сути, можно разделить на две категории типы звуков: чистые (не сжатые данные) PCM, которые содержатся в WAV и сжатые форматы (mp3, ogg и т.п.). Mp3 и ogg можно декодировать и получить всё те же несжатые звуковые данные PCM. Для работы с PCM используем BufferPlayer. Для сжатых форматов AssetPlayer, так как декодирование файлов будет довольно затратно. Если взять mp3, то аппаратно его декодировать на старых телефонах не получится, а с помощью сторонних софтверных решений декодирование займёт не один десяток секунд, что, согласитесь, не приемлемо. К тому же, слишком много весить будут такие PCM данные.
При вызове метода player()
запрашивает свободный плеер у контекста (OSLContext
). Если необходимо зацикливание звука, то получим OSLAssetPlayer
, в другом случае OSLBufferPlayer
.
Проигрывание PCM
Про чтение самого WAV писать снова не буду, можно посмотреть про это в статье про OpenAL. В этой же статье расскажу как с помощью полученных PCM данных создать BufferPlayer.
Инициализация BufferPlayer для работы с PCM
locatorBufferQueue.locatorType = SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE; locatorBufferQueue.numBuffers = 16; // описание формата аудио, об этом чуть ниже расскажу SLDataFormat_PCM formatPCM; formatPCM.formatType = SL_DATAFORMAT_PCM; formatPCM.numChannels = 2; formatPCM.samplesPerSec = SL_SAMPLINGRATE_44_1;// header.samplesPerSec*1000; formatPCM.bitsPerSample = SL_PCMSAMPLEFORMAT_FIXED_16 ;//header.bitsPerSample; formatPCM.containerSize = SL_PCMSAMPLEFORMAT_FIXED_16;// header.fmtSize; formatPCM.channelMask = SL_SPEAKER_FRONT_LEFT|SL_SPEAKER_FRONT_RIGHT ; formatPCM.endianness = SL_BYTEORDER_LITTLEENDIAN; audioSrc.pLocator = &locatorBufferQueue; audioSrc.pFormat = &formatPCM; locatorOutMix.locatorType = SL_DATALOCATOR_OUTPUTMIX; locatorOutMix.outputMix = context->getOutputMixObject(); audioSnk.pLocator = &locatorOutMix; audioSnk.pFormat = NULL; // создание плеера const SLInterfaceID ids[2] = {SL_IID_ANDROIDSIMPLEBUFFERQUEUE,/*SL_IID_MUTESOLO,*/ /*SL_IID_EFFECTSEND,SL_IID_SEEK,*/ /*SL_IID_MUTESOLO,*/ SL_IID_VOLUME}; const SLboolean req[2] = {SL_BOOLEAN_TRUE,SL_BOOLEAN_TRUE}; result = (*context->getEngine())->CreateAudioPlayer(context->getEngine(), &playerObj, &audioSrc, &audioSnk,2, ids, req); assert(SL_RESULT_SUCCESS == result); result = (*playerObj)->Realize(playerObj, SL_BOOLEAN_FALSE ); assert(SL_RESULT_SUCCESS == result); if (result != SL_RESULT_SUCCESS ) { LOGE("Can not CreateAudioPlayer %d", result); playerObj = NULL; } // получение интерфейса result = (*playerObj)->GetInterface(playerObj, SL_IID_PLAY, &player); assert(SL_RESULT_SUCCESS == result); // получение интерфейса для работы с громкостью result = (*playerObj)->GetInterface(playerObj, SL_IID_VOLUME, &fdPlayerVolume); assert(SL_RESULT_SUCCESS == result); result = (*playerObj)->GetInterface(playerObj, SL_IID_ANDROIDSIMPLEBUFFERQUEUE, &bufferQueue); assert(SL_RESULT_SUCCESS == result);
В целом ничего сложного нет. Вот только есть ОГРОМНАЯ проблема. Обратите внимание на структуру SLDataFormat_PCM
. Почему я явно сам заполнил параметры, а не прочитал из хэдеров WAV-файла? Потому что у меня все WAV файлы в едином формате, т.е. одно и тоже количество каналов, частота, битрейт и т.д. Дело в том, что если вы создадите буфер и в параметрах укажите 2 канала, а попытаетесь проиграть дорожку с 1 каналом, то приложение упадёт. Единственный вариант — переинициализировать целиком буфер, если у файла другой формат. Но ведь вся прелесть как раз в том, что мы плеер инициализируем 1 раз, а потом просто меняем буфер на нём. По-этому, тут два варианта, либо создавать несколько плееров с различными параметрами, либо все ваши .wav файлы приводить к одному формату. Ну, или же инициализировать буфер каждый раз заново -_-
Помимо интерфейса для громкости есть ещё два других интерфейса:
SL_IID_MUTESOLO
для управления каналами (только для многоканального звука, это указывается в полеnumChannels
структурыSLDataFormat_PCM
).SL_IID_EFFECTSEND
для наложения эффектов (по спецификации – только эффект реверберации).
Добавление звука в очередь при выборе плеера и установки звука на него:
void OSLBufferPlayer::setSound(OSLSound * sound){ if(bufferQueue == NULL) LOGD("bufferQueue is null"); this->sound = sound; (*bufferQueue)->Clear(bufferQueue); (*bufferQueue)->Enqueue(bufferQueue, sound->getBuffer() , sound->getSize()); }
Проигрывание сжатых форматов
В WAV все звуки хранить не вариант. И не потому что что сами файлы много места занимают (хотя и это тоже), просто когда вы их в память загрузите, то просто не хватит оперативки для этого (:
Я создаю классы для каждого из форматов, чтобы в будущем, если понадобиться, писать часть по декодированию в них. Для mp3 есть класс OSLMp3
, который, по сути, лишь имя файла хранит для того, чтобы в будущем установить на плеер. Тоже самое можно для ogg сделать и других поддерживаемых форматов.
Приведу полностью метод по инициализации, пояснения в комментариях.
Инициализация AssetPlayer для работы со сжатыми форматами
void OSLAssetPlayer::init(char * filename){ SLresult result; AAsset* asset = AAssetManager_open(mgr, filename, AASSET_MODE_UNKNOWN); if (NULL == asset) { return JNI_FALSE; } // открываем дескриптор off_t start, length; int fd = AAsset_openFileDescriptor(asset, &start, &length); assert(0 <= fd); AAsset_close(asset); // настраиваем данные по файлу SLDataLocator_AndroidFD loc_fd = {SL_DATALOCATOR_ANDROIDFD, fd, start, length}; SLDataFormat_MIME format_mime = {SL_DATAFORMAT_MIME, NULL, SL_CONTAINERTYPE_UNSPECIFIED}; SLDataSource audioSrc = {&loc_fd, &format_mime}; SLDataLocator_OutputMix loc_outmix = {SL_DATALOCATOR_OUTPUTMIX, context->getOutputMixObject()}; SLDataSink audioSnk = {&loc_outmix, NULL}; // создаём плеер const SLInterfaceID ids[3] = {SL_IID_SEEK, SL_IID_MUTESOLO, SL_IID_VOLUME}; const SLboolean req[3] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE}; result = (*context->getEngine())->CreateAudioPlayer(context->getEngine(), &playerObj, &audioSrc, &audioSnk, 3, ids, req); assert(SL_RESULT_SUCCESS == result); // реализуем плеер result = (*playerObj)->Realize(playerObj, SL_BOOLEAN_FALSE); assert(SL_RESULT_SUCCESS == result); // получаем интерфейс для работы со звуком result = (*playerObj)->GetInterface(playerObj, SL_IID_PLAY, &player); assert(SL_RESULT_SUCCESS == result); // получение интерфейса для перемещения по файлу result = (*playerObj)->GetInterface(playerObj, SL_IID_SEEK, &fdPlayerSeek); assert(SL_RESULT_SUCCESS == result); // получение интерфейса для управления каналами result = (*playerObj)->GetInterface(playerObj, SL_IID_MUTESOLO, &fdPlayerMuteSolo); assert(SL_RESULT_SUCCESS == result); // получение интерфейса для управления громокстью result = (*playerObj)->GetInterface(playerObj, SL_IID_VOLUME, &fdPlayerVolume); assert(SL_RESULT_SUCCESS == result); // задаём необходимо ли зацикливание файла result = (*fdPlayerSeek)->SetLoop(fdPlayerSeek, sound->isLooping() ? SL_BOOLEAN_TRUE : SL_BOOLEAN_FALSE, 0, SL_TIME_UNKNOWN); assert(SL_RESULT_SUCCESS == result); // return JNI_TRUE; }
Заключение
OpenSL ES достаточно прост в изучении. Да и возможностей у него не мало (к примеру можно записывать аудио). Жаль только, что с кроссплатформенностью проблемы. OpenAL кроссплатформенный, но на Android ведёт себя не очень. Есть у OpenSL пара минусов, странное поведение callback’ов, не все возможности спецификации поддерживаются и т.д. Но в целом, простота реализации и стабильная работы покрывают эти минусы.
Сорсы можно взять на github.com
Доп. инфа
Интересное чтиво по теме:
- The Standard for Embedded Audio Acceleration на сайте разработчика.
- The Khronos Group Inc. OpenSL ES Specification.
- Android NDK. Разработка приложений под Android на С/С++. Можно купить на Озоне.
- Ogg Vorbis