首先,這一系列文章均基於本身的理解和實踐,可能有不對的地方,歡迎你們指正。
其次,這是一個入門系列,涉及的知識也僅限於夠用,深刻的知識網上也有許許多多的博文供你們學習了。
最後,寫文章過程當中,會借鑑參考其餘人分享的文章,會在文章最後列出,感謝這些做者的分享。android
碼字不易,轉載請註明出處!git
教程代碼:【Github傳送門】 |
---|
本文介紹如何使用
FFmpeg
進行音頻解碼,重點講解如何使用OpenSL ES
在 NDK 層實現音頻渲染播放。github
在上篇文章中,詳細介紹了 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; //...... } 複製代碼
其中,SwrContext
是 FFmpeg
提供的音頻轉化工具,位於 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; } } 複製代碼
解碼完畢,退出播放的時候,須要將轉換器、輸出緩衝區釋放。
在 Android
上播放音頻,一般使用的 AudioTrack
,可是在 NDK
層,沒有提供直接的類,須要經過 NDK
調用 Java
層的方式,回調實現播放。相對來講比較麻煩,效率也比較低。
在 NDK
層,提供另外一種播放音頻的方法:OpenSL ES
。
OpenSL ES (Open Sound Library for Embedded Systems)是無受權費、跨平臺、針對嵌入式系統精心優化的硬件音頻加速API。它爲嵌入式移動多媒體設備上的本地應用程序開發者提供標準化,高性能,低響應時間的音頻功能實現方法,並實現軟/硬件音頻性能的直接跨平臺部署,下降執行難度。
OpenSL ES 主要提供了錄製和播放的功能,本文主講播放功能。
播放源支持 PCM
、sdcard資源
、 res/assets資源
、 網絡資源
。
咱們使用的 FFmpeg
解碼,因此播放源是 PCM
。
OpenSL ES 是基於 C
語言開發的庫,可是其接口是使用了面向對象的編程思想編寫的,它的接口不能直接調用,而是要通過對象建立、初始化後,經過對象來調用。
OpenSL ES 提供了一系列 Object
,它們擁有一些基礎操做方法,好比 Realize,Resume,GetState,Destroy,GetInterface等。
一個
Object
擁有一個或多個Interface
方法,可是一個Intefcace
只屬於一個Obejct
。
想要調用 Object
中的 Interface
方法,必需要經過 Object
的 GetInterface
先獲取到接口 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
中的狀態機機制。
Object
被建立後,進入Unrealized
狀態,調用Realize()
方法之後會分配相關的內存資源,進入Realized
狀態,這時Object
的Interface
方法才能被獲取和使用。在後續執行過程當中,若是出現錯誤,
Object
會進入Suspended
狀態。調用Resume()
能夠恢復到Realized
狀態。
來看一張官方的播放流程圖
這張圖很是清晰的展現了 OpenSL ES
是如何運做的。
OpenSL ES
播放須要的兩個核心是 Audio Player
和 Output Mix
,即 播放起
和 混音器
,而這兩個都是由 OpenSL ES
的引擎 Engine
建立(creates)出來的。
因此,整個初始化流程能夠總結爲:
經過 Engine 建立
Output Mix/混音器
,並將混音器
做爲參數,在建立Audio Player/播放器
時,綁定給Audio Player
做爲輸出。
在建立 Audio Player
的時候,須要給其設置 數據源
和 輸出目標
,這樣播放器才知道,如何獲取播放數據、將數據輸出到哪裏進行播放。
這就須要用到 OpenSL ES
的兩個結構體 DataSource
和 DataSink
。
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
渲染很簡單,只需調用播放器的播放接口,而且往緩衝區壓入一幀數據,就能夠啓動渲染流程。
若是是播放一個 sdcard
的 pcm
文件,那隻要在回調方法 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); } } 複製代碼
其實很簡單,就是把解碼獲得的數據壓入隊列,而且發送數據緩衝準備完畢信號,通知播放線程能夠進入播放了。
這樣,就完成了整個流程,總結一下:
OpenSL
,開啓「開始播放等待線程」,並進入播放等待;
OpenSL
設置爲播放狀態,並壓入一幀數據;
OpenSL
播放完一幀數據後,自動回調通知繼續壓入數據;
上文中,已經完成 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(); } } 複製代碼