翻译自OpenSL ES Programming Notes
本节中的注释补充了OpenSL ES 1.0.1规范。
对象和接口初始化
OpenSL ES编程模型的两个方面可能是新开发人员不熟悉的,即对象和接口之间的区别以及初始化顺序。
简单地说,OpenSL ES对象类似于Java和c++等编程语言中的对象概念,只是OpenSL ES对象仅通过其关联接口可见。这包括所有对象的初始接口,称为SLObjectItf
。没有对象本身的句柄,只有对象的SLObjectItf
接口的句柄。
首先创建一个OpenSL ES对象,它返回一个SLObjectItf
,然后实例化它。这类似于常见的编程模式,首先构造一个对象(除非缺少内存或无效参数,否则不会失败),然后完成初始化(可能由于缺乏资源而失败)。实例化这步为实例提供了在需要时分配额外资源的逻辑内存。
作为创建对象的API的一部分,应用程序指定了它计划稍后获取的所需接口数组。注意,这个数组不会自动获得接口;它仅仅表明了将来获取它们的意图。接口被区分为隐式或显式。如果以后要获得显式接口,则必须在数组中列出它。隐式接口不需要在对象创建数组中列出,但是在那里列出它并没有害处。OpenSL ES还有一种称为dynamic的接口,它不需要在对象创建数组中指定,可以在对象创建后添加。Android实现提供了一个方便的特性来避免这种复杂性,这种复杂性在OpenSL ES的Android扩展这篇文章中的对象创建时的动态接口中进行了描述。
在创建和实现对象之后,应用程序应该在SLObjectItf
初始化后使用GetInterface
为它需要的每个特性获取接口。
最后,该对象可以通过其接口使用,不过请注意,有些对象需要进一步设置。特别是,带有URI数据源的音频播放器需要做更多的准备,以检测连接错误。有关详细信息,请参阅下面的音频播放器预读取部分。
应用程序处理完对象后,应该显式地销毁它;参见下面的销毁部分。
音频播放器预读取
对于具有URI数据源的音频播放器,Object::Realize
分配资源,但不连接数据源(准备阶段)或开始预读取数据。一旦将播放器状态设置为sl_playstate_pause
或SL_PLAYSTATE_PLAYING
,就会出现这种情况。
在此序列中,有些信息可能直到相对较晚的时候才会为人所知。特别是,初始时,Player::GetDuration
返回SL_TIME_UNKNOWN
还有 MuteSolo::GetChannelCount
返回0,或者返回错误结果SL_RESULT_PRECONDITIONS_VIOLATED
。当为已知时,才返回正确值。
其他最初未知的属性包括采样率和基于检查内容头的实际的媒体内容类型(与应用程序指定的MIME类型和容器类型相反)。这些也是在准备/预读取期间稍后确定的,但是没有api来检索它们。
预读取状态接口对于检测何时所有可用信息非常有用,或者您的应用程序可以定期轮询。注意,一些信息,例如MP3流的持续时间,可能永远不会知道。
预取状态接口对于检测错误也很有用。注册一个回调,并至少启用SL_PREFETCHEVENT_FILLLEVELCHANGE
和SL_PREFETCHEVENT_STATUSCHANGE
事件。如果这两个事件同时交付,PrefetchStatus::GetFillLevel
报告0级,PrefetchStatus::GetPrefetchStatus
报告SL_PREFETCHSTATUS_UNDERFLOW
,那么这表明数据源中有一个不可恢复的错误。这包括无法连接数据源,因为本地文件名不存在或网络URI无效。
OpenSL ES的下一个版本预计将添加对数据源中错误处理的更加显式的支持。然而 ,为了将来的二进制兼容性,我们打算继续支持当前不可恢复错误报告的方法。
总之,推荐的代码序列是:
Engine::CreateAudioPlayer
Object:Realize
-
Object::GetInterface
forSL_IID_PREFETCHSTATUS
PrefetchStatus::SetCallbackEventsMask
PrefetchStatus::SetFillUpdatePeriod
PrefetchStatus::RegisterCallback
-
Object::GetInterface
forSL_IID_PLAY
-
Play::SetPlayState
toSL_PLAYSTATE_PAUSED
, orSL_PLAYSTATE_PLAYING
注意:这里有准备和预读取;在这段时间内,你的回调会被定期的状态更新调用。
销毁
在退出应用程序时,请确保销毁所有对象。对象应该按照创建对象的相反顺序被销毁,因为销毁具有任何依赖对象的对象是不安全的。例如,按以下顺序销毁:音频播放器和录音机,输出混合,最后是引擎。
OpenSL ES不支持自动垃圾收集或接口的引用计数。在您调用Object::Destroy
之后,所有从关联对象派生的现有接口都将无法定义。
Android OpenSL ES实例不会检测到这些接口的不正确使用情况。在对象被销毁后继续使用这些接口可能导致应用程序崩溃或以不可预知的方式运行。
我们建议您显式地将主对象接口和所有关联接口都设置为NULL,作为对象销毁序列的一部分,这样可以防止对陈旧接口句柄的意外滥用。
立体声平移
当Volume::EnableStereoPosition
用于启用单声道源的立体平移时,总声波功率级别降低了3分贝。允许总声波功率水平保持不变是必要的,因为声源是从一个通道到另一个通道。因此,只有在你需要的时候,才能启用立体声定位。有关更多信息,请参阅Wikipedia关于音频平移的文章。
回调和线程
当实例检测到事件时,通常同步调用回调处理程序。对于应用程序,这一点是异步的,因此应该使用非阻塞同步机制来控制应用程序和回调处理程序之间共享变量的访问权限。在示例代码(例如缓冲区队列)中,为了简单起见,我们要么省略了这个同步,要么使用了阻塞同步。然而,适当的非阻塞同步对于任何代码都是至关重要的。
回调处理程序是从内部非应用程序线程调用的,这些线程不attach到Android runtime ,因此它们不具备使用JNI的资格。因为这些内部线程对OpenSL ES实例的完整性至关重要,所以回调处理程序也不应该阻塞或执行过多的工作。
如果回调处理程序需要使用JNI或执行与回调不相称的工作,则处理程序应该向另一个线程发布一个事件来处理。可接受的回调工作负载的示例包括渲染和排队下一个输出缓冲区(用于AudioPlayer)、处理刚刚填充的输入缓冲区和排队下一个空缓冲区(用于AudioRecorder)或简单api(如Get系列的大部分)。关于工作负载,请参阅下面的性能部分。
注意,反过来是安全的:已使用JNI的Android应用程序线程可以直接调用OpenSL ES api,包括那些阻塞的api。但是,主线程不建议使用阻塞调用,因为它们可能导致应用程序不响应(ANR)。
关于调用回调处理程序的线程的决定很大程度上取决于OpenSL ES实现。这种灵活性的原因是为了允许将来进行优化,特别是在多核设备上。
回调处理程序运行的线程不能保证在不同调用之间具有相同的标识。因此,不要依赖pthread_self()
返回的pthread_t
或gettid()
返回的pid_t
在调用之间保持一致。出于同样的原因,不要从回调中使用线程本地存储(TLS) api,例如pthread_setspecific()
和pthread_getspecific()
。
该实现保证不会对同一对象发生相同类型的并发回调。然而,在不同的线程上,对于相同对象的不同类型的并发回调是可能的。
性能
由于OpenSL ES是一个 native 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 level 17)开始,应用程序可以查询平台原生或最佳输出采样率和设备主输出流的缓冲区大小。当与刚才提到的特性测试结合使用时,应用程序现在可以适当地配置自己,以在声称支持的设备上降低输出延迟。
对于Android 4.2 (API级别17)及更早版本,为了降低延迟,需要两个或更多的缓冲区计数。从Android 4.3 (API级别18)开始,一个缓冲区计数就足以降低延迟。
所有用于输出效果的OpenSL ES接口都排除了较低的延迟路径。
推荐顺序如下:
- 检查API级别9或更高,以确认OpenSL ES的使用。
- 检查
android.hardware.audio.low_latency
特性使用如下代码:
import android.content.pm.PackageManager;
...
PackageManager pm = getContext().getPackageManager();
boolean claimsFeature = pm.hasSystemFeature(PackageManager.FEATURE_AUDIO_LOW_LATENCY);
- 检查API级别17或更高,以确认
android.media.AudioManager.getProperty()
的使用。 - 使用以下代码获得原生或最优输出采样率和此设备的主输出流的缓冲区大小:
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,然后使用Integer.parseInt()
将其转换为int。
- 现在使用OpenSL ES创建一个带有PCM缓冲队列数据定位器的AudioPlayer。
注意:您可以使用音频缓冲区大小测试应用程序来确定音频设备上OpenSL ES音频应用程序的本机缓冲区大小和采样率。您还可以访问GitHub,查看音频缓冲大小的示例。
低延迟音频播放器的数量是有限的。如果您的应用程序需要多个音频源,请考虑在应用程序级别混合音频。当您的活动暂停时,请确保销毁您的音频播放器,因为它们是与其他应用程序共享的全局资源。
为了避免出现可听见的故障,缓冲区队列回调处理程序必须在一个小而可预测的时间窗口内执行。这通常意味着对互斥对象、条件或I/O操作没有不可控制的阻塞。相反,应该考虑使用锁、锁和超时等待以及非阻塞算法。
渲染下一个缓冲区(用于AudioPlayer)或使用前一个缓冲区(用于AudioRecord)所需的计算时间应该与每次回调的时间大致相同。避免在不确定的时间内执行的算法,或者在计算中出现问题。如果在任何给定回调中所花费的CPU时间明显大于平均值,则回调计算就会很激烈。总之,理想的情况是处理程序的CPU执行时间接近于零,处理程序在不设限时间内不阻塞。
只对以下输出做到低延迟音频是可能的:
- 设备内置扬声器。
- 有线耳麦。
- 有线耳机。
- 线路输出(音响)。
- USB数字音频。
在某些设备上,由于需要对扬声器进行校正和保护的数字信号处理,扬声器等待时间比其他路径要长。
在某些设备上,由于需要对扬声器进行校正和维护及数字信号处理,扬声器等待时间比其他路径要长。
从Android 5.0 (API Level 21)开始,被选的设备支持较低的音频输入延迟。要利用这个特性,首先要确认可以使用上面描述的较低的延迟输出。低延迟输出的能力是低延迟输入特性的先决条件。然后,创建一个AudioRecorder,其采样率和缓冲区大小与用于输出的相同。用于输入效果的OpenSL ES接口排除了较低的延迟路径。为了降低延迟,record预设必须使用 SL_ANDROID_RECORDING_PRESET_VOICE_RECOGNITION
;此预设将禁用特定于设备的数字信号处理,这可能会增加输入路径的延迟。有关record预置的更多信息,请参阅OpenSL ES的Android扩展这篇文章中的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下运行,但是这种分离确实使攻击更加困难。