一.前言
二.通过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.1kHz
SL_PCMSAMPLEFORMAT_FIXED_16,// 每个采样位数 16bit
SL_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来渲染音频也是很有必要要熟练掌握的。