Tim's Note

试问Coding应不好,却道:此心安处是吾乡

0%

AudioTrack 与 OpenSL ES

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;

// 1、初始化 AudioTrack
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
);
}

// 2、将 AudioTrack 切换到播放状态
private void playerMode(){
if(audioTrack != null && audioTrack.getState () != AudioTrack.STATE_UNINITIALIZED){
audioTrack.play ();
}
}


// 3、开启播放线程
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 (); //byte、int short long char
}

// 4、停止 AudioTrack
private void stopAudioTrack() throws InterruptedException {
if (null != audioTrack && audioTrack.getState () != AudioTrack.STATE_UNINITIALIZED) {
audioTrack.stop ();
}

// 停止线程
isStop = true;
if (null != playerThread) {
playerThread.join ();
playerThread = null;
}

// 释放 AudioTrack
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>
}

// 定义输出的 TAG
#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;

// 1. 创建引擎并获取引擎接口
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;);
}

// 2. 创建并初始化混音器
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;);
}

// 3. 配置输入数据源参数
SLDataLocator_AndroidSimpleBufferQueue inputBuffQueueLocator = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 10};
SLDataFormat_PCM input_format_pcm = {
SL_DATAFORMAT_PCM, // <<< 输入的音频格式,PCM
2, // <<< 输入的声道数,2 (立体声)
SL_SAMPLINGRATE_44_1, // <<< 输入的采样率,44100hz
SL_PCMSAMPLEFORMAT_FIXED_16, // <<< 输入的采样位数,16bit
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};

// 4. 创建播放器
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");
}

// 5. 设置播放回调函数
(*pcmBufferQueue)->RegisterCallback(pcmBufferQueue, bufferQueueCallback, nullptr);
(*playInterface)->SetPlayState(playInterface, SL_PLAYSTATE_PLAYING);

(*pcmBufferQueue)->Enqueue(pcmBufferQueue, "", 1);


// 6. 销毁对象以及相关的资源
(*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 则代表着输出的信息,即数据输出到哪儿、以什么样的参数来输出。

欢迎关注我的其它发布渠道