音视频开发之旅(36) -FFmpeg +OpenSL ES实现音频解码和播放

article/2025/7/4 8:06:46

目录

  1. OpenSL ES基本介绍
  2. OpenSL ES播放音频流程
  3. 代码实现
  4. 遇到的问题
  5. 资料
  6. 收获

上一篇我们通过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,具体流程如下

  1. 创建引擎对象SLObjectItf engineObj
    初始化引擎 Realize
    获取引擎接口 GetInterface SLEngineItf
  2. 创建混音器对象SLObjectItf outputMixObj
    初始化混音器 Realize
  3. 设置输入输出数据参数
  4. 创建播放器对象 SLPlayItf playerObj
    初始化播放器Realize
    获取播放器接口 GetInterface
  5. 获取播放回调接口(即缓冲队列)SLAndroidSimpleBufferQueueItf bufferQueue
  6. 注册播放回调 `RegisterCallback
  7. 设置播放状态SetPlayState
  8. 等待音频帧加入队列触发播放回调(*mBufferQueue)->Enqueue
  9. 释放资源

具体参考官方提供的示例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进行对比分析排查出问题所在。
根源还在于不够细心和理解的不透彻。

五、资料

  1. OpenSL-ES 官方文档
  2. NDK指南: OpenSL ES
  3. NDK指南demo:native-audio 是一个简单的音频录制器/播放器
  4. 音视频学习 (七) AudioTrack、OpenSL ES 音频渲染
  5. FFmpeg 开发(03):FFmpeg + OpenSL ES 实现音频解码播放
  6. android平台OpenSL ES播放PCM数据
  7. Android通过OpenSL ES播放音频套路详解

六、收获

  1. 了解了OpenSl ES的基本知识和播放音频数据的流程
  2. 代码实现OpenSL ES播放音频流
  3. 和FFmpeg结合,实现opensl播放解码后的音频数据
  4. 解决遇到的问题

感谢你的阅读

学习实践了视频的解码、音频的解码和播放,下一篇我们通过OpenGL ES来实现解码后视频的渲染,欢迎关注公众号“音视频开发之旅”,一起学习成长。

欢迎交流

 

0人点赞

 

日记本

 


http://chatgpt.dhexx.cn/article/yExCAC3k.shtml

相关文章

Android OpenSL ES 开发:Android OpenSL 介绍和开发流程说明

一、Android OpenSL ES 介绍 OpenSL ES (Open Sound Library for Embedded Systems)是无授权费、跨平台、针对嵌入式系统精心优化的硬件音频加速API。它为嵌入式移动多媒体设备上的本地应用程序开发者提供标准化, 高性能,低响应时间的音频功能实现方法&#xff0c;并实现软/硬件…

Xcode7 网络请求报错:The resource could not be loaded because the App Transport Security policy requir...

Google后查证&#xff0c;iOS9引入了新特性App Transport Security (ATS)。详情&#xff1a;App Transport Security (ATS)新特性要求App内访问的网络必须使用HTTPS协议。但是现在公司的项目使用的是HTTP协议&#xff0c;使用私有加密方式保证数据安全。现在也不能马上改成HTTP…

【用户自定义气象站】

自定义气象站系统由数据采集器、传感器、总线模块、网络模块、供电模块等组成&#xff0c;可实现野外无人看守的情况下长期监测。用户可根据自己的需求&#xff0c;自定义测量指标&#xff0c;以达到观测目的。 自定义气象站测量指标 自定义气象站测量参数指标 总辐射、光合…

自动气象站设备的防雷要点

自动气象站设备&#xff0c;其工作场所几乎都是在户外&#xff0c;雨打风吹、电闪雷鸣、日晒雨淋&#xff0c;由于是雷雨天气&#xff0c;气象站也要连续工作。如何科学防雷&#xff0c;直接影响到气象站的运行与使用寿命。 自动气象站设备的工作要点是获取气象环境参数&#x…

12,桥接模式-露娜的召唤师技能

一,前言 7种结构型设计模式:桥接模式,适配器模式,装饰模式,组合模式,享元模式,外观模式,代理模式上篇我们说了装饰模式:动态地将责任附加到对象上,在不修改任何底层代码的情况下,为对象赋予新的职责开发中,我们经常会遇到一个类有两个或两个以上的维度经常在变化 如果我们使用…

浮标水质监测站是什么

浮标水质监测站是设立在河流、湖泊、水库、近岸海域等流域内进行现场水质自动监测的监测仪器&#xff0c;是以水质监测仪为核心&#xff0c;运用传感器技术&#xff0c;结合浮标体、电源供电系统、数据传输设备组成的放置于水域内的小型水质监测系统。用于连续自动监测被测水体…

【赛纳斯】EC Raman电化学拉曼光谱检测系统推动科研新突破

【前言】   近日,Angew在线发表了厦门大学李剑锋教授团队在设计用于氧还原反应的先进材料及改进催化剂的设计最 新综述文章。该论文综述了双金属纳米催化剂有序度对氧还原反应的影响。论文第 一作者为&#xff1a;Heng-Quan Chen,Huajie Ze,Mu-Fei Yue,论文共同通讯作者为&am…

Ethercat学习-从站源码移植

文章目录 简介移植源码1.源码结构2.GD32硬件接口准备1.SPI接口2.PDI中断配置3.Sync0中断配置4.Sync1中断配置5.定时器中断配置 3.移植准备4.源码移植1.修改头文件名2.ecatport.c文件修改1.SPI部分修改2.中断部分3.修改HW_Init()4.报错修改 3.myapp.c文件修改 5.其他 简介 移植…

小说php 站点源码下载,PTCMS小说站源码

必装环境&#xff1a;nginx(apache.iis 也可)&#xff0c;mysql,php5.6,memcached php5.6 安装扩展 memcache 新建站点&#xff0c;注意新建时&#xff0c;PHP 版本必须选择 PHP5.6&#xff0c;不然程序会报错 1.上传网站文件到网站目录&#xff0c;新建网站伪静态选择 thinkph…

PHP是什么

PHP 是服务器端脚本语言。 您应当具备的基础知识 在继续学习之前&#xff0c;您需要对以下知识有基本的了解&#xff1a; HTMLCSS 如果您希望首先学习这些项目&#xff0c;请在我们的 首页 访问这些教程。 PHP 是什么&#xff1f; PHP 代表 PHP: Hypertext PreprocessorPHP 是一…

国家地表水水质自动监测站坐标每四小时数据(共1952个监测站,含省份、城市、河流、流域、断面名称、监测时间、水温、pH、DO、CODMn、TP、TN、NH3-N、浊度等)

1.监测范围 国家地表水水质自动监测网1952 个水质自动监测站。2.监测项目 监测项目为国家水质自动监测站配备的监测指标&#xff0c;主要包括五参数(水温、pH、溶解氧、电导率和浊度)、氨氮、高锰酸盐指数、总氮、总磷&#xff0c;部分水站增测总有机碳、叶绿素a、藻密度、VOCs…

php 跨站脚本,Piwigo register.php页面多个跨站脚本漏洞

发布日期&#xff1a;2010-05-06 更新日期&#xff1a;2010-05-11 受影响系统&#xff1a; Piwigo project Piwigo 2.0.9 描述&#xff1a; -------------------------------------------------------------------------------- BUGTRAQ ID: 39958 CVE(CAN) ID: CVE-2010-1707…

基于Modbus RTU 485通信协议实现对PH、溶解氧传感器的数据采集

modbus rtu 485协议采用的是一主多从方式通信&#xff0c;主机是普中的stm32f103zet6开发板&#xff0c;从机是传感器。代码已经在实物上测试通过&#xff0c;并且也用modbus精灵测试通过了。如果你没有stm32基础的话&#xff0c;建议先去B站搜索“正点原子”了解一下485串口通…

3、基于51单片机的智能水箱控制系统-温度-PH值-水位(仿真+程序+原理图)

目录 基于51单片机的智能水箱控制系统1、主要功能2、实验结果3、仿真工程4、原理图5、程序源码6、资源获取 基于51单片机的智能水箱控制系统 1、主要功能 51单片机检测水箱内温度&#xff0c;ph值&#xff1b;使用pid算法控制温度到设置值&#xff1b;普通控制ph值到设定值&a…

如何下载y站视频

今天看到了一篇B站视频的下载方法&#xff0c;学习了下&#xff0c;然后去看了下y站是不是也能下下来&#xff0c;居然被我试出来了&#xff0c;嘿嘿 B站文章链接&#xff1a;https://blog.csdn.net/Enderman_xiaohei/article/details/94718494 然后看一下y站的&#xff0c;打…

基于STM32(HAL库)的水质检测(浑浊度、PH值、温度、手机APP显示、wifi上云)

本系统由通过wifi将浑浊度、PH值、温度采集的数据发送到手机APP&#xff0c;超过设定的阈值报警。 一、硬件材料清单&#xff1a; 1、STM32C8T6&#xff1a;控制器 2、OLED显示屏&#xff1a;显示传感器采集的数据 3、PH传感器&#xff1a;检测PH值 4、TDS传感器&#xff1…

JEB动态调试与篡改攻防世界Ph0en1x-100

文章目录 题目APK静态分析jadx反编译IDA反汇编 JEB动态调试工具的使用操作内存值 AndroidKiller工具的使用篡改软件包 总结 题目 攻防世界 Mobile 新手区题目链接 Ph0en1x-100&#xff0c;如下&#xff1a; 下载附件得到一个 apk&#xff0c;安装后如下&#xff1a; 要求输入…

php 抓站,如何跨站抓取别的站点的页面的补充

如何跨站抓取别的站点的页面的补充 更新时间&#xff1a;2006年10月09日 00:00:00 作者&#xff1a; 在实际的应用中&#xff0c;经常会遇到一些特殊的情况&#xff0c;比如需要新闻&#xff0c;天气预报&#xff0c;等等&#xff0c;但是作为个人站点或者实力小的站点 我们不…

用python计算ph,用python下载PH上的学习视频

努力学习&#xff0c;天天向上 闲来无事&#xff0c;用python写个脚本下载PH上的学习视频 环境&#xff1a;python3 用法&#xff1a;python ph_downloader.py viewkey viewkey是PH上的一串字符 代码在 https://paste.ubuntu.com/p/jXVYD3NGP9 多线程下载&#xff0c;网络不好时…

浮标水质监测站--河流湖泊水库现场水质自动监测的解决方案

浮标水质监测站--河流湖泊水库现场水质自动监测的解决方案 什么是浮标水质监测站&#xff1f; 浮标水质监测站是设立在河流、湖泊、水库、近岸海域等流域内进行现场水质自动监测的监测仪器&#xff0c;是以水质监测仪为核心&#xff0c;运用传感器技术&#xff0c;结合浮标体…