FFmpeg音訊解碼-音訊視覺化

語言: CN / TW / HK

“我正在參加「掘金·啟航計劃」”最近在做一個音訊視覺化的業務,網上有Java層的實現方法,但是業務需要用C實現,從原理出發其實很簡單,先對音訊進行解碼,再計算分貝。這比把大象放進冰箱還簡單。本文從音訊視覺化的業務為依託,以FFmpeg為基礎實現解碼,計算,繪製。

一、解碼流程

解碼流程大致分為以下三個部分,以FFmpge原始碼下的ffmpeg\doc\examples\decode_audio.c為參考。

1.1、解析音訊資訊

avformat_open_input負責開啟需要解碼的音訊檔案,如果檔案開啟成功的話會初始化AVFormatContext,avformat_find_stream_info開啟音訊流遍歷,av_find_best_stream找到最合適解析資料的幀,解析完後我們可以通過返回的AVStream獲取到我們需要用的解碼器id、通道數、取樣率、位深、音訊時長、資料排列結構。拿到解碼器id我們通過解碼器id獲取解碼器avcodec_find_decoder,有些解碼器並不是FFmpeg內建的,所以有些需要在編譯時就加進去,我之前的文章也有講過AAC和MP3編解碼第三方庫。如果找到了解碼器,下一步就是avcodec_alloc_context3對解碼器上下文AVCodecContext進行初始化,初始化完成後avcodec_parameters_to_context將解碼器引數設定給解碼器上下文,例如通道數,取樣率,取樣位深等等資訊。如果未設定可能會出現invalid argument的錯誤,導致後續無法繼續。最後通過avcodec_open2開啟解碼器,如果開啟成功我們就可以開始對音訊資料進行讀取。

1.2、從原始資料packet到frame

我們解碼的目的就是為了拿到底層播放器能播的PCM資料。既然我們已經獲取到了解碼器,那麼下面就是一幀一幀的讀取解碼器解析出來的資料。首先我們需要av_packet_alloc初始化包物件AVPacket,包物件是未解碼的資料,原始的音訊資料被打包成一個一個的包,然後送給解碼器去把包開啟,變成幀物件,所以我們又需要通過av_frame_alloc初始化幀物件AVFrame,把它送給解碼器,解碼器用資料把它裝滿後返回回來。av_read_frame就是從開啟的檔案讀取一個數據包,對於AAC/MP3來說他們是未解碼的壓縮資料。然後通過avcodec_send_packet把資料包送給解碼器,返回0表示解碼器解包成功,接下來就可以從解碼器讀資料,這時的資料就是以幀的形式存在,avcodec_receive_frame讀取幀,因為一個包可能有幾個幀,所以需要迴圈讀取,當avcodec_receive_frame返回0時表示讀取成功,可以進行下一步操作,當返回值是AVERROR_EOF表示讀取完畢可以跳出迴圈了,返回AVERROR(EAGAIN)表示解碼器輸出已經是不可用的狀態,必須向解碼器送新包來啟用輸出,同樣也可以跳出讀取和解析幀的迴圈。

1.3、從frame到PCM byte

我們的PCM資料就在frame的data裡,但是我們並不能直接拿,首先我們得知道拿多少,怎麼拿。拿多少取決於取樣位數,通道數和幀裡面的樣本數。例如44100HZ的話一秒就有44100通道數個樣本。那一個幀裡面就一共有 取樣位數/8通道數*樣本數個位元組資料。怎麼拿取決於音訊資料的儲存方式,音訊儲存方式有兩種:

  • planar:音訊左右聲道資料分開放置,資料儲存格式為

data[0]:LLLLLLLLLLLLLLLL

data[1]:RRRRRRRRRRRRR

  • packet:音訊左右聲道資料交替放置,資料儲存格式為

data[0]:LRLRLRLRLRLRLRLR

最終拿到的資料都是以LRLRLRLRLR的方式排列,到這裡我們可以把它送給播放器或者在送給播放器前加一些我們自己的音訊演算法,全部解碼完成後,最後記得釋放掉相關資源。在這裡我們簡單點,計算它的分貝,實現音訊視覺化的功能。

二、分貝計算

我們音訊的分貝往往不需要計算每一個樣本的分貝數,第一計算密度太大超出人眼感知對顯示沒有益處,二是計算量太大會導致我們的計算時間大大延長。因為聲音具有一定的延續性,所以我們可以計算一個時間段內的平均值來獲得一段音訊範圍的分佈值,這樣既減小了工作量又達到了合理視覺化的效果。首先是獲取平均值,假設我們每秒想獲取10個分貝值,那麼我們需要計算取樣率通道數取樣位數/8/10個位元組資料的平均值,我們不妨自己把它叫dB取樣區間樣本數,一個16bit位深的音訊每兩個位元組組成一個樣本,將區間內樣本相加再除以樣本數取平均值即可。接下來就是dB的計算,dB其實並不特指分貝,它只是在音訊描述領域。它描述的是音訊的增益關係,如果想詳細瞭解db是什麼可以自行百度相關的知識。分貝的計算公式是

20*log10(value)

所以聲音的分貝描述的並不是線性關係而是指數關係,例如70db比50db的聲音大了20倍,例如16bit可以描述的音訊範圍為0-65535那麼它的最大dB值在96.3左右,32bit可以描述音訊範圍在0-4294967296,那麼它的最大dB值在192.6。把我們剛才計算的平均值帶入value就能獲得我們的區間的分貝,把它存起來解析完一起返回或者逐個回撥都可以,看你的業務需求。下面是計算16bit取樣位數的分貝的方法,32bit的處理方法類似,主要注意值的大小,和每次位移的byte步長。拿到了了分貝我們就可以將它們變成條變成塊的繪製到螢幕了。

C void getPcmDB16(const unsigned char *pcmdata, size_t size) { int db = 0; short int value = 0; double sum = 0; for(int i = 0; i < size; i += bit_format/8) { memcpy(&value, pcmdata+i, bit_format/8); //獲取2個位元組的大小(值) sum += abs(value); //絕對值求和 } sum = sum / (size / (bit_format/8)); //求平均值(2個位元組表示一個振幅,所以振幅個數為:size/2個) if(sum > 0) { db = (int)(20.0*log10(sum)); } memcpy(wave_buffer+wave_index,(char*)&db,1); wave_index++; }

需要注意的是我們在解碼時ffmpeg的音訊格式型別除了packet和planar兩個大類外,對於32位的音訊又區分了32位整形和32位浮點型。

```C enum AVSampleFormat { AV_SAMPLE_FMT_NONE = -1, AV_SAMPLE_FMT_U8, ///< unsigned 8 bits AV_SAMPLE_FMT_S16, ///< signed 16 bits AV_SAMPLE_FMT_S32, ///< signed 32 bits AV_SAMPLE_FMT_FLT, ///< float AV_SAMPLE_FMT_DBL, ///< double

AV_SAMPLE_FMT_U8P,         ///< unsigned 8 bits, planar
AV_SAMPLE_FMT_S16P,        ///< signed 16 bits, planar
AV_SAMPLE_FMT_S32P,        ///< signed 32 bits, planar
AV_SAMPLE_FMT_FLTP,        ///< float, planar
AV_SAMPLE_FMT_DBLP,        ///< double, planar
AV_SAMPLE_FMT_S64,         ///< signed 64 bits
AV_SAMPLE_FMT_S64P,        ///< signed 64 bits, planar

AV_SAMPLE_FMT_NB           ///< Number of sample formats. DO NOT USE if linking dynamically

}; ```

浮點型的取值範圍在-1到1的區間,所以我們在計算時需要乘以0x7fff來獲得和16位同比例的資料,達到同樣的顯示效果。 

C void getPcmDBFloat(const unsigned char *pcmdata, size_t size) { int db = 0; float value = 0; double sum = 0; for(int i = 0; i < size; i += bit_format/8) { memcpy(&value, pcmdata+i, bit_format/8); //獲取4個位元組的大小(值) sum += abs(value*0x7fff); //絕對值求和 } sum = sum / (size / (bit_format/8)); if(sum > 0) { db = (int)(20.0*log10(sum)); } memcpy(wave_buffer+wave_index,(char*)&db,1); wave_index++; }

三、實現效果

歡迎大家交流討論,批評指正。