实时音频编解码之十六 Opus解码
本文謝絕任何形式轉載,謝謝。
第五章 Opus解碼
理論上而言,編碼的逆過程就是解碼,如果理解了第四章編碼的內容,這里敘述解碼過程顯得有所多余,但是筆者在理解Opus編碼原理的時候,發現編解碼交叉多輪重復看更有助于理解編解碼的原理以及工程實現的精髓,因而本章結合Opus解碼的過程分析解碼流程。
5.1 Opus解碼
除了SILK和CELT之外,Opusc解碼器需要解碼信號源信息和編碼信息,信號源信息包括聲道數、采樣率、編碼幀時長等,編碼信息包括編碼比特率模式以及編碼包包含的編碼幀數量等,Opus先解碼出的這些信息,將這些信息放入解碼狀態器中以便SILK/CELT解碼時使用,根據編碼的參數不同,解碼時可能只調用SILK、CELT或同時調用二者解碼,在這是調用SILK和CELT核心解碼之前,還需要解碼一些輔助和編碼音頻信息,如編碼幀長、帶寬、FEC、聲道數和采樣率等信息,本小節的內容主要是解析除調用SILK核心和CELT核心之外的內容,這一節雖不涉及核心語音信號處理算法相關內容,但也是一個完整編解碼器必不可少的細節,此外其涉及一些壓縮/解壓思想也值得借鑒,本小節結合協議和代碼分析相關函數調用和調用流程。
5.1.1 opus_demo解碼
理論上來說,在不考慮計算量和模型復雜度的情況下,不需要使用第四章給出的信號處理方法獲取編碼參數,所有編碼參數都可以使用深度學習的方式提前,解碼器流程必須遵循是Opus規范定義的,解碼的內容相對簡單一些,透過解碼器,可以知道非信號處理方面的壓縮方法以及參數的參數種類和在合成中的使用,至于Opus解碼端的參數是如何計算而來的,第書第四章編碼部分內容給出了參考設計。
opus_demo編碼的比特流使用opus_demo解碼,其解碼命令如下:
//-d表示解碼 //16000表示解碼比特率,還可以是8000等, // 1 表示通道數為1,即單聲道 //.bit文件是編碼的比特流文件 //.pcm文件是解碼輸出的pcm文件 //此外,還有-loss等選項用于模擬丟包解碼結果 ./opus_demo -d 48000 1 out_cbr.bit out_cbr.pcmopus解碼的入口函數在opus_demo.c文件的main()函數,該函數的主要作用是讀取比特文件以及解碼參數,然后調用opus_decode()函數執行進一步解碼,該函數調用流程如下:
//opus_demo.c 210 int main(int argc, char *argv[]) 211 { 649 if (decode_only) 650 { 651 unsigned char ch[4]; //在編碼時,第一個四字節(len[toggle])存放的編碼后opus包大小,實際opus包協議中無該字段,opus編碼包以TOC字段開始 652 num_read = fread(ch, 1, 4, fin); 655 len[toggle] = char_to_int(ch); //在編碼的時候,第二個四字節(enc_final_range[toggle])存放的是區間編碼器編碼后的區間值, //在實際opus包協議中無該字段,解碼器解碼完后的區間值和這里的enc_final_range應該相等 //用于驗證編解碼的正確性,協議中并未規定該字段 661 num_read = fread(ch, 1, 4, fin); 664 enc_final_range[toggle] = char_to_int(ch); //讀取opus包協議定義的字段,包括TOC、編碼比特流、padding字段等, //對于16kHz,20ms幀長,CBR模式,無padding時,一個opus編碼包的大小是60個字節,這里num_read應等于len[toggle] 665 num_read = fread(data[toggle], 1, len[toggle], fin); int opus_decode(OpusDecoder *st, const unsigned char *data,opus_int32 len, opus_int16 *pcm, int frame_size, int decode_fec) //非FEC解碼,正常解碼 //第一個參數是Opus解碼器狀態,第二個參數是解碼結果,第三個字節 //之所以用了toggle這個變量的原因是在進行FEC解碼時,需要模擬前一幀沒有丟失,而當前幀丟失的情況,這時會再次解碼前一幀。 780 output_samples = opus_decode(dec, data[1-toggle], len[1-toggle], out, output_samples, 0); //如果丟包傳NLL,否則將opus包傳遞過去解碼生成pcm放在out開始的首地址里 783 output_samples = opus_decode(dec, lost ? NULL : data[toggle], len[ toggle], out, output_samples, 0);opus_decode()函數的主要作用是解碼信源參數以及編碼輔助參數,獲得這些信息之后,按需調用SILK和CELT按頻帶解碼編碼比特率,最后根據情況將二者解碼的結果想疊加輸出,該函數的調用流程和相關函數的作用如圖5-1所示:
圖5-1 Opus解碼調用流程
圖5-2 Opus編碼器TOC字段
圖5-1中解碼的信源信息和編碼包信息定義由Opus協議給出,Opus協議中將其定義為TOC字段,TOC字段由八個比特組成,這八個比特又分為三個部分,各部分的作用和意義如圖5-2所示,opus_decode()函數最重要的一個作用就是解析該字段,為了更集中于代碼邏輯流程,使用浮點版本的解碼接口API,即opus_decode()調用的是opus_decode_float()函數,該函數調用流程如下所示:
//opus_decoder.c751 int opus_decode_float(OpusDecoder *st, const unsigned char *data,752 opus_int32 len, float *pcm, int frame_size, int decode_fec)753 { //根據參數獲取該opus包解碼后pcm點數,20ms,16kHz,點數為320766 nb_samples = opus_decoder_get_nb_samples(st, data, len); //data是opus包的首地址,len是opus包的字節數,編碼幀長解析需要用到opus包大小,也即這里的len,out用于存放解碼后pcm,frame_size:解碼后幀長775 ret = opus_decode_native(st, data, len, out, frame_size, decode_fec, 0, NULL, 0); //解碼后的浮點pcm轉為int16類型778 for (i=0;i<ret*st->channels;i++)779 pcm[i] = (1.f/32768.f)*(out[i]);}1011 int opus_packet_get_nb_samples(const unsigned char packet[], opus_int32 len, 1012 opus_int32 Fs) 1013 { 1014 int samples; 1015 int count = opus_packet_get_nb_frames(packet, len); 1020 samples = count*opus_packet_get_samples_per_frame(packet, Fs); 1021 //編碼的長度最長為 120 ms 1022 if (samples*25 > Fs*3) 1023 return OPUS_INVALID_PACKET; 1024 else 1025 return samples; 1026 } 1028 int opus_decoder_get_nb_samples(const OpusDecoder *dec, 1029 const unsigned char packet[], opus_int32 len) 1030 { 1031 return opus_packet_get_nb_samples(packet, len, dec->Fs); 1032 }995 int opus_packet_get_nb_frames(const unsigned char packet[], opus_int32 len)996 {997 int count;//這里解析TOC字段c字段,對于測試情況count值等于0,返回值等于1。見圖5-2。 1000 count = packet[0]&0x3; 1001 if (count==0) 1002 return 1; 1003 else if (count!=3) 1004 return 2; 1005 else if (len<2) 1006 return OPUS_INVALID_PACKET; 1007 else 1008 return packet[1]&0x3F; 1009 } //opus.c //解析TOC config字段,見圖5-2,對于測試命令行情況,返回值是320 173 int opus_packet_get_samples_per_frame(const unsigned char *data, 174 opus_int32 Fs) 175 { 176 int audiosize;//因為比特位和采樣率有關系,因而這里直接使用比特位對比方式,而非逐個config字段判斷 177 if (data[0]&0x80) 178 { 179 audiosize = ((data[0]>>3)&0x3); 180 audiosize = (Fs<<audiosize)/400; 181 } else if ((data[0]&0x60) == 0x60) 182 { 183 audiosize = (data[0]&0x08) ? Fs/50 : Fs/100; 184 } else { 185 audiosize = ((data[0]>>3)&0x3); 186 if (audiosize == 3) 187 audiosize = Fs*60/1000; 188 else 189 audiosize = (Fs<<audiosize)/100; 190 } 191 return audiosize; 192 }圖5-2繪制出了解碼的調用流程,在解析完TOC字段頭之后,最終都調用了opus_decode_native()函數進一步解碼,由于編碼包中有用于抗丟包的FEC字段,因而在調用opus_decode()函數時傳遞的參數會有所不同,見圖5-3所示。
圖 5-3 有FEC情況Opus解碼函數調用流程
之所以用了toggle這個變量的原因是在進行FEC解碼時,需要模擬前一幀沒有丟失,而當前幀丟失的情況,這時會再次解碼前一幀。這一過程如圖5-3所示,由于Opus編碼包會有多個編碼幀,opus_decode_frame()是解碼一個編碼幀,opus_decode_native()函數使用for循環方式遍歷各個編碼幀,該函數的核心代碼段如下:
// opus_decoder.c626 int opus_decode_native(OpusDecoder *st, const unsigned char *data,627 opus_int32 len, opus_val16 *pcm, int frame_size, int decode_fec,628 int self_delimited, opus_int32 *packet_offset, int soft_clip)629 { //之所以這里使用48這個具體數字是因為,Opus協議規定編碼包最大的時長為120ms,而編碼幀的最小長度為2.5ms,因而最多一個編碼包只有48個編碼幀 //對于命令行的情況,由于只有一個編碼幀,實際上只有size[0]是記錄編碼幀的編碼數據長度(不包括TOC字段) 635 opus_int16 size[48]; //FEC/PLC時的解碼,opus_decode_frame第二個參數NULL,在FEC/PLC時frame_size必須是2.5ms的倍數 //正常的解碼流程是不會調用到647行的,而是調用掉696行的解碼函數,二者差別在于傳遞給函數的參數647 ret = opus_decode_frame(st, NULL, 0, pcm+pcm_count*st->channels, frame_size- pcm_count, 0) //這里是解析TOC字段,包括模式,幀長,通道數等660 packet_mode = opus_packet_get_mode(data);661 packet_bandwidth = opus_packet_get_bandwidth(data);662 packet_frame_size = opus_packet_get_samples_per_frame(data, st->Fs);663 packet_stream_channels = opus_packet_get_nb_channels(data); //這個count是根據TOC字段解析的編碼幀 //offset是去掉opus包殼之后的silk/celt編碼包的偏移地址,這一偏移地址包括了TOC字段665 count = opus_packet_parse_impl(data, len, self_delimited, &toc, NULL,666 size, &offset, packet_offset);//遍歷編碼包中的各編碼幀718 for (i=0;i<count;i++)719 {720 int ret;//size[i]是對應編碼幀的長度,因為opus格式下,編碼的長度沒有用專門的字段標記;//因為是逐幀處理,第四和第五個參數用于每次幀處理的偏移值721 ret = opus_decode_frame(st, data, size[i], pcm+nb_samples*st->channels, frame_s ize-nb_samples, 0);722 if (ret<0)723 return ret;724 celt_assert(ret==packet_frame_size);//size存放的是編碼幀數據長度,將data地址指向下一個編碼幀TOC字段725 data += size[i];726 nb_samples += ret;727 }//返回解碼數據長度737 return nb_samples;738 }opus_decode_native()函數調用opus_packet_parse_impl()實現編碼幀的信息解析,并剝離除SILK和CELT之外的編碼比特流,將比特流傳遞給SILK和CELT核心解碼器,該函數的調用流程如下所示:
//src/opus.c 194 int opus_packet_parse_impl(const unsigned char *data, opus_int32 len, 195 int self_delimited, unsigned char *out_toc, 196 const unsigned char *frames[48], opus_int16 size[48], 197 int *payload_offset, opus_int32 *packet_offset) 198 { //data存放的是編碼比特流 216 toc = *data++; 217 len--; 218 last_size = len; //TOC的低2個比特在spec中定義為幀數編碼,用“c”標記的比特段 219 switch (toc&0x3) 220 { //case 3是最復雜的情況,多幀且各幀可不同長情況,TOC最低兩個bit等于3時,緊接著TOC之后的是frame count字節,[v,p,M]三個比特段,手冊中figure 5.default: /*case 3:*/ //padding 比特設置之后,在frame count之后還有一個子節是padding的字節數信息,當然雖然padding的標志位p可以設置,但是緊隨其后的長度可以是0,這樣依然不會真正啟用padding。 259 if (ch&0x40) 260 { 261 int p; 262 do { 263 int tmp; 264 if (len<=0) 265 return OPUS_INVALID_PACKET; 266 p = *data++; 267 len--; 268 tmp = p==255 ? 254: p; 269 len -= tmp; 270 pad += tmp; 271 } while (p==255); 272 } //這一offset值是跳過了opus包的頭信息,opus包的有效載荷是silk/celt包,這一偏移是找到silk/celt包的起始地址 330 if (payload_offset) //對于一個編碼包只有一幀的情況,data-data0就是跳過TOC字段,而對于一個編碼包有多個編碼幀時,TOC之后還有編碼幀的信息也要跳過 331 *payload_offset = (int)(data-data0); 343 if (out_toc) 344 *out_toc = toc;//返回opus包的數量 346 return count; }5.1.2 opus_decode_frame
Opus編碼支持在編碼過程中進行模式切換,為了簡單起見,這里分析忽略模式切換,即如命令行參數指示的,一直工作于HYBIRD模式,opus_decode_frame()函數主要實現的功能包括區間編碼器初始化、調用SILK解碼(silk_Decode)以及CELT解碼(celt_decode_with_ec)解碼。由于CELT和SILK編碼的頻帶不重復,因而需要確定解碼的起始頻帶(SILK編碼為16kHz,因而CELT從16kHz開始算起)和終止頻帶(編碼帶寬確定),該函數的調用流程如下:
//src/opus_decoder.c220 static int opus_decode_frame(OpusDecoder *st, const unsigned char *data,221 opus_int32 len, opus_val16 *pcm, int frame_size, int decode_fec)222 {253 silk_dec = (char*)st+st->silk_dec_offset;254 celt_dec = (CELTDecoder*)((char*)st+st->celt_dec_offset);//區間解碼初始化,編碼包之間解碼是沒有依賴關系的,因而收到新編碼包區間編碼器都需要初始化278 ec_dec_init(&dec,(unsigned char*)data,len);//Hybrid 模式下SILK編碼16kHz以下信號,CELT編碼16kHz以上信號393 st->DecControl.internalSampleRate = 16000;//dec中的buf保存了編碼比特流在其成員buf字段,archy用于優化SIMD代碼執行選擇//pcm_ptr用于存放解碼后pcm數據,silk_frame_size是解碼后pcm數據點數//first_frame指示是否是第一幀,這在模式切換等場景會用到,lost_flag指示是否丟包,這影響到解碼402 silk_ret = silk_Decode( silk_dec, &st->DecControl,403 lost_flag, first_frame, &dec, pcm_ptr, &silk_frame_si ze, st->arch );//CELT 從第17個頻帶開始編碼(16kHz開始)450 if (mode != MODE_CELT_ONLY)451 start_band = 17;//根據編碼帶寬參數,確定編碼截止頻帶 468 if (bandwidth)469 {470 int endband=21;472 switch(bandwidth)473 {484 case OPUS_BANDWIDTH_FULLBAND:485 endband = 21;486 break;487 default:488 celt_assert(0);489 break;490 }491 MUST_SUCCEED(celt_decoder_ctl(celt_dec, CELT_SET_END_BAND(endband)));492 }//CELT解碼518 celt_ret = celt_decode_with_ec(celt_dec, decode_fec ? NULL : data,519 len, pcm, celt_frame_size, &dec, celt_accum);//SILK和CELT解碼結果相加 536 if (mode != MODE_CELT_ONLY && !celt_accum)537 {538 #ifdef FIXED_POINT539 for (i=0;i<frame_size*st->channels;i++)540 pcm[i] = SAT16(ADD32(pcm[i], pcm_silk[i]));541 #else542 for (i=0;i<frame_size*st->channels;i++)543 pcm[i] = pcm[i] + (opus_val16)((1.f/32768.f)*pcm_silk[i]);544 #endif545 }//正確解碼時,區間解碼終值 610 st->rangeFinal = dec.rng ^ redundant_rng;}總結
以上是生活随笔為你收集整理的实时音频编解码之十六 Opus解码的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 使用gitbook发布文章生成网站(一)
- 下一篇: 分层强化学习综述:Hierarchica