OpenSL ES 编程笔记

警告:OpenSL ES 已 **弃用**。开发者应使用可在 GitHub 上获取的开源 Oboe 库。Oboe 是一个 C++ 包装器,提供与 AAudio 非常相似的 API。当 AAudio 可用时,Oboe 会调用 AAudio,如果 AAudio 不可用,则回退到 OpenSL ES。

本节中的注释是对 OpenSL ES 1.0.1 规范 的补充。

对象和接口初始化

OpenSL ES 编程模型中两个可能让新手开发者感到陌生的方面是对象和接口之间的区别,以及初始化顺序。

简单来说,OpenSL ES 对象类似于 Java 和 C++ 等编程语言中的对象概念,只是 OpenSL ES 对象只能通过其关联的接口访问。这包括所有对象的初始接口,称为 SLObjectItf。对象本身没有句柄,只有指向对象 SLObjectItf 接口的句柄。

首先创建 OpenSL ES 对象,这会返回一个 SLObjectItf,然后实现它。这类似于常见的编程模式,即首先构造一个对象(除了内存不足或参数无效外,永远不应该失败),然后完成初始化(由于资源不足可能会失败)。实现步骤为实现提供了一个逻辑位置,以便在需要时分配其他资源。

作为创建对象的 API 的一部分,应用程序会指定一个它计划稍后获取的所需接口数组。请注意,此数组不会自动获取接口;它仅仅表示将来获取它们的意图。接口被区分为**隐式**或**显式**。如果稍后要获取显式接口,则必须将其列在数组中。隐式接口不需要在对象创建数组中列出,但在那里列出也没有任何坏处。OpenSL ES 还有另一种称为**动态**的接口,它不需要在对象创建数组中指定,并且可以在对象创建后稍后添加。Android 实现提供了一个方便的功能来避免这种复杂性,这在对象创建时的动态接口中进行了描述。

对象创建并实现后,应用程序应使用初始SLObjectItf上的GetInterface为其需要的每个功能获取接口。

最后,可以通过其接口使用该对象,但请注意,某些对象需要进一步设置。特别是,带有 URI 数据源的音频播放器需要进行更多准备才能检测连接错误。有关详细信息,请参阅音频播放器预取部分。

应用程序使用完对象后,应显式销毁它;请参阅下面的销毁部分。

音频播放器预取

对于具有 URI 数据源的音频播放器,Object::Realize分配资源,但不连接到数据源(**准备**)或开始预取数据。这些操作发生在播放器状态设置为SL_PLAYSTATE_PAUSEDSL_PLAYSTATE_PLAYING时。

某些信息可能直到此序列的相对后期才为人所知。特别是,最初Player::GetDuration返回SL_TIME_UNKNOWN,而MuteSolo::GetChannelCount要么成功返回通道数零,要么返回错误结果SL_RESULT_PRECONDITIONS_VIOLATED。这些 API 在已知后将返回正确的值。

其他最初未知的属性包括采样率和基于检查内容标头(而不是应用程序指定的 MIME 类型和容器类型)的实际媒体内容类型。这些也在准备/预取期间确定,但没有 API 可以检索它们。

预取状态接口可用于检测所有信息何时可用,或者您的应用程序可以定期轮询。请注意,某些信息(例如流式 MP3 的持续时间)可能**永远不会**知道。

预取状态接口也可用于检测错误。注册回调并至少启用SL_PREFETCHEVENT_FILLLEVELCHANGESL_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_PAUSEDSL_PLAYSTATE_PLAYING

注意: 准备和预取在此处发生;在此期间,您的回调将被调用以进行定期状态更新。

销毁

退出应用程序时,请务必销毁所有对象。应按创建的反序销毁对象,因为销毁任何具有依赖对象的​​对象是不安全的。例如,按以下顺序销毁:音频播放器和录音机、输出混音器,最后是引擎。

OpenSL ES 不支持接口的自动垃圾回收或引用计数。在调用Object::Destroy之后,所有派生自关联对象的可存在接口都将变得未定义。

Android OpenSL ES 实现不会检测此类接口的错误使用。在对象销毁后继续使用此类接口可能会导致应用程序崩溃或行为不可预测。

我们建议您将主对象接口和所有关联接口显式设置为NULL作为对象销毁序列的一部分,这可以防止意外误用过时的接口句柄。

立体声声像

当使用Volume::EnableStereoPosition启用单声道源的立体声声像时,总声功率级会降低 3 dB。这是为了允许总声功率级在源从一个通道平移到另一个通道时保持恒定。因此,只有在需要时才启用立体声定位。有关更多信息,请参阅维基百科关于音频声像的文章。

回调和线程

当实现检测到事件时,回调处理程序通常会同步调用。此点相对于应用程序是异步的,因此您应该使用非阻塞同步机制来控制对应用程序和回调处理程序之间共享的任何变量的访问。在示例代码中(例如缓冲区队列),为了简单起见,我们省略了此同步或使用了阻塞同步。但是,对于任何生产代码,正确的非阻塞同步都是至关重要的。

回调处理程序从未附加到 Android 运行时的内部非应用程序线程中调用,因此它们没有资格使用 JNI。由于这些内部线程对于 OpenSL ES 实现的完整性至关重要,因此回调处理程序也不应阻塞或执行过多的工作。

如果您的回调处理程序需要使用 JNI 或执行与回调不成比例的工作,则处理程序应改为发布事件以供另一个线程处理。可接受的回调工作负载的示例包括渲染和入队下一个输出缓冲区(对于 AudioPlayer),处理刚刚填充的输入缓冲区并入队下一个空缓冲区(对于 AudioRecorder),或简单的 API(例如大多数 Get 系列)。有关工作负载,请参阅下面的性能部分。

请注意,反过来是安全的:已进入 JNI 的 Android 应用程序线程允许直接调用 OpenSL ES API,包括那些阻塞的 API。但是,不建议从主线程进行阻塞调用,因为它们可能导致**应用程序无响应**(ANR)。

关于调用回调处理程序的线程的确定在很大程度上取决于实现。这种灵活性的原因是为了允许未来的优化,尤其是在多核设备上。

回调处理程序运行的线程不保证在不同的调用之间具有相同的标识。因此,不要依赖pthread_self()返回的pthread_tgettid()返回的pid_t在调用之间保持一致。出于同样的原因,不要从回调中使用线程局部存储 (TLS) API(例如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)或更高版本,则您仍然可以使用 OpenSL ES API,但输出延迟可能会更高。只有当应用程序请求的缓冲区大小和采样率与设备的本机输出配置兼容时,才会使用较低的输出延迟路径。这些参数是特定于设备的,应按如下所述获取。

从 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 特性

    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);
  3. 检查 API 级别是否为 17 或更高,以确认是否使用 android.media.AudioManager.getProperty()
  4. 使用如下代码获取此设备主输出流的原生或最佳输出采样率和缓冲区大小

    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);
    请注意,sampleRateframesPerBuffer字符串。首先检查是否为空,然后使用 Integer.parseInt() 转换为 int。
  5. 现在使用 OpenSL ES 创建一个带有 PCM 缓冲区队列数据定位器的 AudioPlayer。

注意:您可以使用 音频缓冲区大小 测试应用来确定音频设备上 OpenSL ES 音频应用的原生缓冲区大小和采样率。您也可以访问 GitHub 查看 audio-buffer-size 示例。

低延迟音频播放器的数量是有限的。如果您的应用需要多个音频源,请考虑在应用级别混合音频。请确保在活动暂停时销毁音频播放器,因为它们是与其他应用共享的全局资源。

为了避免出现明显的音频故障,缓冲区队列回调处理程序必须在短时间且可预测的时间窗口内执行。这通常意味着不会在互斥体、条件或 I/O 操作上进行无界阻塞。相反,请考虑使用尝试锁定、带超时的锁定和等待以及 非阻塞算法

渲染下一个缓冲区(对于 AudioPlayer)或使用上一个缓冲区(对于 AudioRecord)所需的计算量应在每次回调时花费大致相同的时间。避免执行时间不确定或计算突发的算法。如果给定回调中花费的 CPU 时间明显大于平均值,则回调计算为突发。总之,理想情况是处理程序的 CPU 执行时间的方差接近于零,并且处理程序不会无限期地阻塞。

只有以下输出才能实现低延迟音频

  • 设备扬声器。
  • 有线耳机。
  • 有线耳机。
  • 线路输出。
  • 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

要设置性能模式,必须使用 Android 配置接口调用 SetConfiguration,如下所示

  // 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。有关更多信息,请参阅 使用系统权限

根据平台版本和实现,媒体内容解析器和软件编解码器可能会在调用 OpenSL ES 的 Android 应用的上下文中运行(硬件编解码器已抽象化,但依赖于设备)。旨在利用解析器和编解码器漏洞的格式错误的内容是一种已知的攻击媒介。我们建议您仅从可信来源播放媒体,或者将您的应用进行分区,以便处理来自不可信来源的媒体的代码在相对沙盒的环境中运行。例如,您可以在单独的进程中处理来自不可信来源的媒体。尽管这两个进程仍将在相同的 UID 下运行,但这分隔确实使攻击更加困难。