Android中使用OpenSLES播放PCM音频

本文主要介绍在Android中使用OpenSLES提供native接口播放PCM音频。已经有现成的java类AudioTrack可以使用,为什么要使用OpenSLES?有些时候需要在native层接收音频流,如果把音频流传到java层,再使用Android java API播放音频流,那么native层和java层之间传递数据需要花费一定的时间(虽然不是很大),既然native层有API就没有必要使用java层的API了。

那为什么不在java层接收音频流呢?网络连接与接收数据部分的代码用java实现就不方便移植到其他平台。


准备工作

  1. 开发环境 Android Studio是必备的,其次ndk-bundle也是需要的。
  2. Android设备 一部扬声器正常的Android手机或者平板。
  3. PCM音频 如果没有现成的录音设备,可以使用ffmpeg将mp3文件转换为PCM文件:
    1
    ffmpeg -i your_audio.mp3 -f s16le -ar 44100 -ac 2 -acodec pcm_s16le your_audio.pcm

如果没有ffmpeg,在Mac上可以使用brew install ffmpeg进行安装,或者下载ffmpeg源码自行编译。

支持C++

方式一 在建立工程时确认勾选或选择了如下选项

  • Include C++ support
  • C++ Standard C++11

方式二 在Module的build.gradle文件中添加如下配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
android {
...
defaultConfig {
...
externalNativeBuild {
cmake {
cppFlags "-std=c++11 -frtti -fexceptions"
}
}
}
...
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
}
}
}

不过要确认path "src/main/cpp/CMakeLists.txt"指定的地方确实存在CMakeLists.txt文件。

申明native方法

要实现的功能非常简单。提供两个jni接口startstop控制播放PCM音频:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MainActivity extends AppCompatActivity {

// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("native-player");
}

...

private native void start(AssetManager assetManager, String filename);

private native void stop();
}

需要把事先准备好的PCM文件放到assets目录下。然后通过start方法把名称传到native层。

native方法实现

先来看一下start方法的实现。

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
...
static FILE *pcmFile;
static bool running;
static std::thread readThread;
static PcmPlayer pcmPlayer;
...
extern "C" JNIEXPORT void JNICALL
Java_me_huntto_openslespcmplayer_MainActivity_start(JNIEnv *env,
jobject /* object */,
jobject assetMgr,
jstring filename) {
android_fopen_set_asset_manager(AAssetManager_fromJava(env, assetMgr));
// convert Java string to UTF-8
const char *utf8 = env->GetStringUTFChars(filename, NULL);
assert(NULL != utf8);

// open the file to play
pcmFile = android_fopen(utf8, "rb");
if (pcmFile == NULL) {
LOGE("Can not open file:%s", utf8);
return;
}

pcmPlayer.Init(2, 44100, 16);
running = true;
readThread = std::thread(ReadPcmLoop);
}

android_fopen_set_asset_managerandroid_fopen方法是使用googlesamples中的实现android_fopen.handroid_fopen.cPcmPlayer的实现在后面介绍。pcmPlayer.Init(2, 44100, 16)中的2是声道数,44100为采样率,16表示一个采样占16bit。readThread不断从文件中读取PCM数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
...
const size_t kBufferSize = 1024 * 8;
static std::vector<uint8_t> buffer(kBufferSize);

static void ReadPcmLoop() {
while (running && !feof(pcmFile)) {
fread(buffer.data(), buffer.size(), 1, pcmFile);
pcmPlayer.FeedPcmData(buffer.data(), buffer.size());
}

pcmPlayer.Stop();
pcmPlayer.Release();
}

这里使用了std::thread,所以要在前面添加C++11的支持。

为什么不使用pthread呢?因为std::thread的可移植性更好,且使用姿势更简单。

再来看看stop方法的实现就更简单了。

1
2
3
4
5
6
7
8
extern "C" JNIEXPORT void JNICALL
Java_me_huntto_openslespcmplayer_MainActivity_stop(JNIEnv * /* env */,
jobject /* object */) {
running = false;
if (readThread.joinable()) {
readThread.join();
}
}

下面将主要介绍PcmPlayer的实现。

使用OpenSLES实现PcmPlayer

要把PcmPlayer设计成什么样呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class PcmPlayer {
public:
PcmPlayer();

~PcmPlayer() {
Release();
}

void Start();

void Stop();

void FeedPcmData(uint8_t *pcm, size_t size);

void Init(SLuint32 channels,
SLuint32 sampleRate,
SLuint32 bitsPerSample);

void Release();

private:
...
};

Start开始播放;但是Start并不是真正的开始播放,需要不断的调用FeedPcmData添加数据,如果添加的数据速度大于播放的速度会阻塞,这就是前面为什么要新开一个线程来读取PCM数据;Stop方法停止播放;不过在调用前面几个方法之前要先调用Init进行初始化。那么先来看看Init方法是怎么实现的。

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
class PcmPlayer {
...
private:
struct PcmBufferPool;
struct PcmBufferBlockingQueue;

SLObjectItf engineObject;
SLEngineItf engineEngine;
SLObjectItf outputMixObject;
SLObjectItf playerObject;
SLPlayItf playerPlay;
SLAndroidSimpleBufferQueueItf audioBufferQueue;

std::shared_ptr<PcmBufferPool> pcmBufferPool;
std::shared_ptr<PcmBufferBlockingQueue> pcmBufferBlockingQueue;

typedef std::vector<uint8_t> PcmBuffer;
PcmBuffer pcmBuffer;
bool callBacked;

friend void BufferQueueCallback(SLAndroidSimpleBufferQueueItf bufferQueue, void *pcmPlayer);
};

void PcmPlayer::Init(SLuint32 channels,
SLuint32 sampleRate,
SLuint32 bitsPerSample) {
SLresult result;
// init engine
result = slCreateEngine(&engineObject, 0, nullptr, 0, nullptr, nullptr);
assert(result == SL_RESULT_SUCCESS);
result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
assert(result == SL_RESULT_SUCCESS);
result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);
assert(result == SL_RESULT_SUCCESS);

//init output mix
result = (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 0, 0,
0);
assert(result == SL_RESULT_SUCCESS);
result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);
assert(result == SL_RESULT_SUCCESS);

//init player
SLDataLocator_OutputMix outputMix = {SL_DATALOCATOR_OUTPUTMIX, outputMixObject};
SLDataLocator_AndroidSimpleBufferQueue dataSourceQueue = {
SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE,
2};

SLDataFormat_PCM dataSourceFormat = {
SL_DATAFORMAT_PCM,
channels,
sampleRate * 1000,
bitsPerSample,
bitsPerSample,
channels == 1 ? SL_SPEAKER_FRONT_CENTER
: SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT,
SL_BYTEORDER_LITTLEENDIAN
};
SLDataSource dataSource = {&dataSourceQueue, &dataSourceFormat};
SLDataSink dataSink = {&outputMix, NULL};
const SLInterfaceID ids[] = {SL_IID_BUFFERQUEUE};
const SLboolean reqs[] = {SL_BOOLEAN_TRUE};

result = (*engineEngine)->CreateAudioPlayer(engineEngine, &playerObject, &dataSource, &dataSink,
1, ids,
reqs);
assert(result == SL_RESULT_SUCCESS);
result = (*playerObject)->Realize(playerObject, SL_BOOLEAN_FALSE);
assert(result == SL_RESULT_SUCCESS);

result = (*playerObject)->GetInterface(playerObject, SL_IID_PLAY, &playerPlay);
assert(result == SL_RESULT_SUCCESS);

result = (*playerObject)->GetInterface(playerObject, SL_IID_BUFFERQUEUE, &audioBufferQueue);
assert(result == SL_RESULT_SUCCESS);
result = (*audioBufferQueue)->RegisterCallback(audioBufferQueue, BufferQueueCallback, this);
assert(result == SL_RESULT_SUCCESS);

pcmBufferPool = std::shared_ptr<PcmBufferPool>(new PcmBufferPool);
pcmBufferBlockingQueue = std::shared_ptr<PcmBufferBlockingQueue>(new PcmBufferBlockingQueue);
}

方法很长!!其实如果去掉中间的assert检查是不是少了很多。然后本方法大致可以分为四步:

  1. 初始化engineEngineengineEngine的初始化是后两步顺利进行的前提条件;
  2. 初始化outputMixObject,在本例中之后没有实际的操作;
  3. 初始化playerPlayaudioBufferQueueplayerPlay是控制播放和停止的接口,audioBufferQueue是数据缓存队列;
  4. 初始PcmBuffer的对象池PcmBufferPool和阻塞缓存队列PcmBufferBlockingQueue

步骤1-3基本是固定的,在本例中没有太多的发挥空间。其实感觉不需要做太多解释,因为代码足够清晰(OpenSLES是用C实现的面向对象编程),不然就是画蛇添足了。
接下来看看PcmBufferPool的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct PcmPlayer::PcmBufferPool {
PcmBuffer Get(size_t size) {
if (pool.empty()) {
return PcmBuffer(size);
} else {
auto buffer = pool.front();
pool.pop_front();
return buffer;
}
}

void Return(const PcmBuffer &buffer) {
pool.push_back(std::move(buffer));
}

private:
std::list<PcmBuffer> pool;
};

很简单但是并不高效,至少够用。PcmBufferBlockingQueue也有同样的特点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct PcmPlayer::PcmBufferBlockingQueue {
void Enqueue(const PcmBuffer &pcmBuffer, size_t maxSize) {
std::unique_lock<std::mutex> lock(queue_mutex);
queue_cond.wait(lock, [this, maxSize] { return queue.size() < maxSize; });
queue.push_back(std::move(pcmBuffer));
queue_cond.notify_one();
}

PcmBuffer Dequeue() {
std::unique_lock<std::mutex> lock(queue_mutex);
queue_cond.wait(lock, [this] { return queue.size() > 0; });
auto buffer = queue.front();
queue.pop_front();
queue_cond.notify_one();
return buffer;
}

private:
std::mutex queue_mutex;
std::condition_variable queue_cond;
std::list<PcmBuffer> queue;
};

接下来看看本例中比较核心的一个方法BufferQueueCallback,为什么是核心呢?因为它是调用次数最多的几个方法之一。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void BufferQueueCallback(SLAndroidSimpleBufferQueueItf bufferQueue, void *context) {
PcmPlayer *pcmPlayer = (PcmPlayer *) context;

PcmPlayer::PcmBuffer buffer = pcmPlayer->pcmBufferBlockingQueue->Dequeue();
if (pcmPlayer->pcmBuffer.capacity() < buffer.size()) {
pcmPlayer->pcmBuffer.resize(buffer.size());
}
memcpy(pcmPlayer->pcmBuffer.data(), buffer.data(), buffer.size());
(*bufferQueue)->Enqueue(bufferQueue, pcmPlayer->pcmBuffer.data(),
static_cast<SLuint32>(buffer.size()));

pcmPlayer->pcmBufferPool->Return(buffer);
pcmPlayer->callBacked = true;
}

这个方法在OpenSLES引擎需要填充缓冲时自动回调的,当回调产生时,需要往缓冲队列里面填充PCM数据。而PCM数据是从pcmBufferBlockingQueue里面获取的(BufferQueueCallback可以理解为pcmBufferBlockingQueue的消费者),pcmBufferBlockingQueue需要我们自己维护的。每一个PcmBuffer用完之后都要放回对象池中。再来看看pcmBufferBlockingQueue的生产者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void PcmPlayer::FeedPcmData(uint8_t *pcm, size_t size) {
PcmBuffer buffer = pcmBufferPool->Get(size);
buffer.resize(size);
memcpy(buffer.data(), pcm, size);
pcmBufferBlockingQueue->Enqueue(buffer, 5);
if (!callBacked) {
SLuint32 playState;
(*playerPlay)->GetPlayState(playerPlay, &playState);
if (playState != SL_PLAYSTATE_PLAYING) {
Start();
}
BufferQueueCallback(audioBufferQueue, this);
}
}

FeedPcmData开始时,先要从对象池中获取一个PcmBuffer,拷贝数据,然后放入阻塞队列中,当队列长度大于5时就阻塞。StartStop方法就相对简单了。

1
2
3
4
5
6
7
8
9
10
11
void PcmPlayer::Start() {
if (playerPlay != nullptr) {
(*playerPlay)->SetPlayState(playerPlay, SL_PLAYSTATE_PLAYING);
}
}

void PcmPlayer::Stop() {
if (playerPlay != nullptr) {
(*playerPlay)->SetPlayState(playerPlay, SL_PLAYSTATE_STOPPED);
}
}

最后一个方法Release

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void PcmPlayer::Release() {
if (playerObject != nullptr) {
(*playerObject)->Destroy(playerObject);
playerObject = nullptr;
playerPlay = nullptr;
audioBufferQueue = nullptr;
}

if (outputMixObject != nullptr) {
(*outputMixObject)->Destroy(outputMixObject);
outputMixObject = nullptr;
}

if (engineObject != nullptr) {
(*engineObject)->Destroy(engineObject);
engineObject = nullptr;
engineEngine = nullptr;
}

pcmBufferBlockingQueue = nullptr;
pcmBufferPool = nullptr;
callBacked = false;
}

需要注意的地方是销毁engineObject之前必须先销毁outputMixObjectplayerObject,不然会报错。代码实现了,要让它正常跑起来还不够,因为CMakeLists.txt还没配置好。

CMakeLists.txt

在Android中,OpenSLES以动态共享库的方式加载的,ndk-bundle中并没有libOpenSLES.so的,而是在实际的运行设备中。

1
2
3
4
5
6
7
8
9
10
11
12
13
cmake_minimum_required(VERSION 3.4.1)

add_library( native-player SHARED
pcm_player_jni.cpp
pcm_player.cpp
android_fopen.c
)

target_link_libraries( native-player
log
OpenSLES
android
)

完整的项目代码已上传至仓库OpenSLESPcmPlayer

后记

在写这篇博客的工程中,感觉整个项目很简单,想着到底有没有必要以博客的形式记录下来,并且文中大部分都是代码块。主要在写的过程中可以加深自己的理解,也可以锻炼自己的文笔(根本谈不上文笔-_-||)。有时候感觉写博客比写代码更难让人理解。