ВНИМАНИЕ! Поддержка OpenSL ES прекращена. Разработчикам следует использовать библиотеку Oboe с открытым исходным кодом, которая доступна на GitHub. Oboe – это оболочка C++, которая предоставляет API, очень похожий на AAudio. Oboe вызывает AAudio, когда AAudio доступен, и возвращается к OpenSL ES, если AAudio недоступен.
Примечания в этом разделе дополняют спецификацию OpenSL ES 1.0.1.
Инициализация объектов и интерфейса
Два аспекта модели программирования OpenSL ES, которые могут быть незнакомы новым разработчикам, – это различие между объектами и интерфейсами, а также последовательность инициализации.
Если вкратце, объект OpenSL ES похож на объект в таких языках программирования, как Java и C++, но виден только через связанные с ним интерфейсы.
Это включает в себя начальный интерфейс для всех объектов, называемый SLObjectItf.
Дескриптор есть только у интерфейса SLObjectItf объекта, но не у самого объекта.
Сначала объект OpenSL ES создается, возвращая SLObjectItf, а затем реализуется. Это похоже на распространенный шаблон программирования, когда сначала создается объект (что всегда должно выполняться успешно, если только не возникнет нехватка памяти или не будут указаны недопустимые параметры), а затем завершается инициализация (что может не получиться из-за нехватки ресурсов). Шаг реализации дает реализации логическое место для выделения дополнительных ресурсов, если это необходимо.
При создании объекта приложение указывает массив интерфейсов, которые оно планирует получить позже. Обратите внимание, что этот массив не получает интерфейсы автоматически, а лишь указывает на намерение получить их в будущем. Интерфейсы делятся на неявные и явные. Если явный интерфейс будет получен позже, его необходимо указать в массиве. Неявный интерфейс не обязательно указывать в массиве создания объекта, но это не будет ошибкой. В OpenSL ES есть ещё один тип интерфейса – динамический. Его не нужно указывать в массиве создания объекта, а можно добавить позже, после создания объекта. В реализации для Android есть удобная функция, которая позволяет избежать этой сложности. Она описана в разделе Динамические интерфейсы при создании объектов.
После создания и реализации объекта приложение должно получить интерфейсы для каждой нужной функции, используя GetInterface в исходном SLObjectItf.
После этого объект можно использовать через его интерфейсы. Обратите внимание, что для некоторых объектов требуется дополнительная настройка. В частности, аудиопроигрыватель с источником данных URI требует дополнительной подготовки для обнаружения ошибок подключения. Подробную информацию вы найдете в разделе Предварительная загрузка аудиопроигрывателя.
После того как приложение завершит работу с объектом, его необходимо удалить. Подробнее об этом рассказывается в разделе Удаление ниже.
Предварительная выборка аудиопроигрывателя
Для аудиопроигрывателя с источником данных URI Object::Realize выделяет ресурсы, но не подключается к источнику данных (prepare) и не начинает предварительное извлечение данных. Они возникают, когда для состояния проигрывателя задано значение SL_PLAYSTATE_PAUSED или SL_PLAYSTATE_PLAYING.
Некоторая информация может оставаться неизвестной до относительно позднего этапа этой последовательности. В частности, сначала Player::GetDuration возвращает SL_TIME_UNKNOWN, а MuteSolo::GetChannelCount – либо успешный результат с нулевым количеством каналов, либо ошибку SL_RESULT_PRECONDITIONS_VIOLATED. Эти API возвращают правильные значения, как только они становятся известны.
Другие свойства, которые изначально неизвестны, включают частоту дискретизации и фактический тип медиаконтента, основанный на проверке заголовка контента (в отличие от указанного приложением MIME-типа и типа контейнера). Они также определяются позже, во время подготовки или предварительной выборки, но API для их получения нет.
Интерфейс статуса предварительной выборки позволяет определить, когда вся информация станет доступна. Также ваше приложение может периодически отправлять запросы. Обратите внимание, что некоторые данные, например продолжительность потоковой передачи MP3, могут быть неизвестны.
Интерфейс статуса предзагрузки также полезен для обнаружения ошибок. Зарегистрируйте обратный вызов и включите как минимум события SL_PREFETCHEVENT_FILLLEVELCHANGE и SL_PREFETCHEVENT_STATUSCHANGE. Если оба события происходят одновременно, а PrefetchStatus::GetFillLevel сообщает о нулевом уровне и PrefetchStatus::GetPrefetchStatus сообщает SL_PREFETCHSTATUS_UNDERFLOW, это указывает на неисправимую ошибку в источнике данных. В частности, это может быть связано с тем, что не удается подключиться к источнику данных, поскольку локальный файл не существует или URI сети недействителен.
В следующей версии OpenSL ES планируется добавить более явную поддержку обработки ошибок в источнике данных. Однако для обеспечения совместимости двоичного кода в будущем мы планируем и дальше поддерживать текущий метод сообщения о неисправимой ошибке.
Рекомендуемая последовательность кода:
Engine::CreateAudioPlayerObject:RealizeObject::GetInterfaceнаSL_IID_PREFETCHSTATUSPrefetchStatus::SetCallbackEventsMaskPrefetchStatus::SetFillUpdatePeriodPrefetchStatus::RegisterCallbackObject::GetInterfaceнаSL_IID_PLAYPlay::SetPlayStateдоSL_PLAYSTATE_PAUSEDилиSL_PLAYSTATE_PLAYING
Примечание. На этом этапе происходит подготовка и предварительная выборка. В это время ваш обратный вызов получает периодические обновления статуса.
Удалить
При выходе из приложения обязательно удаляйте все объекты. Объекты следует удалять в порядке, обратном порядку их создания, поскольку удалять объект, от которого зависят другие объекты, небезопасно. Например, удалите в следующем порядке: аудиопроигрыватели и рекордеры, выходной микс, а затем движок.
OpenSL ES не поддерживает автоматическую сборку мусора и подсчет ссылок для интерфейсов. После вызова Object::Destroy все существующие интерфейсы, производные от связанного объекта, становятся неопределенными.
Реализация OpenSL ES для Android не обнаруживает неправильное использование таких интерфейсов. Если вы продолжите использовать такие интерфейсы после удаления объекта, приложение может работать со сбоями или вести себя непредсказуемо.
Рекомендуем явно задать для основного интерфейса объекта и всех связанных с ним интерфейсов значение NULL в рамках последовательности уничтожения объекта. Это позволит избежать случайного неправильного использования дескриптора устаревшего интерфейса.
Панорамирование стерео
Если Volume::EnableStereoPosition используется для панорамирования монофонического источника, общий уровень звуковой мощности снижается на 3 дБ. Это необходимо для того, чтобы общий уровень звуковой мощности оставался постоянным при панорамировании источника из одного канала в другой. Поэтому включайте стереопозиционирование, только если оно вам действительно нужно. Подробнее о панорамировании звука…
Обратные вызовы и потоки
Обработчики обратных вызовов обычно вызываются синхронно, когда реализация обнаруживает событие. Эта точка асинхронна по отношению к приложению, поэтому для управления доступом к любым переменным, общим для приложения и обработчика обратных вызовов, следует использовать неблокирующий механизм синхронизации. В примерах кода, например для очередей буфера, мы либо опустили эту синхронизацию, либо использовали блокирующую синхронизацию для простоты. Однако правильная неблокирующая синхронизация критически важна для любого рабочего кода.
Обработчики обратных вызовов вызываются из внутренних потоков, не связанных с приложением и не подключенных к среде выполнения Android, поэтому они не могут использовать JNI. Поскольку эти внутренние потоки критически важны для целостности реализации OpenSL ES, обработчик обратных вызовов также не должен блокировать или выполнять чрезмерную работу.
Если обработчику обратных вызовов нужно использовать JNI или выполнить работу, не пропорциональную обратному вызову, он должен опубликовать событие для обработки другим потоком. Примеры допустимой рабочей нагрузки обратного вызова включают рендеринг и постановку в очередь следующего выходного буфера (для AudioPlayer), обработку только что заполненного входного буфера и постановку в очередь следующего пустого буфера (для AudioRecorder) или простые API, такие как большинство семейств Get. Подробную информацию о рабочей нагрузке вы найдете в разделе Производительность ниже.
Обратите внимание, что обратное безопасно: поток приложения для Android, который вошел в JNI, может напрямую вызывать API OpenSL ES, в том числе те, которые блокируют. Однако блокирующие вызовы не рекомендуются для основного потока, поскольку они могут привести к ошибке Приложение не отвечает.
Решение о том, какой поток вызывает обработчик обратных вызовов, в основном зависит от реализации. Это сделано для того, чтобы в будущем можно было оптимизировать работу приложения, особенно на многоядерных устройствах.
Не гарантируется, что поток, в котором выполняется обработчик обратных вызовов, будет иметь один и тот же идентификатор в разных вызовах. Поэтому не полагайтесь на то, что значение pthread_t, возвращаемое методом pthread_self(), или значение pid_t, возвращаемое методом gettid(), будет одинаковым для разных вызовов. По той же причине не используйте в обратном вызове API локальной памяти потока (TLS), такие как pthread_setspecific() и pthread_getspecific().
Реализация гарантирует, что одновременные обратные вызовы одного и того же типа для одного и того же объекта не происходят. Однако параллельные обратные вызовы разных типов для одного и того же объекта возможны в разных потоках.
Эффективность
Поскольку OpenSL ES – это нативный C API, потоки приложения, не связанные со средой выполнения, которые вызывают OpenSL ES, не имеют накладных расходов, связанных со средой выполнения, таких как паузы сборки мусора. За одним исключением, описанным ниже, использование OpenSL ES не дает никаких дополнительных преимуществ в производительности. В частности, использование OpenSL ES не гарантирует улучшений, таких как меньшая задержка аудио и более высокий приоритет планирования по сравнению с тем, что обычно предоставляет платформа. С другой стороны, по мере развития платформы Android и конкретных реализаций устройств приложение OpenSL ES может извлечь выгоду из любых будущих улучшений производительности системы.
Одним из таких изменений стала поддержка уменьшенной задержки вывода звука.
Основы для уменьшения задержки вывода были заложены в Android 4.1 (уровень API 16) и усовершенствованы в Android 4.2 (уровень API 17). Эти улучшения доступны через OpenSL ES для устройств, поддерживающих функцию android.hardware.audio.low_latency.
Если устройство не поддерживает эту функцию, но поддерживает Android 2.3 (уровень API 9) или более позднюю версию, вы все равно можете использовать API OpenSL ES, но задержка вывода может быть выше.
Путь с меньшей задержкой вывода используется, только если приложение запрашивает размер буфера и частоту дискретизации, совместимые с конфигурацией вывода устройства. Эти параметры зависят от устройства и должны быть получены, как описано ниже.
Начиная с Android 4.2 (уровень API 17), приложение может запрашивать у платформы оптимальную частоту дискретизации и размер буфера для основного выходного потока устройства. В сочетании с упомянутой выше проверкой функций приложение теперь может соответствующим образом настраиваться для вывода с меньшей задержкой на устройствах, которые заявляют о поддержке.
Для Android 4.2 (уровень API 17) и более ранних версий требуется не менее двух буферов. Начиная с Android 4.3 (уровень API 18), для снижения задержки достаточно одного буфера.
Все интерфейсы OpenSL ES для выходных эффектов исключают путь с низкой задержкой.
Рекомендуемая последовательность действий:
- Проверьте, используется ли OpenSL ES (уровень API 9 или выше).
- Проверьте, доступна ли функция
android.hardware.audio.low_latency, с помощью следующего кода:Kotlin
import android.content.pm.PackageManager ... val pm: PackageManager = context.packageManager val claimsFeature: Boolean = pm.hasSystemFeature(PackageManager.FEATURE_AUDIO_LOW_LATENCY)
Java
import android.content.pm.PackageManager; ... PackageManager pm = getContext().getPackageManager(); boolean claimsFeature = pm.hasSystemFeature(PackageManager.FEATURE_AUDIO_LOW_LATENCY);
- Проверьте, используется ли уровень API 17 или выше, чтобы подтвердить использование
android.media.AudioManager.getProperty(). - Получите исходную или оптимальную частоту дискретизации и размер буфера для основного выходного потока устройства, используя следующий код:
Обратите внимание, что
Kotlin
import android.media.AudioManager ... val am = getSystemService(Context.AUDIO_SERVICE) as AudioManager val sampleRate: String = am.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE) val framesPerBuffer: String = am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER)
Java
import android.media.AudioManager; ... AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE); String sampleRate = am.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE); String framesPerBuffer = am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER);
sampleRateиframesPerBuffer– это строки. Сначала проверьте, не равно ли значение null, а затем преобразуйте его в int с помощьюInteger.parseInt(). - Теперь используйте OpenSL ES, чтобы создать AudioPlayer с указателем данных очереди буфера PCM.
Примечание. Чтобы определить размер нативного буфера и частоту дискретизации для аудиоприложений OpenSL ES на вашем аудиоустройстве, используйте тестовое приложение Audio Buffer Size. Вы также можете посмотреть примеры audio-buffer-size на GitHub.
Количество аудиопроигрывателей с низкой задержкой ограничено. Если вашему приложению требуется больше нескольких источников звука, попробуйте микшировать звук на уровне приложения. Не забудьте удалить аудиопроигрыватели, когда действие приостановлено, поскольку они являются глобальным ресурсом, общим для других приложений.
Чтобы избежать сбоев звука, обработчик обратных вызовов очереди буфера должен выполняться в течение небольшого и предсказуемого временного окна. Обычно это означает отсутствие неограниченной блокировки мьютексов, условий или операций I/O. Вместо этого используйте блокировки с попыткой захвата, блокировки и ожидания с тайм-аутами, а также неблокирующие алгоритмы.
Вычисления, необходимые для отрисовки следующего буфера (для AudioPlayer) или использования предыдущего буфера (для AudioRecord), должны занимать примерно одинаковое количество времени для каждого обратного вызова. Избегайте алгоритмов, которые выполняются за недетерминированное время или скачкообразно в своих вычислениях. Вычисление обратного вызова является прерывистым, если процессорное время, затраченное на любой данный обратный вызов, значительно больше среднего. В идеале время выполнения ЦП обработчика должно иметь дисперсию, близкую к нулю, а сам обработчик не должен блокироваться на неопределенное время.
Низкая задержка звука возможна только для следующих выходов:
- Динамики на устройстве.
- Проводные наушники.
- Проводные гарнитуры.
- Линейный выход.
- Цифровое аудиоустройство USB.
На некоторых устройствах задержка динамика выше, чем у других путей, из-за цифровой обработки сигналов для коррекции и защиты динамика.
Начиная с Android 5.0 (уровень API 21) на некоторых устройствах поддерживается аудиовход с меньшей задержкой. Чтобы воспользоваться этой функцией, сначала убедитесь, что вывод с низкой задержкой доступен, как описано выше. Возможность вывода с меньшей задержкой – это необходимое условие для ввода с меньшей задержкой. Затем создайте AudioRecorder с той же частотой дискретизации и размером буфера, которые будут использоваться для вывода. Интерфейсы OpenSL ES для эффектов ввода исключают путь с меньшей задержкой. Для уменьшения задержки необходимо использовать набор настроек SL_ANDROID_RECORDING_PRESET_VOICE_RECOGNITION, который отключает цифровую обработку сигналов на устройстве, способную увеличивать задержку на пути ввода.
Дополнительную информацию о наборах настроек для записи можно найти в разделе Интерфейс конфигурации Android выше.
Для одновременного ввода и вывода используются отдельные обработчики завершения очереди буфера для каждой стороны. Относительный порядок этих обратных вызовов или синхронизация аудиочасов не гарантируются, даже если обе стороны используют одну и ту же частоту дискретизации. Приложение должно буферизовать данные с надлежащей синхронизацией буфера.
Одним из следствий потенциально независимых аудиочасов является необходимость асинхронного преобразования частоты дискретизации. Простой (хотя и не идеальный с точки зрения качества звука) метод асинхронного преобразования частоты дискретизации заключается в дублировании или удалении отсчетов по мере необходимости вблизи точки пересечения нуля. Возможно более сложное отслеживание конверсий.
Режимы производительности
Начиная с Android 7.1 (уровня API 25) в OpenSL ES можно указывать режим производительности для аудиопути. Доступны следующие варианты:
SL_ANDROID_PERFORMANCE_NONE: нет особых требований к эффективности. Разрешает аппаратные и программные эффекты.SL_ANDROID_PERFORMANCE_LATENCY– приоритет отдается задержке. Никаких аппаратных или программных эффектов. Это значение по умолчанию.SL_ANDROID_PERFORMANCE_LATENCY_EFFECTS– приоритет отдается задержке, но аппаратные и программные эффекты по-прежнему разрешены.SL_ANDROID_PERFORMANCE_POWER_SAVING– приоритет отдается экономии энергии. Разрешает аппаратные и программные эффекты.
Примечание. Если вам не требуется низкая задержка и вы хотите использовать встроенные аудиоэффекты устройства (например, чтобы улучшить качество звука при воспроизведении видео), необходимо явно задать режим производительности SL_ANDROID_PERFORMANCE_NONE.
Чтобы задать режим производительности, вызовите SetConfiguration с помощью интерфейса конфигурации Android, как показано ниже.
// Obtain the Android configuration interface using a previously configured SLObjectItf. SLAndroidConfigurationItf configItf = nullptr; (*objItf)->GetInterface(objItf, SL_IID_ANDROIDCONFIGURATION, &configItf); // Set the performance mode. SLuint32 performanceMode = SL_ANDROID_PERFORMANCE_NONE; result = (*configItf)->SetConfiguration(configItf, SL_ANDROID_KEY_PERFORMANCE_MODE, &performanceMode, sizeof(performanceMode));
Безопасность и разрешения
Безопасность в Android обеспечивается на уровне процессов. Код на языке программирования Java не может делать ничего большего, чем нативный код, и наоборот. Единственное различие между ними – доступные API.
Приложения, использующие OpenSL ES, должны запрашивать разрешения, необходимые для аналогичных несистемных API. Например, если приложение записывает аудио, ему требуется разрешение android.permission.RECORD_AUDIO. Приложениям, использующим аудиоэффекты, требуется разрешение android.permission.MODIFY_AUDIO_SETTINGS. Приложениям, которые воспроизводят сетевые ресурсы URI, требуется разрешение android.permission.NETWORK. Подробнее о работе с системными разрешениями…
В зависимости от версии платформы и реализации синтаксические анализаторы медиаконтента и программные кодеки могут выполняться в контексте приложения Android, которое вызывает OpenSL ES (аппаратные кодеки абстрагированы, но зависят от устройства). Контент, созданный с целью использовать уязвимости синтаксического анализатора и кодека, – это известный вектор атаки. Рекомендуем воспроизводить медиаконтент только из надежных источников или разделить приложение так, чтобы код, обрабатывающий медиаконтент из ненадежных источников, выполнялся в относительно изолированной среде. Например, вы можете обрабатывать медиафайлы из ненадежных источников в отдельном процессе. Хотя оба процесса по-прежнему будут выполняться с одним и тем же UID, такое разделение усложнит атаку.