Замечания по программированию OpenSL ES

ВНИМАНИЕ: OpenSL ES устарел . Разработчикам следует использовать библиотеку Oboe с открытым исходным кодом, доступную на GitHub . Гобой — это оболочка C++, предоставляющая API, очень похожий на AAudio . Гобой вызывает AAudio, когда AAudio доступен, и возвращается к OpenSL ES, если AAudio недоступен.

Примечания в этом разделе дополняют спецификацию OpenSL ES 1.0.1 .

Объекты и инициализация интерфейса

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

Вкратце, объект OpenSL ES аналогичен концепции объекта в таких языках программирования, как Java и C++, за исключением того, что объект OpenSL ES виден только через связанные с ним интерфейсы. Сюда входит начальный интерфейс для всех объектов, называемый SLObjectItf . Не существует дескриптора самого объекта, только дескриптор интерфейса SLObjectItf объекта.

Сначала создается объект OpenSL ES, который возвращает SLObjectItf , а затем реализуется . Это похоже на общепринятый шаблон программирования: сначала создается объект (который никогда не должен давать сбой, кроме как из-за нехватки памяти или неверных параметров), а затем завершается инициализация (которая может завершиться неудачей из-за нехватки ресурсов). Шаг реализации дает реализации логическое место для выделения дополнительных ресурсов в случае необходимости.

В рамках API для создания объекта приложение указывает массив желаемых интерфейсов, которые оно планирует получить позже. Обратите внимание, что этот массив не получает интерфейсы автоматически; это просто указывает на будущее намерение их приобрести. Интерфейсы различают на неявные и явные . Явный интерфейс должен быть указан в массиве, если он будет получен позже. Неявный интерфейс не обязательно должен быть указан в массиве создания объекта, но его указание там не повредит. OpenSL ES имеет еще один тип интерфейса, называемый динамическим , который не нужно указывать в массиве создания объекта и который можно добавить позже, после создания объекта. Реализация Android предоставляет удобную функцию, позволяющую избежать этой сложности, которая описана в разделе Динамические интерфейсы при создании объекта .

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

Наконец, объект доступен для использования через его интерфейсы, однако учтите, что некоторые объекты требуют дальнейшей настройки. В частности, аудиоплееру с источником данных URI требуется немного больше подготовки, чтобы обнаружить ошибки соединения. Подробности см. в разделе «Предварительная выборка аудиоплеера» .

После того, как ваше приложение закончит работу с объектом, вы должны явно уничтожить его; см. раздел «Уничтожение» ниже.

Предварительная загрузка аудиоплеера

Для аудиопроигрывателя с источником данных URI Object::Realize выделяет ресурсы, но не подключается к источнику данных ( подготовка ) и не начинает предварительную выборку данных. Это происходит, когда состояние игрока установлено на 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 будет добавлена ​​более явная поддержка обработки ошибок в источнике данных. Однако для будущей двоичной совместимости мы намерены продолжать поддерживать текущий метод сообщения о неисправимой ошибке.

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

  1. Engine::CreateAudioPlayer
  2. Object:Realize
  3. Object::GetInterface для SL_IID_PREFETCHSTATUS
  4. PrefetchStatus::SetCallbackEventsMask
  5. PrefetchStatus::SetFillUpdatePeriod
  6. PrefetchStatus::RegisterCallback
  7. Object::GetInterface для SL_IID_PLAY
  8. Play::SetPlayState в SL_PLAYSTATE_PAUSED или SL_PLAYSTATE_PLAYING

Примечание. Здесь происходят подготовка и предварительная выборка; в течение этого времени ваш обратный вызов вызывается с периодическими обновлениями статуса.

Разрушать

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

OpenSL ES не поддерживает автоматическую сборку мусора или подсчет ссылок на интерфейсах. После вызова Object::Destroy все существующие интерфейсы, производные от связанного объекта, становятся неопределенными.

Реализация Android OpenSL ES не обнаруживает неправильное использование таких интерфейсов. Продолжение использования таких интерфейсов после уничтожения объекта может привести к сбою вашего приложения или его непредсказуемому поведению.

Мы рекомендуем вам явно установить для основного интерфейса объекта и всех связанных интерфейсов значение NULL в рамках последовательности уничтожения объекта, что предотвращает случайное неправильное использование устаревшего дескриптора интерфейса.

Стерео панорамирование

Когда Volume::EnableStereoPosition используется для включения стереопанорамирования монофонического источника, общий уровень звуковой мощности снижается на 3 дБ. Это необходимо для того, чтобы общий уровень звуковой мощности оставался постоянным при панорамировании источника с одного канала на другой. Поэтому включайте стереопозиционирование только в том случае, если оно вам необходимо. Для получения дополнительной информации см. статью Википедии о панорамировании звука .

Обратные вызовы и потоки

Обработчики обратного вызова обычно вызываются синхронно, когда реализация обнаруживает событие. Эта точка является асинхронной по отношению к приложению, поэтому вам следует использовать неблокирующий механизм синхронизации для управления доступом к любым переменным, совместно используемым приложением и обработчиком обратного вызова. В примере кода, например, для буферных очередей, мы либо опустили эту синхронизацию, либо использовали блокирующую синхронизацию в целях простоты. Однако правильная неблокирующая синхронизация имеет решающее значение для любого производственного кода.

Обработчики обратного вызова вызываются из внутренних потоков, не связанных с приложением, которые не подключены к среде выполнения Android, поэтому они не имеют права использовать JNI. Поскольку эти внутренние потоки имеют решающее значение для целостности реализации OpenSL ES, обработчик обратного вызова также не должен блокировать или выполнять чрезмерную работу.

Если вашему обработчику обратного вызова необходимо использовать JNI или выполнять работу, не пропорциональную обратному вызову, вместо этого обработчик должен отправить событие для обработки другим потоком. Примеры приемлемой рабочей нагрузки обратного вызова включают отрисовку и постановку в очередь следующего выходного буфера (для AudioPlayer), обработку только что заполненного входного буфера и постановку в очередь следующего пустого буфера (для AudioRecorder) или простые API, такие как большая часть семейства Get . См. раздел «Производительность» ниже, чтобы узнать о рабочей нагрузке.

Обратите внимание, что обратное безопасно: потоку приложения Android, вошедшему в JNI, разрешено напрямую вызывать API OpenSL ES, включая те, которые блокируют. Однако блокировать вызовы из основного потока не рекомендуется, так как это может привести к тому, что приложение не отвечает (ANR).

Определение потока, вызывающего обработчик обратного вызова, в значительной степени остается на усмотрение реализации. Причиной такой гибкости является возможность будущей оптимизации, особенно на многоядерных устройствах.

Не гарантируется, что поток, в котором выполняется обработчик обратного вызова, будет иметь один и тот же идентификатор при разных вызовах. Поэтому не полагайтесь на то, что pthread_t , возвращаемое функцией pthread_self() , или pid_t , возвращаемое gettid() будут согласованными между вызовами. По той же причине не используйте API-интерфейсы локального хранилища потоков (TLS), такие как pthread_setspecific() и pthread_getspecific() из обратного вызова.

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

Производительность

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

Рекомендуемая последовательность следующая:

  1. Проверьте уровень API 9 или выше, чтобы подтвердить использование OpenSL ES.
  2. Проверьте наличие функции android.hardware.audio.low_latency , используя такой код:

    Котлин

    import android.content.pm.PackageManager
    ...
    val pm: PackageManager = context.packageManager
    val claimsFeature: Boolean = pm.hasSystemFeature(PackageManager.FEATURE_AUDIO_LOW_LATENCY)

    Ява

    import android.content.pm.PackageManager;
    ...
    PackageManager pm = getContext().getPackageManager();
    boolean claimsFeature = pm.hasSystemFeature(PackageManager.FEATURE_AUDIO_LOW_LATENCY);
  3. Проверьте уровень API 17 или выше, чтобы подтвердить использование android.media.AudioManager.getProperty() .
  4. Получите собственную или оптимальную частоту дискретизации вывода и размер буфера для основного выходного потока этого устройства, используя такой код:

    Котлин

    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)

    Ява

    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() .
  5. Теперь используйте OpenSL ES для создания AudioPlayer с локатором данных очереди буфера PCM.

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

Количество аудиоплееров с меньшей задержкой ограничено. Если вашему приложению требуется несколько источников звука, рассмотрите возможность микширования звука на уровне приложения. Обязательно уничтожайте аудиоплееры, когда ваша активность приостановлена, поскольку они представляют собой глобальный ресурс, используемый другими приложениями.

Чтобы избежать звуковых сбоев, обработчик обратного вызова буферной очереди должен выполняться в течение небольшого и предсказуемого временного окна. Обычно это подразумевает отсутствие неограниченной блокировки мьютексов, условий или операций ввода-вывода. Вместо этого рассмотрите try locks , блокировки и ожидания с таймаутами и неблокирующие алгоритмы .

Вычисления, необходимые для визуализации следующего буфера (для 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 не может делать ничего, кроме собственного кода, и собственный код не может делать ничего, кроме кода языка программирования Java. Единственная разница между ними — доступные API.

Приложения, использующие OpenSL ES, должны запрашивать разрешения, которые им потребуются для аналогичных неродных API. Например, если ваше приложение записывает звук, ему необходимо разрешение android.permission.RECORD_AUDIO . Приложениям, использующим звуковые эффекты, требуется android.permission.MODIFY_AUDIO_SETTINGS . Приложениям, которые воспроизводят сетевые ресурсы URI, требуется android.permission.NETWORK . Дополнительную информацию см. в разделе Работа с системными разрешениями .

В зависимости от версии платформы и реализации анализаторы медиаконтента и программные кодеки могут работать в контексте приложения Android, которое вызывает OpenSL ES (аппаратные кодеки абстрактны, но зависят от устройства). Искаженный контент, предназначенный для использования уязвимостей парсера и кодека, является известным вектором атаки. Мы рекомендуем воспроизводить мультимедиа только из надежных источников или разделять приложение таким образом, чтобы код, обрабатывающий мультимедиа из ненадежных источников, выполнялся в относительно изолированной среде. Например, вы можете обрабатывать медиафайлы из ненадежных источников в отдельном процессе. Хотя оба процесса по-прежнему будут работать под одним и тем же UID, такое разделение усложняет атаку.