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

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

Destroy

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

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 查看音频缓冲区大小示例。

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

为了避免出现明显的故障,缓冲区队列回调处理程序必须在较小且可预测的时间窗口内执行。这通常意味着不对互斥锁、条件或 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 下运行,但这项分离确实使攻击更加困难。