开源编解码器 SOLO 源码解读:带宽扩展与窄带编码
本文作者:聲網(wǎng)Agora 音頻算法工程師 趙曉涵。
聲網(wǎng) Agora 在 2019 年 10 月 24 日,正式對(duì)所有開發(fā)者開源自研的抗丟包音頻編解碼器 SOLO。該編解碼器適用于需要實(shí)時(shí)音頻交互的場(chǎng)景,特別針對(duì)弱網(wǎng)對(duì)抗進(jìn)行了優(yōu)化,并且在相同弱網(wǎng)環(huán)境下 MOS 分優(yōu)于 Opus。SOLO 可應(yīng)用于各類 RTC 應(yīng)用,并且可不與 Agora SDK 綁定使用。本文將從源碼角度解讀 SOLO 的帶寬擴(kuò)展與窄帶編碼。
SOLO 源碼:https://github.com/AgoraIO-Community/Solo
1
帶寬擴(kuò)展
SOLO 在 Silk 的基礎(chǔ)上擴(kuò)展了帶寬擴(kuò)展模塊,用來分別處理低頻信息(0-8kHz 采樣部分)和高頻信息(8-16kHz 采樣部分),在編碼端,兩者使用兩套耦合的分析編碼系統(tǒng)進(jìn)行碼流生成。在解碼端,利用低頻信號(hào)和高頻信息,SOLO 可以解碼出寬帶信號(hào)。SOLO 使用帶寬擴(kuò)展主要有兩個(gè)原因,首先,帶寬擴(kuò)展可以讓更多的碼率分配到更重要的低頻部分,提升編碼效率;第二個(gè)原因是帶寬擴(kuò)展可以減少進(jìn)入到信號(hào)分析模塊的采樣點(diǎn)數(shù),從而減少信號(hào)分析部分的復(fù)雜度(之前需要分析全部的信號(hào),現(xiàn)在只需要分析低頻部分)。在減少了原有復(fù)雜度的前提下,SOLO 才能夠在低頻部分額外增加較多計(jì)算以選取最佳的多描述編碼狀態(tài),讓編解碼音質(zhì)達(dá)到預(yù)期。
編碼端
SOLO 編碼端的大部分操作都是在下述函數(shù)中完成的:
首先,輸入的16kHz 采樣率的語音幀會(huì)先進(jìn)入到一個(gè)正交鏡像濾波器組(QMF)里進(jìn)行頻帶的劃分:
該函數(shù)的輸出的是兩個(gè)時(shí)域幀,分別包含低頻信息和高頻信息。低頻信息和高頻信息會(huì)在后續(xù)分別進(jìn)行處理,其中,低頻信息會(huì)通過函數(shù)?SKP_Silk_SDK_Encode?進(jìn)行分析和編碼,這部分內(nèi)容我們會(huì)在稍后的“窄帶編碼”中進(jìn)行詳細(xì)解讀。
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 */ )高頻信息的編碼以線性濾波分析為基礎(chǔ),同時(shí)為了減少碼率,部分依賴于低頻信號(hào)的殘差信息,因此在進(jìn)行高頻信息編碼之前,需要通過下述函數(shù)提取低頻編碼信息中的殘差信息:
SKP_int SKP_Silk_SDK_Get_Encoder_Residue( void *encState,SKP_int32 *r )高頻信息的分析和編碼在函數(shù) AGR_Bwe_encode_frame_FLP 中進(jìn)行:
SKP_int32 AGR_Bwe_encode_frame_FLP(AGR_Sate_HB_encoder_control_FLP *hbEncCtrl,AGR_Sate_encoder_hb_state_FLP *psHBEnc,NovaBits *bits, /* I bitstream operator */SKP_float *high,SKP_int32 *residue,SKP_int16 *nBytesOut /* I/O: Number of bytes in outData (input: Max bytes) */ )首先高頻信息通過?AGR_Sate_find_HB_LPC_FLP?進(jìn)行分析得到自身的 8 階 LPC 系數(shù),并將其轉(zhuǎn)化為編碼誤差較小的 LSP 系數(shù):
SKP_int32 AGR_Sate_find_HB_LPC_FLP(AGR_Sate_encoder_hb_state_FLP *psEnc, /* I/O Encoder state FLP */AGR_Sate_HB_encoder_control_FLP *hbEncCtrl, /* I/O HB Encoder control FLP */SKP_int32 hb_subfr_length, /* I subframe length */SKP_int32 hb_lpc_order, /* I high band lpc order */SKP_int32 first /* I */ )隨后通過?AGR_Sate_lsp_quant_highband?進(jìn)行雙碼本量化
SKP_int32 AGR_Sate_lsp_quant_highband(SKP_float *lsp, /* I/O lsp coefficients */SKP_int32 order /* I lpc order */ )量化后,編碼器會(huì)將 LSP 系數(shù)轉(zhuǎn)化為 1 個(gè) index 來表示:
idx1 = lsp_weight_quant(qlsp, quant_weight1, AGR_Sate_highband_lsp_cdbk1, HB_LSP_CB1, order); idx2 = lsp_weight_quant(qlsp, quant_weight2, AGR_Sate_highband_lsp_cdbk2, HB_LSP_CB2, order); idx = (idx2<<8)+idx1;隨后,該幀被分為 4 個(gè)子幀,計(jì)算各個(gè)子幀的殘差信號(hào),并計(jì)算其對(duì)應(yīng)窄帶殘差信號(hào)子幀的增益,共計(jì)4個(gè),使用單碼本量化。量化后的 LSP index 和 gain 使用下述函數(shù)寫入獨(dú)立碼流。
void AGR_Sate_bits_pack(NovaBits *bits, int data, int nbBits)其中,LSP index 使用 12 bits 編碼,每個(gè)子幀 gain 使用 5 bits 編碼,所以高頻信息的碼流共計(jì) 12+4*5=32 bits,即 4 bytes,該段碼流位于窄帶碼流之后,和窄帶碼流中的第二組多描述碼流綁定在一起組包。
解碼端
解碼器可以看做編碼器的鏡像,解碼器收到碼流后,首先會(huì)通過下述函數(shù)解碼得到 0-8kHz 采樣率的低頻信息,這部分我們稍后會(huì)做詳細(xì)解讀。
隨后,解碼器通過下述函數(shù)得到低頻殘差信息以用來解碼 8-16kHz 的高頻信息。
同時(shí),解碼器會(huì)使用以下函數(shù)來進(jìn)行高頻信息的解碼。
SKP_int32 AGR_Bwe_decode_frame_FLP(AGR_Sate_HB_decoder_control_FLP *hbDecCtrl,AGR_Sate_decoder_hb_state_FLP *psHBDec,NovaBits *bits, /* I bitstream operator */SKP_float *OutHigh,SKP_int32 *residue_Q10,SKP_int32 lostflag /* I lost falg */)該函數(shù)內(nèi)的處理整體上可以分成兩種 case,第一種是沒有正常接收到包含高頻信息的多描述碼流,這種情況下會(huì)復(fù)用上一幀解碼出的 LSP index 和子幀增益;如果正常接收到了包含高頻信息的多描述碼流,則會(huì)從 4 bytes 的高頻信息中解碼、反量化出所需的 LPC 濾波器系數(shù)和 4 個(gè)子幀增益。
恢復(fù)高頻信號(hào)使用的殘差信號(hào)是乘上子幀增益后的低頻殘差信號(hào)。使用高頻殘差再加上高頻 LPC 系數(shù),通過以下函數(shù)就可以解碼得到高頻信號(hào)。
void AGR_Sate_LPC_synthesizer(SKP_float *output, /* O output signal */SKP_float *ipexc, /* I excitation signal */SKP_float *sLPC, /* I/O state vector */SKP_float *a_tmp, /* I filter coefficients */SKP_int32 LPC_order, /* I filter order */SKP_int32 subfr_length /* I signal length */ )隨后,低頻信息和高頻信息會(huì)進(jìn)入到以下函數(shù)中,進(jìn)行高低頻的合成,函數(shù)輸出的是 16kHz 采樣率的寬帶信號(hào)。
void AGR_Sate_qmf_synth(const spx_word16_t *x1, /* I Low band signal */const spx_word16_t *x2, /* I High band signal */const spx_word16_t *a, /* I Qmf coefficients */spx_word16_t *y, /* O Synthesised signal */SKP_int32 N, /* I Signal size */SKP_int32 M, /* I Qmf order */spx_word16_t *mem1, /* I/O Qmf low band state */spx_word16_t *mem2, /* I/O Qmf high band state */SKP_int8 *stack )至此,解碼端就完成了將窄帶信號(hào)擴(kuò)展成寬帶信號(hào)的操作。
2
窄帶編碼
編碼模塊
SOLO 的窄帶編碼入口函數(shù)是?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 */ )在該函數(shù)內(nèi),Solo首先會(huì)進(jìn)行一些帶寬檢測(cè)、重采樣(如需)等操作,最終輸入到SKP_Silk_encode_frame_FLP?的是8khz采樣率的信號(hào)
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 */ )在該函數(shù)內(nèi),首先通過?SKP_Silk_VAD_FLP?進(jìn)行信號(hào)的靜音檢測(cè)并得到當(dāng)前信號(hào)是語音的概率值,該語音概率值會(huì)用來參與控制?LBRR?編碼、LSF?轉(zhuǎn)化、噪聲整型等模塊。
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 */ )接下來的主要步驟有進(jìn)行長(zhǎng)時(shí)預(yù)測(cè)、短時(shí)預(yù)測(cè)、噪聲整形、編碼等,主要函數(shù)及其作用依次為:
SKP_Silk_find_pitch_lags_FLP是用于分析信號(hào)的基音周期和清濁音的函數(shù)。對(duì)于濁音幀,因?yàn)橹芷谛暂^強(qiáng),所以需要做長(zhǎng)時(shí)預(yù)測(cè)(LTP);而對(duì)于清音幀,因?yàn)橹芷谛圆幻黠@,便不需要做長(zhǎng)時(shí)預(yù)測(cè)。
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?是用來進(jìn)行噪聲整形分析的函數(shù),噪聲整形可以通過調(diào)整量化增益,使得量化噪聲隨著原始信號(hào)能量一起起伏。這樣利用掩蔽效應(yīng),就難以感知到量化噪聲。在這個(gè)函數(shù)里,除了?Silk?原有的增益控制,SOLO?還有著一套自己的增益計(jì)算系統(tǒng),其邏輯和?Silk?原有增益控制相似,部分參數(shù)細(xì)節(jié)不同。因?yàn)樵?SOLO?里是雙流編碼,所以?SOLO?重新進(jìn)行了碼率分配,并根據(jù)所分配碼率,計(jì)算出當(dāng)前各個(gè)碼流的理論?SNR。隨后,該?SNR?會(huì)用于后續(xù)增益的計(jì)算,該增益用來控制后續(xù)處理殘差信號(hào)時(shí)的殘差幅值分割比例。
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是進(jìn)行線性預(yù)測(cè)的函數(shù),包括短時(shí)預(yù)測(cè)系數(shù)和長(zhǎng)時(shí)預(yù)測(cè)系數(shù)都會(huì)在這里被計(jì)算出來。其中,LPC?系數(shù)會(huì)被轉(zhuǎn)化成為?LSF?系數(shù),LSF?系數(shù)經(jīng)過量化、反量化后還原成?LPC?系數(shù),用于隨后的信號(hào)重建函數(shù)?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是編碼模塊前的重建分析函數(shù)。其思想是?Analysis by sythesis,即在這個(gè)函數(shù)里,會(huì)有一個(gè)模擬的解碼器,使用上述線性預(yù)測(cè)參數(shù)、增益、量化殘差等對(duì)語音信號(hào)進(jìn)行重建,重建的信號(hào)會(huì)直接和當(dāng)前編碼信號(hào)進(jìn)行比較,通過噪聲整形、隨機(jī)殘差擾動(dòng)等方法,使模擬解碼器內(nèi)的重建信號(hào)和輸入信號(hào)的誤差盡量小。這樣就可以使得真正解碼器的解碼信號(hào)盡量逼近原始信號(hào)。
該函數(shù)進(jìn)行分析的模式有兩種,SKP_Silk_NSQ?和?SKP_Silk_NSQ_del_dec。這兩者最大的不同是,SKP_Silk_NSQ_del_dec?使用了Delay-Decision,其復(fù)雜度要高于?SKP_Silk_NSQ。但因?yàn)?Delay-Decision?的本質(zhì)是把?Silk?中各個(gè)殘差點(diǎn)的標(biāo)量量化轉(zhuǎn)化為了32個(gè)點(diǎn)的矢量量化,所以其效果比較好。因此Silk默認(rèn)使用的是?SKP_Silk_NSQ_del_dec,本文只對(duì)該默認(rèn)函數(shù)進(jìn)行分析。
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 */ )在該函數(shù)內(nèi),核心操作有以下幾步:
1)通過?Agora_Silk_DelDec_Rewhitening、Agora_Silk_DelDec_Rewhitening_Side?和?SKP_Silk_nsq_del_dec_scale_states等函數(shù)初始化合成碼流和兩個(gè)多描述碼流的狀態(tài),準(zhǔn)備在編碼器內(nèi)進(jìn)行各條碼流的模擬解碼。
2)接下來在編碼前的操作都是在?SKP_Silk_md_noise_shape_quantizer_del_dec中完成的,該函數(shù)完成了所有解碼分析的操作,分析的第一步是在?SKP_Silk_md_noise_shape_quantizer_del_dec?中使用各個(gè)量化后的參數(shù)進(jìn)行殘差的求取。
3)求取殘差后,區(qū)別于Silk會(huì)對(duì)單殘差進(jìn)行分析,SOLO?會(huì)使用特殊的增益DeltaGains_Q16?將殘差按子幀劃分為兩條子幀能量互補(bǔ)的殘差流,相鄰子幀所進(jìn)行增益分配的方式是相反的。
4)隨后,SOLO?會(huì)在?Agora_Silk_RDCx1中計(jì)算兩條流的兩種不同量化方式的誤差并將累積誤差保存起來。隨后,在?Agora_Silk_CenterRD中,SOLO?會(huì)計(jì)算兩條殘差兩兩組合起來的合成殘差與實(shí)際殘差的誤差,并依據(jù)此誤差和兩條流各自的誤差計(jì)算出一個(gè)加權(quán)誤差,該加權(quán)誤差的累積最終決定了使用哪兩條殘差碼流作為編碼的對(duì)象。計(jì)算加權(quán)誤差所使用的權(quán)重?INTERNAL_JOINT_LAMBDA?可以進(jìn)行調(diào)整,權(quán)重越靠向合成誤差,那解碼端無丟包下兩條碼流合成的音頻的誤差就越小;權(quán)重越靠向多描述碼流的誤差,解碼端各條碼流單獨(dú)解碼的信號(hào)誤差就越小,但兩條碼流合成后的誤差可能會(huì)較大。
最終,該函數(shù)會(huì)輸出兩條用于編碼的殘差信號(hào)和一個(gè)擾動(dòng)的初始?seed,因?yàn)?seed會(huì)隨著每個(gè)時(shí)域點(diǎn)的幅值信息進(jìn)行變化,所以只需編碼該幀的擾動(dòng)初始?seed,解碼器就可以推算出該幀所有時(shí)域點(diǎn)對(duì)應(yīng)的擾動(dòng),用于后續(xù)編碼。
SOLO?的低頻部分編碼沿用了?Silk?的編碼方案(高頻部分使用了獨(dú)立的編碼方案,具體實(shí)現(xiàn)可見于前一篇?SOLO?代碼解讀),所有需要編碼的低頻信息全部都在?SKP_Silk_encode_parameters中使用?range coding?進(jìn)行編碼。range coding??編碼新增參數(shù)所需要的概率密度函數(shù)是根據(jù)大量中英文語料計(jì)算出來的,在一定程度上是編碼效率較高的概率密度函數(shù)。
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_intmd_type, /* I Use MDC or not */const SKP_int8 *q /* I Quantization indices */ )解碼模塊
在進(jìn)行了高低頻碼流分離后,攜帶低頻信息的碼流被送到低頻解碼器,低頻解碼器可以看做是兩個(gè)并行的?Silk?解碼器加上前后處理模塊。其中,前處理模塊的主要功能是根據(jù)不同收包情況對(duì)碼流進(jìn)行分割。收包情況分為四種,分別為:
1. 只收到第一條描述碼流;
2. 只收到第二條描述碼流;
3. 兩條碼流都收到;
4. 該幀對(duì)應(yīng)碼流都沒有收到。
SOLO?根據(jù)不同收包情況設(shè)置不同參數(shù),傳入?AgoraSateDecodeTwoDesps?進(jìn)行解碼。
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_intdesp_type,SKP_int decBytes[] /* O Used bytes */ )在該函數(shù)里,通過?SKP_Silk_decode_parameters可以從碼流中解碼出增益、線性預(yù)測(cè)系數(shù)以及殘差信號(hào)等重建音頻所需要的信息。需要注意的是,在前兩種收包情況下,解碼出的殘差并不是完整的殘差,而是兩段互補(bǔ)殘差中的一段。但因?yàn)榱硪欢位パa(bǔ)碼流沒有按時(shí)到達(dá)解碼器,所以解碼器無法獲得另一段互補(bǔ)殘差。因此,解碼出當(dāng)前殘差后,需要使用在編碼器中計(jì)算并傳輸?shù)浇獯a器的特殊增益將該殘差恢復(fù)為完整的殘差信號(hào);如果當(dāng)前收包情況是第三種,那只需要將解碼出的兩條互補(bǔ)殘差相加,即可得到完整的殘差數(shù)據(jù)。得到殘差信號(hào)后,再結(jié)合其他參數(shù)就可以進(jìn)行語音信號(hào)的重建;如果當(dāng)前收包情況是第四種,那么 SOLO 會(huì)去呼叫丟包補(bǔ)償模塊,使用上一幀的增益和線性預(yù)測(cè)系數(shù),以及隨機(jī)的殘差信號(hào)進(jìn)行補(bǔ)償幀的生成。
最后,在經(jīng)過一些和?Silk?相同的后處理后,解碼器的流程就結(jié)束了。
如果有 SOLO 相關(guān)的疑問,歡迎大家在 RTC 開發(fā)者社區(qū)(https://rtcdeveloper.com/t/topic/18020)與作者交流,如遇到問題可以在 Github 提交 issue。
掃描圖中二維碼,了解更多講師及話題信息
總結(jié)
以上是生活随笔為你收集整理的开源编解码器 SOLO 源码解读:带宽扩展与窄带编码的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: LiveVideoStackCon 一次
- 下一篇: 【WebRTC专场】WebRTC的下个1