Про OpenSL ES, его реализацию в Android и сбоку Qt

в 14:33, , рубрики: android, necessitas, OpenSL ES, qt, Qt Software, Разработка под android, метки: , , ,

Вступление

Используя порт Qt для Android я наткнулся на проблему работы со звуком. На данный момент QtMultimediaKit для Android не работает. Проблема связана с банальной нехваткой либы FFmpeg, но багу фиксить не спешат, т.к. у комьюнити пока другие приоритеты. Разбираться, как работает Necessitas мне абсолютно не хотелось, да и нашелся человек в списках рассылки, который проделал необходимую работу, но почему-то не поделился результатами своего труда. Я написал ему письмо, но ответа к сожалению не последовало. Ну и ладно, подумал я… И тут на одном из форумов наткнулся на предложение использовать OpenSL ES, который появился в Android начиная с версии 2.3. Скажу сразу, что у меня ничего путного не получилось. Что такое OpenSL ES я до этого не знал. А значит, надо заполнять пробелы в знаниях. Тем более повод есть.

Назвать OpenSL ES библиотекой у меня даже как-то язык не поворачивается. Это скорее спецификация для 2D и 3D аудио на языке Си. Первый взгляд на API привел меня в ужас. Все показалось каким-то мрачным и запутанным. Но, как говорится, не все то золото, что блестит. Позже, пытаясь реализовать QAndroidMediaPlayer, я проникся духом “библиотеки” и первое мнение рассеялось в пух и прах.

Краткий экскурс в OpenSL ES 1.0.1

Много писать о предназначении и возможностях OpenSL ES я не стану, для этого есть Википедия. Попробую коротко ввести Вас, товарищи, в курс дела, не касаясь конкретной реализации. OpenSL ES API базируется на двух фундаментальных концепциях: объект и интерфейс. Хочу так же напомнить, что используется язык Си, а не Си++. Рассмотрим детально обе концепции:

Объект — это абстракция для различных ресурсов, предназначенных для четко определенного набора задач, и определения состояния этих ресурсов. Тип объекта определяет набор задач, которые этот объект может выполнять, а сам тип определяется во время создания объекта.

Интерфейс — это набор, связанных между собой одной задачей, функций, которые предоставляет определенный объект. Интерфейс включает в себя набор методов, которые являются функциями интерфейса. У интерфейса есть тип, который определяет точный набор его методов.

Основные операции, которые приходится выполнять с объектами это: создание объекта, изменение его состояния и получение того или иного интерфейса объекта.

Самым первым объектом, который нам придется создать — это движок. Сделать это можно с помощью глобальной функции.

SLresult res;
SLObjectItf m_engineObj;
// API will be thread-safe
SLEngineOption engineOption[2] = { (SLuint32) SL_ENGINEOPTION_THREADSAFE,
                                   (SLuint32) SL_BOOLEAN_TRUE };
// Create SLObjectItf
res = slCreateEngine(&m_engineObj, 1, engineOption, 0, NULL, NULL);

Это единственное исключение из правил, т.к. все остальные объекты можно создавать только с помощью других объектов. Движок на приложение должен быть только один!

Всего существует три состояния объекта:

  • нереализованное: объект существует, но никакие ресурсы для его работы не выделены;
  • реализованное: ресурсы для объекта выделены и он готов к использованию;
  • подвешенное: если объект находился в реализованном состояние, но какой либо из ресурсов потерялся, то объект переходит в подвешенное состояние.

Теперь, когда у нас есть объект движка, нам необходимо выделить для него ресурсы, переведя его в реализованное состояние:

// Realizing OpenSL Engine in synchronous mode
res = (*m_engineObj)->Realize(m_engineObj, SL_BOOLEAN_FALSE);

Когда есть готовый реализованный объект, приходит очередь интерфейсов.

SLEngineItf m_engineItf;
res = (*m_engineObj)->GetInterface(m_engineObj, SL_IID_ENGINE, &m_engineItf);

У каждого объекта может быть несколько интерфейсов разного типа. Выше, мы получили интерфейс типа SL_IID_ENGINE у объекта движка. Что же умеет этот интерфейс? Он умеет очень много, с полным списком можно ознакомиться в спецификации, а мы рассмотрим только две функции: CreateOutputMix и CreateAudioPlayer.

Для проигрывания аудио файла нам необходимо создать объект аудио плеера. Но тут есть некоторая тонкость в трактовании понятий. В нашем понимание плеер — это объект, который может играть аудио файлЫ. В понятие OpenSL ES — это объект, который может играть только ОДИН аудио файл, который необходимо определить заранее. Например, если нам необходимо проиграть файл foo.mp3, то нам надо создать отдельно объект плеера только для этого одного файла. Что бы проиграть другой файл, нам нужно создать другой плеер. Говоря лозунгами: “Каждому файлу по плееру”.

Имея ввиду все вышесказанное (т.е. у нас есть несколько источников аудио), появляется необходимость в микширование звука. Создадим объект микшера и реализуем его:

SLObjectItf m_outputObj;
// Create Output Mix object to be used by player.
res = (*m_engineItf)->CreateOutputMix(m_engineItf, &m_outputObj, 0, NULL, NULL);
checkResult(res);
// Realizing Output Mix
res = (*m_outputObj)->Realize(m_outputObj, SL_BOOLEAN_FALSE);
checkResult(res);

У микшера тоже есть интерфейсы (управление громкостью, эквалайзер, различные эффекты), но мы не будем получать к ним доступ. Это вы сможете сделать сами, по аналогии, просто вооружившись спецификацией.

Создадим теперь объект плеера. Пускай источником будет не просто локальный файл, а удаленный.

// configure audio source
SLDataLocator_URI loc_uri = { SL_DATALOCATOR_URI, (SLchar *) "http://192.168.0.1/foo.mp3" };
SLDataFormat_MIME format_mime = { SL_DATAFORMAT_MIME, NULL, SL_CONTAINERTYPE_UNSPECIFIED };
SLDataSource audioSrc = { &loc_uri, &format_mime };
// configure audio sink
SLDataLocator_OutputMix loc_outmix = {SL_DATALOCATOR_OUTPUTMIX, m_outputObj};
SLDataSink audioSnk = {&loc_outmix, NULL};
// create audio player
const SLInterfaceID ids[1] = {SL_IID_SEEK};
const SLboolean req[1] = {SL_BOOLEAN_TRUE};
SLObjectItf m_playerObject;
res = (*m_engineItf)->CreateAudioPlayer(m_engineItf, &m_playerObject, &audioSrc,
        &audioSnk, 1, ids, req);
checkResult(res);
// realize the player
res = (*m_playerObject)->Realize(m_playerObject, SL_BOOLEAN_FALSE);
checkResult(res);

Ура! Практически все готово. Осталось только запустить проигрывание. Для управления проигрыванием существует интерфейс SL_IID_PLAY.

SLPlayItf m_playItf;
// get the play interface
res = (*m_playerObject)->GetInterface(m_playerObject, SL_IID_PLAY, &m_playItf);
checkResult(res);

Интерфейс готов! Послушаем?

// set the player's state
res = (*m_playItf)->SetPlayState(m_playItf, SL_PLAYSTATE_PLAYING);
checkResult(res);

Слышно? Поздравляю. Тернистый путь преодолен. Нет? Ничего не слышно? Так и у меня было то же само… Этому я и посвящаю следующую главу статьи.

Доза гнева к платформе Android

Сразу скажу, что все изложенное в этом подпункте — это лично мое мнение. Людям, со слабой психикой или тем, кто обожает Android читать дальше противопоказано.

Начиная с Android 2.3 заявлена поддержка OpenSL ES 1.0.1. Заявлена… В жизни все оказалось несколько иначе. Я пожалуй начну с аргументов, а потом сделаю вывод.

Многопоточность. Сейчас трудно встретить однопоточное приложение. В OpenSL ES об этом позаботились и предлагают нам создать движок в потокобезопасном режиме. Увы, разработчикам Android видимо показалось, что любой, уважающий себя человек, сам сможет реализовать синхронизацию в своем приложение. Итог: работать в потокобезопасном режиме с OpenSL ES в Android нельзя.

Любое, даже самое примитивное приложение, связанное с аудио, требует управление громкостью. Ликуйте товарищи, его нет! Я конечно рад за платформу Android, и за то что управление громкостью выполняется на уровне операционной системы, но можно ведь было реализовать интерфейс управления громкостью в движке обычными вызовами функций ОС. Ах да, интерфейс для работы с громкостью для аудио плеера доступен, но это не выход.

Колбэки. В OpenSL ES для Android нет реализации колбэков. В итоге, вы даже не можете получить информацию о том, в какой именно позиции проигрывается ваш файл. Нет, можно конечно по таймеру опрашивать плеер, но это же тоже воркэраунд.

Проигрывание urlов… Я не знаю почему, но проигрывание http ссылок работает через раз. А ведь это потом достанется пользователю.

Это то что меня лично задело, но это программа-минимум недоступных фич. Остальное можно посмотреть тут.

А вот теперь вывод. Android мне напоминает китайский медиаплеер. На коробке много ярких плюсов, а внутри все тот же китаец с убогой реализацией этих самых плюсов. И я соглашусь, что такие недостатки достаются разработчикам, а не конечным юзерам. Но ведь разработчики потом пишут такие же корявые приложения, которые заставляют пользователя нервничать. И я не считаю, что в данной ситуации виноват этот самый разработчик приложения. Как по мне, включать недоделыш OpenSL ES в официальные релизы — просто кощунство.

Я упоминал Qt...

Я уже говорил, что у меня ничего не получилось. Я бросил этим занимать на пол дороги, потому что не считаю воркэраунды решением проблем. Мой брошенный класс ради ознакомления можно посмотреть тут.
Кстати, включить управление звуком с помощью боковых клавиш в Qt приложение можно так.

Благодарю за внимание!

Автор: surik


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js