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

发布于:2025-07-13 ⋅ 阅读:(23) ⋅ 点赞:(0)

一.前言

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

网站公告

今日签到

点亮在社区的每一天
去签到