[recovery mode] Android NDK: работа с OpenSL ES
День добрый, Хабражители.Я ранее писал про OpenAL. Позже товарищ zagayevskiy написал хорошую статью по OpenSL ES. В одной из наших игр, дабы не переписывать весь код по работе со звуком, мы не стали переписать всё на OpenSL ES (при порте на Android). В игре использовалось не так много звуков, поэтому проблем с OpenAL не было. А вот в последней игре у нас использовалось много звуков (специфика игры обязывает), вот тут-то мы и столкнулись с большой проблемой (задержки при воспроизведении — меньшая из них). Было решено переписать всё на 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 же надо ещё декомпрессить с помощью 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
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