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++; }

三、實現效果

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