本文主要介绍在Android中使用OpenSLES提供native接口播放PCM音频。已经有现成的java类AudioTrack可以使用,为什么要使用OpenSLES?有些时候需要在native层接收音频流,如果把音频流传到java层,再使用Android java API播放音频流,那么native层和java层之间传递数据需要花费一定的时间(虽然不是很大),既然native层有API就没有必要使用java层的API了。
那为什么不在java层接收音频流呢?网络连接与接收数据部分的代码用java实现就不方便移植到其他平台。
准备工作
- 开发环境 Android Studio是必备的,其次ndk-bundle也是需要的。
- Android设备 一部扬声器正常的Android手机或者平板。
- 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
17android {
...
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接口start
和stop
控制播放PCM音频:1
2
3
4
5
6
7
8
9
10
11
12
13public 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_manager
和android_fopen
方法是使用googlesamples中的实现android_fopen.h和android_fopen.c。PcmPlayer
的实现在后面介绍。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
8extern "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
23class 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
81class 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
检查是不是少了很多。然后本方法大致可以分为四步:
- 初始化
engineEngine
,engineEngine
的初始化是后两步顺利进行的前提条件; - 初始化
outputMixObject
,在本例中之后没有实际的操作; - 初始化
playerPlay
和audioBufferQueue
,playerPlay
是控制播放和停止的接口,audioBufferQueue
是数据缓存队列; - 初始
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
18struct 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
22struct 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
14void 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
14void 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时就阻塞。Start
和Stop
方法就相对简单了。1
2
3
4
5
6
7
8
9
10
11void 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
23void 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
之前必须先销毁outputMixObject
和playerObject
,不然会报错。代码实现了,要让它正常跑起来还不够,因为CMakeLists.txt还没配置好。
CMakeLists.txt
在Android中,OpenSLES以动态共享库的方式加载的,ndk-bundle中并没有libOpenSLES.so
的,而是在实际的运行设备中。1
2
3
4
5
6
7
8
9
10
11
12
13cmake_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。
后记
在写这篇博客的工程中,感觉整个项目很简单,想着到底有没有必要以博客的形式记录下来,并且文中大部分都是代码块。主要在写的过程中可以加深自己的理解,也可以锻炼自己的文笔(根本谈不上文笔-_-||)。有时候感觉写博客比写代码更难让人理解。