Android开发播放pcm格式音频,Android中使用OpenSLES播放PCM音频

66b52468c121889b900d4956032f1009.png

8种机械键盘轴体对比

本人程序员,要买一个写代码的键盘,请问红轴和茶轴怎么选?

本文主要介绍在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文件:1ffmpeg -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 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;

std::shared_ptr pcmBufferBlockingQueue;

typedef std::vector 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(new PcmBufferPool);

pcmBufferBlockingQueue = std::shared_ptr(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 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<: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<: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 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(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

)

后记

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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值