本文谢绝任何形式转载,谢谢。

第五章 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.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解码相关推荐

  1. 实时音频编解码之十七 Opus解码 SILK解码

    本文谢绝任何形式转载,谢谢. 5.2 Silk解码流程 解码器线性预测层主要使用长短时预测合成滤波器对激励信号滤波实现,线性预测层内部的工作带宽为NB.MB以及WB,对于SWB以及FB的混合编码工作模 ...

  2. 视频编解码(十六):VE解码器解码demo解码流程

    一.VE解码器解码demo解码流程 解码器初始化.创建.读取码流.解码.显示

  3. 实时音频编解码之五 噪声整形

    本文谢绝任何形式转载,谢谢. 1.4.5 噪声整形 因压缩比特率而带来的量化误差会导致规律的噪声产生,即使量化带来的噪声能量上远小于语音信号,但是由于人的听觉系统对规律性的噪声非常敏感,因而非常影响听 ...

  4. 实时音频编解码之八 频带扩展

    本文谢绝任何形式转载,谢谢. 1.4.8 频带扩展 在线性预测应用中,由于极点过于靠近单位圆,合成滤波器可能处于临界稳定的状态,在定点实现中这一问题更加严重,定点的量化和计算中的精度损失可能使得临界稳 ...

  5. vs2013编译ffmpeg之二十六 opus、shine

    opus 对应ffmpeg configure选项–enable-libopus. 官网下载opus-1.1.tar.gz版本 解压后在opus-1.1\win32\VS2010下面有sln文件,打开 ...

  6. 即时通讯音视频开发(十八):详解音频编解码的原理、演进和应用选型

    1.引言 大家好,我是刘华平,从毕业到现在我一直在从事音视频领域相关工作,也有一些自己的创业项目,曾为早期Google Android SDK多媒体架构的构建作出贡献. 就音频而言,无论是算法多样性, ...

  7. 即时通讯音视频开发(六):如何开始音频编解码技术的学习

    前言 即时通讯应用中的实时音视频技术,几乎是IM开发中的最后一道高墙.原因在于:实时音视频技术 = 音视频处理技术 + 网络传输技术 的横向技术应用集合体,而公共互联网不是为了实时通信设计的. 系列文 ...

  8. 移植Opus音频编解码库到FreeScale iMX6q(飞凌嵌入式的OKMX6Q-C开发板)平台

    移植Opus音频编解码库到FreeScale iMX6q(飞凌嵌入式的OKMX6Q-C开发板)平台 交叉编译器 使用飞凌提供的最新版交叉编译工具链,fsl-imx-x11-glibc-x86_64-m ...

  9. 详解音频编解码的原理、演进和应用选型等

    本文来自网易云音乐音视频实验室负责人刘华平在LiveVideoStackCon 2017大会上的分享,并由LiveVideoStack根据演讲内容整理而成(本次演讲PPT文稿,请从文末附件下载). 1 ...

最新文章

  1. NoSQL还是SQL?这一篇讲清楚
  2. lintcode:排颜色 II
  3. formdata怎么传数组_如何使用formData上传file数组
  4. p服务器不响应,无法加载资源:服务器响应状态为500
  5. 手把手教你用java读写excel表格文件(POI,EasyExcel)
  6. 立根融资租赁:内部系统平台上云
  7. java 线程内存模型_JAVA内存模型与线程
  8. docker部署express项目
  9. SQL2005中row_number( )、rank( )、dense_rank( )、ntile( )函数的用法(1)
  10. 2.高性能MySQL --- MySQL 基准测试
  11. python 建筑计算_写给潘石屹的 Python 自学指南
  12. 免费分享9本经典的MySQL书籍。
  13. 函数计算机显示RAD,计算器rad是什么意思
  14. IP地址最后一位斜杠是什么意思?比如192.168.1.10/27?还有IP地址和子网掩码相加得到的网络地址是什么意思
  15. Linux课程--实验四 shell 编程
  16. 读周志华《机器学习》第一章有感(白话总结)
  17. eureka 自我保护机制
  18. 洛谷—P1330 封锁阳光大学
  19. ubuntu18.10安装网易云音乐,并解决网易云音乐图标无法启动的问题
  20. python中函数的定义_Python函数是什么_如何定义和调用函数?

热门文章

  1. ORB-SLAM3笔记(编译、踩坑、论文、看代码)
  2. 工商银行u盾 java_中国工商银行u盾怎么用
  3. invalid python sd,Fatal Python error: init_fs_encoding: failed to get the Python cod如何解决
  4. 解析微信开发之搜索歌曲
  5. 好书推荐:《黑客秘笈:渗透测试实用指南》
  6. 数据结构:“大根堆、小根堆”的向上调整算法和向下调整算法
  7. 任你和QQ陌生人聊天
  8. 如何快速设计一款门磁传感器产品?App即可确认门窗关闭
  9. 10招有效预防电脑辐射
  10. 平步青云:Windows Azure(二)