开源音频编解码器 SOLO 源码解读(二):窄带编码

声网 Agora 在 2019 年 10 月 24 日,正式对所有开发者开源自研的抗丢包音频编解码器 SOLO。该编解码器适用于所有需要实时音频交互的场景,特别针对弱网对抗进行了优化,并且在相同弱网环境下 MOS 分优于 Opus。SOLO 可应用于各类 RTC 应用,并且可不与 Agora SDK 绑定。

本文作者:声网 Agora 音频算法工程师 赵晓涵。

上一期 SOLO 源码解析分析了 SOLO 的带宽扩展系统,本期 SOLO 源码解析将会介绍一下 SOLO 的窄带编码流程。因为 SOLO 的编码框架是基于 Silk 修改而成,所以本文对于 Silk 原生代码的介绍会比较简略。

SOLO 源码:GitHub - AgoraIO-Community/Solo: Agora Solo is an open source speech codec, it was developed based on Silk with BWE(Bandwidth Extension) and MDC(Multi Description Coding). With these technologies, Solo is enable to resist weak networks at low bitrates.

一、编码模块

Solo的窄带编码入口函数是SKP_Silk_SDK_Encode

SKP_int SKP_Silk_SDK_Encode( 
    void                              *encState,      /* I/O: State                    */
    const SKP_SILK_SDK_EncControlStruct  *encControl, /* I:   Control structure        */
    const SKP_int16                   *samplesIn,     /* I:   Input samples            */
    SKP_int                           nSamplesIn,     /* I:   Number of samples        */
    SKP_uint8                         *outData,       /* O:   Encoded output           */
    SKP_int16                         *nBytesOut      /* I/O: I: Max bytes O:out bytes */
)

在该函数内,Solo首先会进行一些带宽检测、重采样(如需)等操作,最终输入到SKP_Silk_encode_frame_FLP

的是8khz采样率的信号

SKP_int SKP_Silk_encode_frame_FLP( 
    SKP_Silk_encoder_state_FLP      *psEnc,             /* I/O  Encoder state FLP      */
    SKP_uint8                       *pCode,             /* O    Payload                */
    SKP_int16                       *pnBytesOut,        /* I/O  Payload bytes          */
                                                        /* input: max ;  output: used  */
    const SKP_int16                 *pIn                /* I    Input speech frame     */
)

在该函数内,首先通过SKP_Silk_VAD_FLP进行信号的静音检测并得到当前信号是语音的概率值,该语音概率值会用来参与控制LBRR编码、LSF转化、噪声整型等模块。

SKP_int SKP_Silk_VAD_FLP(
    SKP_Silk_encoder_state_FLP      *psEnc,             /* I/O  Encoder state FLP      */
    SKP_Silk_encoder_control_FLP    *psEncCtrl,         /* I/O  Encoder control FLP    */
    const SKP_int16                 *pIn                /* I    Input signal           */
)

接下来的主要步骤有进行长时预测、短时预测、噪声整形、编码等,主要函数及其作用依次为:

SKP_Silk_find_pitch_lags_FLP是用于分析信号的基音周期和清浊音的函数,对于浊音帧,因为周期性较强,所以需要做长时预测(LTP),而对于清音帧,因为周期性不明显,便不需要做长时预测。

void SKP_Silk_find_pitch_lags_FLP(
    SKP_Silk_encoder_state_FLP      *psEnc,             /* I/O  Encoder state FLP      */
    SKP_Silk_encoder_control_FLP    *psEncCtrl,         /* I/O  Encoder control FLP    */
    SKP_float                       res[],              /* O    Residual               */
    const SKP_float                 x[]                 /* I    Speech signal          */
)

SKP_Silk_noise_shape_analysis_FLP是用来进行噪声整形分析的函数,噪声整形可以通过调整量化增益,使得量化噪声随着原始信号能量一起起伏,这样利用掩蔽效应,就难以感知到量化噪声。在这个函数里,除了Silk原有的增益控制,Solo还有着一套自己的增益计算系统,其逻辑和Silk原有增益控制相似,部分参数细节不同,因为在Solo里是双流编码,所以Solo重新进行了码率分配,并根据所分配码率,计算出当前各个码流的理论SNR,随后,该SNR会用于后续增益的计算,该增益用来控制后续处理残差信号时的残差幅值分割比例。

void SKP_Silk_noise_shape_analysis_FLP(
    SKP_Silk_encoder_state_FLP      *psEnc,             /* I/O  Encoder state FLP      */
    SKP_Silk_encoder_control_FLP    *psEncCtrl,         /* I/O  Encoder control FLP    */
    const SKP_float                 *pitch_res,         /* I    LPC residual           */
    const SKP_float                 *x                  /* I    Input signal           */
)

SKP_Silk_find_pred_coefs_FLP是进行线性预测的函数,包括短时预测系数和长时预测系数都会在这里被计算出来,其中,LPC系数会被转化成为LSF系数,LSF系数经过量化、反量化后还原成LPC系数,用于随后的信号重建函数SKP_Silk_NSQ_wrapper_FLP

void SKP_Silk_find_pred_coefs_FLP(
    SKP_Silk_encoder_state_FLP      *psEnc,         /* I/O  Encoder state FLP          */
    SKP_Silk_encoder_control_FLP    *psEncCtrl,     /* I/O  Encoder control FLP        */
    const SKP_float                 res_pitch[]     /* I    Residual                   */
)

SKP_Silk_NSQ_wrapper_FLP是编码模块前的重建分析函数,其思想是Analysis by sythesis,即在这个函数里,会有一个模拟的解码器,使用上述线性预测参数、增益、量化残差等对语音信号进行重建,重建的信号会直接和当前编码信号进行比较,通过噪声整形、随机残差扰动等方法,使模拟解码器内的重建信号和输入信号的误差尽量小,这样就可以使得真正解码器的解码信号尽量逼近原始信号。

void SKP_Silk_NSQ_wrapper_FLP(
    SKP_Silk_encoder_state_FLP      *psEnc,         /* I/O  Encoder state FLP          */
    SKP_Silk_encoder_control_FLP    *psEncCtrl,     /* I/O  Encoder control FLP        */
    const SKP_float                 x[],            /* I    Prefiltered input signal   */
    SKP_int8                        q[],            /* O    Quantized pulse signal     */
    SKP_int8                       *q_md[],         /* O    Quantized pulse signal     */
    const SKP_int                   useLBRR         /* I    LBRR flag                  */
)

该函数进行分析的模式有两种,SKP_Silk_NSQSKP_Silk_NSQ_del_dec,这两者最大的不同是,SKP_Silk_NSQ_del_dec使用了Delay-Decision,其复杂度要高于SKP_Silk_NSQ,但因为Delay-Decision的本质是把Silk中各个残差点的标量量化转化为了32个点的矢量量化,所以其效果比较好,因此Silk默认使用的是SKP_Silk_NSQ_del_dec,本文只对该默认函数进行分析。

void SKP_Silk_NSQ_del_dec(
    SKP_Silk_encoder_state          *psEncC,                                                 /* I/O  Encoder State                       */
    SKP_Silk_encoder_control        *psEncCtrlC,                                             /* I    Encoder Control                     */
    SKP_Silk_nsq_state              *NSQ,                                                     /* I/O  NSQ state                           */
    SKP_Silk_nsq_state              NSQ_md[MAX_INTERLEAVE_NUM],                               /* I/O  NSQ state                           */
    const SKP_int16                 x[],                                                     /* I    Prefiltered input signal            */
    SKP_int8                        q[],                                                     /* O    Quantized pulse signal              */
    SKP_int8                        *q_md[ MAX_INTERLEAVE_NUM ],                             /* O    Quantized qulse signal              */
    SKP_int32                       r[],                                                     /* O    Output residual signal              */
    const SKP_int                   LSFInterpFactor_Q2,                                       /* I    LSF interpolation factor in Q2      */
    const SKP_int16                 PredCoef_Q12[ 2 * MAX_LPC_ORDER ],                       /* I    Prediction coefs                    */
    const SKP_int16                 LTPCoef_Q14[ LTP_ORDER * NB_SUBFR ],                     /* I    LT prediction coefs                 */
    const SKP_int16                 AR2_Q13[ NB_SUBFR * MAX_SHAPE_LPC_ORDER ],               /* I    Noise shaping filter                */
    const SKP_int                   HarmShapeGain_Q14[ NB_SUBFR ],                           /* I    Smooth coefficients                 */
    const SKP_int                   Tilt_Q14[ NB_SUBFR ],                                     /* I    Spectral tilt                       */
    const SKP_int32                 LF_shp_Q14[ NB_SUBFR ],                                   /* I    Short-term shaping coefficients     */
    const SKP_int32                 Gains_Q16[ NB_SUBFR ],                                   /* I    Gain for each subframe              */
	const SKP_int32 				MDGains_Q16[ NB_SUBFR ],                                 /* I    New gain, no use now                */
	const SKP_int32			        DeltaGains_Q16,                                           /* I    Gain for odd subframe               */
    const SKP_int                   Lambda_Q10,                                               /* I    Quantization coefficient            */
    const SKP_int                   LTP_scale_Q14                                             /* I    LTP state scaling                   */
)

在该函数内,核心操作有以下几步:

1)通过Agora_Silk_DelDec_RewhiteningAgora_Silk_DelDec_Rewhitening_SideSKP_Silk_nsq_del_dec_scale_states等函数初始化合成码流和两个多描述码流的状态,准备在编码器内进行各条码流的模拟解码。

2)接下来在编码前的操作都是在SKP_Silk_md_noise_shape_quantizer_del_dec中完成的,该函数完成了所有解码分析的操作,分析的第一步是在SKP_Silk_md_noise_shape_quantizer_del_dec中使用各个量化后的参数进行残差的求取。

3)求取残差后,区别于Silk会对单残差进行分析,Solo会使用特殊的增益DeltaGains_Q16将残差按子帧划分为两条子帧能量互补的残差流,相邻子帧所进行增益分配的方式是相反的。

4)随后,Solo会在Agora_Silk_RDCx1中计算两条流的两种不同量化方式的误差并将累积误差保存起来,随后,在Agora_Silk_CenterRD中,Solo会计算两条残差两两组合起来的合成残差与实际残差的误差,并依据此误差和两条流各自的误差计算出一个加权误差,该加权误差的累积最终决定了使用哪两条残差码流作为编码的对象。计算加权误差所使用的权重INTERNAL_JOINT_LAMBDA可以进行调整,权重越靠向合成误差,那解码端无丢包下两条码流合成的音频的误差就越小;权重越靠向多描述码流的误差,解码端各条码流单独解码的信号误差就越小,但两条码流合成后的误差可能会较大。

最终,该函数会输出两条用于编码的残差信号和一个扰动的初始seed,因为seed会随着每个时域点的幅值信息进行变化,所以只需编码该帧的扰动初始seed,解码器就可以推算出该帧所有时域点对应的扰动,用于后续编码。

Solo的低频部分编码沿用了Silk的编码方案(高频部分使用了独立的编码方案,具体实现可见于前一篇Solo代码解读),所有需要编码的低频信息全部都在SKP_Silk_encode_parameters中使用range coding进行编码,range coding 编码新增参数所需要的概率密度函数是根据大量中英文语料计算出来的,在一定程度上是编码效率较高的概率密度函数。

void SKP_Silk_encode_parameters(
    SKP_Silk_encoder_state          *psEncC,        /* I/O  Encoder state              */
    SKP_Silk_encoder_control        *psEncCtrlC,    /* I/O  Encoder control            */
    SKP_Silk_range_coder_state      *psRC,          /* I/O  Range encoder state        */
    SKP_int							md_type,        /* I    Use MDC or not             */
    const SKP_int8                  *q              /* I    Quantization indices       */
)

二、解码模块

在进行了高低频码流分离后,携带低频信息的码流被送到低频解码器,低频解码器可以看做是两个并行的Silk解码器加上前后处理模块。其中,前处理模块的主要功能是根据不同收包情况对码流进行分割。收包情况分为四种,分别为(1)只收到第一条描述码流(2)只收到第二条描述码流(3)两条码流都收到(4)该帧对应码流都没有收到。

Solo根据不同收包情况设置不同参数,传入AgoraSateDecodeTwoDesps进行解码。

SKP_int AgoraSateDecodeTwoDesps(
    SKP_Silk_decoder_state          *psDec,              /* I/O  Silk decoder state    */
    SKP_Silk_decoder_control		*psDecCtrl,
    SKP_int16                       pOut[],              /* O    Output speech frame   */
    const SKP_int                   nBytes1,             /* I    Payload length        */
    const SKP_int                   nBytes2,             /* I    Payload length        */
    const SKP_uint8                 pCode1[],            /* I    Pointer to payload    */
    const SKP_uint8                 pCode2[],            /* I    Pointer to payload    */
    SKP_int							desp_type,
    SKP_int                         decBytes[]           /* O    Used bytes            */
)

在该函数里,通过SKP_Silk_decode_parameters可以从码流中解码出增益、线性预测系数以及残差信号等重建音频所需要的信息,需要注意的是,在前两种收包情况下,解码出的残差并不是完整的残差,而是两段互补残差中的一段,但因为另一段互补码流没有按时到达解码器,所以解码器无法获得另一段互补残差,因此,解码出当前残差后,需要使用在编码器中计算并传输到解码器的特殊增益将该残差恢复为完整的残差信号;如果当前收包情况是第三种,那只需要将解码出的两条互补残差相加,即可得到完整的残差数据。得到残差信号后,再结合其他参数就可以进行语音信号的重建;如果当前收包情况是第四种,那么Solo会去呼叫丢包补偿模块,使用上一帧的增益和线性预测系数,以及随机的残差信号进行补偿帧的生成。

最后,在经过一些和Silk相同的后处理后,解码器的流程就结束了。

相关阅读:
开源编解码器 SOLO 源码解读(一):带宽扩展 - 开源技术 - RTC开发者社区-WebRTC中文论坛|RTC实时技术论坛

推荐阅读
相关专栏
开源技术
106 文章
本专栏仅用于分享音视频相关的技术文章,与其他开发者和声网 研发团队交流、分享行业前沿技术、资讯。发帖前,请参考「社区发帖指南」,方便您更好的展示所发表的文章和内容。