深入解析PCM文件播放技术:OpenSL ES应用详解(第二部分)

更新:11-12 名人轶事 我要投稿 纠错 投诉

很多朋友对于深入解析PCM文件播放技术:OpenSL ES应用详解(第二部分)和不太懂,今天就由小编来为大家分享,希望可以帮助到大家,下面一起来看看吧!

准备

1.笔者这里的开发环境是Mac 10.14.4,android studio是3.3.2;

2、在android studio中配置c++环境。详情请参阅官方网站。将我的Cmakelists.txt 配置文件粘贴到此处。

cmake_minimum_required(版本3.4.1)

#丁一cpp原文建木路边两

设置(SRC_DIR ${PROJECT_SOURCE_DIR}/src/main/cpp)

设置(COMMON_DIR ${SRC_DIR}/common)

设置(OPENSLES_DIR ${SRC_DIR}/opensles)

#她之cpp元妈母录

aux_source_directory(${SRC_DIR} src_cpp)

aux_source_directory(${COMMON_DIR} com_cpp)

aux_source_directory(${OPENSLES_DIR} opensles_cpp)

#摄之.h投文见木录

包含目录(${COMMON_DIR})

包含目录(${OPENSLES_DIR})

# yin ru android 日志ku

find_library( # 设置路径变量的名称。

日志库

# 指定NDK库的名称

# 你想要CMake 定位。

日志)

#鼎一讲该酷大爆金app的雷星

add_library(广告媒体共享

${src_cpp}

${com_cpp}

${opensles_cpp}

target_link_libraries(adMedia android 日志

开放式SLES

中位数

${日志库}

)app下build.gradle的C++部分配置代码

安卓{

编译SDK版本28

默认配置{

applicationId "com.media"

minSdkVersion 23

目标SDK版本28

版本代码1

版本名称“1.0”

testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

//abi的这个配置必须放在defaultConfig{}中,否则会报错

NDK {

abiFilters "armeabi-v7a","arm64-v8a"

}

}

外部NativeBuild {

cmake {

路径文件("CMakeLists.txt")

}

}

构建类型{

发布{

minifyEnabled false

proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"

}

}

}对于abi的配置,必须放在defaultConfig{}中,否则会报错,如上所述。

使用OpenSL ES

头文件必须加引号

//这是标准的OpenSL ES 库

#include//这是Android 的扩展。如果你想破坏这个平台,你需要注意。

#include1、初始化OpenSL ES引擎对象

//用于OpenSL ES的各种运算结果; SL_RESULT_SUCCESS 表示成功

SLresult结果;

//1. 创建OpenSL ES 对象

//OpenSL ES for Android 被设计为线程安全的,

//所以这个选项请求将被忽略,但它会

//使源代码可移植到其他平台。

SLEngineOption选项[]={{SL_ENGINEOPTION_THREADSAFE,SL_BOOLEAN_TRUE}};

//创建引擎对象

结果=slCreateEngine(slObject,

ARRAY_LEN(选项),

选项,

0, //无接口

NULL,//没有接口

NULL //不需要

);

如果(结果!=SL_RESULT_SUCCESS){

LOGD("slCreateEngine 失败%lu",结果);

}对于slObject对象,Android默认创建时是线程安全的,不需要options[]选项。

2.实例化对象

//2. 实例化SL 对象。就像声明一个类变量一样,你也需要初始化一个实例;以阻塞模式初始化实例(第二个参数表示阻塞或非阻塞模式)

结果=(*slObject)-Realize(slObject, SL_BOOLEAN_FALSE);

如果(结果!=SL_RESULT_SUCCESS){

LOGD("实现失败%lu",结果);

}3.获取引擎对象接口

//3. 获取引擎接口对象,必须在实例化SLObjectItf对象后获取。

结果=(*slObject)-GetInterface(slObject, SL_IID_ENGINE, engineObject);

如果(结果!=SL_RESULT_SUCCESS){

LOGD("获取接口失败%lu",结果);

}可以看到OpenSL ES类、实例、接口的使用规则。首先创建一个类,然后初始化修改后的类的实例,然后获取该类的接口,该接口对应特定的功能。后面要用到的音频播放组件也遵循这个流程。

要正常播放音频,需要两个相关的组件,即混音器和音频组件。这里介绍一下,mixer组件是实现音频播放功能的。音频组件负责通过回调从应用程序获取音频数据。下面介绍如何初始化混音器以及如何将混音器串联到音频组件上。

4. 搅拌机

/** 第二步创建混音器;她可能不仅仅代表一个音效器,它默认是音频播放输出,是音频播放的必备组件;

*CreateOutputMix()参数说明如下:

* 参数1:引擎接口对象

* 参数2:混频器对象地址

* 参数3: 组件可配置的属性ID个数;如果为0,则忽略后面两个参数;不同的组件有不同类型和数量的属性。如果组件不支持某个属性,GetInterface将

* 返回SL_RESULT_FEATURE_UNSUPPORTED

* 参数4:需要配置属性ID,数组

* 参数5:配置的这些属性ID是否为组件所必需,数组,与参数4一一对应

* */

//这只是一个简单的音频播放输出,所以没有创建混合效果的属性ID。只需传递0 作为第三个参数即可。

结果=(*engineItf)-CreateOutputMix(engineItf,outputMixObject,0,NULL,NULL);

如果(结果!=SL_RESULT_SUCCESS){

LOGD("CreateOutputMix 失败%d",结果);

}

//实例化混响器,需要在创建功能组件类型后实例化; OpenSL ES

结果=(*outputMixObject)-Realize(outputMixObject,SL_BOOLEAN_FALSE);

如果(结果!=SL_RESULT_SUCCESS){

LOGD("实现outputMixObject失败%d",结果);

}5.创建音频组件并将它们与混音器串联

/** SLDataLocator_AndroidSimpleBufferQueue 表示数据缓冲区的结构体,用来表示一个缓冲区

* 1.第一个参数必须是SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE

* 2.第二个参数表示队列中缓冲区的数量。这里测试1 2 3 4都是正常的。

* */

SLDataLocator_AndroidSimpleBufferQueue android_queue={SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE,2};

SLuint32 chs=getChannel_layout_Channels(fChannel_layout);

SLuint32 ch=getChannel_layout_Type(fChannel_layout);

SLuint32 sr=getSampleRate(fSample_rate);

SLuint32 fo=getPCMSample_format(fSample_format);

LOGD("通道数%d 通道类型%d 采样率%d 采样格式%d",chs,ch,sr,fo);

SLDataFormat_PCM pcm={

SL_DATAFORMAT_PCM,//播放pcm格式的数据

chs,//通道数

sr,//采样率

fo,//位数

fo,//只要位数相同

ch,//立体声(左前、右前)

SL_BYTEORDER_LITTLEENDIAN//数据存储为little endian

};

/** SLDataSource 表示输入缓冲区。与输出缓冲区一样,它由数据类型和数据格式组成。

* 1.参数1;指向由SLDataLocator_xxx 结构定义的指定数据缓冲区。可能的值如下:

*SLDataLocator_地址

* SLDataLocator_BufferQueue(数据缓冲区,对于Android来说是SLDataLocator_AndroidSimpleBufferQueue)

*SLDataLocator_IODevice

*SLDataLocator_MIDIBufferQueue

*SLDataLocator_URI

* 2.参数2;表示缓冲区中数据的格式。可能的值如下:

*SLDataFormat_PCM

*SLDataFormat_MIME

* 如果第一个参数是SLDataLocator_IODevice,则忽略该参数,可以传入NULL。

* */

SLDataSource slDataSource={android_queue, pcm};

SLDataLocator_OutputMix outputMix={SL_DATALOCATOR_OUTPUTMIX,outputMixObject};

/** SLDataSink表示输出缓冲区,包括数据类型和数据格式

* 1.参数1;指向指定的数据缓冲区类型,一般由SLDataLocator_xxx结构体定义。可能的值如下:

*SLDataLocator_地址

*SLDataLocator_IODevice

* SLDataLocator_OutputMix(混音器,代表音频输出)

*SLDataLocator_URI

* SLDataLocator_BufferQueue(数据缓冲区,对于Android来说是SLDataLocator_AndroidSimpleBufferQueue)

*SLDataLocator_MIDIBufferQueue

* 2.参数2;表示缓冲区中数据的格式。可能的值如下:

*SLDataFormat_PCM

*SLDataFormat_MIME

* 如果第一个参数是SLDataLocator_IODevice或SLDataLocator_OutputMix,则该参数被忽略,可以传递NULL。

* Live:我们要做的就是给出缓冲区类型和缓冲区中的数据格式,系统会自动为我们分配内存

* */

SLDataSink audioSink={outputMix, NULL};

/** 第三步创建AudioPlayer组件;这个组件必须有一个输入数据缓冲区(提供要播放的音频数据),一个输出数据缓冲区(硬件最终从这里获取数据),

* 一个音频数据中间缓冲区(由于应用程序无法直接向输入数据缓冲区写入数据,所以通过这个缓冲区间接写入,所以这样的缓冲区是必要的)和可选属性(如控制音量等)

* 参数1:openSL es引擎接口

* 参数2:AudioPlayer组件对象

* 参数3: 输入缓冲区地址

* 参数4: 输出缓冲区地址

* 参数5: 属性ID 数量

* 参数6:属性ID数组

* 参数7:属性ID是否必须是数组

* 注:可以看到输入输出缓冲区是通过参数3和4直接配置的,而音频数据中间缓冲区是通过属性ID配置的。

*

* 音频播放器的数据驱动流程为:首先,播放系统需要从audioSnk获取数据进行播放,而audioSnk需要从audioSrc获取音频数据。 audioSrc中的数据通过ids1中配置的回调函数不断传输。

* 将数据写入其中,使整个播放过程精简。

* 1.CreateAudioPlayer()的第五个参数不能为0,否则audioSrc将没有数据发送到audioSnk。

* 2.写数据的回调函数在单独的线程中,每12ms-20ms定时调用一次。

* */

const SLInterfaceID ids[1]={SL_IID_BUFFERQUEUE};

const SLboolean req[1]={SL_BOOLEAN_TRUE};

结果=(*engineItf)-CreateAudioPlayer(engineItf,playerObject,slDataSource,audioSink,1,ids,req);

结果=(*playerObject)-Realize(playerObject, SL_BOOLEAN_FALSE);

//注册回调缓冲区并获取缓冲区队列接口

结果=(*playerObject)-GetInterface(playerObject, SL_IID_BUFFERQUEUE, pcmBufferQueueItf);

//注册缓冲区接口回调。开始播放后,此功能将

结果=(*pcmBufferQueueItf)-RegisterCallback(pcmBufferQueueItf, SLAudioPlayer:pcmBufferCallBack, this);

如果(结果!=SL_RESULT_SUCCESS){

LOGD("注册回调失败%d",结果);

}

//获取音量接口,用于设置音量

//(*playerObject)-GetInterface(playerObject, SL_IID_VOLUME, volInf);

//(*volInf)-SetVolumeLevel(volInf,100*50);

//获取接口后调用获取Player接口

结果=(*playerObject)-GetInterface(playerObject,SL_IID_PLAY,playerInf);

//开始播放

结果=(*playerInf)-SetPlayState(playerInf, SL_PLAYSTATE_PLAYING);

如果(结果!=SL_RESULT_SUCCESS){

LOGD("SetPlayState 失败%d",结果);

}整个串联过程是:

首先,播放系统需要来自audioSnk的数据进行播放,而audioSnk需要来自audioSrc的音频数据。通过ids1中配置的回调函数pcmBufferCallBack不断地将audioSrc中的数据写入其中,从而使整个播放过程变得精简。

6.应用程序向OpenSL ES传输数据

与AudioTrack一样,必须打开一个单独的线程作为应用程序的工作线程,才能将数据传输到OpenSL ES。这里需要注意的是,OpenSL ES 仅支持小端顺序的16 位和32 位整数音频数据。回调函数pcmBufferCallBack是一个渲染线程,与这里的工作线程不同,所以这里必须有一个线程同步机制,它是使用pthread_mutex_t和pthread_con_t来实现的。

/** outBufSamples: 是固定值;表示每次发送到音频数据缓冲区的样本数

* outputBuffer:是一个包含两个缓冲区的内存块,这是一个巧妙的设计。这样就保证了内存不被频繁创建,保证了内存中数据的安全。 (具体过程是:当一个buffer数据经过Enqueue

* 当函数发送到OpenSL ES时,当应用程序没有从OpenSL ES缓冲区回调接口接收到需要数据的回调时,这个内存块就不能再使用,因此使用另一个缓冲区。如果还使用了其他缓冲区

* 如果应用程序已充满数据但尚未收到需要数据的OpenSL ES 缓冲区回调接口的回调,该怎么办?说明OpenSL ES正忙,暂时不需要数据。应用程序端被阻塞,直到收到通知)

* currentOutputIndex:表示应用端当前已填充数据的缓冲区中下一个填充的数据位置

* currentOutputBuffer: 表示应用程序当前使用哪个缓冲区

**/

void SLAudioPlayer:putAudioData(char * buff, int size)

{

//从最后一个位置开始填充数据

int indexOfOutput=当前输出索引;

if (fSample_format==Sample_format_SignedInteger_8) {

char *newBuffer=(char *)buff;

char *useBuffer=outputBuffer[currentOutputBuffer];

for (int i=0; i 大小; ++i) {

useBuffer[indexOfOutput++]=newBuffer[i];

if (indexOfOutput=outBufSamples) { //指示缓冲区已填满并发送到OpenSL ES

//发送之前需要获取可以发送的条件。

等待线程锁();

//发送数据

(*pcmBufferQueueItf)-Enqueue(pcmBufferQueueItf,useBuffer,outBufSamples* sizeof(char));

//更改为另一个缓冲区

当前输出缓冲区=当前输出缓冲区?0:1;

输出索引=0;

useBuffer=输出缓冲区[当前输出缓冲区];

}

}

//更新最后的位置

当前输出索引=输出索引;

} else if (fSample_format==Sample_format_SignedInteger_16) {

短*newBuffer=(短*)buff;

短*useBuffer=(短*)outputBuffer[currentOutputBuffer];

for (int i=0; i 大小; ++i) {

useBuffer[indexOfOutput++]=newBuffer[i];

if (indexOfOutput=outBufSamples) { //指示缓冲区已填满并发送到OpenSL ES

LOGD("被阻止");

//发送之前需要获取可以发送的条件。

等待线程锁();

LOGD("阻塞完成");

//发送数据

(*pcmBufferQueueItf)-Enqueue(pcmBufferQueueItf,useBuffer,outBufSamples* sizeof(short));

//更改为另一个缓冲区

当前输出缓冲区=当前输出缓冲区?0:1;

输出索引=0;

useBuffer=(短*)outputBuffer[当前OutputBuffer];

}

}

//更新最后的位置

当前输出索引=输出索引;

} else if (fSample_format==Sample_format_SignedInteger_32) {

int *newBuffer=(int *)buff;

int *useBuffer=(int *)outputBuffer[当前OutputBuffer];

for (int i=0; i 大小; ++i) {

useBuffer[indexOfOutput++]=newBuffer[i];

if (indexOfOutput=outBufSamples) { //指示缓冲区已填满并发送到OpenSL ES

//发送之前需要获取可以发送的条件。

等待线程锁();

//发送数据

(*pcmBufferQueueItf)-Enqueue(pcmBufferQueueItf,useBuffer,outBufSamples* sizeof(int));

//更改为另一个缓冲区

当前输出缓冲区=当前输出缓冲区?0:1;

输出索引=0;

useBuffer=(int*)outputBuffer[当前OutputBuffer];

}

}

//更新最后的位置

当前输出索引=输出索引;

}

}7.释放资源

** 释放OpenSL ES资源,

* 由于OpenSL ES的内存是系统自动分配的,所以释放内存时,只需调用Destroy释放对应的SLObjectItf对象,然后将其属性ID接口对象直接设置为NULL。

* */

无效SLAudioPlayer:closeAudioPlayer() {

LOGD("关闭音频播放器()");

//释放音频播放器组件对象

if (playerObject !=NULL) {

(*playerObject)-销毁(playerObject);

玩家对象=NULL;

玩家信息=NULL;

卷信息=NULL;

pcmBufferQueueItf=NULL;

}

//释放混音器组件对象

如果(outputMixObject!=NULL){

(*outputMixObject)-销毁(outputMixObject);

输出混合对象=NULL;

}

//释放OpenSL ES 引擎对象

如果(slContext!=NULL){

slContext-releaseResources();

slContext=NULL;

}

销毁缓冲区();

销毁ThreadLock();

总结:

音频播放器的数据驱动流程为:首先播放系统需要从audioSnk中获取数据进行播放,而audioSnk则需要从audioSrc中获取音频数据。 audioSrc中的数据是通过ids1中配置的回调函数不断写入其中的。从而简化了整个播放过程。

项目地址

演示

遇到问题

1、项目中引入c/c++文件时,需要添加CMakelists.txt文件,并在build.gradle中进行相关配置。在定义api的时候,遇到了上面的问题

解决方案:

通过查阅官方文档,我们发现需要在defaultConfig中写入ndk来解决这个问题,如下:

OK,本文到此结束,希望对大家有所帮助。

用户评论

∞◆暯小萱◆

看这个标题就知道是继续讲上次关于 OpenSL ES 播放PCM文件的讲解了!

    有14位网友表示赞同!

青瓷清茶倾城歌

终于等到了第二部分!第一部分我都没太明白呢,希望这篇能解释清楚。

    有20位网友表示赞同!

空谷幽兰

OpenSL ES 好难学啊!感觉这篇文章能给我带来很大的帮助。

    有13位网友表示赞同!

陌上花

我也想学习怎么用 OpenSL ES 播放 PCM 文件,这个标题看起来很专业的样子!

    有14位网友表示赞同!

◆乱世梦红颜

希望能详细介绍下不同 PCM 格式的播放方法,比如 WAV 和 MP3。

    有6位网友表示赞同!

颓废人士

上次看了第一篇,对 OpenSL ES 的基本概念有了点了解,希望这篇能讲更深入的内容。

    有12位网友表示赞同!

淡抹烟熏妆丶

最近在学音频处理,感觉 OpenSL ES 很强大,这本书应该很有用。

    有11位网友表示赞同!

日久见人心

学习完这个系列文章能玩转音频吧?期待期待!

    有15位网友表示赞同!

羁绊你

不知道这篇文章会详细讲解下错误处理和调试技巧吗?

    有20位网友表示赞同!

肆忌

看标题就知道是讲更高级的内容了,以前学过的基础知识估计要用到。

    有7位网友表示赞同!

各自安好ぃ

分享一下学习 OpenSL ES 的资源吧,我正在找!

    有10位网友表示赞同!

あ浅浅の嘚僾

希望这篇文章能用通俗易懂的语言讲解技术细节,方便小白理解。

    有5位网友表示赞同!

为爱放弃

听起来这个标题很专业啊,需要准备学习一下相关的基础知识。

    有20位网友表示赞同!

烟雨萌萌

最近在 Android 开发项目中遇到音频播放问题,希望能找到解决方案!

    有8位网友表示赞同!

陌颜

OpenSL ES 的应用范围应该很大吧?除了播放 PCM 文件还有什么吗?

    有10位网友表示赞同!

念安я

希望这篇文章能解决我之前遇到的 OpenSL ES 相关的难题!

    有8位网友表示赞同!

打个酱油卖个萌

我正在研究音频开发,这个标题对我有很大的帮助!

    有14位网友表示赞同!

信仰

学习 OpenSL ES 真让人兴奋,期待了解更多精彩内容!

    有8位网友表示赞同!

【深入解析PCM文件播放技术:OpenSL ES应用详解(第二部分)】相关文章:

1.动物故事精选:寓教于乐的儿童故事宝库

2.《寓教于乐:精选动物故事助力儿童成长》

3.探索动物旅行的奇幻冒险:专为儿童打造的童话故事

4.《趣味动物刷牙小故事》

5.探索坚韧之旅:小蜗牛的勇敢冒险

6.传统风味烤小猪,美食探索之旅

7.探索奇幻故事:大熊的精彩篇章

8.狮子与猫咪的奇妙邂逅:一场跨界的友谊故事

9.揭秘情感的力量:如何影响我们的生活与决策

10.跨越两岸:探索彼此的独特世界

上一篇:云服务器价格对比:阿里云、腾讯云、京东云、华为云全面解析 下一篇:高效编程:代码精简之道