当前位置: 首页 > news >正文

Android音视频探索之旅 | C++层使用OpenGL ES实现音频渲染

一.前言

  • OpenGL ES实现视频渲染已经实现-在Android音视频探索之旅 | C++层使用OpenGL ES实现视频渲染中,这一次我们使用OpenGL ES实现音频渲染。

二.通过OpenSL ES播放音频

2.1.整体流程

  • 1.创建OpenSL引擎
  • 2.创建混音器
  • 3.创建播放器
  • 4.执行播音操作(OpenSL ES的播音过程比较特别,不像视频那样每放完一帧就主动休眠,而是每帧音频播放结束会自己回调,在回调的时候才获取下一帧音频。为此,整个播音过程又分为三个步骤)
    • a.轮询音频帧
    • b.控制播放状态
    • c.开始遍历音频文件

2.2.代码环节

  • 整个核心就写在一个cpp文件中,按照上方步骤将代码注意进行展示
  • 创建OpenSL引擎
SLEngineItf CreateSL()
{SLresult re;// 用于接收 OpenSL ES 操作结果SLEngineItf en;// 引擎接口// slCreateEngine:创建引擎对象。是 OpenSL ES 的入口点,必须先调用它才能使用其他 OpenSL ES 功能//参数1:pEngine (输出参数),指向 SLObjectItf 指针的指针,用于接收创建的引擎对象。如果创建成功,*pEngine 会被赋值为新引擎对象的引用。//参数2:numOptions (输入参数),指定 pEngineOptions 数组中的选项数量。如果为 0,表示不使用任何选项。//参数3:pEngineOptions (输入参数),指向 SLEngineOption 结构体数组的指针,用于配置引擎。//每个 SLEngineOption 包含一个 SLuint32 类型的键值对(例如性能模式、线程优先级等)//如果 numOptions 为 0,此参数应为 NULL//参数4:numInterfaces (输入参数)//指定 pInterfaceIds 数组中请求的接口数量。如果为 0,表示不立即请求任何接口。//参数5:pInterfaceIds (输入参数)//指向 SLInterfaceID 数组的指针,列出需要从引擎对象获取的接口(如 SL_IID_ENGINE)。//如果 numInterfaces 为 0,此参数应为 NULL。//参数6:pInterfaceRequired (输入参数)//指向 SLboolean 数组的指针,标记每个请求的接口是否是必需的(SL_BOOLEAN_TRUE/SL_BOOLEAN_FALSE)。//如果 numInterfaces 为 0,此参数应为 NULL。re = slCreateEngine(&engineSL,0,0,0,0,0);if(re != SL_RESULT_SUCCESS) return NULL;// 检查是否创建成功// 实现(初始化)引擎对象  Realize:初始化一个 OpenSL ES 对象.第二个参数为:是否异步re = (*engineSL)->Realize(engineSL,SL_BOOLEAN_FALSE);if(re != SL_RESULT_SUCCESS) return NULL;// 获取引擎接口 GetInterface:用于从已初始化的对象中获取特定功能的接口//通过它,可以访问对象提供的各种音频操作功能(如播放控制、音量调节等)。re = (*engineSL)->GetInterface(engineSL,SL_IID_ENGINE,&en);if(re != SL_RESULT_SUCCESS) return NULL;return en;
}
  • 创建混音器
extern "C"
JNIEXPORT jstring JNICALL
Java_com_jack_ffmpeg_1simple01_MainActivity_stringFromJNI(JNIEnv *env, jobject thiz) {std::string hello = "播放PCM";//省略...//2 创建音频输出混音器SLObjectItf mix = NULL;// OpenSL ES 引擎对象SLresult re = 0;// 获取混音器//CreateOutputMix:是 OpenSL ES 中用于创建音频输出混音器(Output Mix)的函数,属于 SLEngineItf 接口。//参数1:引擎接口//参数2:返回的混音器对象//参数3:请求的混音器接口数量     不请求接口//参数4:请求的接口ID数组        无接口ID数组//参数5:无接口必需标记          接口是否必需的数组re = (*eng)->CreateOutputMix(eng,&mix,0,0,0);if(re !=SL_RESULT_SUCCESS ){LOGD("SL_RESULT_SUCCESS failed!");}// 实例化混音器//(*mix)->Realize:初始化混音器对象(Output Mix)的关键函数。创建混音器后,必须调用 Realize 才能使用它re = (*mix)->Realize(mix,SL_BOOLEAN_FALSE);if(re !=SL_RESULT_SUCCESS ){LOGD("(*mix)->Realize failed!");}//省略...return env->NewStringUTF(hello.c_str());
}
  • 创建播放器(关于音频格式部分-SLDataFormat_PCM,目前是写定的数据,实际项目根据需求做调整)
extern "C"
JNIEXPORT jstring JNICALL
Java_com_jack_ffmpeg_1simple01_MainActivity_stringFromJNI(JNIEnv *env, jobject thiz) {std::string hello = "播放PCM";//省略...//***** 定义了一个 OpenSL ES 的音频数据定位器(Data Locator) ***** ,用于指定音频数据的输出目标是一个混音器(Output Mix)对象。//它是音频数据流(如播放器的输出)和物理音频设备(如扬声器)之间的桥梁。//参数1:定位器类型(固定为 SL_DATALOCATOR_OUTPUTMIX,常量:表示数据定位器的类型是“输出混音器”(即音频数据的目标是混音器))//参数2:指向混音器对象的指针SLDataLocator_OutputMix outmix = {SL_DATALOCATOR_OUTPUTMIX,mix};// 数据最终输出到混音器//这行代码定义了一个 OpenSL ES 的数据接收端(Data Sink),用于指定音频数据的最终输出目标。//参数2:可选的格式信息(通常设为 NULL 或 0)SLDataSink audioSink= {&outmix,0};//3 配置 PCM 音频格式参数//缓冲队列          定义数据来源(PCM 缓冲队列)//定义了一个 OpenSL ES 的 Android 专用缓冲队列定位器。用于指定音频数据的来源是一个内存中的 PCM 缓冲队列。它是实现音频流式播放(如实时解码网络音频或播放 PCM 数据)的核心组件。//参数1:固定值,表示这是一个 Android 专用的缓冲队列定位器。SLDataLocator_AndroidSimpleBufferQueue que = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE,10};// 队列缓冲区数量//音频格式//SLDataFormat_PCM 结构体:用于明确音频数据的存储格式和参数     定义了一个 PCM 音频格式描述符SLDataFormat_PCM pcm = {SL_DATAFORMAT_PCM,//播放PCM格式的数据  // 数据类型(固定为SL_DATAFORMAT_PCM)2,//    声道数SL_SAMPLINGRATE_44_1,// 采样率 44.1kHzSL_PCMSAMPLEFORMAT_FIXED_16,// 每个采样位数 16bitSL_PCMSAMPLEFORMAT_FIXED_16,//容器大小(通常等于bitsPerSample)//bitsPerSample 是 PCM 音频的“分辨率”,决定每个采样值的精度。16-bit 是通用选择,兼顾音质和效率。SL_SPEAKER_FRONT_LEFT|SL_SPEAKER_FRONT_RIGHT,// 声道布局SL_BYTEORDER_LITTLEENDIAN //字节序,小端(Intel架构常用)           其它:大端(网络传输常用)};//组合了数据来源定位器和数据格式,构成完整的音频输入源描述。//参数1:数据定位器(如缓冲队列)//参数2:数据格式(如PCM描述)//为什么需要这两者?//缓冲队列定位器 (que):解决数据从哪里来的问题(内存实时填充)。//PCM格式描述 (pcm):解决数据如何解析的问题(避免乱码或杂音)。SLDataSource ds = {&que,&pcm};// 数据源//音频数据流逻辑       [RAW PCM数据] → [缓冲队列(que)] → [格式解析(pcm)] → [音频播放器]//4 创建音频播放器(涉及对象初始化、接口获取和播放器配置)SLObjectItf player = NULL;                      // 播放器对象(未初始化)  音频播放器的基础对象,通过 CreateAudioPlayer 创建,后续需要 Realize 初始化SLPlayItf iplayer = NULL;                       // 播放控制接口(未初始化) 播放控制接口(SLPlayItf),用于控制播放状态(如播放/暂停/停止)。SLAndroidSimpleBufferQueueItf pcmQue = NULL;    // 缓冲队列接口(未初始化) Android 专用的缓冲队列接口(SLAndroidSimpleBufferQueueItf),用于动态填充 PCM 数据const SLInterfaceID ids[] = {SL_IID_BUFFERQUEUE};// 需要的接口 ID        指定播放器需要支持的接口类型,此处仅请求 SL_IID_BUFFERQUEUE(缓冲队列接口)。const SLboolean req[] = {SL_BOOLEAN_TRUE};// 是否必须                   标记接口是否强制需要(SL_BOOLEAN_TRUE 表示必需,若无法获取则播放器创建失败)。//创建播放器时传入数据源//参数1:引擎接口(SLEngineItf),用于创建播放器。//参数2:输出参数,接收创建的播放器对象。//参数3:数据源(SLDataSource),指定音频数据的来源(如缓冲队列 + PCM 格式)。//参数4:数据接收端(SLDataSink),指定音频输出目标(如混音器)。//参数5:计算接口数量(此处为1)。//参数6:需要的接口 ID 数组(此处为 SL_IID_BUFFERQUEUE)。re = (*eng)->CreateAudioPlayer(eng,&player,&ds,&audioSink,sizeof(ids)/sizeof(SLInterfaceID),ids,req);if(re !=SL_RESULT_SUCCESS ){LOGD("CreateAudioPlayer failed!");} else{LOGD("CreateAudioPlayer success!");}(*player)->Realize(player,SL_BOOLEAN_FALSE);// 初始化播放器//通过 GetInterface 获取具体功能接口//获取播放控制接口re = (*player)->GetInterface(player,SL_IID_PLAY,&iplayer);if(re !=SL_RESULT_SUCCESS ){LOGD("GetInterface SL_IID_PLAY failed!");}// 获取缓冲队列接口re = (*player)->GetInterface(player,SL_IID_BUFFERQUEUE,&pcmQue);if(re !=SL_RESULT_SUCCESS ){LOGD("GetInterface SL_IID_BUFFERQUEUE failed!");}//省略...return env->NewStringUTF(hello.c_str());
}
  • 执行播音操作(先调用registerCallback函数注册回调入口,再调用控制播放状态为SL_PLAYSTATE_PLAYING,即:将播放状态改为正在播放,最后手动触发,启动队列回调,开始播放首帧音频。一旦首帧播放完毕,OpenSL就回调之前注册的回调入口playerCallback,然后每帧播放完都回到PcmCall这里,如此往复,直至遍历结束,从而实现持续播放音频文件的目标。)

//播放器会不断调用该函数,需要在此回调中持续向缓冲区填充数据
void PcmCall(SLAndroidSimpleBufferQueueItf bf,void *contex)
{LOGD("PcmCall");static FILE *fp = NULL;// 静态文件指针,用于读取 PCM 文件static char *buf = NULL;// 静态缓冲区,用于存储音频数据if(!buf){buf = new char[1024*1024];// 分配 1MB 缓冲区}if(!fp){fp = fopen("/sdcard/test.pcm","rb");// 打开 PCM 文件}if(!fp)return;if(feof(fp) == 0)// 检查是否到达文件末尾{int len = fread(buf,1,1024,fp);// 读取 1024 字节数据//***** 获取到音频数据 ***** if(len > 0)(*bf)->Enqueue(bf,buf,len);// ***** 将数据加入播放队列 *****}}extern "C"
JNIEXPORT jstring JNICALL
Java_com_jack_ffmpeg_1simple01_MainActivity_stringFromJNI(JNIEnv *env, jobject thiz) {std::string hello = "播放PCM";//省略...//***** 设置回调函数从文件读取 PCM 数据 *****//注册回调函数(当队列需要数据时触发)//参数1:缓冲队列接口指针(通过 GetInterface 获取)//参数2:回调函数指针,原型为 void callback(SLAndroidSimpleBufferQueueItf bq, void *context)。//参数3:传递给回调函数的用户上下文(此处未使用,故为 0)。//回调时机:当缓冲队列为空或即将耗尽时,系统调用 PcmCall 请求新数据。(*pcmQue)->RegisterCallback(pcmQue,PcmCall,0);//***** 启动播放流程 *****//启动播放(控制播放状态)//参数1:播放控制接口指针//参数2:设置为播放状态。其他可选值://- SL_PLAYSTATE_PAUSED(暂停)//- SL_PLAYSTATE_STOPPED(停止)//关键点之一,无数据时行为:若缓冲队列为空,播放可能静音或卡顿(需提前或通过回调填充数据)。//异步操作,无阻塞。(*iplayer)->SetPlayState(iplayer,SL_PLAYSTATE_PLAYING);// ***** 手动触发第一次数据填充 启动队列回调(传入空数据触发第一次回调) ***** //作用:主动向缓冲队列提交一个空数据块,强制触发回调函数 PcmCall。//参数1:缓冲队列接口指针。//参数2:空数据指针(此处无实际意义,仅用于触发回调)//参数3:数据大小(字节数)。此处传 1 仅满足参数要求,实际无效。 仅触发回调,不提供有效数据//替代方案:也可直接填充有效数据(如首帧 PCM)//安全注意:空数据不会导致崩溃,但后续回调中必须提交有效数据。(*pcmQue)->Enqueue(pcmQue,"",1);return env->NewStringUTF(hello.c_str());
}

三.总结

  • 项目代码可以在码云上面进行下载,6.0以上的设备需要手动开启动态权限,这部分代码没有写在项目里面。通过OpenGL ES来渲染音频也是很有必要要熟练掌握的。
http://www.lryc.cn/news/585873.html

相关文章:

  • 10. 垃圾回收的算法
  • 【字符串移位包含问题】2022-8-7
  • 【飞算JavaAI】一站式智能开发,驱动Java开发全流程革新
  • 缺陷特征粘贴增强流程
  • 13. G1垃圾回收器
  • git版本发布
  • Kotlin基础学习记录
  • 基于定制开发开源AI智能名片S2B2C商城小程序的社群游戏定制策略研究
  • 云计算三大服务模式深度解析:IaaS、PaaS、SaaS
  • AI:机器人行业发展现状
  • GoC之汉诺塔绘制
  • Leaflet面试题及答案(41-60)
  • 电商广告市场惊现“合规黑洞”,企业如何避免亿元罚单
  • 11. JVM中的分代回收
  • JVM的垃圾回收算法和多种GC算法
  • 9. JVM垃圾回收
  • Opencv---深度学习开发
  • 初阶数据结构易错点整理
  • leetcode:HJ18 识别有效的IP地址和掩码并进行分类统计[华为机考][字符串]
  • 华为IPD(集成产品开发)流程是其研发管理的核心体系
  • Edge浏览器:报告不安全的站点的解决方案
  • 用YOLOv5系列教程(1)-用YOLOv5轻松实现设备状态智能监控!工业级教程来了
  • (C++)STL标准库(vector动态数组)(list列表)(set集合)(map键值对)相关对比,基础教程
  • 【Lucene/Elasticsearch】**Query Rewrite** 机制
  • U盘直接拔出不在电脑上弹出有何影响
  • 张量拼接操作
  • 文件上传漏洞2-常规厂商检测限制绕过原理讲解
  • 【学习笔记】Nginx常用安全配置
  • 新型深度神经网络架构:ENet模型
  • 零基础搭建监控系统:Grafana+InfluxDB 保姆级教程,5分钟可视化服务器性能!​