资讯详情

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

谢谢你以任何形式转载这篇文章。

第四章 Opus编码

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

Opus编码比特率范围从窄带6开始kbps510到高品质立体声kbps,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.类似类似,编码时会调用ec_enc_init()函数初始化区间编码器的状态,调用位置见图4-3所示,针对不同的编码对象,Opus用三种间隔编码方法包装音频特征:

  • 使用固定概率的熵编码符号ec_encode()(entenc.c),
  • 0 ~ 2 M ? 1 0 \sim2^M-1 0~2M?1使用ec_enc_uint()或者ec_enc_bits()编码,
  • 0 ~ f t ? 1 0 \sim ft-1 0~ft−1内非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 );
200     tot_blocks = ( nBlocksOf10ms > 1 ) ? nBlocksOf10ms >> 1 : 1;
//将sCmn.inputBuf里缓存的数据按帧长度编码完
272     while( 1 ) { 
        
//将输入数据拷贝到buf缓冲区,silk_resampler对输入信号重采样,因为设置采样率都是16kHz,所以实际上并没有重采样,只是进行了数据拼接,完成数据移动之后,更新记录数据长度的inputBufIx变量
321             silk_memcpy(buf, samplesIn, nSamplesFromInput*sizeof(opus_int16));
buf存放的是int16格式的wav文件读取到的数据,nSamplesFromInput是buf内有效数据的长度(320),
//inputBuf用于存放重采样之后的数据,重采样的状态是放在re

标签: p48k5s圆形连接器

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台