实时音频编解码之十六 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.pcm

opus解码的入口函数在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.c
 751 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,点数为320
 766       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.c
 626 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.c
 220 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_POINT
 539       for (i=0;i<frame_size*st->channels;i++)
 540          pcm[i] = SAT16(ADD32(pcm[i], pcm_silk[i]));
 541 #else
 542       for (i=0;i<frame_size*st->channels;i++)
 543          pcm[i] = pcm[i] + (opus_val16)((1.f/32768.f)*pcm_silk[i]);
 544 #endif
 545    }
 //正确解码时,区间解码终值 
 610       st->rangeFinal = dec.rng ^ redundant_rng;
 }

猜你喜欢

转载自blog.csdn.net/shichaog/article/details/124778290