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

目录

  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-lib
        avformat
        avcodec
        avfilter
        avutil
        swresample
        swscale
        OpenSLES

        ${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获取引擎接口SLEngineItf
    result = (*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 获取混音接口 SLEnvironmentalReverbItf
    result = (*outputMixObject)->GetInterface(outputMixObject, SL_IID_ENVIRONMENTALREVERB,
                                           &outputMixEnvironmentalReverb);
    if (SL_RESULT_SUCCESS == result) {
        result = (*outputMixEnvironmentalReverb)->SetEnvironmentalReverbProperties(
                outputMixEnvironmentalReverb, &reverbSettings);
    }


    //3 设置输入输出数据源
//setSLData();
//3.1 设置输入 SLDataSource
    SLDataLocator_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 设置输出 SLDataSink
    SLDataLocator_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接口 SLAndroidSimpleBufferQueueItf
    result = (*pcmPlayerObject)->GetInterface(pcmPlayerObject, SL_IID_BUFFERQUEUE, &pcmBufferQueue);
    if (SL_RESULT_SUCCESS != result) {
        LOGE(" SLAndroidSimpleBufferQueueItf GetInterface error");
        return -1;
    }
    //5.2 注册回调 RegisterCallback
    result = (*pcmBufferQueue)->RegisterCallback(pcmBufferQueue, playerCallback, NULL);
    if (SL_RESULT_SUCCESS != result) {
        LOGE(" SLAndroidSimpleBufferQueueItf RegisterCallback error");
        return -1;
    }

    //6. 设置播放状态为Playing
    result = (*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来实现解码后视频的渲染,欢迎关注公众号“音视频开发之旅”,一起学习成长。

欢迎交流

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,219评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,363评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,933评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,020评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,400评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,640评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,896评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,597评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,327评论 1 244
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,581评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,072评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,399评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,054评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,849评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,672评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,585评论 2 270

推荐阅读更多精彩内容