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