Android NDK: OpenSL ES

от автора

Здравствуйте, уважемые хабражители!
Недавно, читая хабр, я увидел статью об Android NDK и OpenAL. А в комментариях был задан вопрос о OpenSL ES. Тогда у меня и родилась мысль написать статью об этой библиотеке. Я занимался этой темой, когда мне понадобилось добавить звуки и музыку в игру под Android, написанную на C++, под NDK. Статья не претендует на полноту, здесь будут лишь основы.

Содержание:

  1. Краткое описание структур OpenSL ES
  2. Инициализация механизма библиотеки и создание объекта для работы с динамиками
  3. Проигрывание PCM(wav)
  4. Проигрывание MP3, OGG
  5. Заключение

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, с помощью которого мы будем иметь доступ к динамикам, проигрыванию музыки, звуков и тд.

Получение интерфейса 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. При желании, добавить поддержку этого достаточно легко. Здесь заголовок нужен лишь для корректного определения размера данных.

Работа с PCM-буфером

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.

Литература

  1. Сильвен Ретабоуил. Android NDK. Разработка приложений под Android на С/С++.
  2. The Khronos Group Inc. OpenSL ES Specification.

Хорошие и более полные примеры кода можно посмотреть в стандартной поставке Android NDK (проект NativeAudio).
Предвосхищая вопросы по поводу необходимости использования Android NDK вообще и OpenSL ES в частности, а так же за неимением аккаунта, отвечу сразу. Android NDK нужен был по условию тестового задания от известной геймдев-компании (были конкурсы на хабре). Позже это переросло в вызов мне: смогу ли я красиво закончить начатое. Смог. OpenSL ES выбрал по наитию, т.к. опыта работы ни с ним, ни с OpenAL не было, а привлекать вызовы в Java для этого посчитал некрасивым решением.

ссылка на оригинал статьи http://habrahabr.ru/post/176933/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *