Здравствуйте, уважемые читатели!
Недавно, читая хабр, я увидел статью об Android NDK и OpenAL. А в комментариях был задан вопрос о OpenSL ES. Тогда у меня и родилась мысль написать статью об этой библиотеке. Я занимался этой темой, когда мне понадобилось добавить звуки и музыку в игру под Android, написанную на C++, под NDK. Статья не претендует на полноту, здесь будут лишь основы.
Содержание:
- Краткое описание структур OpenSL ES
- Инициализация механизма библиотеки и создание объекта для работы с динамиками
- Проигрывание PCM(wav)
- Проигрывание MP3, OGG
- Заключение
1. Краткое описание структур
Работа с OpenSL ES построена на основе псевдообъектно-ориентированных структур языка Си. Они используются, когда проект пишется на Си, но хочется объекто-ориентированности. В общем псевдообъектно-ориентированные структуры представляют из себя обычные структуры языка Си, содержащие указатели на функции, получающие первым аргументом указатели на саму структуру, подобно this в С++, но явно.
В OpenSL ES существуют два основных типа описанных выше структур:
- Объект(SLObjectItf) – абстракция набора ресурсов, предназначенная для выполнения определенного круга задач и хранения информации об этих ресурсах. При создании объекта определяется его тип, определяющий круг задач, которые можно решать с его помощью. Объект напоминает Object языка Java, может считаться подобием класса в С++
- Интерфейс(SLEngineItf, SLPlayItf, SLSeekItf и тд) – абстракция набора взаимосвязанных функциональных возможностей, предоставляемых конкретным объектом. Интерфейс включает в себя множество методов, используемых для выполнения действий над объектом. Интерфейс имеет тип, определяющий точный перечень методов, поддерживаемых данным интерфейсом. Интерфейс определяется его идентификатором, который можно использовать в коде для ссылки на тип интерфейса.
Проще говоря, объекты нужны для выделения ресурсов и получения интерфейсов, а интерфейсы обеспечивают доступ к возможностям объектов. Один объект может иметь несколько интерфейсов. В зависимости от устройства, некоторые интерфейсы могут быть недоступны. Однако, я с этим не сталкивался.
2. Инициализация механизма библиотеки и создание объекта для работы с динамиками
Чтобы подключить OpenSL ES в Android NDK, достаточно добавить в секцию LOCAL_LDLIBS файла Android.mk флаг lOpenSLES:
LOCAL_LDLIBS := /*...*/ -lOpenSLES
Используемые заголовочные файлы:
#include <SLES/OpenSLES.h>
#include <SLES/OpenSLES_Android.h>
Для начала работы с OpenSL ES необходимо инициализировать объект механизма OpenSL ES(SLObjectItf) с помощью вызова slCreateEngine, указав, что для работы с ним будет использоваться интерфейс SL_IID_ENGINE. Это нужно для того, чтобы иметь возможность создавать другие объекты. Объект, полученный с помощью такого вызова, становится центральным объектом для доступа к OpenSL ES API. Далее объект необходимо реализовать, используя псевдометод Realize, который является аналогом конструктора в С++. Первым параметром Realize указывается сам реализуемый объект(аналог this), а вторым — флаг async, указывающий будет ли объект асинхронным.
Текущая реализация Android NDK дает возможность создать только один механизм библиотеки и до 32 объектов вообще. Тем не менее, любая операция создания объекта может закончиться неудачей (например, из-за недостатка системных ресурсов).
SLObjectItf engineObj;
const SLInterfaceID pIDs[1] = {SL_IID_ENGINE};
const SLboolean pIDsRequired[1] = {SL_TRUE};
SLresult result = slCreateEngine(
&engineObj, /*Указатель на результирующий объект*/
0, /*Количество элементов в массиве дополнительных опций*/
NULL, /*Массив дополнительных опций, NULL, если они Вам не нужны*/
1, /*Количество интерфесов, которые должен будет поддерживать создаваемый объект*/
pIDs, /*Массив ID интерфейсов*/
pIDsRequired /*Массив флагов, указывающих, необходим ли соответствующий интерфейс. Если указано SL_TRUE, а интерфейс не поддерживается, вызов завершится неудачей, с кодом возврата SL_RESULT_FEATURE_UNSUPPORTED*/
);
/*Проверяем результат. Если вызов slCreateEngine завершился неуспехом – ничего не поделаешь*/
if(result != SL_RESULT_SUCCESS){
LOGE("Error after slCreateEngine");
return;
}
/*Вызов псевдометода. Первым аргументом всегда идет аналог this*/
result = (*engineObj)->Realize(engineObj, SL_BOOLEAN_FALSE); //Реализуем объект в синхронном режиме
/*В дальнейшем я буду опускать проверки результата, дабы не загромождать код*/
if(result != SL_RESULT_SUCCESS){
LOGE("Error after Realize engineObj");
return;
}
Далее необходимо получить интерфейс SL_IID_ENGINE, с помощью которого мы будем иметь доступ к динамикам, проигрыванию музыки, звуков и тд.
SLEngineItf engine;
result = (*engineObj)->GetInterface(
engineObj, /*this*/
SL_IID_ENGINE, /*ID интерфейса*/
&engine /*Куда поместить результат*/
);
Остановимся немного на общей схеме работы с объектами:
- Получить объект, указав желаемые интерфейсы
- Реализовать его, вызвав
(*obj)->Realize(obj, async);
- Получить необходимые интерфейсы вызвав
(*obj)-> GetInterface (obj, ID, &itf);
- Работать с интерфейсами
- Удалить объект, вызвав
(*obj)->Destroy(obj);
Для работы с динамиками создадим объект outputMixObj, используя псевдометод CreateOutputMix интерфейса engine объекта engineObj (Это только звучит страшно, дабы читатель научился различать объекты и интерфейсы). Этот объект понадобится нам позже для вывода звука.
SLObjectItf outputMixObj;
const SLInterfaceID pOutputMixIDs[] = {};
const SLboolean pOutputMixRequired[] = {};
/*Аналогично slCreateEngine()*/
result = (*engine)->CreateOutputMix(engine, &outputMixObj, 0, pOutputMixIDs, pOutputMixRequired);
result = (*outputMixObj)->Realize(outputMixObj, SL_BOOLEAN_FALSE);
SLOutputMixItf – это объект, представляющий устройство вывода звука(динамик, наушники). Спецификация OpenSL ES предусматривает возможность получения списка доступных устройств ввода/вывода, но реализация Android NDK недостаточно полна и не поддерживает ни получение перечня устройств, ни выбор желаемого (официально для этого предназначен интерфейс SLAudioIODeviceCapabilitiesItf).
3. Проигрывание PCM(wav)
Сразу оговорюсь, что для упрощения я не использую данные из заголовка WAV. При желании, добавить поддержку этого достаточно легко. Здесь заголовок нужен лишь для корректного определения размера данных.
struct WAVHeader{
char RIFF[4];
unsigned long ChunkSize;
char WAVE[4];
char fmt[4];
unsigned long Subchunk1Size;
unsigned short AudioFormat;
unsigned short NumOfChan;
unsigned long SamplesPerSec;
unsigned long bytesPerSec;
unsigned short blockAlign;
unsigned short bitsPerSample;
char Subchunk2ID[4];
unsigned long Subchunk2Size;
};
struct SoundBuffer{
WAVHeader* header;
char* buffer;
int length;
};
/*Для чтения буфера PCM из файла используется AAssetManager:*/
SoundBuffer* loadSoundFile(const char* filename){
SoundBuffer* result = new SoundBuffer();
AAsset* asset = AAssetManager_open(assetManager, filename, AASSET_MODE_UNKNOWN);
off_t length = AAsset_getLength(asset);
result->length = length - sizeof(WAVHeader);
result->header = new WAVHeader();
result->buffer = new char[result->length];
AAsset_read(asset, result->header, sizeof(WAVHeader));
AAsset_read(asset, result->buffer, result->length);
AAsset_close(asset);
return result;
}
Теперь займемся настройкой быстрого буферного вывода звука. Для этого используем специализированное расширение SLDataLocator_AndroidSimpleBufferQueue. Также, для воспроизведения музыки необходимо заполнить две структуры: SLDataSource и SLDataSink, описывающие ввод и вывод аудиоканала соответственно.
/*Данные, которые необходимо передать в CreateAudioPlayer() для создания буферизованного плеера */
SLDataLocator_AndroidSimpleBufferQueue locatorBufferQueue = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 1}; /*Один буфер в очереди*/
/*Информация, которую можно взять из заголовка wav*/
SLDataFormat_PCM formatPCM = {
SL_DATAFORMAT_PCM, 1, SL_SAMPLINGRATE_44_1,
SL_PCMSAMPLEFORMAT_FIXED_16, SL_PCMSAMPLEFORMAT_FIXED_16,
SL_SPEAKER_FRONT_CENTER, SL_BYTEORDER_LITTLEENDIAN
};
SLDataSource audioSrc = {&locatorBufferQueue, &formatPCM};
SLDataLocator_OutputMix locatorOutMix = {SL_DATALOCATOR_OUTPUTMIX, outputMixObj};
SLDataSink audioSnk = {&locatorOutMix, NULL};
const SLInterfaceID pIDs[1] = {SL_IID_BUFFERQUEUE};
const SLboolean pIDsRequired[1] = {SL_BOOLEAN_TRUE };
/*Создаем плеер*/
result = (*engine)->CreateAudioPlayer(engine, &playerObj, &audioSrc, &audioSnk, 1, pIDs, pIDsRequired);
result = (*playerObj)->Realize(playerObj, SL_BOOLEAN_FALSE);
SLPlayItf player;
Реализация OpenSL ES в Android NDK не является строгой. Если какие-то интерфейсы не указаны, это не значит, что их невозможно получить. Но лучше так не делать. Самостоятельно укажите интерфейс SL_IID_PLAY выше.
result = (*playerObj)->GetInterface(playerObj, SL_IID_PLAY, &player);
SLBufferQueueItf bufferQueue;
result = (*playerObj)->GetInterface(playerObj, SL_IID_BUFFERQUEUE, &bufferQueue);
result = (*player)->SetPlayState(player, SL_PLAYSTATE_PLAYING);
Помимо SL_IID_PLAY и SL_IID_BUFFERQUEUE можно запросить другие интерфейсы, например:
- SL_IID_VOLUME для управления громкостью
- SL_IID_MUTESOLO для управления каналами (только для многоканального звука, это указывается в поле numChannels структуры SLDataFormat_PCM).
- SL_IID_EFFECTSEND для наложения эффектов(по спецификации – только эффект реверберации)
и т.д.
Вызовом (*player)->SetPlayState(player, SL_PLAYSTATE_PLAYING);
мы включаем вновь созданный плеер. Пока очередь пуста, поэтому слышно лишь тишину. Давайте поместим какой-нибудь звук в очередь.
SoundBuffer* sound = loadSoundFile("mySound.wav");
(*soundsBufferQueue)->Clear(bufferQueue); /*Очищаем очередь на случай, если там что-то было. Можно опустить, если хочется, чтобы очередь реально была очередью*/
(*soundsBufferQueue)->Enqueue(bufferQueue, sound->buffer, sound->length);
/*Не забудьте почистить за собой SoundBuffer, когда он перестанет быть нужен*/
Вот и все, простейший проигрыватель wav готов.
Следует обратить внимание, что в пику спецификации, Android NDK не поддерживает буферизованный вывод музыки в отличных от PCM форматах.
4. Проигрывание MP3, OGG
Описанная выше схема плохо подходит для проигрывания длинных музыкальных файлов. В первую очередь из-за того, что длинный wav файл будет весить очень и очень много. Здесь лучше использовать MP3 или OGG. OpenSL ES поддерживает стриминг файлов «из коробки». Отличие от буферизованого вывода так же в том, что на каждый музыкальный файл необходимо создавать отдельный объект-плеер. Поменять файл в процессе воспроизведения для данного плеера невозможно.
Подготовимся к проигрыванию музыки:
struct ResourseDescriptor{
int32_t decriptor;
off_t start;
off_t length;
};
/*Вновь используем AAssetManager*/
ResourseDescriptor loadResourceDescriptor(const char* path){
AAsset* asset = AAssetManager_open(assetManager, path, AASSET_MODE_UNKNOWN);
ResourseDescriptor resourceDescriptor;
resourceDescriptor.decriptor = AAsset_openFileDescriptor(asset, &resourceDescriptor.start, &resourceDescriptor.length);
AAsset_close(asset);
return resourceDescriptor;
}
Далее вновь заполняем SLDataSource и SLDataSink. И создаем аудиоплеер.
ResourseDescriptor resourceDescriptor = loadResourceDescriptor("myMusic.mp3");
SLDataLocator_AndroidFD locatorIn = {
SL_DATALOCATOR_ANDROIDFD,
resourseDescriptor.decriptor,
resourseDescriptor.start,
resourseDescriptor.length
}
SLDataFormat_MIME dataFormat = {
SL_DATAFORMAT_MIME,
NULL,
SL_CONTAINERTYPE_UNSPECIFIED
};
SLDataSource audioSrc = {&locatorIn, &dataFormat};
SLDataLocator_OutputMix dataLocatorOut = {
SL_DATALOCATOR_OUTPUTMIX,
outputMixObj
};
SLDataSink audioSnk = {&dataLocatorOut, NULL};
const SLInterfaceID pIDs[2] = {SL_IID_PLAY, SL_IID_SEEK};
const SLboolean pIDsRequired[2] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE};
SLObjectItf playerObj;
SLresult result = (*engine)->CreateAudioPlayer(engine, &playerObj, &audioSrc, &audioSnk, 2, pIDs, pIDsRequired);
result = (*playerObj)->Realize(playerObj, SL_BOOLEAN_FALSE);
Для описания исходных данных используем MIME-тип, это обеспечивает автоматическое определение типа файла.
Далее получим интерфейсы SL_IID_PLAY и SL_IID_SEEK. Последний нужен для изменения позиции воспроизведения в файле и зацикливания. Его можно использовать вне зависимости от состояния воспроизведения и скорости.
SLPlayItf player;
result = (*playerObj)->GetInterface(playerObj, SL_IID_PLAY, &player);
SLSeekItf seek;
result = (*playerObj)->GetInterface(playerObj, SL_IID_SEEK, &seek);
(*seek)->SetLoop(
seek,
SL_BOOLEAN_TRUE, /*Воспроизведение зациклено*/
0, /*Зациклено на начало файла(0 мс)*/
SL_TIME_UNKNOWN /*По достижению конца*/
);
(*player)->SetPlayState(player, SL_PLAYSTATE_PLAYING);
В теории, механизм зацикливания должен быть удобен для установки фоновой музыки в игре. На практике между концом композиции и ее началом проходит 0.5-1.0 секунд (время на слух, на разных девайсах плавает). Я поборол это, сделав в фоновой музыке несколько плавных затуханий в середине и конце. Т.о. разрыв незаметен.
По спецификации, на интерфейс SLPlayerItf можно навесить различные callback’и. В Android NDK фича не поддерживается (метод возвращает SL_RESULT_SUCCESS, но callback’и не отрабатывают).
Для остановки или паузы плеера можно воспользоваться методом SetPlayState интерфейса SLPlayerItf со значениями SL_PLAYSTATE_STOPPED или SL_PLAYSTATE_PAUSED соответственно. Узнать состояние плеера позволяет метод GetPlayState, возвращающий те же значения.
5. Заключение
OpenSL ES API достаточно богато, и кроме воспроизведения звука, позволяет записывать его. Здесь я не буду касаться записи звука, скажу лишь, что она есть и работает достаточно хорошо. Для получения данных используется очередь буферов. Данные приходят в формате PCM.
Библиотеку сложно использовать в кроссплатформенной разработке, т.к. многие фичи реализованы специализированными для Android методами. Тем не менее, она показалась мне достаточно удобной.
В минусах видится вольная реализация, не поддерживаются многие вещи из спецификации. Кроме того, это API не быстрее, чем API, доступные в Android SDK.
Литература
- Сильвен Ретабоуил. Android NDK. Разработка приложений под Android на С/С++.
- The Khronos Group Inc. OpenSL ES Specification.
Хорошие и более полные примеры кода можно посмотреть в стандартной поставке Android NDK (проект NativeAudio).
Предвосхищая вопросы по поводу необходимости использования Android NDK вообще и OpenSL ES в частности, а так же за неимением аккаунта, отвечу сразу. Android NDK нужен был по условию тестового задания от известной геймдев-компании (были конкурсы на хабре). Позже это переросло в вызов мне: смогу ли я красиво закончить начатое. Смог. OpenSL ES выбрал по наитию, т.к. опыта работы ни с ним, ни с OpenAL не было, а привлекать вызовы в Java для этого посчитал некрасивым решением.
Автор: zagayevskiy