音視頻開發之旅(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來實現解碼后視頻的渲染,歡迎關注公眾號“音視頻開發之旅”,一起學習成長。

歡迎交流

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容