Android 的 SDK 提供了 MediaPlayer、SoundPool 和 AudioTrack 三套音频播放的 API。其中 AudioTrack 适合低延迟的播放,是更加底层的 API,提供了非常强大的控制能力,适合流媒体的播放等场景,由于其属于底层 API,需要结合解码器来使用。
OpenSL ES 全称为 Open Sound Library for Embedded Systems,即嵌入式音频加速标准。OpenSL ES 是无授权费、跨平台、针对嵌入式系统精心优化的硬件音频加速 API。它为嵌入式移动多媒体设备上的本地应用程序开发者提供了标准化、高性能、低响应时间的音频功能实现方法,同时还实现了软 / 硬件音频性能的直接跨平台部署。本次要介绍的就是 AudioTrack 与 OpenSL ES。
AudioTrack 简介 MediaPlayer 适合在后台长时间播放本地音乐文件或者在线的流式媒体文件,它的封装度高,且使用简单。
SoundPool 适合播放比较短的音频片段,比如按键声、铃声片段、闹钟片段等,它可以同时播放多个音频。
AudioTrack 适合低延迟的播放,是更加底层的 API,提供了非常强大的控制能力,适合流媒体的播放等场景,由于其属于底层 API,需要结合解码器来使用。
AudioTrack 工作流程 由于 AudioTrack 是 Android 的最底层的音频播放 API,只允许输入裸数据。所以它需要自行实现解码操作和缓冲区控制。因为这里只涉及 AudioTrack 的音频渲染端,那么如何使用 AudioTrack 渲染音频 PCM 数据呢?
首先来看一下 AudioTrack 的工作流程,具体如下。 1、根据音频参数信息,配置出一个 AudioTrack 的实例。 2、调用 play 方法,将 AudioTrack 切换到播放状态。 3、启动播放线程,循环向 AudioTrack 的缓冲区中写入音频数据。 4、当数据写完或者停止播放的时候,停止播放线程,并且释放所有资源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 public class AudioTrackDemo { private AudioTrack audioTrack; private int minBufferSize; private boolean isStop = true ; private Thread playerThread; private void initAudioTrack () { minBufferSize = AudioTrack.getMinBufferSize (48000 , 2 , AudioFormat.ENCODING_PCM_16BIT); audioTrack = new AudioTrack ( AudioManager.STREAM_SYSTEM, 48000 , 2 , AudioFormat.ENCODING_PCM_16BIT, minBufferSize, AudioTrack.MODE_STREAM ); } private void playerMode () { if (audioTrack != null && audioTrack.getState () != AudioTrack.STATE_UNINITIALIZED){ audioTrack.play (); } } public void startPlayThread () { playerThread = new Thread (()->{ short [] samples = new short [minBufferSize]; while (!isStop) { int actualSize = decoder.readSamples (samples); audioTrack.write (samples, actualSize); } }, "playerThread" ); playerThread.start (); } private void stopAudioTrack () throws InterruptedException { if (null != audioTrack && audioTrack.getState () != AudioTrack.STATE_UNINITIALIZED) { audioTrack.stop (); } isStop = true ; if (null != playerThread) { playerThread.join (); playerThread = null ; } if (audioTrack != null ) { audioTrack.release (); } } }
注意:上述代码中的 decoder 是一个解码器,此处并未实现,假设已经初始化成功,最后将调用 write 方法把从解码器中获得的 PCM 采样数据写入 AudioTrack 的缓冲区中,注意此方法是阻塞的方法,比如:一般要写入 200ms 的音频数据需要执行接近 200ms 的时间。
现在对上面的部分参数进行解释:
音频管理策略:
1 2 3 4 5 6 7 8 9 10 11 12 AudioSystem.STREAM_VOICE_CALL; AudioSystem.STREAM_SYSTEM; AudioSystem.STREAM_RING; AudioSystem.STREAM_MUSIC; AudioSystem.STREAM_ALARM; AudioSystem.STREAM_NOTIFICATION;
sampleRateInHz :采样率,即播放的音频每秒钟会有没少次采样,可选用的采样频率列表为: 8000 , 16000 , 22050 , 24000 ,32000 , 44100 , 48000 等,大家可以根据自己的应用场景进行合理的选择。
channelConfig : 声道数的配置,可选值以常量的形式配置在类 AudioFormat 中,常用的是 CHANNEL_IN_MONO (单声道)、CHANNEL_IN_STEREO (双声道) ,因为现在大多数手机的麦克风都是伪立体声采集,为了性能考虑,建议使用单声道进行采集。
audioFormat : 该参数是用来配置 “数据位宽” 的,即采样格式,可选值以常量的形式定义在类 AudioFormat 中,分别为 ENCODING_PCM_16BIT (兼容所有手机)、ENCODING_PCM_8BIT。
bufferSizeInBytes : 配置内部的音频缓冲区的大小, AudioTrack 类提供了一个帮助开发者确定的 bufferSizeInBytes 的函数,AudioTrack.getMinBufferSize ()。建议使用此函数进行计算,而非手动计算。
mode : AudioTrack 提供了两种播放模式,可选的值以常量的形式定义在类 AudioTrack 中,一个是 MODE_STATIC , 需要一次性将所有的数据都写入播放缓冲区中,简单高效,通常用于播放铃声、系统提醒的音频片段;另一个是 MODE_STREAM ,需要按照一定的时间间隔不断地写入音频数据,理论上它可以应用于任何音频播放的场景。
OpenSL ES 简介 OpenSL ES 定义了一套适用于嵌入式系统、跨平台、且免费的音频处理的 API。Android 的 OpenSL ES 库是在 NDK 的 platforms 文件夹对应 android 平台先相应 cpu 类型里面,如:
下图中描述了 OpenSL ES 的架构,在 Android 中,OpenSL ES 则是底层的 API,属于 C 语言 API 。在开发中,一般会直接使用高级 API , 除非遇到性能瓶颈,如语音实时聊天、3D Audio 、某些 Effects 等,开发者可以直接通过 C/C++ 开发基于 OpenSL ES 音频的应用。
OpenSL ES 开发流程 在 OpenSL ES 中,对象是对一组资源及其状态的抽象,每个对象都有一个在其创建时指定的类型,类型决定了对象可以执行的任务集,对象有点类似于 C++ 中类的概念。接口是对象提供的一组特征的抽象,这些抽象会为开发者 提供一组方法以及每个接口的类型功能,在代码中,接口的类型由接口 ID 来标识(这和 FFmpeg 的实现是一致的思想,FFmpeg 是如何实现面向对象的思想呢?后面我也会举例说明)。
所以一个对象在代码中其实是没有实际的表示形式的,可以通过接口来改变对象的状态以及使用对象提供的功能。对象可以有一个或者多个接口的实例,但是接口实例肯定只属于一个对象。这确实属于如何用 C 语言实现面向对象的范围了。下面是开发流程:
1、 创建接口对象 2、设置混音器 3、创建播放器(录音器) 4、设置缓冲队列和回调函数 5、设置播放状态 6、启动回调函数
下面这个代码示例用于播放 SDCard 上的 test.pcm 文件:
CMakeList.txt
1 2 3 4 5 6 7 8 9 cmake_minimum_required (VERSION 3.10 .2 )project ("opensles" )add_library (native-lib SHARED native-lib.cpp)find_library (log-lib log)target_link_libraries (native-lib OpenSLES ${log-lib} )
native-lib.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 #include <jni.h> #include <string> extern "C" {#include <SLES/OpenSLES.h> #include <SLES/OpenSLES_Android.h> #include <android/log.h> } #define LOG_TAG "OpenSL-ES-Demo" #define LOGI (...) __android_log_print (ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) void bufferQueueCallback (SLAndroidSimpleBufferQueueItf bufferQueue, void *pContext) { static FILE *fp = nullptr ; static char *buf = nullptr ; if (!buf) { buf = new char [1024 * 1024 ]; } if (!fp) { fp = fopen ("/sdcard/test.pcm" , "rb" ); } if (!fp) return ; if (feof (fp) == 0 ) { int len = fread (buf, 1 , 1024 , fp); if (len > 0 ) { (*bufferQueue)->Enqueue (bufferQueue, buf, len); } } } extern "C" JNIEXPORT void JNICALL Java_com_tal_opensles_MainActivity_stringFromJNI ( JNIEnv* env, jobject) { SLresult sLResult; SLObjectItf engineObject; SLEngineItf slAudioEngine; sLResult = slCreateEngine (&engineObject, 0 , nullptr , 0 , nullptr , nullptr ); if (sLResult != SL_RESULT_SUCCESS) { LOGI (" 引擎创建失败 & quot; ); } sLResult = (*engineObject)->Realize (engineObject, SL_BOOLEAN_FALSE); if (sLResult != SL_RESULT_SUCCESS) { LOGI (" 引擎初始化失败 & quot; ); } sLResult = (*engineObject)->GetInterface (engineObject, SL_IID_ENGINE, &slAudioEngine); if (sLResult != SL_RESULT_SUCCESS) { LOGI (" 获取引擎接口失败 & quot; ); } SLObjectItf outputMix; sLResult = (*slAudioEngine)->CreateOutputMix (slAudioEngine, &outputMix, 0 , nullptr , nullptr ); if (sLResult != SL_RESULT_SUCCESS) { LOGI (" 创建混音器失败 & quot; ); } sLResult = (*outputMix)->Realize (outputMix, SL_BOOLEAN_FALSE); if (sLResult != SL_RESULT_SUCCESS) { LOGI (" 混音器初始化失败 & quot; ); } SLDataLocator_AndroidSimpleBufferQueue inputBuffQueueLocator = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 10 }; SLDataFormat_PCM input_format_pcm = { SL_DATAFORMAT_PCM, 2 , SL_SAMPLINGRATE_44_1, SL_PCMSAMPLEFORMAT_FIXED_16, SL_PCMSAMPLEFORMAT_FIXED_16, SL_SPEAKER_FRONT_LEFT|SL_SPEAKER_FRONT_RIGHT, SL_BYTEORDER_LITTLEENDIAN }; SLDataSource dataSource = {&inputBuffQueueLocator, &input_format_pcm}; SLDataLocator_OutputMix outputMixLocator = {SL_DATALOCATOR_OUTPUTMIX, outputMix}; SLDataSink dataSink = {&outputMixLocator, nullptr }; SLObjectItf audioPlayer; SLAndroidSimpleBufferQueueItf pcmBufferQueue; SLPlayItf playInterface; SLInterfaceID audioPlayerInterfaceIDs [] = {SL_IID_ANDROIDSIMPLEBUFFERQUEUE}; SLboolean audioPlayerInterfaceRequired [] = {SL_BOOLEAN_TRUE}; sLResult = (*slAudioEngine)->CreateAudioPlayer (slAudioEngine, &audioPlayer, &dataSource, &dataSink, 1 , audioPlayerInterfaceIDs, audioPlayerInterfaceRequired); if (sLResult != SL_RESULT_SUCCESS) { LOGI (" 创建播放器失败 & quot; ); } sLResult = (*audioPlayer)->Realize (audioPlayer, SL_BOOLEAN_FALSE); if (sLResult != SL_RESULT_SUCCESS) { LOGI (" 播放器初始化失败 & quot; ); } sLResult = (*audioPlayer)->GetInterface (audioPlayer, SL_IID_PLAY, &playInterface); if (sLResult != SL_RESULT_SUCCESS) { LOGI (" 获取引擎接口失败 -> SL_IID_PLAY" ); } sLResult = (*audioPlayer)->GetInterface (audioPlayer, SL_IID_BUFFERQUEUE, &pcmBufferQueue); if (sLResult != SL_RESULT_SUCCESS) { LOGI (" 获取引擎接口失败 -> SL_IID_BUFFERQUEUE" ); } (*pcmBufferQueue)->RegisterCallback (pcmBufferQueue, bufferQueueCallback, nullptr ); (*playInterface)->SetPlayState (playInterface, SL_PLAYSTATE_PLAYING); (*pcmBufferQueue)->Enqueue (pcmBufferQueue, "" , 1 ); (*audioPlayer)->Destroy (engineObject); (*outputMix)->Destroy (engineObject); }
结构体中的 numInterfaces , pInterfaceIds , pInterfaceRequired ,这里以创建播放器所调用的 CreateAudioPlayer 函数为例说明:
1 2 3 4 5 6 7 8 9 SLresult (*CreateAudioPlayer) ( SLEngineItf self, SLObjectItf * pPlayer, SLDataSource *pAudioSrc, SLDataSink *pAudioSnk, SLuint32 numInterfaces, const SLInterfaceID * pInterfaceIds, const SLboolean * pInterfaceRequired );
各参数含义如下:
1、SLEngineItf C 语言不像 C++,没有 this 指针,所以只能通过每次调用 SLEngineItf 的方法的时候手动传入。
2、SLObjectItf 用于保存创建出来的 AudioPlayerObject。
3、SLDataSource 输入数据源的信息。
4、SLDataSink 输出的信息。
5、numInterfaces 与下面的 SLInterfaceID 和 SLboolean 配合使用,用于标记 SLInterfaceID 数组和 SLboolean 数组的大小。
6、SLInterfaceID 这里需要传入一个数组,指定创建的 AudioPlayerObject 需要包含哪些 Interface。
7、SLboolean 这里也是一个数组,用来标记每个需要包含的 Interface 在 AudioPlayerObject 不支持的情况下,是不是需要在创建 AudioPlayerObject 时返回失败。
最后的三个参数用于指定 AudioPlayerObject 需要包含哪些 Interface,如果不包含是不是要直接创建失败。之前也提到过,并不是每个系统上都实现了 OpenSL ES 为 Object 定义的所有 Interface,所以在获取 Interface 的时候需要做一些选择和判断,如果创建成功的话我们就能使用 AudioPlayerObject 的 GetInterface 方法获取到这些 Interface 了。
DataSouce 和 DataSink 在 OpenSL ES 里,这两个结构体均是作为创建 Media Object 对象时的参数而存在的,data source 代表着输入源的信息,即数据从哪儿来、输入的数据参数是怎样的;而 data sink 则代表着输出的信息,即数据输出到哪儿、以什么样的参数来输出。