音视频开发之旅(36) -FFmpeg +OpenSL ES实现音频解码和播放
目录
- OpenSL ES基本介绍
- OpenSL ES播放音频流程
- 代码实现
- 遇到的问题
- 资料
- 收获
上一篇我们通过AudioTrack实现了FFmpeg解码后的PCM音频数据的播放,在Android上还有一种播放音频的方式即OpenSL ES, 什么是OpenSL ES,这个我们平时接触的很少,原因是平时业务中大部分播放可以通过Java层的MediaPlayer或者AudioTrack实现音频播放。如果遇到一些特殊的需求,比如添加音效等这是不容易实现。而OpenSL可以很好的解决此类问题,并且还有很多丰富的功能。下面我们一起来学习实践吧。
一、OpenSL ES基本介绍
1.1 OPenSL ES 是什么?
OpenSL ES (Open Sound Library for Embedded System) ,即嵌入式音频加速标准与 Android Java 框架中的 MediaPlayer 和 MediaRecorderAPI 提供类似的音频功能。OpenSL ES 提供 C 语言接口和 CPP 绑定,让您可以从使用任意一种语言编写的代码中调用 API。
相对MediaPlayer 和 MediaRecorderAPI 等java层API来说,OpenSL ES 则是比价低层级的 API, 属于 C 语言 API 。在开发中,一般会直接使用高级 API , 除非遇到性能瓶颈,如语音实时聊天、3D Audio 、某些 Effects 等,开发者可以直接通过 C/CPP开发基于 OpenSL ES 音频的应用, 提升应用的音频性能。
1.2 OpenSL ES有哪些能力呐?
我们通过下图的OpenSL ES使用指南中可以看到支持,音频的播放、混音、音效、以及录制等功能。
上述两种图片来自:官方指南:OpenSL ES
1.3 如何引入?
OpenSL ES 编程说明
OpenSL ES的库我们可以在NDK 软件包中找到
eg: $NDK_PATH_/platforms/android-30/arch-arm/usr/lib/libOpenSLES.so
引入方式只需要在CmakeList.txt的target_link_libraries中加入OpenSLES即可
target_link_libraries( native-libavformatavcodecavfilteravutilswresampleswscaleOpenSLES${log-lib})
1.4 对象与接口
OpenES SL虽然是面向过程的C语言编写的,但是以面向对象的思想提供了对象和接口,方便开发的在项目中使用。
OpenSL ES 对象类似于 Java 和 CPP 等编程语言中的对象概念,不过 OpenSL ES 对象仅能通过其关联接口进行访问。其中包括所有对象的初始接口,称为 SLObjectItf。对象本身没有句柄,只有一个连接到对象的 SLObjectItf 接口的句柄。
需要注意的是 OpenSL ES 对象不能直接使用,必须通过其 GetInterface 函数用ID号拿到指定接口(如播放器的播放接口),然后通过该接口来访问功能函数
OpenSL ES 对象是先创建的,它会返回 SLObjectItf,然后再实现 (realize),然后使用 GetInterface,为其需要的每种功能获取接口
音频播放会用到 引擎、混音器以及播放器对象和接口,下一小节我们来看下具体流程。
二、OpenSL ES播放音频流程
图片来源: OpenSL-ES 官方文档
在CmakeList引入OpenSL库,然后在对应的CPP文件中导入相应的头文件即可使用OpenSL ES,具体流程如下
- 创建引擎对象
SLObjectItf engineObj
初始化引擎Realize
获取引擎接口GetInterface SLEngineItf
- 创建混音器对象
SLObjectItf outputMixObj
初始化混音器Realize
- 设置输入输出数据参数
- 创建播放器对象
SLPlayItf playerObj
初始化播放器Realize
获取播放器接口GetInterface
- 获取播放回调接口(即缓冲队列)
SLAndroidSimpleBufferQueueItf bufferQueue
- 注册播放回调 `RegisterCallback
- 设置播放状态
SetPlayState
- 等待音频帧加入队列触发播放回调
(*mBufferQueue)->Enqueue
- 释放资源
具体参考官方提供的示例demo native-audio 是一个简单的音频录制器/播放器
三、OpenSL ES播放解码PCM的代码实现
了解了OpenSL ES的基本知识和使用流程,下面我们开始具体的代码实现。
#include <jni.h>
#include <string>
#include <unistd.h>extern "C" {
#include "include/libavcodec/avcodec.h"
#include "include/libavformat/avformat.h"
#include "include/log.h"
#include <libswscale/swscale.h>
#include <libavutil/imgutils.h>
#include <libswresample/swresample.h>
#include <SLES/OpenSLES.h>
#include <SLES/OpenSLES_Android.h>
}//函数声明
jint playPcmBySL(JNIEnv *env, jstring pcm_path);extern "C"
JNIEXPORT jint JNICALL
Java_android_spport_mylibrary2_Demo_decodeAudio(JNIEnv *env, jobject thiz, jstring video_path,jstring pcm_path) {....
//在音频解码完成后调用使用sl播放的函数playPcmBySL(env,pcm_path);
}// engine interfaces
static SLObjectItf engineObject = NULL;
static SLEngineItf engineEngine;// output mix interfaces
static SLObjectItf outputMixObject = NULL;
static SLEnvironmentalReverbItf outputMixEnvironmentalReverb = NULL;static SLObjectItf pcmPlayerObject = NULL;
static SLPlayItf pcmPlayerPlay;
static SLAndroidSimpleBufferQueueItf pcmBufferQueue;FILE *pcmFile;
void *buffer;
uint8_t *out_buffer;jint playPcmBySL(JNIEnv *env, const _jstring *pcm_path);// aux effect on the output mix, used by the buffer queue player
static const SLEnvironmentalReverbSettings reverbSettings = SL_I3DL2_ENVIRONMENT_PRESET_STONECORRIDOR;//播放回调
void playerCallback(SLAndroidSimpleBufferQueueItf bufferQueueItf, void *context) {if (bufferQueueItf != pcmBufferQueue) {LOGE("SLAndroidSimpleBufferQueueItf is not equal");return;}while (!feof(pcmFile)) {size_t size = fread(out_buffer, 44100 * 2 * 2, 1, pcmFile);if (out_buffer == NULL || size == 0) {LOGI("read end %ld", size);} else {LOGI("reading %ld", size);}buffer = out_buffer;break;}if (buffer != NULL) {LOGI("buffer is not null");SLresult result = (*pcmBufferQueue)->Enqueue(pcmBufferQueue, buffer, 44100 * 2 * 2);if (SL_RESULT_SUCCESS != result) {LOGE("pcmBufferQueue error %d",result);}}}jint playPcmBySL(JNIEnv *env, jstring pcm_path) {const char *pcmPath = env->GetStringUTFChars(pcm_path, NULL);pcmFile = fopen(pcmPath, "r");if (pcmFile == NULL) {LOGE("open pcmfile error");return -1;}out_buffer = (uint8_t *) malloc(44100 * 2 * 2);//1. 创建引擎`
// SLresult result;
//1.1 创建引擎对象SLresult result = slCreateEngine(&engineObject, 0, 0, 0, 0, 0);if (SL_RESULT_SUCCESS != result) {LOGE("slCreateEngine error %d", result);return -1;}//1.2 实例化引擎result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);if (SL_RESULT_SUCCESS != result) {LOGE("Realize engineObject error");return -1;}//1.3获取引擎接口SLEngineItfresult = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);if (SL_RESULT_SUCCESS != result) {LOGE("GetInterface SLEngineItf error");return -1;}slCreateEngine(&engineObject, 0, 0, 0, 0, 0);(*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);(*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);//获取到SLEngineItf接口后,后续的混音器和播放器的创建都会使用它//2. 创建输出混音器const SLInterfaceID ids[1] = {SL_IID_ENVIRONMENTALREVERB};const SLboolean req[1] = {SL_BOOLEAN_FALSE};//2.1 创建混音器对象result = (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 1, ids, req);if (SL_RESULT_SUCCESS != result) {LOGE("CreateOutputMix error");return -1;}//2.2 实例化混音器result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);if (SL_RESULT_SUCCESS != result) {LOGE("outputMixObject Realize error");return -1;}//2.3 获取混音接口 SLEnvironmentalReverbItfresult = (*outputMixObject)->GetInterface(outputMixObject, SL_IID_ENVIRONMENTALREVERB,&outputMixEnvironmentalReverb);if (SL_RESULT_SUCCESS == result) {result = (*outputMixEnvironmentalReverb)->SetEnvironmentalReverbProperties(outputMixEnvironmentalReverb, &reverbSettings);}//3 设置输入输出数据源
//setSLData();
//3.1 设置输入 SLDataSourceSLDataLocator_AndroidSimpleBufferQueue loc_bufq = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE,2};SLDataFormat_PCM formatPcm = {SL_DATAFORMAT_PCM,//播放pcm格式的数据2,//2个声道(立体声)SL_SAMPLINGRATE_44_1,//44100hz的频率SL_PCMSAMPLEFORMAT_FIXED_16,//位数 16位SL_PCMSAMPLEFORMAT_FIXED_16,//和位数一致就行SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT,//立体声(前左前右)SL_BYTEORDER_LITTLEENDIAN//结束标志};SLDataSource slDataSource = {&loc_bufq, &formatPcm};//3.2 设置输出 SLDataSinkSLDataLocator_OutputMix loc_outmix = {SL_DATALOCATOR_OUTPUTMIX, outputMixObject};SLDataSink audioSnk = {&loc_outmix, NULL};//4.创建音频播放器//4.1 创建音频播放器对象const SLInterfaceID ids2[1] = {SL_IID_BUFFERQUEUE};const SLboolean req2[1] = {SL_BOOLEAN_TRUE};result = (*engineEngine)->CreateAudioPlayer(engineEngine, &pcmPlayerObject, &slDataSource, &audioSnk,1, ids2, req2);if (SL_RESULT_SUCCESS != result) {LOGE(" CreateAudioPlayer error");return -1;}//4.2 实例化音频播放器对象result = (*pcmPlayerObject)->Realize(pcmPlayerObject, SL_BOOLEAN_FALSE);if (SL_RESULT_SUCCESS != result) {LOGE(" pcmPlayerObject Realize error");return -1;}//4.3 获取音频播放器接口result = (*pcmPlayerObject)->GetInterface(pcmPlayerObject, SL_IID_PLAY, &pcmPlayerPlay);if (SL_RESULT_SUCCESS != result) {LOGE(" SLPlayItf GetInterface error");return -1;}//5. 注册播放器buffer回调 RegisterCallback//5.1 获取音频播放的buffer接口 SLAndroidSimpleBufferQueueItfresult = (*pcmPlayerObject)->GetInterface(pcmPlayerObject, SL_IID_BUFFERQUEUE, &pcmBufferQueue);if (SL_RESULT_SUCCESS != result) {LOGE(" SLAndroidSimpleBufferQueueItf GetInterface error");return -1;}//5.2 注册回调 RegisterCallbackresult = (*pcmBufferQueue)->RegisterCallback(pcmBufferQueue, playerCallback, NULL);if (SL_RESULT_SUCCESS != result) {LOGE(" SLAndroidSimpleBufferQueueItf RegisterCallback error");return -1;}//6. 设置播放状态为Playingresult = (*pcmPlayerPlay)->SetPlayState(pcmPlayerPlay, SL_PLAYSTATE_PLAYING);if (SL_RESULT_SUCCESS != result) {LOGE(" SetPlayState error");return -1;}//7.触发回调playerCallback(pcmBufferQueue,NULL);return 0;
}
OpenSL ES 还有更多丰富的功能,比如,混音、设置音量、录音、播放url或者assert中的音频。详细了解可以查看官方文档和NDK的demo,
本篇就学习实践到这里,越学习发下身边优秀的人越多,自己不会的东西、要学习的就越多,抓住一个核心痛点,一起学习实践吧。
代码已上传至github。[https://github.com/ayyb1988/ffmpegvideodecodedemo] 欢迎交流,一起学习成长。
四、遇到的问题
问题1: 拿到混音接口对象后没有SetEnvironmentalReverbProperties设置后result不为0导致家了为0判断,导致这里一直提示出错。
解决方案,去掉此处的result检查,官方的demo也返回一样的值16
result = (*outputMixObject)->GetInterface(outputMixObject, SL_IID_ENVIRONMENTALREVERB,&outputMixEnvironmentalReverb);if (SL_RESULT_SUCCESS == result) {result = (*outputMixEnvironmentalReverb)->SetEnvironmentalReverbProperties(outputMixEnvironmentalReverb, &reverbSettings);if (SL_RESULT_SUCCESS != result) {LOGE(" SetEnvironmentalReverbProperties error");return -1;}}改为如下:
result = (*outputMixEnvironmentalReverb)->SetEnvironmentalReverbProperties(outputMixEnvironmentalReverb, &reverbSettings);
问题2: 创建播放器对象一直为空,导致无法播放
原因:给SLData 设置数据源时
SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE错误的写成了SL_DATALOCATOR_ANDROIDBUFFERQUEUE
SLDataLocator_AndroidSimpleBufferQueue loc_bufq = {SL_DATALOCATOR_ANDROIDBUFFERQUEUE, 2};-->改为SLDataLocator_AndroidSimpleBufferQueue loc_bufq = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE,2};
问题3. 播放音频时音频卡住不断重复
while (!feof(pcmFile)) {size_t size = fread(out_buffer, 44100 * 2 * 2, 1, pcmFile);if (out_buffer == NULL || size == 0) {LOGI("read end %ld", size);} else {LOGI("reading %ld", size);}buffer = out_buffer;//原因是,忘记跳出循环了break;}
在学习的初期一个小错误就可能折腾几个小时,在采用逐步排查流程和查看细节、以及和可运行的demo进行对比分析排查出问题所在。
根源还在于不够细心和理解的不透彻。
五、资料
- OpenSL-ES 官方文档
- NDK指南: OpenSL ES
- NDK指南demo:native-audio 是一个简单的音频录制器/播放器
- 音视频学习 (七) AudioTrack、OpenSL ES 音频渲染
- FFmpeg 开发(03):FFmpeg + OpenSL ES 实现音频解码播放
- android平台OpenSL ES播放PCM数据
- Android通过OpenSL ES播放音频套路详解
六、收获
- 了解了OpenSl ES的基本知识和播放音频数据的流程
- 代码实现OpenSL ES播放音频流
- 和FFmpeg结合,实现opensl播放解码后的音频数据
- 解决遇到的问题
感谢你的阅读
学习实践了视频的解码、音频的解码和播放,下一篇我们通过OpenGL ES来实现解码后视频的渲染,欢迎关注公众号“音视频开发之旅”,一起学习成长。
欢迎交流
0人点赞
日记本
音视频开发之旅(36) -FFmpeg +OpenSL ES实现音频解码和播放相关推荐
- Android音视频学习系列(十) — 基于FFmpeg + OpenSL ES实现音频万能播放器
系列文章 Android音视频学习系列(一) - JNI从入门到精通 Android音视频学习系列(二) - 交叉编译动态库.静态库的入门 Android音视频学习系列(三) - Shell脚本入门 ...
- 音视频开发之旅(32)-音视频学习资料
目录 为什么要学习音视频? 如何学习系统性音视频? 音视频相关的资料 学习实践的输出文章分类聚合 收获 最近有朋友问想学习音视频,应该怎么学,有什么资料吗? 这个问题也困扰我很久,几年前就想开始音视频 ...
- 安卓 camera 调用流程_音视频开发之旅(四)Camera视频采集
目录 Camera基础知识 视频采集的流程 遇到的问题和常见的坑(重点) 收获 一. Camera基础知识 Camera 有几个重要的基础概念. facing相机的方向,一般后置摄像头和前置摄像头. ...
- 音视频开发之旅(16) OpenGL ES粒子效果-烟花爆炸
目录 烟花爆竹场景和属性 实践以及遇到的问题 资料 收获 通过该篇的实践实现如下效果 一.烟花爆竹场景和属性 在上一篇 音视频开发之旅(15) OpenGL ES粒子系统 - 喷泉 的基础上 实现烟花 ...
- 音视频开发之旅(15) OpenGL ES粒子系统 - 喷泉
目录 粒子和粒子系统 实践:喷泉效果 遇到的问题 资料 收获 通过该篇的实践实现如下效果 一.什么是粒子和粒子系统 如何定义粒子? 一个粒子有位置信息(x,y,z).运动方向.颜色.生命值(开始和结束 ...
- 即时通讯音视频开发(十八):详解音频编解码的原理、演进和应用选型
1.引言 大家好,我是刘华平,从毕业到现在我一直在从事音视频领域相关工作,也有一些自己的创业项目,曾为早期Google Android SDK多媒体架构的构建作出贡献. 就音频而言,无论是算法多样性, ...
- 音视频开发之旅(34) - 基于FFmpeg实现简单的视频解码器
目录 FFmpeg解码过程流程图和关键的数据结构 mp4通过FFmpeg解码YUV裸视频数据 遇到的问题 资料 收获 一.FFmpeg解码过程流程图和关键的数据结构 FFmpeg解码涉及的知识点比较多 ...
- 音视频开发之旅(二)AudioRecord录制PCM音频
目录 音频采集API AudioRecord和MediaRecorder介绍 PCM的介绍 AudioRecord的使用(构造.开始录制.停止录制.其他细节点) ffplay播放pcm pcm转为wa ...
- 音视频开发之旅(51)-M3U8边缓存边播放
目录 MP4的"问题" m3u8是什么 m3u8的好处 源码分析 扩展思考:mp4能不能像m3u8一样进行分片缓存呐? 资料 收获 一.MP4的"问题" 我们上 ...
最新文章
- 正则 不区分大小写_4.nginx的server_name正则匹配
- SpringMVC-获得Restful风格的参数
- array_multisort - 如何保持键值,不重置键值,键名保持不变
- android系统中sd卡各文件夹功能详解 guessword,AndroidStudio LiveTemplate函数说明
- WordPress博客后台不能显示所有主题和无法编辑主题的问题的解决方法
- 动态规划训练12 [G - You Are the One HDU - 4283 ]
- jquery获取select中的option的text值
- java 整数 引用传递_关于Java引用传递的一个困惑?
- AxonFramework,存储库
- ES6新特性_let使用案例---JavaScript_ECMAScript_ES6-ES11新特性工作笔记004
- python人像和图片比对_python 使用OpenCV进行简单的人像分割与合成
- 独家 | 精彩!这27本书籍,每位数据科学家都应该阅读(附说明图表)
- pytorch个人学习笔记(2)—Normalize()参数详解及用法
- IPv6系列-彻底弄明白有状态与无状态配置IPv6地址
- 微信,知道你所有的秘密
- OneNav一为主题魔改教程(五):点赞后自动加入到首页“我的导航”--洞五洞洞幺
- CSS学习之position属性
- 将两台交换机虚拟化为一台设备的操作过程(VSU)锐捷设备
- Python 如何安装第三方模块
- 《刀剑封魔录》原创全攻略 一