X264代码分析
相關(guān)說明:
1.???? 使用版本: x264-cvs-2004-05-11
2.???? 這次的分析基本上已經(jīng)將代碼中最難理解的部分做了闡釋,對代碼的主線也做了剖析,如果這個主線理解了,就容易設(shè)置幾個區(qū)間,進(jìn)行分工閱讀,將各個區(qū)間擊破了.
3.???? 需要學(xué)習(xí)的知識:
a)?????? 編碼器的工作流程.
b)????? H.264的碼流結(jié)構(gòu),像x264_sps_t,x264_pps_t等參數(shù)的定義基本上都完全符合標(biāo)準(zhǔn)文檔中參數(shù)集的定義,抓住主要參數(shù),次要參數(shù)也應(yīng)該有所了解.
c)?????? 數(shù)學(xué)知識,對dct變換等與數(shù)學(xué)相關(guān)的知識的編程實現(xiàn)要有較好理解.
d)????? C語言的知識.涉及到c語言的較多不經(jīng)常用的特性,如函數(shù)指針數(shù)組,移位運算,結(jié)構(gòu)體的嵌套定義等.
e)?????? 耐心,對h.264的復(fù)雜性要有清醒的認(rèn)識.
3.參考資料:
a)?????? 新一代視頻壓縮編碼標(biāo)準(zhǔn)-h.264/avc 畢厚杰主編,人民郵電出版社.
b)????? 網(wǎng)上的流媒體論壇,百度,google等搜索引擎.
4. 閱讀代碼的方法:
a)?????? 較好的方法是利用vc的調(diào)試器,如果對某個函數(shù)感興趣,可以將斷點設(shè)置在它的前面.然后采用step into,step over等方法進(jìn)去該函數(shù)一步步分析.當(dāng)然本身要對程序執(zhí)行流程要有較清楚認(rèn)識,不然不知道何時step into,何時step over.
b)????? 建議應(yīng)該先對照標(biāo)準(zhǔn)弄清各個結(jié)構(gòu)體成員的意義.
l?????? 源代碼主要過程分析:
?
1.?????? 進(jìn)入x264.c中的main函數(shù).
剛開始是讀取默認(rèn)參數(shù),如果你設(shè)置了參數(shù)的話會修改param的.
?? i_ret = Encode( ¶m, fin, fout );
這條語句使過程進(jìn)入x264.c中的Encode函數(shù).
2.?????? X.264的encode函數(shù).
A.???? i_frame_total = 0;
if( !fseek( fyuv, 0, SEEK_END ) )
???? {
??????? int64_t i_size = ftell( fyuv );
??????? fseek( fyuv, 0, SEEK_SET );
??????? i_frame_total = i_size / ( param->i_width * param->i_height * 3 / 2 )
}
上面這段計算出輸入文件的總幀數(shù).
B.????? h = x264_encoder_open( param )這個函數(shù)是對不正確的參數(shù)進(jìn)行修改,并對各結(jié)構(gòu)體參數(shù)和cabac編碼,預(yù)測等需要的參數(shù)進(jìn)行初始化.
C.???? pic = x264_picture_new( h );
該函數(shù)定義在/CORE/common.c中.首先分給能容納sizeof(x264_picture_t)字節(jié)數(shù)的空間,然后進(jìn)行初始化.
????? 這里看一下x264_picture_t和x264_frame_t的區(qū)別.前者是說明一個視頻序列中每幀的特點.后者存放每幀實際的象素值.注意區(qū)分.
D.???? for( i_frame = 0, i_file = 0; i_ctrl_c == 0 ; i_frame++ )
??? {
??????? int???????? i_nal;
??????? x264_nal_t *nal;
?int???????? i;
/* read a frame */
??????? if( fread( pic->plane[0], 1, param->i_width * param->i_height, fyuv ) <= 0 ||
??????????? fread( pic->plane[1], 1, param->i_width * param->i_height / 4, fyuv ) <= 0 ||
??????????? fread( pic->plane[2], 1, param->i_width * param->i_height / 4, fyuv ) <= 0 )
??????? {
??????????? break;
??????? }
??? //文件位置指示器自己變化了.
?
??????? if( x264_encoder_encode( h, &nal, &i_nal, pic ) < 0 )
??????? {
??????????? fprintf( stderr, "x264_encoder_encode failed/n" );
??????? }
??????? ……
}
凡是出現(xiàn)for循環(huán)的地方都應(yīng)該認(rèn)真對待,這是我的一個體會,也是進(jìn)入分層結(jié)構(gòu)認(rèn)真分析的好方法.
fread()函數(shù)一次讀入一幀,分亮度和色度分別讀取.這里要看到c語言中的File文件有一個文件位置指示器,調(diào)用fread()函數(shù)會使文件指示器自動移位,這就是一幀一幀讀取的實現(xiàn)過程.
E.????? 然后進(jìn)入x264_encoder_encode( h, &nal, &i_nal, pic )函數(shù),該函數(shù)定義在/Enc/encoder.c中.
開始進(jìn)入比較復(fù)雜的地方了.
這個函數(shù)前面有一段注釋(如下):
****************************************************************************
?* x264_encoder_encode:
?* XXX: i_poc?? : is the poc of the current given picture
?*?????? i_frame : is the number of the frame being coded
?* ex: type frame poc
?*?????? I????? 0?? 2*0//poc是實際的幀的位置.
?*?????? P????? 1?? 2*3//frame是編碼的順序.
?*?????? B????? 2?? 2*1
?*?????? B????? 3?? 2*2
?*?????? P????? 4?? 2*6
?*?????? B????? 5?? 2*4
?*?????? B????? 6?? 2*5
?****************************************************************************/
要搞清poc和frame的區(qū)別.
假設(shè)一個視頻序列如下:
I???? B??? B??? P???? B???? B?? P
我們編碼是按I P B B P B B的順序,這就是frame的編號.
而我們視頻序列的播放序號是POC的序號,這里是乘以了2.
函數(shù)中先定義了如下三個參數(shù):
int???? i_nal_type;
nal存放的數(shù)據(jù)類型, 可以是sps,pps等多種.??????????????????
int???? i_nal_ref_idc;
nal的優(yōu)先級,nal重要性的標(biāo)志位.
前面兩個參數(shù)雖然簡單,但如果不參照標(biāo)準(zhǔn),也不容易理解,所以標(biāo)準(zhǔn)中的句法表是很重要的,可以說是最關(guān)鍵的.
int???? i_slice_type;
slice的類型,在x264中我的感覺好像一幀只有一個slice.如果確定了幀的類型,slice的類型也就確定了.
?
我們來看看編碼器是如何區(qū)分讀入的一幀是I幀,P幀,或者B幀,這個過程需要好好理解.
還以I? B?? B?? P?? B?? B?? P為例.
?
if( h->i_frame % (h->param.i_iframe * h->param.i_idrframe) == 0 ){
確定這是立即刷新片.
}
???????? 這里很好理解.
但到了if( h->param.i_bframe > 0 )//可以B幀編碼時.
就有問題了.
注意我們編完I幀后碰到了一個B幀,這時我們先不對它進(jìn)編碼.而是采用frame = x264_encoder_frame_put_from_picture( h, h->frame_next, pic )函數(shù)將這個B幀放進(jìn)h->frame_next中.
好,這里出現(xiàn)了h->frame_next,在h中同時定義了下面幾個幀數(shù)組用以實現(xiàn)幀的管理.
x264_frame_t? *bframe_current[X264_BFRAME_MAX]; /* store the sequence of b frame being encoded */
??? x264_frame_t??? *frame_next[X264_BFRAME_MAX+1];?? /* store the next sequence of frames to be encoded *///搞清意義,下一個幀,而不一定是B幀.
??? x264_frame_t??? *frame_unused[X264_BFRAME_MAX+1]; /* store unused frames */
注意區(qū)分這3個數(shù)組.
同時還有下面4個函數(shù)(定義在/ENCODER/encoder.c中).
x264_encoder_frame_put_from_picture();
x264_encoder_frame_put();
x264_encoder_frame_get();
x264_frame_copy_picture();
這3個數(shù)組和4個函數(shù)可以說完成了整個幀的類型的判定問題.這個里面if ,else語句較多,容易使人迷惑.但我們只要把握下面一個觀點就可以看清實質(zhì):在不對P幀進(jìn)行編碼之前,我們不對B幀進(jìn)行編碼,只是把B幀放進(jìn)緩沖區(qū)(就是前面提到的數(shù)組).
比如視頻序列:I???? B B P? B B P
先確立第一個幀的類型,然后進(jìn)行編碼.然后是2個B幀,我們把它放進(jìn)緩沖區(qū)數(shù)組.然后是P幀,我們可以判定它的類型并進(jìn)行編碼.同時,我們將緩沖區(qū)的B幀放進(jìn)h->bframe_current[i],不過這時P幀前的兩個B幀并沒有編碼.當(dāng)讀到P幀后面的第一個B幀時,我們實際上才將h->bframe_current數(shù)組中的第一個B幀編碼,也就是將在I幀后面的第一個B幀(說成P幀前面的第一個B幀容易誤解J)編碼.
依此類推,把握好上面4個函數(shù)的調(diào)用流程和指針操作的用法,就可以將幀的類型判定這個問題搞明白了.
F.????? 然后是速率控制(先不說這個,因為它對編碼的流程影響不大),看看建立參考幀列表的操作,也就是
x264_reference_build_list( h, h->fdec->i_poc ); (定義在/ENCODER/encoder.c中).
光看這個函數(shù)是不行的,它是和后面的這個函數(shù)(如下)一起配合工作的.
if( i_nal_ref_idc != NAL_PRIORITY_DISPOSABLE )//B幀時.
??? {
??????? x264_reference_update( h );
}
???? If條件是判斷當(dāng)前幀是否是B幀,如果是的話就不更新參考列表,因為B幀本來就不能作為參考幀嘛!如果是I幀或P幀的話,我們就更新參考幀列表.
我們看到了一個for循環(huán),兩個do—while循環(huán).這是實現(xiàn)的關(guān)鍵,具體看代碼,不好用語言說明白.
?
G.???? 進(jìn)入另一個復(fù)雜的領(lǐng)域:寫slice的操作,剛開使挺簡單,如我下面的注釋.
/* ---------------------- Write the bitstream -------------------------- */
??? /* Init bitstream context */
??? h->out.i_nal = 0;//out的聲明在bs.h中.
??? bs_init( &h->out.bs, h->out.p_bitstream, h->out.i_bitstream );//空出8位.
?
??? /* Write SPS and PPS */
??? if( i_nal_type == NAL_SLICE_IDR )//不是每次都要寫SPS and PPS,只有碰見立即刷新片時才寫.
??? {
??????? /* generate sequence parameters */
??????? x264_nal_start( h, NAL_SPS, NAL_PRIORITY_HIGHEST );
??????? x264_sps_write( &h->out.bs, h->sps );
??????? x264_nal_end( h );
?
??????? /* generate picture parameters */
??????? x264_nal_start( h, NAL_PPS, NAL_PRIORITY_HIGHEST );
??????? x264_pps_write( &h->out.bs, h->pps );
??????? x264_nal_end( h );
}
不過看下面那個函數(shù)(就進(jìn)入了復(fù)雜的領(lǐng)域).
H.???? x264_slice_write()(定義在/ENCODER/encoder.c中),這里面是編碼的最主要部分,下面仔細(xì)分析.
前面不說,看下面這個循環(huán),它是采用for循環(huán)對一幀圖像的所有塊依次進(jìn)行編碼.
for( mb_xy = 0, i_skip = 0; mb_xy < h->sps->i_mb_width * h->sps->i_mb_height; mb_xy++ )//h->sps->i_mb_width指的是從寬度上說有多少個宏快.對于寬度也就是288 / 16 = 18
??? {
??????? const int i_mb_y = mb_xy / h->sps->i_mb_width;
??????? const int i_mb_x = mb_xy % h->sps->i_mb_width;//這兩個變量是定義宏塊的位置.而不是指宏塊中元素的位置.
?
??????? /* load cache */
??????? x264_macroblock_cache_load( h, i_mb_x, i_mb_y );//是把當(dāng)前宏塊的up宏塊和left宏塊的intra4x4_pred_mode,non_zero_count加載進(jìn)來,放到一個數(shù)組里面,這個數(shù)組用來直接得到當(dāng)前宏塊的左側(cè)和上面宏塊的相關(guān)值.要想得到當(dāng)前塊的預(yù)測值,要先知道上面,左面的預(yù)測值,它的目的是替代getneighbour函數(shù).
/* analyse parameters
???????? * Slice I: choose I_4x4 or I_16x16 mode
???????? * Slice P: choose between using P mode or intra (4x4 or 16x16)
???????? * */
??????? TIMER_START( i_mtime_analyse );
??????? x264_macroblock_analyse( h );//定義在analyse.h中.
??????? TIMER_STOP( i_mtime_analyse );
?
??????? /* encode this macrobock -> be carefull it can change the mb type to P_SKIP if needed */
??????? TIMER_START( i_mtime_encode );
??????? x264_macroblock_encode( h );//定義在Enc/encoder.c中.
??????? TIMER_STOP( i_mtime_encode );
截止到這就已經(jīng)完成編碼的主要過程了,后面就是熵編碼的過程了(我也沒看到那,但認(rèn)為前面才是編碼的主要過程).下面對這個過程進(jìn)行分析.
A.???? x264_macroblock_cache_load( h, i_mb_x, i_mb_y );它是將要編碼的宏塊的周圍的宏塊的值讀進(jìn)來,要想得到當(dāng)前塊的預(yù)測值,要先知道上面,左面的預(yù)測值,它的作用相當(dāng)于jm93中的getneighbour函數(shù).
B.????? 進(jìn)入x264_macroblock_analyse( h )函數(shù)(定義在/Enc/analyse.c中,這里涉及到了函數(shù)指針數(shù)組,需要好好復(fù)習(xí),個人認(rèn)為這也是x264代碼最為復(fù)雜的一個地方了).既然已經(jīng)將該宏塊周圍的宏塊的值讀了出來,我們就可以對該宏塊進(jìn)行分析了(其實主要就是通過計算sad值分析是否要將16*16的宏塊進(jìn)行分割和采用哪種分割方式合適).
看似很復(fù)雜,但我們只要把握一個東西就有利于理解了:
舉個生活中的例子來說:
如果你有2元錢,你可以去買2袋1元錢的瓜子,也可以買一袋2元錢的瓜子,如果2袋1元錢的瓜子數(shù)量加起來比1袋2元錢的瓜子數(shù)量多,你肯定會買2袋1元的.反之你會去買那2元1袋的.
具體來說,對于一個16*16的塊,
如果它是I幀的塊,我們可以將它分割成16個4*4的塊,如果這16個塊的sad加起來小于按16*16的方式計算出來的sad值,我們就將這個16*16的塊分成16個4*4的塊進(jìn)行編碼(在計算每個4*4的塊的最小sad值時已經(jīng)知道它采用何種編碼方式最佳了),否則采用16*16的方式編碼(同樣我們也已知道對它采用哪種編碼方式最為合適了.
如果它是P幀或B幀的塊,同樣是循環(huán)套循環(huán),但更為復(fù)雜了,可以看我在analyse.c中的注釋.
這里還要注意的是提到了
x264_predict_t????? predict_16x16[4+3];
typedef void (*x264_predict_t)( uint8_t *src, int i_stride );
這是函數(shù)指針數(shù)組,有很多對它的調(diào)用.
C.???? 退出x264_macroblock_analyse( h )函數(shù),進(jìn)入x264_macroblock_encode( )函數(shù)(定義在/ENCODER/macroblock.c中).
我拿宏塊類型為I_16*16為例.
if( h->mb.i_type == I_16x16 )
??? {
??????? const int i_mode = h->mb.i_intra16x16_pred_mode;
??????? /* do the right prediction */
??????? h->predict_16x16[i_mode]( h->mb.pic.p_fdec[0], h->mb.pic.i_fdec[0] );//這兩個參數(shù)的關(guān)系.
?????????????????????????????????????????????????????????????????? //涉及到x264_predict_t(函數(shù)指針數(shù)組),聲明在core/predict.h中,core/predict.c里有不同定義.
??????? /* encode the 16x16 macroblock */
??????? x264_mb_encode_i16x16( h, i_qscale );//
/* fix the pred mode value */
?????? …???? }
我們看到h->predict_16x16[i_mode]( h->mb.pic.p_fdec[0], h->mb.pic.i_fdec[0] );只調(diào)用了一次,這是因為在x264_macroblock_analyse(? )中我們已經(jīng)確定了采用4種方式中的哪種最合適.而在x264_macroblock_analyse(? )中判定一個塊是否為I_16*16,我們調(diào)用了四次.這是因為當(dāng)時我們需要拿最小的sad值進(jìn)行比較.
繼續(xù),是x264_mb_encode_i16x16( h, i_qscale )函數(shù)(定義在/ENCODER/macroblock.c中).在這個函數(shù)中我們就可以看到量化,zig-掃描等函數(shù)了,這些都是直來直去的,需要的只是我們的細(xì)心和對數(shù)學(xué)知識的掌握了
c)?????? 到這里還沒完,我們接著看
void x264_macroblock_encode( x264_t *h ){
…….前面省略.
執(zhí)行到下面這條語句,看看下面是干啥的.
?
??? /* encode chroma */
?? i_qscale = i_chroma_qp_table[x264_clip3( i_qscale + h->pps->i_chroma_qp_index_offset, 0, 51 )];
??? if( IS_INTRA( h->mb.i_type ) )
??? {
??????? const int i_mode = h->mb.i_chroma_pred_mode;
??????? /* do the right prediction */
??????? h->predict_8x8[i_mode]( h->mb.pic.p_fdec[1], h->mb.pic.i_fdec[1] );
??????? h->predict_8x8[i_mode]( h->mb.pic.p_fdec[2], h->mb.pic.i_fdec[2] );
?
??????? /* fix the pred mode value */
??????? h->mb.i_chroma_pred_mode = x264_mb_pred_mode8x8_fix[i_mode];
??? }
?
??? /* encode the 8x8 blocks */
x264_mb_encode_8x8( h, !IS_INTRA( h->mb.i_type ), i_qscale );//對色度塊進(jìn)行編碼了.
到這我們可以看到原來我們在這前面是對宏塊中的亮度系數(shù)進(jìn)行了編碼,我們到上面那個函數(shù)才開始對色度系數(shù)進(jìn)行編碼.進(jìn)入x264_mb_encode_8x8()函數(shù)看到for循環(huán)里面有個2可以證明是對2個色度系數(shù)進(jìn)行編碼,想法沒錯.
那下面這些又是干啥的呢?它們是計算cbp系數(shù)看需要對殘差(包括ac,dc)中的哪個系數(shù)進(jìn)行傳輸?shù)?
?
??? /* Calculate the Luma/Chroma patern and non_zero_count */
??? if( h->mb.i_type == I_16x16 )
??? {
??????? h->mb.i_cbp_luma = 0x00;
??????? for( i = 0; i < 16; i++ )
??????? {
??????????? const int nz = array_non_zero_count( h->dct.block[i].residual_ac, 15 );
??????????? h->mb.cache.non_zero_count[x264_scan8[i]] = nz;
??????????? if( nz > 0 )
??????????? {
??????????????? h->mb.i_cbp_luma = 0x0f;
??????????? }
??????? }
??? }
??? else
??? {
??????? h->mb.i_cbp_luma = 0x00;
??????? for( i = 0; i < 16; i++ )
??????? {
??????????? const int nz = array_non_zero_count( h->dct.block[i].luma4x4, 16 );//統(tǒng)計非0個數(shù).
??????????? h->mb.cache.non_zero_count[x264_scan8[i]] = nz;
??????????? if( nz > 0 )
??????????? {
??????????????? h->mb.i_cbp_luma |= 1 << (i/4);// %16的意義.
??????????? }
??????? }
??? }
?
??? /* Calculate the chroma patern *///色度的cbp有3種方式.
??? h->mb.i_cbp_chroma = 0x00;
??? for( i = 0; i < 8; i++ )
??? {
??????? const int nz = array_non_zero_count( h->dct.block[16+i].residual_ac, 15 );
??????? h->mb.cache.non_zero_count[x264_scan8[16+i]] = nz;
??????? if( nz > 0 )??????????????????????
??????? {
??????????? h->mb.i_cbp_chroma = 0x02;??? /* dc+ac (we can't do only ac) */
??????? }
??? }
??? if( h->mb.i_cbp_chroma == 0x00 &&
??????? ( array_non_zero_count( h->dct.chroma_dc[0], 4 ) > 0 || array_non_zero_count( h->dct.chroma_dc[1], 4 ) ) > 0 )
??? {
??????? h->mb.i_cbp_chroma = 0x01;??? /* dc only */
??? }
?
??? if( h->param.b_cabac )
??? {
??????? if( h->mb.i_type == I_16x16 && array_non_zero_count( h->dct.luma16x16_dc, 16 ) > 0 )
??????????? i_cbp_dc = 0x01;
??????? else
??????????? i_cbp_dc = 0x00;
?
??????? if( array_non_zero_count( h->dct.chroma_dc[0], 4 ) > 0 )
??????????? i_cbp_dc |= 0x02;
??????? if( array_non_zero_count( h->dct.chroma_dc[1], 4 ) > 0 )
??????????? i_cbp_dc |= 0x04;
??? }
?
??? /* store cbp */
h->mb.cbp[h->mb.i_mb_xy] = (i_cbp_dc << 8) | (h->mb.i_cbp_chroma << 4) | h->mb.i_cbp_luma;
?
到這,基本上x264_macroblock_encode( h )(定義在Enc/encoder.c)基本上就分析完了.剩下的就是熵編碼的部分了.以后的部分更需要的應(yīng)該是耐心和數(shù)學(xué)知識吧,相對前面來說應(yīng)該簡單些.
?
l?????? 總結(jié):
1. 我對代碼的理解應(yīng)該還算比較深入,把代碼的主線已經(jīng)分析了出來,對代碼中幾個最難理解的地方(最難理解的地方就是幀的類型的判定,參考幀是如何管理的,一個16*16的塊是采用到底需不需要分割,分割的話分成什么大小的,子塊又采用何種預(yù)測方式,這些實際上就是整個編碼的主線.)基本上已經(jīng)明白,但有些過分復(fù)雜的函數(shù)的實現(xiàn)(或者涉及數(shù)學(xué)知識較多的地方)還有待深入研究,但我相信沿著這條主線應(yīng)該能夠繼續(xù)深入下去,自己需要的是更多的時間和耐心. 自己需要的是更多的時間和耐心,爭取以后能寫出更詳細(xì)更準(zhǔn)確的流程分析,并盡量思考能改進(jìn)的地方.
2.層次性,就像網(wǎng)絡(luò)的7層結(jié)構(gòu)一樣,每一幀圖像也可以分成很多層,只有對每層的語法結(jié)構(gòu)(具體來說就是各個結(jié)構(gòu)體中變量的意思)有了很好的理解,才有可能真正認(rèn)清代碼,這需要對標(biāo)準(zhǔn)認(rèn)真研習(xí).比如說量化參數(shù),就在3個地方有定義,不讀標(biāo)準(zhǔn)根本不會明白意思.
3. 很多過分復(fù)雜的東西不容易在本文中表達(dá)出來(比如說預(yù)測部分),只有通過自己的鉆研才能真正悟到,直覺也很重要,還有就是信心了.看這種程序的收獲就好像是真地肉眼看到了原子那樣.
4.由于代碼過分復(fù)雜,對某些函數(shù)的實現(xiàn)過程還沒能徹底理解,比如說x264_macroblock_cache_load()函數(shù)的具體實現(xiàn)過程,我只是知道它的功能,實現(xiàn)過程還有待認(rèn)真理解.dct變換是如何實現(xiàn)的,是如何計算殘差的等等,這些都需要很多功夫,當(dāng)然這里也需要大家的共同學(xué)習(xí)和交流.實現(xiàn)分工閱讀不同代碼部分并進(jìn)行交流,才有可能對代碼做到徹底的理解.
1.???? 使用版本: x264-cvs-2004-05-11
2.???? 這次的分析基本上已經(jīng)將代碼中最難理解的部分做了闡釋,對代碼的主線也做了剖析,如果這個主線理解了,就容易設(shè)置幾個區(qū)間,進(jìn)行分工閱讀,將各個區(qū)間擊破了.
3.???? 需要學(xué)習(xí)的知識:
a)?????? 編碼器的工作流程.
b)????? H.264的碼流結(jié)構(gòu),像x264_sps_t,x264_pps_t等參數(shù)的定義基本上都完全符合標(biāo)準(zhǔn)文檔中參數(shù)集的定義,抓住主要參數(shù),次要參數(shù)也應(yīng)該有所了解.
c)?????? 數(shù)學(xué)知識,對dct變換等與數(shù)學(xué)相關(guān)的知識的編程實現(xiàn)要有較好理解.
d)????? C語言的知識.涉及到c語言的較多不經(jīng)常用的特性,如函數(shù)指針數(shù)組,移位運算,結(jié)構(gòu)體的嵌套定義等.
e)?????? 耐心,對h.264的復(fù)雜性要有清醒的認(rèn)識.
3.參考資料:
a)?????? 新一代視頻壓縮編碼標(biāo)準(zhǔn)-h.264/avc 畢厚杰主編,人民郵電出版社.
b)????? 網(wǎng)上的流媒體論壇,百度,google等搜索引擎.
4. 閱讀代碼的方法:
a)?????? 較好的方法是利用vc的調(diào)試器,如果對某個函數(shù)感興趣,可以將斷點設(shè)置在它的前面.然后采用step into,step over等方法進(jìn)去該函數(shù)一步步分析.當(dāng)然本身要對程序執(zhí)行流程要有較清楚認(rèn)識,不然不知道何時step into,何時step over.
b)????? 建議應(yīng)該先對照標(biāo)準(zhǔn)弄清各個結(jié)構(gòu)體成員的意義.
l?????? 源代碼主要過程分析:
?
1.?????? 進(jìn)入x264.c中的main函數(shù).
剛開始是讀取默認(rèn)參數(shù),如果你設(shè)置了參數(shù)的話會修改param的.
?? i_ret = Encode( ¶m, fin, fout );
這條語句使過程進(jìn)入x264.c中的Encode函數(shù).
2.?????? X.264的encode函數(shù).
A.???? i_frame_total = 0;
if( !fseek( fyuv, 0, SEEK_END ) )
???? {
??????? int64_t i_size = ftell( fyuv );
??????? fseek( fyuv, 0, SEEK_SET );
??????? i_frame_total = i_size / ( param->i_width * param->i_height * 3 / 2 )
}
上面這段計算出輸入文件的總幀數(shù).
B.????? h = x264_encoder_open( param )這個函數(shù)是對不正確的參數(shù)進(jìn)行修改,并對各結(jié)構(gòu)體參數(shù)和cabac編碼,預(yù)測等需要的參數(shù)進(jìn)行初始化.
C.???? pic = x264_picture_new( h );
該函數(shù)定義在/CORE/common.c中.首先分給能容納sizeof(x264_picture_t)字節(jié)數(shù)的空間,然后進(jìn)行初始化.
????? 這里看一下x264_picture_t和x264_frame_t的區(qū)別.前者是說明一個視頻序列中每幀的特點.后者存放每幀實際的象素值.注意區(qū)分.
D.???? for( i_frame = 0, i_file = 0; i_ctrl_c == 0 ; i_frame++ )
??? {
??????? int???????? i_nal;
??????? x264_nal_t *nal;
?int???????? i;
/* read a frame */
??????? if( fread( pic->plane[0], 1, param->i_width * param->i_height, fyuv ) <= 0 ||
??????????? fread( pic->plane[1], 1, param->i_width * param->i_height / 4, fyuv ) <= 0 ||
??????????? fread( pic->plane[2], 1, param->i_width * param->i_height / 4, fyuv ) <= 0 )
??????? {
??????????? break;
??????? }
??? //文件位置指示器自己變化了.
?
??????? if( x264_encoder_encode( h, &nal, &i_nal, pic ) < 0 )
??????? {
??????????? fprintf( stderr, "x264_encoder_encode failed/n" );
??????? }
??????? ……
}
凡是出現(xiàn)for循環(huán)的地方都應(yīng)該認(rèn)真對待,這是我的一個體會,也是進(jìn)入分層結(jié)構(gòu)認(rèn)真分析的好方法.
fread()函數(shù)一次讀入一幀,分亮度和色度分別讀取.這里要看到c語言中的File文件有一個文件位置指示器,調(diào)用fread()函數(shù)會使文件指示器自動移位,這就是一幀一幀讀取的實現(xiàn)過程.
E.????? 然后進(jìn)入x264_encoder_encode( h, &nal, &i_nal, pic )函數(shù),該函數(shù)定義在/Enc/encoder.c中.
開始進(jìn)入比較復(fù)雜的地方了.
這個函數(shù)前面有一段注釋(如下):
****************************************************************************
?* x264_encoder_encode:
?* XXX: i_poc?? : is the poc of the current given picture
?*?????? i_frame : is the number of the frame being coded
?* ex: type frame poc
?*?????? I????? 0?? 2*0//poc是實際的幀的位置.
?*?????? P????? 1?? 2*3//frame是編碼的順序.
?*?????? B????? 2?? 2*1
?*?????? B????? 3?? 2*2
?*?????? P????? 4?? 2*6
?*?????? B????? 5?? 2*4
?*?????? B????? 6?? 2*5
?****************************************************************************/
要搞清poc和frame的區(qū)別.
假設(shè)一個視頻序列如下:
I???? B??? B??? P???? B???? B?? P
我們編碼是按I P B B P B B的順序,這就是frame的編號.
而我們視頻序列的播放序號是POC的序號,這里是乘以了2.
函數(shù)中先定義了如下三個參數(shù):
int???? i_nal_type;
nal存放的數(shù)據(jù)類型, 可以是sps,pps等多種.??????????????????
int???? i_nal_ref_idc;
nal的優(yōu)先級,nal重要性的標(biāo)志位.
前面兩個參數(shù)雖然簡單,但如果不參照標(biāo)準(zhǔn),也不容易理解,所以標(biāo)準(zhǔn)中的句法表是很重要的,可以說是最關(guān)鍵的.
int???? i_slice_type;
slice的類型,在x264中我的感覺好像一幀只有一個slice.如果確定了幀的類型,slice的類型也就確定了.
?
我們來看看編碼器是如何區(qū)分讀入的一幀是I幀,P幀,或者B幀,這個過程需要好好理解.
還以I? B?? B?? P?? B?? B?? P為例.
?
if( h->i_frame % (h->param.i_iframe * h->param.i_idrframe) == 0 ){
確定這是立即刷新片.
}
???????? 這里很好理解.
但到了if( h->param.i_bframe > 0 )//可以B幀編碼時.
就有問題了.
注意我們編完I幀后碰到了一個B幀,這時我們先不對它進(jìn)編碼.而是采用frame = x264_encoder_frame_put_from_picture( h, h->frame_next, pic )函數(shù)將這個B幀放進(jìn)h->frame_next中.
好,這里出現(xiàn)了h->frame_next,在h中同時定義了下面幾個幀數(shù)組用以實現(xiàn)幀的管理.
x264_frame_t? *bframe_current[X264_BFRAME_MAX]; /* store the sequence of b frame being encoded */
??? x264_frame_t??? *frame_next[X264_BFRAME_MAX+1];?? /* store the next sequence of frames to be encoded *///搞清意義,下一個幀,而不一定是B幀.
??? x264_frame_t??? *frame_unused[X264_BFRAME_MAX+1]; /* store unused frames */
注意區(qū)分這3個數(shù)組.
同時還有下面4個函數(shù)(定義在/ENCODER/encoder.c中).
x264_encoder_frame_put_from_picture();
x264_encoder_frame_put();
x264_encoder_frame_get();
x264_frame_copy_picture();
這3個數(shù)組和4個函數(shù)可以說完成了整個幀的類型的判定問題.這個里面if ,else語句較多,容易使人迷惑.但我們只要把握下面一個觀點就可以看清實質(zhì):在不對P幀進(jìn)行編碼之前,我們不對B幀進(jìn)行編碼,只是把B幀放進(jìn)緩沖區(qū)(就是前面提到的數(shù)組).
比如視頻序列:I???? B B P? B B P
先確立第一個幀的類型,然后進(jìn)行編碼.然后是2個B幀,我們把它放進(jìn)緩沖區(qū)數(shù)組.然后是P幀,我們可以判定它的類型并進(jìn)行編碼.同時,我們將緩沖區(qū)的B幀放進(jìn)h->bframe_current[i],不過這時P幀前的兩個B幀并沒有編碼.當(dāng)讀到P幀后面的第一個B幀時,我們實際上才將h->bframe_current數(shù)組中的第一個B幀編碼,也就是將在I幀后面的第一個B幀(說成P幀前面的第一個B幀容易誤解J)編碼.
依此類推,把握好上面4個函數(shù)的調(diào)用流程和指針操作的用法,就可以將幀的類型判定這個問題搞明白了.
F.????? 然后是速率控制(先不說這個,因為它對編碼的流程影響不大),看看建立參考幀列表的操作,也就是
x264_reference_build_list( h, h->fdec->i_poc ); (定義在/ENCODER/encoder.c中).
光看這個函數(shù)是不行的,它是和后面的這個函數(shù)(如下)一起配合工作的.
if( i_nal_ref_idc != NAL_PRIORITY_DISPOSABLE )//B幀時.
??? {
??????? x264_reference_update( h );
}
???? If條件是判斷當(dāng)前幀是否是B幀,如果是的話就不更新參考列表,因為B幀本來就不能作為參考幀嘛!如果是I幀或P幀的話,我們就更新參考幀列表.
我們看到了一個for循環(huán),兩個do—while循環(huán).這是實現(xiàn)的關(guān)鍵,具體看代碼,不好用語言說明白.
?
G.???? 進(jìn)入另一個復(fù)雜的領(lǐng)域:寫slice的操作,剛開使挺簡單,如我下面的注釋.
/* ---------------------- Write the bitstream -------------------------- */
??? /* Init bitstream context */
??? h->out.i_nal = 0;//out的聲明在bs.h中.
??? bs_init( &h->out.bs, h->out.p_bitstream, h->out.i_bitstream );//空出8位.
?
??? /* Write SPS and PPS */
??? if( i_nal_type == NAL_SLICE_IDR )//不是每次都要寫SPS and PPS,只有碰見立即刷新片時才寫.
??? {
??????? /* generate sequence parameters */
??????? x264_nal_start( h, NAL_SPS, NAL_PRIORITY_HIGHEST );
??????? x264_sps_write( &h->out.bs, h->sps );
??????? x264_nal_end( h );
?
??????? /* generate picture parameters */
??????? x264_nal_start( h, NAL_PPS, NAL_PRIORITY_HIGHEST );
??????? x264_pps_write( &h->out.bs, h->pps );
??????? x264_nal_end( h );
}
不過看下面那個函數(shù)(就進(jìn)入了復(fù)雜的領(lǐng)域).
H.???? x264_slice_write()(定義在/ENCODER/encoder.c中),這里面是編碼的最主要部分,下面仔細(xì)分析.
前面不說,看下面這個循環(huán),它是采用for循環(huán)對一幀圖像的所有塊依次進(jìn)行編碼.
for( mb_xy = 0, i_skip = 0; mb_xy < h->sps->i_mb_width * h->sps->i_mb_height; mb_xy++ )//h->sps->i_mb_width指的是從寬度上說有多少個宏快.對于寬度也就是288 / 16 = 18
??? {
??????? const int i_mb_y = mb_xy / h->sps->i_mb_width;
??????? const int i_mb_x = mb_xy % h->sps->i_mb_width;//這兩個變量是定義宏塊的位置.而不是指宏塊中元素的位置.
?
??????? /* load cache */
??????? x264_macroblock_cache_load( h, i_mb_x, i_mb_y );//是把當(dāng)前宏塊的up宏塊和left宏塊的intra4x4_pred_mode,non_zero_count加載進(jìn)來,放到一個數(shù)組里面,這個數(shù)組用來直接得到當(dāng)前宏塊的左側(cè)和上面宏塊的相關(guān)值.要想得到當(dāng)前塊的預(yù)測值,要先知道上面,左面的預(yù)測值,它的目的是替代getneighbour函數(shù).
/* analyse parameters
???????? * Slice I: choose I_4x4 or I_16x16 mode
???????? * Slice P: choose between using P mode or intra (4x4 or 16x16)
???????? * */
??????? TIMER_START( i_mtime_analyse );
??????? x264_macroblock_analyse( h );//定義在analyse.h中.
??????? TIMER_STOP( i_mtime_analyse );
?
??????? /* encode this macrobock -> be carefull it can change the mb type to P_SKIP if needed */
??????? TIMER_START( i_mtime_encode );
??????? x264_macroblock_encode( h );//定義在Enc/encoder.c中.
??????? TIMER_STOP( i_mtime_encode );
截止到這就已經(jīng)完成編碼的主要過程了,后面就是熵編碼的過程了(我也沒看到那,但認(rèn)為前面才是編碼的主要過程).下面對這個過程進(jìn)行分析.
A.???? x264_macroblock_cache_load( h, i_mb_x, i_mb_y );它是將要編碼的宏塊的周圍的宏塊的值讀進(jìn)來,要想得到當(dāng)前塊的預(yù)測值,要先知道上面,左面的預(yù)測值,它的作用相當(dāng)于jm93中的getneighbour函數(shù).
B.????? 進(jìn)入x264_macroblock_analyse( h )函數(shù)(定義在/Enc/analyse.c中,這里涉及到了函數(shù)指針數(shù)組,需要好好復(fù)習(xí),個人認(rèn)為這也是x264代碼最為復(fù)雜的一個地方了).既然已經(jīng)將該宏塊周圍的宏塊的值讀了出來,我們就可以對該宏塊進(jìn)行分析了(其實主要就是通過計算sad值分析是否要將16*16的宏塊進(jìn)行分割和采用哪種分割方式合適).
看似很復(fù)雜,但我們只要把握一個東西就有利于理解了:
舉個生活中的例子來說:
如果你有2元錢,你可以去買2袋1元錢的瓜子,也可以買一袋2元錢的瓜子,如果2袋1元錢的瓜子數(shù)量加起來比1袋2元錢的瓜子數(shù)量多,你肯定會買2袋1元的.反之你會去買那2元1袋的.
具體來說,對于一個16*16的塊,
如果它是I幀的塊,我們可以將它分割成16個4*4的塊,如果這16個塊的sad加起來小于按16*16的方式計算出來的sad值,我們就將這個16*16的塊分成16個4*4的塊進(jìn)行編碼(在計算每個4*4的塊的最小sad值時已經(jīng)知道它采用何種編碼方式最佳了),否則采用16*16的方式編碼(同樣我們也已知道對它采用哪種編碼方式最為合適了.
如果它是P幀或B幀的塊,同樣是循環(huán)套循環(huán),但更為復(fù)雜了,可以看我在analyse.c中的注釋.
這里還要注意的是提到了
x264_predict_t????? predict_16x16[4+3];
typedef void (*x264_predict_t)( uint8_t *src, int i_stride );
這是函數(shù)指針數(shù)組,有很多對它的調(diào)用.
C.???? 退出x264_macroblock_analyse( h )函數(shù),進(jìn)入x264_macroblock_encode( )函數(shù)(定義在/ENCODER/macroblock.c中).
我拿宏塊類型為I_16*16為例.
if( h->mb.i_type == I_16x16 )
??? {
??????? const int i_mode = h->mb.i_intra16x16_pred_mode;
??????? /* do the right prediction */
??????? h->predict_16x16[i_mode]( h->mb.pic.p_fdec[0], h->mb.pic.i_fdec[0] );//這兩個參數(shù)的關(guān)系.
?????????????????????????????????????????????????????????????????? //涉及到x264_predict_t(函數(shù)指針數(shù)組),聲明在core/predict.h中,core/predict.c里有不同定義.
??????? /* encode the 16x16 macroblock */
??????? x264_mb_encode_i16x16( h, i_qscale );//
/* fix the pred mode value */
?????? …???? }
我們看到h->predict_16x16[i_mode]( h->mb.pic.p_fdec[0], h->mb.pic.i_fdec[0] );只調(diào)用了一次,這是因為在x264_macroblock_analyse(? )中我們已經(jīng)確定了采用4種方式中的哪種最合適.而在x264_macroblock_analyse(? )中判定一個塊是否為I_16*16,我們調(diào)用了四次.這是因為當(dāng)時我們需要拿最小的sad值進(jìn)行比較.
繼續(xù),是x264_mb_encode_i16x16( h, i_qscale )函數(shù)(定義在/ENCODER/macroblock.c中).在這個函數(shù)中我們就可以看到量化,zig-掃描等函數(shù)了,這些都是直來直去的,需要的只是我們的細(xì)心和對數(shù)學(xué)知識的掌握了
c)?????? 到這里還沒完,我們接著看
void x264_macroblock_encode( x264_t *h ){
…….前面省略.
執(zhí)行到下面這條語句,看看下面是干啥的.
?
??? /* encode chroma */
?? i_qscale = i_chroma_qp_table[x264_clip3( i_qscale + h->pps->i_chroma_qp_index_offset, 0, 51 )];
??? if( IS_INTRA( h->mb.i_type ) )
??? {
??????? const int i_mode = h->mb.i_chroma_pred_mode;
??????? /* do the right prediction */
??????? h->predict_8x8[i_mode]( h->mb.pic.p_fdec[1], h->mb.pic.i_fdec[1] );
??????? h->predict_8x8[i_mode]( h->mb.pic.p_fdec[2], h->mb.pic.i_fdec[2] );
?
??????? /* fix the pred mode value */
??????? h->mb.i_chroma_pred_mode = x264_mb_pred_mode8x8_fix[i_mode];
??? }
?
??? /* encode the 8x8 blocks */
x264_mb_encode_8x8( h, !IS_INTRA( h->mb.i_type ), i_qscale );//對色度塊進(jìn)行編碼了.
到這我們可以看到原來我們在這前面是對宏塊中的亮度系數(shù)進(jìn)行了編碼,我們到上面那個函數(shù)才開始對色度系數(shù)進(jìn)行編碼.進(jìn)入x264_mb_encode_8x8()函數(shù)看到for循環(huán)里面有個2可以證明是對2個色度系數(shù)進(jìn)行編碼,想法沒錯.
那下面這些又是干啥的呢?它們是計算cbp系數(shù)看需要對殘差(包括ac,dc)中的哪個系數(shù)進(jìn)行傳輸?shù)?
?
??? /* Calculate the Luma/Chroma patern and non_zero_count */
??? if( h->mb.i_type == I_16x16 )
??? {
??????? h->mb.i_cbp_luma = 0x00;
??????? for( i = 0; i < 16; i++ )
??????? {
??????????? const int nz = array_non_zero_count( h->dct.block[i].residual_ac, 15 );
??????????? h->mb.cache.non_zero_count[x264_scan8[i]] = nz;
??????????? if( nz > 0 )
??????????? {
??????????????? h->mb.i_cbp_luma = 0x0f;
??????????? }
??????? }
??? }
??? else
??? {
??????? h->mb.i_cbp_luma = 0x00;
??????? for( i = 0; i < 16; i++ )
??????? {
??????????? const int nz = array_non_zero_count( h->dct.block[i].luma4x4, 16 );//統(tǒng)計非0個數(shù).
??????????? h->mb.cache.non_zero_count[x264_scan8[i]] = nz;
??????????? if( nz > 0 )
??????????? {
??????????????? h->mb.i_cbp_luma |= 1 << (i/4);// %16的意義.
??????????? }
??????? }
??? }
?
??? /* Calculate the chroma patern *///色度的cbp有3種方式.
??? h->mb.i_cbp_chroma = 0x00;
??? for( i = 0; i < 8; i++ )
??? {
??????? const int nz = array_non_zero_count( h->dct.block[16+i].residual_ac, 15 );
??????? h->mb.cache.non_zero_count[x264_scan8[16+i]] = nz;
??????? if( nz > 0 )??????????????????????
??????? {
??????????? h->mb.i_cbp_chroma = 0x02;??? /* dc+ac (we can't do only ac) */
??????? }
??? }
??? if( h->mb.i_cbp_chroma == 0x00 &&
??????? ( array_non_zero_count( h->dct.chroma_dc[0], 4 ) > 0 || array_non_zero_count( h->dct.chroma_dc[1], 4 ) ) > 0 )
??? {
??????? h->mb.i_cbp_chroma = 0x01;??? /* dc only */
??? }
?
??? if( h->param.b_cabac )
??? {
??????? if( h->mb.i_type == I_16x16 && array_non_zero_count( h->dct.luma16x16_dc, 16 ) > 0 )
??????????? i_cbp_dc = 0x01;
??????? else
??????????? i_cbp_dc = 0x00;
?
??????? if( array_non_zero_count( h->dct.chroma_dc[0], 4 ) > 0 )
??????????? i_cbp_dc |= 0x02;
??????? if( array_non_zero_count( h->dct.chroma_dc[1], 4 ) > 0 )
??????????? i_cbp_dc |= 0x04;
??? }
?
??? /* store cbp */
h->mb.cbp[h->mb.i_mb_xy] = (i_cbp_dc << 8) | (h->mb.i_cbp_chroma << 4) | h->mb.i_cbp_luma;
?
到這,基本上x264_macroblock_encode( h )(定義在Enc/encoder.c)基本上就分析完了.剩下的就是熵編碼的部分了.以后的部分更需要的應(yīng)該是耐心和數(shù)學(xué)知識吧,相對前面來說應(yīng)該簡單些.
?
l?????? 總結(jié):
1. 我對代碼的理解應(yīng)該還算比較深入,把代碼的主線已經(jīng)分析了出來,對代碼中幾個最難理解的地方(最難理解的地方就是幀的類型的判定,參考幀是如何管理的,一個16*16的塊是采用到底需不需要分割,分割的話分成什么大小的,子塊又采用何種預(yù)測方式,這些實際上就是整個編碼的主線.)基本上已經(jīng)明白,但有些過分復(fù)雜的函數(shù)的實現(xiàn)(或者涉及數(shù)學(xué)知識較多的地方)還有待深入研究,但我相信沿著這條主線應(yīng)該能夠繼續(xù)深入下去,自己需要的是更多的時間和耐心. 自己需要的是更多的時間和耐心,爭取以后能寫出更詳細(xì)更準(zhǔn)確的流程分析,并盡量思考能改進(jìn)的地方.
2.層次性,就像網(wǎng)絡(luò)的7層結(jié)構(gòu)一樣,每一幀圖像也可以分成很多層,只有對每層的語法結(jié)構(gòu)(具體來說就是各個結(jié)構(gòu)體中變量的意思)有了很好的理解,才有可能真正認(rèn)清代碼,這需要對標(biāo)準(zhǔn)認(rèn)真研習(xí).比如說量化參數(shù),就在3個地方有定義,不讀標(biāo)準(zhǔn)根本不會明白意思.
3. 很多過分復(fù)雜的東西不容易在本文中表達(dá)出來(比如說預(yù)測部分),只有通過自己的鉆研才能真正悟到,直覺也很重要,還有就是信心了.看這種程序的收獲就好像是真地肉眼看到了原子那樣.
4.由于代碼過分復(fù)雜,對某些函數(shù)的實現(xiàn)過程還沒能徹底理解,比如說x264_macroblock_cache_load()函數(shù)的具體實現(xiàn)過程,我只是知道它的功能,實現(xiàn)過程還有待認(rèn)真理解.dct變換是如何實現(xiàn)的,是如何計算殘差的等等,這些都需要很多功夫,當(dāng)然這里也需要大家的共同學(xué)習(xí)和交流.實現(xiàn)分工閱讀不同代碼部分并進(jìn)行交流,才有可能對代碼做到徹底的理解.
總結(jié)
- 上一篇: 基于DM642的X264开源代码实现的研
- 下一篇: 学习笔记(一)(x264编码流程)