OpenSL ES 编程说明

本部分中的说明是对 OpenSL ES 1.0.1 规范的补充。

对象与接口初始化

新开发者可能不熟悉 OpenSL ES 编程模型的以下两个方面:对象与接口之间的区别和初始化序列。

简要来说,OpenSL ES 对象类似于 Java 和 C++ 等编程语言中的对象概念,不过 OpenSL ES 对象仅能通过其关联接口进行访问。其中包括所有对象的初始接口,称为 SLObjectItf。对象本身没有句柄,只有一个连接到对象的 SLObjectItf 接口的句柄。

OpenSL ES 对象是先创建的,它会返回 SLObjectItf,然后再实现 (realize)。这个过程与常见的编程模式类似,即先构建一个对象(不会因缺少内存或参数无效以外的原因而失败),然后再完成初始化(可能会因缺少资源而失败)。实现 (realize) 步骤为实现 (implementation) 提供了一个根据需要分配更多资源的逻辑位置。

在 API 创建对象的过程中,应用将指定一组计划于稍后获取的所需接口。请注意,此数组不会自动获取接口;它仅表明未来计划获取这些接口。接口有隐式和显式之分。如果以后会获取显式接口,则显式接口必须列在数组中。不需要将隐式接口列在对象创建数组中,不过列在其中也无妨。OpenSL ES 还有一种动态接口,这种接口不需要在对象创建数组中指定,并且可以在创建对象后添加。Android 实现提供了一种可以避免这种复杂状况的便捷功能,创建对象时使用动态接口对此功能进行了说明。

创建并实现 (realize) 对象后,应用应当在初始 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::SetPlayStateSL_PLAYSTATE_PAUSEDSL_PLAYSTATE_PLAYING

注意:此时会出现准备和预提取动作;在此期间,您的回调将通过周期性状态更新调用。

销毁

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

OpenSL ES 不支持自动垃圾回收或对接口的引用计数。在您调用 Object::Destroy 后,衍生自关联对象的全部现有接口都将变为未定义。

Android OpenSL ES 实现不会检测对此类接口的不正确使用。在对象销毁后继续使用此类接口将导致您的应用崩溃或者行为不可预测。

在对象销毁流程中,我们建议您将主要对象接口和所有关联接口明确设置为 NULL,这样可以防止意外误用过时的接口句柄。

立体声平移

使用 Volume::EnableStereoPosition 启用单声道源的立体声平移时,整体声功率水平会有 3-dB 的衰减。要在源从一个频道平移到另一个频道时使整体声功率级仍然保持恒定,需要进行立体声平移。因此,请仅根据需要启用立体声定位。如需了解详细信息,请参阅维基百科上有关音频平移的文章。

回调和线程

回调处理程序一般会在实现检测到事件时得到同步调用。此时间与应用异步,因此,您应使用非阻塞型同步机制控制对在应用与回调处理程序之间共享的任何变量的访问。在示例代码中,例如对于缓冲区队列,我们要么已将此同步省略,要么为简单起见而使用了阻塞型同步。不过,正确的非阻塞型同步对任何正式版代码都至关重要。

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

如果您的回调处理程序需要使用 JNI 或执行与回调不成比例的工作,处理程序应将事件交给另一个线程处理。可接受的回调工作负载示例包括渲染和将下一个输出缓冲区加入队列(针对音频播放器)、处理刚刚填充的输入缓冲区和将下一个空缓冲区加入队列(针对 AudioRecorder),或者简单的 API,例如 Get 系列中的大部分 API。有关工作负载,请参阅下面的性能部分。

请注意,逆向是安全的:已经进入 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 为字符串。首先检查有无 null,然后使用 Integer.parseInt() 转换为 int。
  5. 现在,使用 OpenSL ES 创建一个支持 PCM 缓冲区队列数据定位器的音频播放器。

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

短延迟时间音频播放器的数量受到限制。如果您的应用需要较多音频源,请考虑在应用级别混合您的音频。确保在您的 Activity 暂停后销毁音频播放器,因为它们是与其他应用共享的全局资源。

为了避免声音故障,缓冲区队列回调处理程序必须在一个很小且可预测的时间窗口内执行。这通常预示着不存在对互斥量、条件或 I/O 操作的无限制阻止。请考虑使用尝试锁定、带超时的锁定和等待,以及非阻塞型算法

对于每个回调,渲染下一个缓冲区(针对音频播放器)或消耗上一个缓冲区(针对音频录制器)所需的计算花费的时间应大致相同。请避免执行时间无法确定或者在计算中存在突发性的算法。如果在任意给定回调中花费的 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 编程语言代码的功能与原生代码的功能相当。两者之间唯一的区别在于可用的 API。

使用 OpenSL ES 的应用必须请求其执行相似非原生 API 所需的权限。例如,如果您的应用要录制音频,则需要 android.permission.RECORD_AUDIO 权限。使用音频效果的应用需要 android.permission.MODIFY_AUDIO_SETTINGS。播放网络 URI 资源的应用需要 android.permission.NETWORK。如需了解详细信息,请参阅使用系统权限

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