音視頻開發(fā)之旅(36) -FFmpeg +OpenSL ES實現(xiàn)音頻解碼和播放

目錄

  1. OpenSL ES基本介紹
  2. OpenSL ES播放音頻流程
  3. 代碼實現(xiàn)
  4. 遇到的問題
  5. 資料
  6. 收獲

上一篇我們通過AudioTrack實現(xiàn)了FFmpeg解碼后的PCM音頻數(shù)據(jù)的播放窟勃,在Android上還有一種播放音頻的方式即OpenSL ES, 什么是OpenSL ES哥艇,這個我們平時接觸的很少,原因是平時業(yè)務中大部分播放可以通過Java層的MediaPlayer或者AudioTrack實現(xiàn)音頻播放。如果遇到一些特殊的需求祠挫,比如添加音效等這是不容易實現(xiàn)。而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 。在開發(fā)中衷敌,一般會直接使用高級 API , 除非遇到性能瓶頸勿侯,如語音實時聊天、3D Audio 逢享、某些 Effects 等,開發(fā)者可以直接通過 C/CPP開發(fā)基于 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語言編寫的,但是以面向對象的思想提供了對象和接口粥血,方便開發(fā)的在項目中使用柏锄。

OpenSL ES 對象類似于 Java 和 CPP 等編程語言中的對象概念酿箭,不過 OpenSL ES 對象僅能通過其關聯(lián)接口進行訪問。其中包括所有對象的初始接口趾娃,稱為 SLObjectItf缭嫡。對象本身沒有句柄,只有一個連接到對象的 SLObjectItf 接口的句柄抬闷。
需要注意的是 OpenSL ES 對象不能直接使用妇蛀,必須通過其 GetInterface 函數(shù)用ID號拿到指定接口(如播放器的播放接口),然后通過該接口來訪問功能函數(shù)

OpenSL ES 對象是先創(chuàng)建的笤成,它會返回 SLObjectItf评架,然后再實現(xiàn) (realize),然后使用 GetInterface,為其需要的每種功能獲取接口
音頻播放會用到 引擎炕泳、混音器以及播放器對象和接口纵诞,下一小節(jié)我們來看下具體流程。

二喊崖、OpenSL ES播放音頻流程

圖片來源: OpenSL-ES 官方文檔

在CmakeList引入OpenSL庫挣磨,然后在對應的CPP文件中導入相應的頭文件即可使用OpenSL ES,具體流程如下

  1. 創(chuàng)建引擎對象SLObjectItf engineObj
    初始化引擎 Realize
    獲取引擎接口 GetInterface SLEngineItf
  2. 創(chuàng)建混音器對象SLObjectItf outputMixObj
    初始化混音器 Realize
  3. 設置輸入輸出數(shù)據(jù)參數(shù)
  4. 創(chuàng)建播放器對象 SLPlayItf playerObj
    初始化播放器Realize
    獲取播放器接口 GetInterface
  5. 獲取播放回調接口(即緩沖隊列)SLAndroidSimpleBufferQueueItf bufferQueue
  6. 注冊播放回調 `RegisterCallback
  7. 設置播放狀態(tài)SetPlayState
  8. 等待音頻幀加入隊列觸發(fā)播放回調(*mBufferQueue)->Enqueue
  9. 釋放資源

具體參考官方提供的示例demo native-audio 是一個簡單的音頻錄制器/播放器

三荤懂、OpenSL ES播放解碼PCM的代碼實現(xiàn)

了解了OpenSL ES的基本知識和使用流程茁裙,下面我們開始具體的代碼實現(xiàn)。

#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>
}

//函數(shù)聲明
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播放的函數(shù)
 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. 創(chuàng)建引擎`
//    SLresult result;
//1.1 創(chuàng)建引擎對象
    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接口后节仿,后續(xù)的混音器和播放器的創(chuàng)建都會使用它

    //2. 創(chuàng)建輸出混音器

    const SLInterfaceID ids[1] = {SL_IID_ENVIRONMENTALREVERB};
    const SLboolean req[1] = {SL_BOOLEAN_FALSE};

    //2.1 創(chuàng)建混音器對象
    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 設置輸入輸出數(shù)據(jù)源
//setSLData();
//3.1 設置輸入 SLDataSource
    SLDataLocator_AndroidSimpleBufferQueue loc_bufq = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE,2};

    SLDataFormat_PCM formatPcm = {
            SL_DATAFORMAT_PCM,//播放pcm格式的數(shù)據(jù)
            2,//2個聲道(立體聲)
            SL_SAMPLINGRATE_44_1,//44100hz的頻率
            SL_PCMSAMPLEFORMAT_FIXED_16,//位數(shù) 16位
            SL_PCMSAMPLEFORMAT_FIXED_16,//和位數(shù)一致就行
            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.創(chuàng)建音頻播放器

    //4.1 創(chuàng)建音頻播放器對象

    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. 設置播放狀態(tài)為Playing
    result = (*pcmPlayerPlay)->SetPlayState(pcmPlayerPlay, SL_PLAYSTATE_PLAYING);
    if (SL_RESULT_SUCCESS != result) {
        LOGE(" SetPlayState  error");
        return -1;
    }

    //7.觸發(fā)回調
    playerCallback(pcmBufferQueue,NULL);

    return 0;
}

OpenSL ES 還有更多豐富的功能晤锥,比如,混音廊宪、設置音量矾瘾、錄音、播放url或者assert中的音頻箭启。詳細了解可以查看官方文檔和NDK的demo壕翩,

本篇就學習實踐到這里,越學習發(fā)下身邊優(yōu)秀的人越多傅寡,自己不會的東西放妈、要學習的就越多,抓住一個核心痛點荐操,一起學習實踐吧芜抒。

代碼已上傳至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: 創(chuàng)建播放器對象一直為空唠亚,導致無法播放

原因:給SLData 設置數(shù)據(jù)源時
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;
        //原因是链方,忘記跳出循環(huán)了
        break;
    }

在學習的初期一個小錯誤就可能折騰幾個小時,在采用逐步排查流程和查看細節(jié)灶搜、以及和可運行的demo進行對比分析排查出問題所在祟蚀。
根源還在于不夠細心和理解的不透徹。

五割卖、資料

  1. OpenSL-ES 官方文檔
  2. NDK指南: OpenSL ES
  3. NDK指南demo:native-audio 是一個簡單的音頻錄制器/播放器
  4. 音視頻學習 (七) AudioTrack前酿、OpenSL ES 音頻渲染
  5. FFmpeg 開發(fā)(03):FFmpeg + OpenSL ES 實現(xiàn)音頻解碼播放
  6. android平臺OpenSL ES播放PCM數(shù)據(jù)
  7. Android通過OpenSL ES播放音頻套路詳解

六、收獲

  1. 了解了OpenSl ES的基本知識和播放音頻數(shù)據(jù)的流程
  2. 代碼實現(xiàn)OpenSL ES播放音頻流
  3. 和FFmpeg結合鹏溯,實現(xiàn)opensl播放解碼后的音頻數(shù)據(jù)
  4. 解決遇到的問題

感謝你的閱讀

學習實踐了視頻的解碼罢维、音頻的解碼和播放,下一篇我們通過OpenGL ES來實現(xiàn)解碼后視頻的渲染丙挽,歡迎關注公眾號“音視頻開發(fā)之旅”肺孵,一起學習成長。

歡迎交流

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末颜阐,一起剝皮案震驚了整個濱河市平窘,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌凳怨,老刑警劉巖瑰艘,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異肤舞,居然都是意外死亡紫新,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門李剖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來芒率,“玉大人,你說我怎么就攤上這事篙顺∨忌郑” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵慰安,是天一觀的道長腋寨。 經(jīng)常有香客問我聪铺,道長化焕,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任铃剔,我火速辦了婚禮撒桨,結果婚禮上查刻,老公的妹妹穿的比我還像新娘。我一直安慰自己凤类,他們只是感情好穗泵,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著谜疤,像睡著了一般佃延。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上夷磕,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天履肃,我揣著相機與錄音,去河邊找鬼坐桩。 笑死尺棋,一個胖子當著我的面吹牛,可吹牛的內容都是我干的绵跷。 我是一名探鬼主播膘螟,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼碾局!你這毒婦竟也來了荆残?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤擦俐,失蹤者是張志新(化名)和其女友劉穎脊阴,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蚯瞧,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡嘿期,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了埋合。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片备徐。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖甚颂,靈堂內的尸體忽然破棺而出蜜猾,到底是詐尸還是另有隱情,我是刑警寧澤振诬,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布蹭睡,位于F島的核電站,受9級特大地震影響赶么,放射性物質發(fā)生泄漏肩豁。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望清钥。 院中可真熱鬧琼锋,春花似錦、人聲如沸祟昭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽篡悟。三九已至谜叹,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間搬葬,已是汗流浹背叉谜。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留踩萎,地道東北人停局。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像香府,于是被迫代替她去往敵國和親董栽。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內容