一.前言
- 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来渲染音频也是很有必要要熟练掌握的。