实时音频编解码之十一Opus编码

Posted shichaog

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了实时音频编解码之十一Opus编码相关的知识,希望对你有一定的参考价值。

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

第四章 Opus编码

Opus是较为成熟的开源商用语音编解码器,其编码质量高且无版权使用费,因WebRTC标准中规定要支持该音频编码器,所以当今各大浏览器都支持Opus编码器。Opus有很多突出的优点,如延迟低、编码范围宽、输出比特率可控等。Opus常用于实时通信和实时流媒体等程序中,通常伴随视频流,由于人耳对声音更为敏感,所以常以音频流RTP时间戳为基准同步视频流,音视频同步并不再本书范畴。

Opus编码比特率范围从窄带的6kbps到高品质立体声的510kbps,Opus使用LP和MDCT两种技术,在语音和音乐场景中都取得较好的压缩率和音频质量,其中LP技术基于Silk编码器,MDCT技术基于CELT编码器,Opus编码器是SILK和CELT编码器的集成,将SILK对语音编码的优势和CELT对音乐编码的优势相结合,通过混合编码的方式以便在语音和音乐场景下获得最佳的编码质量,Opus编码器的核心是SILK和CELT,两者之间相互独立,输出比特率流是SILK和CELT比特流的混合,SILK和CELT两者和Opus的关系如图4-1所示:

图4-1 Opus编码器结构框图

由于本书侧重于编解码原理及其实现,因而Opus编码器的一些逻辑控制流并不专门介绍,这些控制流包括编码器模式选择、编码比特率分配等。Opus编码的规范手册是RFC6716,规范中定义了比特流的组织格式,和比特流解码步骤,编码侧如何获取解码端需要的参数手册并没有做强制要求,当然手册中也给了一套编码端的实现方法,这就意味着在不改变解码端行为的前提下,编码侧的实现是可以改的,而后文所述的一些基于深度学习的方法可以在丢包补偿等方面提升改编码器的抗丢包性,Opus编解码的核心基于于LP和MDCT算法,本书基于Opus编码器开源工程中的opus_demo.c例子阐述编解码过程及其代码实现。

4.1 SILK编码

为了便于简便,测试工程代码基于浮点c语言实现的源代码,一些定点的技巧在《实时语音处理实践指南》中有所涉及,opus_demo.c编译生成了opus_demo二进制(因为使用了xcode编译了该工程,工程源码见作者GitHub)程序,测试程序使用如下的命令行参数分析Opus编码,这一参数下测试命令使得Opus内部将调用SILK和CELT混合编码。

//编码命令参数
//[-e] <application> <sampling rate (Hz)> <channels (1/2)> <bits per second>  [options] <input><output>
 -e voip 48000 1 64000 -cvbr -framesize 20 -complexity 10  -bandwidth FB -inbandfec -loss 10 -dtx   ~/Downloads/48k_mono.wav out.bit
//解码命令参数
-d 48000 1 -loss 10 out.bit out.pcm

opus_demo即可以测试编码也可以测试解码,根据参数不同区分,-e表示只运行编码,voip表示测试场景是VoIP场景,此外还有audio和低延迟场景,紧接其后的16000、1和24000分别表示输入信号采样率、编码通道和编码比特率,cvbr表示使用约束定比特率,framesize 20是编码的帧长,指定20ms为帧长编码,内部是以5ms子帧编码,因而有四个子帧,complexity是影响编码语音质量的复杂度参数,取值范围[0, 10],值越大复杂度越高,编码语音质量也越高,计算量也越大,bandwidth WB是编码带宽指定,如图4.1所示,SILK部分编码的最高采样率信号为16kHz,高于该采样率的信号如果参与编码则会采用CELT部分编码,inbandfec是启用带内FEC用于抗网络丢包和抖动,dtx表示启用不连续传输,在连续每400ms无语音情况下,会传输一个环境音包,以节约传输带宽,因为SILK编码信号最高采样率为16kHz,因而这里测试用例使用该采样率信号测试,loss是模拟丢包参数10是表示有10%的丢包。

解码时-d表示只测试解码,voip 、16000 、1和 24000参数和编码相同,-loss是表示有解码时丢弃10%的包,当不需要测试FEC和PLC时,只测试SILK和CELT的解码时,可以使用-loss 0,因为涉及到FEC和PLC的分析,这也是采用深度学习方法可以改进的一个点,所以分析其传统的信号处理实现也是有必要的。解码之后的信号是裸PCM数据,没有WAV头信息,因而需要使用audacity导入原始数据查看时频域信号。

SILK编码的核心框架如图4-2所示,编码的参数包括LTP、LSF、Gain以及Noise Shaping和激励码本几个部分,当编码参数选择cvbr时,最终输出的编码比特率在这一指定的编码比特率上下波动,而LTP、LSF在参数一定时编码所占比特率是不可变的,因而可以在Gain和Noise Shaping部分调节输出编码比特率。下文就图4-2中各模块分析其原理和相关代码实现。

图4-2 opus编码器核心框架

4.1.1 区间编码器

区间编码器(Range Encoder)是比特率打包器,是熵编码的一种,并不涉及人类发音模型,基于深度学习建模的语音编码器,也可以使用区间编码器对特征参数编码以降低传输比特率,熵编码的核心在于编码对象出现的概率是不一样的,当概率是一样的时候并不能起到比特率压缩的目的,Opus使用的区间编码方法和1.3.6小节类似,在编码时会,调用ec_enc_init()函数初始化区间编码器的状态,调用的位置见图4-3所示,针对不同的编码对象,Opus使用三种区间编码方法对音频特征打包:

  • 固定概率的熵编码符号使用ec_encode()(entenc.c),
  • 0 ∼ 2 M − 1 0 \\sim2^M-1 02M1使用ec_enc_uint()或者ec_enc_bits()编码,
  • 0 ∼ f t − 1 0 \\sim ft-1 0ft1内非2指数的整数使用ec_enc_uint()编码。
    区间编码器内部使用四元组向量(val, rng, rem, ext)标识编码器状态,val是当前距离的下限值,rng是当前距离范围,rem单字节缓冲的输出,ext是进位个数计数值,解码时用到该值以便获得正确的编码距离值。val和rng是32位无符号整型值,rem是字节值,小于255或者是-1,ext是位宽至少为11比特的无符号整数,该向量初始化为 ( 0 , 2 3 1 , − 1 , 0 ) (0, 2^31, -1, 0) (0,231,1,0),在编码完一个序列之后,编码器的rng值将等于解码器解码该序列之后的rng值,这可以用来检测编解码过程中是否发生错误。解码器中没有ext和rem字段。
    4.1.2 编码流程

opus编码流程涉及的主要函数如图4-3所示,核心算法一列涉及的函数是SILK编码器核心算法的实现函数,其它函数为预处理和模式相关设置,本章以该图为主线,着重分析SILK编码器的核心算法的实现。

图4-3 SILK编码核心流程图

在命令行设置Opus编码参数时,这些参数会传递给SILK编码器,SILK根据这些参数设置编码的工作参数,这些参数编码采样率、编码比特率、编码复杂度等参数,这些参数在调用SILK编码函数silk_encode_frame_Fxx()之前,由opus_encdoe_native()和silk_Encode()函数设置完成。

4.1.3 opus_encode函数

opus_encode()函数是opus_demo测试程序调用的opus编码接口函数,该函数返回值是以字节为单位的编码长度,区间编码器编码的比特率流存放于参数data指向的存储空间,max_data_bytes是网络传输最大的MTU单元为1500字节,用于限制编码输出比特率,analysis_frame_size是编码分帧的长度,pcm指向的地址空间存放的是16比特wav数据。

//opus_encoder.c
2222 opus_int32 opus_encode(OpusEncoder *st, const opus_int16 *pcm, int analysis_frame_size,
2223       unsigned char *data, opus_int32 max_data_bytes)
2224 
//选择合适的帧长,帧最小为2.5ms,即1秒钟最多编码400帧,200帧/秒对应于5ms,依次类推,最大是120ms的帧长度编码
//由于4.1小节选择的编码帧长为20ms,是一个合理值,所以这里frame_size=320
2230    frame_size = frame_size_select(analysis_frame_size, st->variable_duration, st->Fs);
 //根据frame_size_select获得编码帧长度,申请空间,单通道数据也可以立体声编码,channels可由编码参数设定
2236    ALLOC(in, frame_size*st->channels, float);
//编码数据使用的是浮点数,进来的数据时16bit定点数,逐点变成浮点数
2238    for (i=0;i<frame_size*st->channels;i++)
2239       in[i] = (1.0f/32768)*pcm[i];
//in:浮点格式的输入采用数据,frame_size:以采样点计数的帧长,data:编码后的字节序存放的地址,
//max_data_bytes:编码字节序的最大长度,lsb_depth:least significant bit 最低有效位数16
//pcm:short型音频数据,analysis_frame_size:分析帧长20ms,320点
//C1:0 C2:-2, 编码通道数st->channels, downmix_int是下采样函数地址所在,
//最后一个参数指示浮点API
2240    ret = opus_encode_native(st, in, frame_size, data, max_data_bytes, 16,
2241                   pcm, analysis_frame_size, 0, -2, st->channels, downmix_int, 0);

4.1.4 opus_encode_native

该函数对根据输入参数,对数据进行预处理,计算编码比特率,以及调用相应的SILK和CELT编码器对声音进行编码等内容。代码的主要脉络见代码中注释。

//opus_encoder.c
//out_data_bytes设置为MTU(maximum transmission unit),MTU是数据链路层可以传输的最大协议数据单元,对于以太网,就是IP数据报的大小。MTU的考虑是数据包在以太网上传输时不分包,这样的好处是网络抖动和丢包处理方便一些。
1047 opus_int32 opus_encode_native(OpusEncoder *st, const opus_val16 *pcm, int frame_size,
1048                 unsigned char *data, opus_int32 out_data_bytes, int lsb_depth,
1049                 const void *analysis_pcm, opus_int32 analysis_size, int c1, int c2,
1050                 int analysis_channels, downmix_func downmix, int float_api)
1051 
//1276是TOC字段(一个字节)和1275编码最大比特率之和,其中1275对应于510kbps, 编码立体声music时可占到该编码字节数,对于每个编码包,TOC是必须存在的。
1092     max_data_bytes = IMIN(1276, out_data_bytes);

//存储区间编码最终结果,区间编码后的range和距离解码后的range是一样的,这可以用来发现错误,OPUS_GET_FINAL_RANGE_REQUEST命令获取该值  
1094     st->rangeFinal = 0;
//强制地址偏移,获得silk和celt编码器state  
1108     silk_enc = (char*)st+st->silk_enc_offset;
1109     celt_enc = (CELTEncoder*)((char*)st+st->celt_enc_offset);
 //如果是低延迟模式,比如游戏等应用场景,这时delay_compensation=0以减少延迟,但这会带来参数估计误差比有delay补偿的时候大 
1110     if (st->application == OPUS_APPLICATION_RESTRICTED_LOWDELAY)
1111        delay_compensation = 0;
1112     else
1113        delay_compensation = st->delay_compensation;
 //如果complexity 比较大,则先分析,分析内容包括音调、语音、音乐概率等; 
 //对于定点情况,则复杂度大于等于10才会启用该分析,这有助于减少计算资源消耗。
1120 #ifdef FIXED_POINT
1121     if (st->silk_mode.complexity >= 10 && st->Fs>=16000)
1122 #else
1123     if (st->silk_mode.complexity >= 7 && st->Fs>=16000)
1124 #endif
1125     
//根据帧能量,判断是否是静音帧
1126        is_silence = is_digital_silence(pcm, frame_size, st->channels, lsb_depth);
//分析的结果存储在analysis_info结构体中,analysis_pcm是short类型音频输入数据,frame_size:20ms,16khz,即320
1129        run_analysis(&st->analysis, celt_mode, analysis_pcm, analysis_size, frame_size,
1130              c1, c2, analysis_channels, st->Fs,
1131              lsb_depth, downmix, &analysis_info);
1132
// 跟踪信号峰值能量,在run_analysis()计算的语音概率小于门限DTX_ACTIVITY_THRESHOLD时,
// 比较当前帧(由于run_analysis判决小于语音门限,因而该帧到此处是被当成噪声帧)和这里跟踪的峰值能力,
// 当峰值能量大于当前帧的能量时,则会强制认为依然按语音帧编码,以避免误判引起语音帧编码质量下降
1134        if (!is_silence && analysis_info.activity_probability > DTX_ACTIVITY_THRESHOLD)
1135           st->peak_signal_energy = MAX32(MULT16_32_Q15(QCONST16(0.999f, 15), st->peak_signal_energy),
1136                 compute_frame_energy(pcm, frame_size, st->channels, st->arch));
1137      
//由于帧长是20ms,所以frame_rate=50,  max_data_bytes=1276,这里可以计算得到最大编码比特率
1287     max_rate = frame_rate*max_data_bytes*8;
1288
//根据 mode/channel/bandwidth 等参数,计算20ms帧长比特率
//实际的比特率会比命令行参数的24000小,这是因为CBR以及复杂度都需要一些开销
1290     equiv_rate = compute_equiv_rate(st->bitrate_bps, st->channels, st->Fs/frame_size,
1291           st->use_vbr, 0, st->silk_mode.complexity, st->silk_mode.packetLossPercentage);
//计算是否编码LBRR帧  
1544     st->silk_mode.LBRR_coded = decide_fec(st->silk_mode.useInBandFEC, st->silk_mode.packetLossPercentage,
1545           st->silk_mode.LBRR_coded, st->mode, &st->bandwidth, equiv_rate);
//这里跳过一个字节是为TOC字段预留的,是因为在编码完之后再填充TOC字段  
1623     data += 1;
//区间编码器初始化,每一帧都会初始化,这样当存在网络丢包、抖动时帧间是不会有影响的。
1625     ec_enc_init(&enc, data, max_data_bytes-1);

 //根据是语音还是music情况确定高通频率
1630     if (st->mode == MODE_CELT_ONLY)
1631        hp_freq_smth1 = silk_LSHIFT( silk_lin2log( VARIABLE_HP_MIN_CUTOFF_HZ ), 8 );
1632     else
1633        hp_freq_smth1 = ((silk_encoder*)silk_enc)->state_Fxx[0].sCmn.variable_HP_smth1_Q15;
1634
1635     st->variable_HP_smth2_Q15 = silk_SMLAWB( st->variable_HP_smth2_Q15,
1636           hp_freq_smth1 - st->variable_HP_smth2_Q15, SILK_FIX_CONST( VARIABLE_HP_SMTH_COEF2, 16 ) );
1637
//对于语音情况,cutoff_Hz=60Hz
1639     cutoff_Hz = silk_log2lin( silk_RSHIFT( st->variable_HP_smth2_Q15, 8 ) );
  
 //根据使用模式,进行预滤波,music场景只去除直流;这里的判决条件在命令行参数中设置了voip 
1641     if (st->application == OPUS_APPLICATION_VOIP)
1642     
 //对于语音,低于截止频率cutoff_Hz以下的信号因人类发声不会低于该频率,因而可以cutoff,
  //但是对于音乐场景,乐器的频率是可以很低的,只需要去直流即可。
1643        hp_cutoff(pcm, cutoff_Hz, &pcm_buf[total_buffer*st->channels], st->hp_mem, frame_size, st->channels, st->Fs, st->arch);
1644      else 
1645        dc_reject(pcm, 3, &pcm_buf[total_buffer*st->channels], st->hp_mem, frame_size, st->channels, st->Fs);
1646     
   //short类型pcm数据存储首地址pcm_silk
    //frame_size:320 16kHz 20ms帧长
    //nByte:编码的字节数
    //activity:指示语音存在与否
1833         ret = silk_Encode( silk_enc, &st->silk_mode, pcm_silk, frame_size, &enc, &nBytes, 0, activity );
 //由于本小节只有SILK编码,故而SILK之后的CELT编码这里跳过了相关代码
//根据silk_Encode结果,生成TOC字段  
2117     data--;
2118     data[0] = gen_toc(st->mode, st->Fs/frame_size, curr_bandwidth, st->stream_channels);

4.1.5 run_analysis函数

在编码复杂度较高的情况下,先基于原始音频分析音调、语音概率和音乐概率,分析的内容存放在如下结构体中:

 //celt.h
 55 typedef struct 
 56    int valid;
 57    float tonality;
 58    float tonality_slope;
 59    float noisiness;
 60    float activity;
 61    float music_prob;
 62    float music_prob_min;
 63    float music_prob_max;
 64    int   bandwidth;
 65    float activity_probability;
 66    float max_pitch_ratio;
 67    /* 以Q6方式存储以节约存储空间 */
 68    unsigned char leak_boost[LEAK_BANDS];
 69  AnalysisInfo;

run_analysis函数主要调用tonality_analysis函数完成相关参数计算,

//analysis.c
//音调分析C:是通道数
971          tonality_analysis(analysis, celt_mode, analysis_pcm, IMIN(Fs/50, pcm_len), offset, c1, c2, C, lsb_depth, downmix);


446 static void tonality_analysis(TonalityAnalysisState *tonal, const CELTMode *celt_mode, const void *x, int len, int offset, int c1, int c2,     int C, int lsb_depth, downmix_func downmix)
447 

504     if (tonal->Fs == 48000)
505     
 // 音调分析基于24kHz,所以对于48kHz输入信号需要除以二 
507        len/= 2;
508        offset /= 2;
509      else if (tonal->Fs == 16000) 
 //内部使用24kHz分析,所以len从320变为480
510        len = 3*len/2;  
511        offset = 3*offset/2;
512     
//mdct.kfft[0]是480点fft,即对20ms输入数据长度进行FFT变换  
514     kfft = celt_mode->mdct.kfft[0];

//x:是输入数据
//tonal->inmem 大小是30ms @24kHz,这里偏移tonal->mem_fill=240,相当于10ms,该偏移地址将存放重采样之后的数据
//IMIN(len, ANALYSIS_BUF_SIZE-tonal->mem_fill):480,是偏移值
//offset=0,c1:0,c2:-2,C:通道数1,c1和c2影响重采样,对于16kHz,单声道数据,可以忽略c1,c2这两个参数
//tonal->Fs:16000
//对于16kHz输入信号,这里重采样算法直接采取3倍插相同值的,再2倍下采样实现16kHz-->48kHz--->24kHz的重采样,虽然8kHz~12kHz会频域混叠,
//但是原始的16kHz的信号使得我们这里仅关系8kHz以下信号,所以并无影响 
515     tonal->hp_ener_accum += (float)downmix_and_resample(downmix, x,
516           &tonal->inmem[tonal->mem_fill], tonal->downmix_state,
517           IMIN(len, ANALYSIS_BUF_SIZE-tonal->mem_fill), offset, c1, c2, C, tonal->Fs);
//tonal->info的大小是宏DETECT_SIZE=100个,循环使用 
526     info = &tonal->info[tonal->write_pos++];
527     if (tonal->write_pos>=DETECT_SIZE)
528        tonal->write_pos-=DETECT_SIZE;
//基于一帧最大值和最小值之差,判断是否是静音帧
530     is_silence = is_digital_silence32(tonal->inmem, ANALYSIS_BUF_SIZE, 1, lsb_depth);
//480点 @24khz,20ms; 240点 @24khz, 10ms  
532     ALLOC(in, 480, kiss_fft_cpx);
533     ALLOC(out, 480, kiss_fft_cpx);
534     ALLOC(tonality, 240, float);
535     ALLOC(noisiness, 240, float);
//把重采样之后的信号按照kissfft的要求排列数据
536     for (i=0;i<N2;i++)
537     
538        float w = analysis_window[i];
539        in[i].r = (kiss_fft_scalar)(w*tonal->inmem[i]);
540        in[i].i = (kiss_fft_scalar)(w*tonal->inmem[N2+i]);
541        in[N-i-1].r = (kiss_fft_scalar)(w*tonal->inmem[N-i-1]);
542        in[N-i-1].i = (kiss_fft_scalar)(w*tonal->inmem[N+N2-i-1]);
543     
//把最后的10ms数据移到tonal->inmem内存起始,下一帧的480ms数据接在其后放置  
544     OPUS_MOVE(tonal->inmem, tonal->inmem+ANALYSIS_BUF_SIZE-240, 240);
545     remaining = len - (ANALYSIS_BUF_SIZE-tonal->mem_fill);
//对于16kHz,20ms帧长,remaining = 0,下面这个函数直接跳出不做运算
546     tonal->hp_ener_accum = (float)downmix_and_resample(downmix, x,
547           &tonal->inmem[240], tonal->downmix_state, remaining,
548           offset+ANALYSIS_BUF_SIZE-tonal->mem_fill, c1, c2, C, tonal->Fs);
//如果是静音帧,拷贝之前的tonal->info信息,至此静音帧计算完毕
556        OPUS_COPY(info, &tonal->info[prev_pos], 1);
//非静音帧则需要计算音调的信息  
560     opus_fft(kfft, in, out, tonal->arch);

4.1.6 silk_Encode函数

//返回值为错误码
140 opus_int silk_Encode(
//编码状态
141     void                            *encState,                                          
//编码控制状态
142     silk_EncControlStruct           *encControl, 
//输入语音,即测试程序opus_demo读取到的PCM
143     const opus_int16                *samplesIn, 
//输入语音采样点数,由4.1命令行fram_size和采样率参数决定
//这里的值为320,即20ms*16(采样率为16kHz,1ms对应采样点数为16)
144     opus_int                        nSamplesIn, 
//区间编码压缩数据结构体
145     ec_enc                          *psRangeEnc, 
//区间编码数据长度  
146     opus_int32                      *nBytesOut,  
//指示prefilling buffer数据是否编码,置位后表示不参与编码,测试情况没有设置该位
147     const opus_int                  prefillFlag, 
//Opus语音检测概率
148     opus_int                        activity 
149 )
150 
//命令行参数使用的是20ms帧长编码,故按10ms分的块数nBlocksOf10ms=2,总的块数依然等于tot_blocks=1
199     nBlocksOf10ms = silk_DIV32( 100 * nSamplesIn, encControl->API_sampleRate );
以上是关于实时音频编解码之十一Opus编码的主要内容,如果未能解决你的问题,请参考以下文章

实时音频编解码之十六 Opus解码

实时音频编解码之十八 Opus解码 CELT解码

实时音频编解码之十六 Opus解码

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

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

实时音频编解码之十五 Opus编码-CELT编码