【Android 音視頻開發打怪升級:FFmpeg音視頻編解碼篇】4、Android FFmpeg+OpenSL ES音頻解碼播放

聲 明

首先,這一系列文章均基於本身的理解和實踐,可能有不對的地方,歡迎你們指正。
其次,這是一個入門系列,涉及的知識也僅限於夠用,深刻的知識網上也有許許多多的博文供你們學習了。
最後,寫文章過程當中,會借鑑參考其餘人分享的文章,會在文章最後列出,感謝這些做者的分享。android

碼字不易,轉載請註明出處!git

教程代碼:【Github傳送門

目錄

1、Android音視頻硬解碼篇:

2、使用OpenGL渲染視頻畫面篇

3、Android FFmpeg音視頻解碼篇

本文你能夠了解到

本文介紹如何使用 FFmpeg 進行音頻解碼,重點講解如何使用 OpenSL ES 在 NDK 層實現音頻渲染播放。github

1、音頻解碼

上篇文章中,詳細介紹了 FFmepg 的播放流程,以及抽象瞭解碼流程框架,整合視頻和音頻解碼流程的共同點,造成了 BaseDecoder 類。經過繼承 BaseDecoder 實現了視頻解碼子類 VideoDeocder,並整合到了 Player 中,實現了視頻的播放渲染。web

本文就利用已經定義好的解碼基類 BaseDecoder 實現音頻解碼子類 AudioDecoder編程

實現音頻解碼子類

首先來看下,實現音頻解碼,須要實現哪些內容。緩存

  • 定義解碼流程

咱們經過頭文件 a_decoder.h ,將須要的成員變量和流程方法定義好。網絡

i. 成員變量定義框架

//a_decoder.h
 class AudioDecoder: public BaseDecoder { private:   const char *TAG = "AudioDecoder";   // 音頻轉換器  SwrContext *m_swr = NULL;   // 音頻渲染器  AudioRender *m_render = NULL;   // 輸出緩衝  uint8_t *m_out_buffer[1] = {NULL};   // 重採樣後,每一個通道包含的採樣數  // acc默認爲1024,重採樣後可能會變化  int m_dest_nb_sample = 1024;   // 重採樣之後,一幀數據的大小  size_t m_dest_data_size = 0;   //...... } 複製代碼

其中,SwrContextFFmpeg 提供的音頻轉化工具,位於 swresample 中,可用來轉換採樣率、解碼通道數、採樣位數等。這裏用來將音頻數據轉換爲 雙通道立體 聲音,統一 採樣位數編輯器

⚠️ AudioRender 是自定義的音頻渲染器,將在後面介紹。ide

其餘的變量,則是音頻轉換中須要配合使用的,轉換輸出緩衝、緩衝區大小、採樣數。

ii. 定義成員方法

// a_decoder.h
 class AudioDecoder: public BaseDecoder { private:   // 省略成員變量......   // 初始化轉換工具  void InitSwr();   // 初始化輸出緩衝  void InitOutBuffer();   // 初始化渲染器  void InitRender();   // 釋放緩衝區  void ReleaseOutBuffer();   // 採樣格式:16位  AVSampleFormat GetSampleFmt() {  return AV_SAMPLE_FMT_S16;  }   // 目標採樣率  int GetSampleRate(int spr) {  return AUDIO_DEST_SAMPLE_RATE; //44100Hz  }  public:  AudioDecoder(JNIEnv *env, const jstring path, bool forSynthesizer);  ~AudioDecoder();   void SetRender(AudioRender *render);  protected:  void Prepare(JNIEnv *env) override;  void Render(AVFrame *frame) override;  void Release() override;   bool NeedLoopDecode() override {  return true;  }   AVMediaType GetMediaType() override {  return AVMEDIA_TYPE_AUDIO;  }   const char *const LogSpec() override {  return "AUDIO";  } }; 複製代碼

以上代碼也不復雜,都是一些初始化相關的方法,以及對 BaseDecoder 中定義的抽象方法的實現。

重點講解一下這兩個方法:

/**  * 採樣格式:16位  */ AVSampleFormat GetSampleFmt() {  return AV_SAMPLE_FMT_S16; }  /**  * 目標採樣率  */ int GetSampleRate(int spr) {  return AUDIO_DEST_SAMPLE_RATE; //44100Hz } 複製代碼

首先要知道的是,這兩個方法的目的是爲了兼容之後編碼的。

咱們知道音頻的採樣率和採樣位數是音頻數據特有的,而且每一個音頻都有可能不同,因此在播放或者從新編碼的時候,一般會將數據轉換爲固定的規格,這樣才能正常播放或從新編碼。

播放和編碼的配置也稍有不一樣,這裏,採樣位數是 16 位,採樣率使用 44100 。

接下來,看看具體的實現。

  • 實現解碼流程
// a_decoder.cpp
 AudioDecoder::AudioDecoder(JNIEnv *env, const jstring path, bool forSynthesizer) : BaseDecoder(env, path, forSynthesizer) {  }  void AudioDecoder::~AudioDecoder() {  if (m_render != NULL) {  delete m_render;  } }  void AudioDecoder::SetRender(AudioRender *render) {  m_render = render; }  void AudioDecoder::Prepare(JNIEnv *env) {  InitSwr();  InitOutBuffer();  InitRender(); }  //省略其餘.... 複製代碼

i. 初始化

重點看 Prepare 方法,這個方法在基類 BaseDecoder 初始化完解碼器之後,就會調用。

Prepare 方法中,依次調用了:

InitSwr(),初始化轉換器
  InitOutBuffer(),初始化輸出緩衝   InitRender(),初始化渲染器 複製代碼

下面具體解析如何配置初始化參數。

SwrContext 配置:

// a_decoder.cpp
 void AudioDecoder::InitSwr() {   // codec_cxt() 爲解碼上下文,從子類 BaseDecoder 中獲取  AVCodecContext *codeCtx = codec_cxt();   //初始化格式轉換工具  m_swr = swr_alloc();   // 配置輸入/輸出通道類型  av_opt_set_int(m_swr, "in_channel_layout", codeCtx->channel_layout, 0);   // 這裏 AUDIO_DEST_CHANNEL_LAYOUT = AV_CH_LAYOUT_STEREO,即 立體聲  av_opt_set_int(m_swr, "out_channel_layout", AUDIO_DEST_CHANNEL_LAYOUT, 0);   // 配置輸入/輸出採樣率  av_opt_set_int(m_swr, "in_sample_rate", codeCtx->sample_rate, 0);  av_opt_set_int(m_swr, "out_sample_rate", GetSampleRate(codeCtx->sample_rate), 0);   // 配置輸入/輸出數據格式  av_opt_set_sample_fmt(m_swr, "in_sample_fmt", codeCtx->sample_fmt, 0);  av_opt_set_sample_fmt(m_swr, "out_sample_fmt", GetSampleFmt(), 0);   swr_init(m_swr); } 複製代碼

初始化很簡單,首先調用 FFmpeg 的 swr_alloc 方法,分配內存,獲得一個轉化工具 m_swr ,接着調用對應的方法,設置輸入和輸出的音頻數據參數。

輸入輸出參數的設置,也可經過一個統一的方法 swr_alloc_set_opts 設置,具體能夠參看該接口註釋。

輸出緩衝配置:

// a_decoder.cpp
 void AudioDecoder::InitOutBuffer() {  // 重採樣後一個通道採樣數  m_dest_nb_sample = (int)av_rescale_rnd(ACC_NB_SAMPLES, GetSampleRate(codec_cxt()->sample_rate),  codec_cxt()->sample_rate, AV_ROUND_UP);  // 重採樣後一幀數據的大小  m_dest_data_size = (size_t)av_samples_get_buffer_size(  NULL, AUDIO_DEST_CHANNEL_COUNTS,  m_dest_nb_sample, GetSampleFmt(), 1);   m_out_buffer[0] = (uint8_t *) malloc(m_dest_data_size); }  void AudioDecoder::InitRender() {  m_render->InitRender(); } 複製代碼

在轉換音頻數據以前,咱們須要一個數據緩衝區來存儲轉換後的數據,所以須要知道轉換後的音頻數據有多大,並以此來分配緩衝區。

影響數據緩衝大小的因素有三個,分別是:採樣個數通道數採樣位數


採樣個數計算

咱們知道 AAC 一幀數據包含採樣個數是 1024 個。若是對一幀音頻數據進行重採樣的話,那麼採樣個數就會發生變化。

若是採樣率變大,那麼採樣個數會變多;採樣率變小,則採樣個數變少。而且成比例關係。


計算方式以下:【目標採樣個數 = 原採樣個數 *(目標採樣率 / 原採樣率)】

FFmpeg 提供了 av_rescale_rnd 用於計算這種縮放關係,優化了計算益處問題。

FFmpeg 提供了 av_samples_get_buffer_size 方法來幫咱們計算這個緩存區的大小。只需提供計算出來的目標採樣個數通道數採樣位數

獲得緩存大小後,經過 malloc 分配內存。

ii. 渲染

// a_decoder.cpp
 void AudioDecoder::Render(AVFrame *frame) {  // 轉換,返回每一個通道的樣本數  int ret = swr_convert(m_swr, m_out_buffer, m_dest_data_size/2,  (const uint8_t **) frame->data, frame->nb_samples);  if (ret > 0) {  m_render->Render(m_out_buffer[0], (size_t) m_dest_data_size);  } } 複製代碼

子類 BaseDecoder 解碼數據後,回調子類渲染方法 Render 。在渲染以前,調用 swr_convert 方法,轉換音頻數據。

如下爲接口原型:

/**  * out:輸出緩衝區  * out_count:輸出數據單通道採樣個數  * in:待轉換原音頻數據  * in_count:原音頻單通道採樣個數  */ int swr_convert(struct SwrContext *s, uint8_t **out, int out_count,  const uint8_t **in , int in_count); 複製代碼

最後調用渲染器 m_render 渲染播放。

iii.釋放資源

// a_decoder.cpp
 void AudioDecoder::Release() {  if (m_swr != NULL) {  swr_free(&m_swr);  }  if (m_render != NULL) {  m_render->ReleaseRender();  }  ReleaseOutBuffer(); }  void AudioDecoder::ReleaseOutBuffer() {  if (m_out_buffer[0] != NULL) {  free(m_out_buffer[0]);  m_out_buffer[0] = NULL;  } } 複製代碼

解碼完畢,退出播放的時候,須要將轉換器、輸出緩衝區釋放。

2、接入 OpenSL ES

Android 上播放音頻,一般使用的 AudioTrack ,可是在 NDK 層,沒有提供直接的類,須要經過 NDK 調用 Java 層的方式,回調實現播放。相對來講比較麻煩,效率也比較低。

NDK 層,提供另外一種播放音頻的方法:OpenSL ES

什麼是 OpenSL ES

OpenSL ES (Open Sound Library for Embedded Systems)是無受權費、跨平臺、針對嵌入式系統精心優化的硬件音頻加速API。它爲嵌入式移動多媒體設備上的本地應用程序開發者提供標準化,高性能,低響應時間的音頻功能實現方法,並實現軟/硬件音頻性能的直接跨平臺部署,下降執行難度。

OpenSL ES 提供哪些功能

OpenSL ES 主要提供了錄製和播放的功能,本文主講播放功能。

播放源支持 PCMsdcard資源res/assets資源網絡資源

咱們使用的 FFmpeg 解碼,因此播放源是 PCM

OpenSL ES 狀態機

OpenSL ES 是基於 C 語言開發的庫,可是其接口是使用了面向對象的編程思想編寫的,它的接口不能直接調用,而是要通過對象建立、初始化後,經過對象來調用。

  • Object 和 Interface

OpenSL ES 提供了一系列 Object ,它們擁有一些基礎操做方法,好比 Realize,Resume,GetState,Destroy,GetInterface等。

一個 Object 擁有一個或多個 Interface 方法,可是一個 Intefcace 只屬於一個 Obejct

想要調用 Object 中的 Interface 方法,必需要經過 ObjectGetInterface 先獲取到接口 Interface ,再經過獲取到的 Interface 來調用。

好比:

// 建立引擎
SLObjectItf m_engine_obj = NULL; SLresult result = slCreateEngine(&m_engine_obj, 0, NULL, 0, NULL, NULL);  // 初始化引擎 result = (*m_engine_obj)->Realize(m_engine_obj, SL_BOOLEAN_FALSE);  // 獲取引擎接口 SLEngineItf m_engine = NULL; result = (*m_engine_obj)->GetInterface(m_engine_obj, SL_IID_ENGINE, &m_engine); 複製代碼

能夠看到,Object 須要通過建立、初始化以後才能使用,這就是 OpenSL ES 中的狀態機機制。

OpenSL ES 狀態機
OpenSL ES 狀態機

Object 被建立後,進入 Unrealized 狀態,調用 Realize() 方法之後會分配相關的內存資源,進入 Realized 狀態,這時 ObjectInterface 方法才能被獲取和使用。

在後續執行過程當中,若是出現錯誤,Object 會進入 Suspended 狀態。調用 Resume() 能夠恢復到 Realized 狀態。

OpenSL ES 播放初始化配置

來看一張官方的播放流程圖

OpenSL ES 播放流程
OpenSL ES 播放流程

這張圖很是清晰的展現了 OpenSL ES 是如何運做的。

OpenSL ES 播放須要的兩個核心是 Audio PlayerOutput Mix ,即 播放起混音器 ,而這兩個都是由 OpenSL ES 的引擎 Engine 建立(creates)出來的。

因此,整個初始化流程能夠總結爲:

經過 Engine 建立 Output Mix/混音器,並將 混音器 做爲參數,在建立 Audio Player/播放器 時,綁定給 Audio Player 做爲輸出。

  • DataSource 和 DataSink

在建立 Audio Player 的時候,須要給其設置 數據源輸出目標 ,這樣播放器才知道,如何獲取播放數據、將數據輸出到哪裏進行播放。

這就須要用到 OpenSL ES 的兩個結構體 DataSourceDataSink

typedef struct SLDataSource_ {
 void *pLocator;  void *pFormat; } SLDataSource;  typedef struct SLDataSink_ {  void *pLocator;  void *pFormat; } SLDataSink; 複製代碼

其中, SLDataSource pLocator 有如下幾種類型:

SLDataLocator_Address
SLDataLocator_BufferQueue
SLDataLocator_IODevice
SLDataLocator_MIDIBufferQueue
SLDataLocator_URI
複製代碼

播放 PCM 使用的是 SLDataLocator_BufferQueue 緩衝隊列。

SLDataSink pLocator 通常爲 SL_DATALOCATOR_OUTPUTMIX

另一個參數 pFormat 爲數據的格式。

實現渲染流程

在接入 OpenSL ES 以前,先定義好上文提到的音頻渲染接口,方便規範和拓展。

// audio_render.h
 class AudioRender { public:  virtual void InitRender() = 0;  virtual void Render(uint8_t *pcm, int size) = 0;  virtual void ReleaseRender() = 0;  virtual ~AudioRender() {} }; 複製代碼

CMakeList.txt 中,打開 OpenSL ES 支持。

# CMakeList.txt
 # 省略其餘...  # 指定編譯目標庫時,cmake要連接的庫 target_link_libraries(   native-lib   avutil  swresample  avcodec  avfilter  swscale  avformat  avdevice   -landroid   # 打開opensl es支持  OpenSLES   # Links the target library to the log library  # included in the NDK.  ${log-lib} ) 複製代碼
  • 初始化

i. 定義成員變量

先定義須要用到的引擎、混音器、播放器、以及緩衝隊列接口、音量調節接口等。

// opensl_render.h
 class OpenSLRender: public AudioRender {  private:   // 引擎接口  SLObjectItf m_engine_obj = NULL;  SLEngineItf m_engine = NULL;   //混音器  SLObjectItf m_output_mix_obj = NULL;  SLEnvironmentalReverbItf m_output_mix_evn_reverb = NULL;  SLEnvironmentalReverbSettings m_reverb_settings = SL_I3DL2_ENVIRONMENT_PRESET_DEFAULT;   //pcm播放器  SLObjectItf m_pcm_player_obj = NULL;  SLPlayItf m_pcm_player = NULL;  SLVolumeItf m_pcm_player_volume = NULL;   //緩衝器隊列接口  SLAndroidSimpleBufferQueueItf m_pcm_buffer;   //省略其餘...... } 複製代碼

ii. 定義相關成員方法

// opensl_render.h
 class OpenSLRender: public AudioRender {  private:   // 省略成員變量...   // 建立引擎  bool CreateEngine();   // 建立混音器  bool CreateOutputMixer();   // 建立播放器  bool CreatePlayer();   // 開始播放渲染  void StartRender();   // 音頻數據壓入緩衝隊列  void BlockEnqueue();   // 檢查是否發生錯誤  bool CheckError(SLresult result, std::string hint);   // 數據填充通知接口,後續會介紹這個方法的做用  void static sReadPcmBufferCbFun(SLAndroidSimpleBufferQueueItf bufferQueueItf, void *context);  public:  OpenSLRender();  ~OpenSLRender();   void InitRender() override;  void Render(uint8_t *pcm, int size) override;  void ReleaseRender() override; 複製代碼

iii. 實現初始化流程

// opensl_render.cpp
 OpenSLRender::OpenSLRender() { }  OpenSLRender::~OpenSLRender() { }  void OpenSLRender::InitRender() {  if (!CreateEngine()) return;  if (!CreateOutputMixer()) return;  if (!CreatePlayer()) return; }  // 省略其餘...... 複製代碼

建立引擎

// opensl_render.cpp
 bool OpenSLRender::CreateEngine() {  SLresult result = slCreateEngine(&m_engine_obj, 0, NULL, 0, NULL, NULL);  if (CheckError(result, "Engine")) return false;   result = (*m_engine_obj)->Realize(m_engine_obj, SL_BOOLEAN_FALSE);  if (CheckError(result, "Engine Realize")) return false;   result = (*m_engine_obj)->GetInterface(m_engine_obj, SL_IID_ENGINE, &m_engine);  return !CheckError(result, "Engine Interface"); } 複製代碼

建立混音器

// opensl_render.cpp
 bool OpenSLRender::CreateOutputMixer() {  SLresult result = (*m_engine)->CreateOutputMix(m_engine, &m_output_mix_obj, 1, NULL, NULL);  if (CheckError(result, "Output Mix")) return false;   result = (*m_output_mix_obj)->Realize(m_output_mix_obj, SL_BOOLEAN_FALSE);  if (CheckError(result, "Output Mix Realize")) return false;   return true; } 複製代碼

按照前面狀態機的機制,先建立引擎對象 m_engine_obj、而後 Realize 初始化,而後再經過 GetInterface 方法,獲取到引擎接口 m_engine

建立播放器

// opensl_render.cpp
 bool OpenSLRender::CreatePlayer() {   //【1.配置數據源 DataSource】----------------------   //配置PCM格式信息  SLDataLocator_AndroidSimpleBufferQueue android_queue = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, SL_QUEUE_BUFFER_COUNT};  SLDataFormat_PCM pcm = {  SL_DATAFORMAT_PCM,//播放pcm格式的數據  (SLuint32)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 = {&android_queue, &pcm};   //【2.配置輸出 DataSink】----------------------   SLDataLocator_OutputMix outputMix = {SL_DATALOCATOR_OUTPUTMIX, m_output_mix_obj};  SLDataSink slDataSink = {&outputMix, NULL};   const SLInterfaceID ids[3] = {SL_IID_BUFFERQUEUE, SL_IID_EFFECTSEND, SL_IID_VOLUME};  const SLboolean req[3] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE};   //【3.建立播放器】----------------------   SLresult result = (*m_engine)->CreateAudioPlayer(m_engine, &m_pcm_player_obj, &slDataSource, &slDataSink, 3, ids, req);  if (CheckError(result, "Player")) return false;   //初始化播放器  result = (*m_pcm_player_obj)->Realize(m_pcm_player_obj, SL_BOOLEAN_FALSE);  if (CheckError(result, "Player Realize")) return false;   //【4.獲取播放器接口】----------------------   //獲得接口後調用,獲取Player接口  result = (*m_pcm_player_obj)->GetInterface(m_pcm_player_obj, SL_IID_PLAY, &m_pcm_player);  if (CheckError(result, "Player Interface")) return false;   //獲取音量接口  result = (*m_pcm_player_obj)->GetInterface(m_pcm_player_obj, SL_IID_VOLUME, &m_pcm_player_volume);  if (CheckError(result, "Player Volume Interface")) return false;   //【5. 獲取緩衝隊列接口】----------------------   //註冊回調緩衝區,獲取緩衝隊列接口  result = (*m_pcm_player_obj)->GetInterface(m_pcm_player_obj, SL_IID_BUFFERQUEUE, &m_pcm_buffer);  if (CheckError(result, "Player Queue Buffer")) return false;   //註冊緩衝接口回調  result = (*m_pcm_buffer)->RegisterCallback(m_pcm_buffer, sReadPcmBufferCbFun, this);  if (CheckError(result, "Register Callback Interface")) return false;   LOGI(TAG, "OpenSL ES init success")   return true; } 複製代碼

播放器的初始化比較麻煩一些,不過都是根據前面介紹的初始化流程,循序漸進。

配置數據源、輸出器、以及初始化後,獲取播放接口、音量調節接口等。

⚠️ 要注意的是最後一步,即代碼中的第【5】。

數據源爲 緩衝隊列 的時候,須要獲取一個緩衝接口,用於將數據填入緩衝區。

那麼何時填充數據呢?這就是最後註冊回調接口的做用。

咱們須要註冊一個回調函數到播放器中,當播放器中的數據播放完,就會回調這個方法,告訴咱們:數據播完啦,要填充新的數據了。

sReadPcmBufferCbFun 是一個靜態方法,能夠推測出,OpenSL ES 播放音頻內部是一個獨立的線程,這個線程不斷的讀取緩衝區的數據,進行渲染,並在數據渲染完了之後,經過這個回調接口通知咱們填充新數據。

  • 實現播放

啓動 OpenSL ES 渲染很簡單,只需調用播放器的播放接口,而且往緩衝區壓入一幀數據,就能夠啓動渲染流程。

若是是播放一個 sdcardpcm 文件,那隻要在回調方法 sReadPcmBufferCbFun 中讀取一幀數據填入便可。

可是,在咱們這裏沒有那麼簡單,還記得咱們的 BaseDeocder 中啓動了一個解碼線程嗎?而 OpenSL ES 渲染也是一個獨立的線程,所以,在這裏變成兩個線程的數據同步問題

固然了,也能夠將 FFmpeg 作成一個簡單的解碼模塊,在 OpenSL ES 的渲染線程實現解碼播放,處理起來就會簡單得多。

爲了解碼流程的統一,這裏將會採用兩個獨立線程。

i. 開啓播放等待

上面已經提到,播放和解碼是兩個因此數據須要同步,所以,在初始化爲 OpenSL 之後,不能立刻開始進入播放狀態,而是要等待解碼數據第一幀,才能開始播放。

這裏,經過線程的等待方式,等待數據。

在前面的 InitRender 方法中,首先初始化了 OpenSL ,在這方法的最後,咱們讓播放進入等待狀態。

// opensl_render.cpp
 OpenSLRender::OpenSLRender() { }  OpenSLRender::~OpenSLRender() { }  void OpenSLRender::InitRender() {  if (!CreateEngine()) return;  if (!CreateOutputMixer()) return;  if (!ConfigPlayer()) return;   // 開啓線程,進入播放等待  std::thread t(sRenderPcm, this);  t.detach(); }  void OpenSLRender::sRenderPcm(OpenSLRender *that) {  that->StartRender(); }  void OpenSLRender::StartRender() {  while (m_data_queue.empty()) {  WaitForCache();  }  (*m_pcm_player)->SetPlayState(m_pcm_player, SL_PLAYSTATE_PLAYING);  sReadPcmBufferCbFun(m_pcm_buffer, this); }  // 線程進入等待 void OpenSLRender::WaitForCache() {  pthread_mutex_lock(&m_cache_mutex);  pthread_cond_wait(&m_cache_cond, &m_cache_mutex);  pthread_mutex_unlock(&m_cache_mutex); }  // 通知線程恢復執行 void OpenSLRender::SendCacheReadySignal() {  pthread_mutex_lock(&m_cache_mutex);  pthread_cond_signal(&m_cache_cond);  pthread_mutex_unlock(&m_cache_mutex); } 複製代碼

最後的 StartRender() 方法是真正被線程執行的方法,進入該方法,首先判斷數據緩衝隊列是否有數據,沒有則進入等待,直到數據到來。

其中,m_data_queue 是自定義的數據緩衝隊列,以下:

// opensl_render.h
 class OpenSLRender: public AudioRender {  private:  /**  * 封裝 PCM 數據,主要用於實現數據內存的釋放  */  class PcmData {  public:  PcmData(uint8_t *pcm, int size) {  this->pcm = pcm;  this->size = size;  }  ~PcmData() {  if (pcm != NULL) {  //釋放已使用的內存  free(pcm);  pcm = NULL;  used = false;  }  }  uint8_t *pcm = NULL;  int size = 0;  bool used = false;  };   // 數據緩衝列表  std::queue<PcmData *> m_data_queue;   // 省略其餘... } 複製代碼

ii. 數據同步與播放

接下來,就來看看如何盡心數據同步與播放。

初始化 OpenSL 的時候,在最後註冊了播放回調接口 sReadPcmBufferCbFun,首先來看看它的實現。

// opensl_render.cpp
 void OpenSLRender::sReadPcmBufferCbFun(SLAndroidSimpleBufferQueueItf bufferQueueItf, void *context) {  OpenSLRender *player = (OpenSLRender *)context;  player->BlockEnqueue(); }  void OpenSLRender::BlockEnqueue() {  if (m_pcm_player == NULL) return;   // 先將已經使用過的數據移除  while (!m_data_queue.empty()) {  PcmData *pcm = m_data_queue.front();  if (pcm->used) {  m_data_queue.pop();  delete pcm;  } else {  break;  }  }   // 等待數據緩衝  while (m_data_queue.empty() && m_pcm_player != NULL) {// if m_pcm_player is NULL, stop render  WaitForCache();  }   PcmData *pcmData = m_data_queue.front();  if (NULL != pcmData && m_pcm_player) {  SLresult result = (*m_pcm_buffer)->Enqueue(m_pcm_buffer, pcmData->pcm, (SLuint32) pcmData->size);  if (result == SL_RESULT_SUCCESS) {  // 只作已經使用標記,在下一幀數據壓入前移除  // 保證數據能正常使用,不然可能會出現破音  pcmData->used = true;  }  } } 複製代碼

StartRender() 等待到緩衝數據的到來時,就會經過如下方法啓動播放

(*m_pcm_player)->SetPlayState(m_pcm_player, SL_PLAYSTATE_PLAYING);
sReadPcmBufferCbFun(m_pcm_buffer, this); 複製代碼

這時候,通過一層層調用,最後調用的是 BlockEnqueue() 方法。

在這個方法中,

首先,將 m_data_queue 中已經使用的數據先刪除,回收資源;

接着,判斷是否還有未播放的緩衝數據,沒有則進入等待;

最後,經過 (*m_pcm_buffer)->Enqueue() 方法,將數據壓入 OpenSL 隊列。

⚠️ 注:在接下來的播放過程當中,OpenSL 只要播放完數據,就會自動回調 sReadPcmBufferCbFun 從新進入以上的播放流程。

  • 壓入數據,開啓播放

以上是整個播放的流程,最後還有關鍵的一點,來開啓這個播放流程,那就是 AudioRender 定義的渲染播放接口 void Render(uint8_t *pcm, int size)

// opensl_render.cpp
 void OpenSLRender::Render(uint8_t *pcm, int size) {  if (m_pcm_player) {  if (pcm != NULL && size > 0) {  // 只緩存兩幀數據,避免佔用太多內存,致使內存申請失敗,播放出現雜音  while (m_data_queue.size() >= 2) {  SendCacheReadySignal();  usleep(20000);  }   // 將數據複製一份,並壓入隊列  uint8_t *data = (uint8_t *) malloc(size);  memcpy(data, pcm, size);   PcmData *pcmData = new PcmData(pcm, size);  m_data_queue.push(pcmData);   // 通知播放線程推出等待,恢復播放  SendCacheReadySignal();  }  } else {  free(pcm);  } } 複製代碼

其實很簡單,就是把解碼獲得的數據壓入隊列,而且發送數據緩衝準備完畢信號,通知播放線程能夠進入播放了。

這樣,就完成了整個流程,總結一下:

  1. 初始化 OpenSL ,開啓「開始播放等待線程」,並進入播放等待;
  2. 將數據壓入緩衝隊列,通知播放線程恢復執行,進入播放;
  3. 開啓播放時,將 OpenSL 設置爲播放狀態,並壓入一幀數據;
  4. OpenSL 播放完一幀數據後,自動回調通知繼續壓入數據;
  5. 解碼線程不斷壓入數據到緩衝隊列;
  6. 在接下來的過程當中,「OpenSL ES 播放線程」和「FFMpeg 解碼線程」會同時執行,重複「2 ~ 5 」,而且在數據緩衝不足的狀況下,「播放線程 」會等待「解碼線程」壓入數據後,再繼續執行,直到完成播放,雙方退出線程。

3、整合播放

上文中,已經完成 OpenSL ES 播放器的相關功能,而且實現了 AudioRander 中定義的接口,只要在 AudioDecoder 中正確調用就能夠了。

如何調用也已經在第一節中介紹,如今只需把它們整合到 Player 中,就能夠實現音頻的播放了。

在播放器中,新增音頻解碼器和渲染器:

//player.h
 class Player { private:  VideoDecoder *m_v_decoder;  VideoRender *m_v_render;   // 新增音頻解碼和渲染器  AudioDecoder *m_a_decoder;  AudioRender *m_a_render;  public:  Player(JNIEnv *jniEnv, jstring path, jobject surface);  ~Player();   void play();  void pause(); }; 複製代碼

實例化音頻解碼器和渲染器:

// player.cpp
 Player::Player(JNIEnv *jniEnv, jstring path, jobject surface) {  m_v_decoder = new VideoDecoder(jniEnv, path);  m_v_render = new NativeRender(jniEnv, surface);  m_v_decoder->SetRender(m_v_render);   // 實例化音頻解碼器和渲染器  m_a_decoder = new AudioDecoder(jniEnv, path, false);  m_a_render = new OpenSLRender();  m_a_decoder->SetRender(m_a_render); }  Player::~Player() {  // 此處不須要 delete 成員指針  // 在BaseDecoder中的線程已經使用智能指針,會自動釋放 }  void Player::play() {  if (m_v_decoder != NULL) {  m_v_decoder->GoOn();  m_a_decoder->GoOn();  } }  void Player::pause() {  if (m_v_decoder != NULL) {  m_v_decoder->Pause();  m_a_decoder->Pause();  } } 複製代碼
相關文章
相關標籤/搜索