День добрый, Хабражители.
Я ранее писал про OpenAL. Позже товарищ zagayevskiy написал хорошую статью по OpenSL ES. В одной из наших игр, дабы не переписывать весь код по работе со звуком, мы не стали переписывать всё на OpenSL ES (при порте на Android). В игре использовалось не так много звуков, поэтому проблем с OpenAL не было. А вот в последней игре у нас использовалось много звуков (специфика игры обязывает), вот тут-то мы и столкнулись с большой проблемой (задержки при воспроизведении — меньшая из них). Было решено переписать всё на OpenSL ES. Для этого я написал парочку враперов, про которые уже рассказывал. Решил поделиться этим и на хабре, может кому-то пригодится.
Примечание: хотя оно и основано на OpenSL ES, это API не является полной реализацией любого профиля из OpenSL ES 1.0.1.
Либа, как вы могли догадаться, написана на чистом C. По-сему полноценного ООП там нет. Используются специальные структуры (назовём их псевдообъектно-ориентированными структуры (: ), которые представляют собой обычные структуры языка C, содержащие указатели на функции, получающие первым аргументом указатели на саму структуру. Что-то вроде this в С++, но явно. В OpenSL ES два вида таких структур:
Если обобщить: объекты используются для выделения ресурсов и получения интерфейсов. А уже потом с помощью этих интерфейсов работаем с объектом. Один объект может иметь несколько интерфейсов (для изменения громкости, для изменения позиции т.п.). В зависимости от устройства (или типа объекта), некоторые интерфейсы могут быть недоступны. Скажу наперёд, вы можете стримить аудио из вашей директории assets, используя
Теперь необходимо создать объект, через который будем работать с библиотекой, (что-то аналогичное контексту в OpenAL) с помощь метода
Теперь необходимо получить интерфейс
Остаётся получить и инициализировать объект OutputMix для работы с динамиками с помощью метода
Помимо инициализации основных объектов в конструкторе моего врапера
При вызове метода player() запрашивает свободный плеер у контекста (
В целом ничего сложного нет. Вот только есть ОГРОМНАЯ проблема. Обратите внимание на структуру
Помимо интерфейса для громкости есть ещё два других интерфейса:
Добавление звука в очередь при выборе плеера и установки звука на него:
Я создаю классы для каждого из форматов, чтобы в будущем, если понадобиться, писать часть по декодированию в них. Для mp3 есть класс
Приведу полностью метод по инициализации, пояснения в комментариях.
Сорсы можно взять на github.com
Интересное чтиво по теме:
Я ранее писал про 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 <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