本文作者:聲網Agora 音頻算法工程師 趙曉涵。
聲網 Agora 在 2019 年 10 月 24 日,正式對所有開發者開源自研的抗丟包音頻編解碼器 SOLO。該編解碼器適用於需要實時音頻交互的場景,特別針對弱網對抗進行了優化,並且在相同弱網環境下 MOS 分優於 Opus。SOLO 可應用於各類 RTC 應用,並且可不與 Agora SDK 綁定使用。本文將從源碼角度解讀 SOLO 的帶寬擴展與窄帶編碼。
SOLO 源碼:https://github.com/AgoraIO-Community/Solo
- 帶寬擴展
SOLO 在 Silk 的基礎上擴展了帶寬擴展模塊,用來分別處理低頻信息(0-8kHz 採樣部分)和高頻信息(8-16kHz 採樣部分),在編碼端,兩者使用兩套耦合的分析編碼系統進行碼流生成。在解碼端,利用低頻信號和高頻信息,SOLO 可以解碼出寬頻信號。SOLO 使用帶寬擴展主要有兩個原因,首先,帶寬擴展可以讓更多的碼率分配到更重要的低頻部分,提升編碼效率;第二個原因是帶寬擴展可以減少進入到信號分析模塊的採樣點數,從而減少信號分析部分的複雜度(之前需要分析全部的信號,現在只需要分析低頻部分)。在減少了原有複雜度的前提下,SOLO 才能夠在低頻部分額外增加較多計算以選取最佳的多描述編碼狀態,讓編解碼音質達到預期。
編碼端
SOLO 編碼端的大部分操作都是在下述函數中完成的:
SKP_int32 AGR_Sate_encode_process(
SATEEncCtl *sateCtl, /* I/O SATE Encoder state */
const SKP_int16 *vin, /* I input signal */
NovaBits *bits, /* I bitstream operator */
void *skctrl,
void *hbctrl,
SKP_int16 *nBytesOut /* I encoded bits */)
首先,輸入的16kHz 採樣率的語音幀會先進入到一個正交鏡像濾波器組(QMF)里進行頻帶的劃分:
void AGR_Sate_qmf_decomp(
const spx_word16_t *xx, /* I Input signal */
const spx_word16_t *aa, /* I Qmf coefficients */
spx_word16_t *y1, /* O Output low band signal */
spx_word16_t *y2, /* O Output high band signal */
SKP_int32 N, /* I frame size */
SKP_int32 M, /* I Qmf order */
spx_word16_t *mem, /* I/O Qmf state */
SKP_int8 *stack)
該函數的輸出的是兩個時域幀,分別包含低頻信息和高頻信息。低頻信息和高頻信息會在後續分別進行處理,其中,低頻信息會通過函數 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 */)
高頻信息的編碼以線性濾波分析為基礎,同時為了減少碼率,部分依賴於低頻信號的殘差信息,因此在進行高頻信息編碼之前,需要通過下述函數提取低頻編碼信息中的殘差信息:
SKP_int SKP_Silk_SDK_Get_Encoder_Residue( void *encState,SKP_int32 *r )
高頻信息的分析和編碼在函數 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 係數,並將其轉化為編碼誤差較小的 LSP 係數:
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 係數轉化為 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 個子幀,計算各個子幀的殘差信號,並計算其對應窄帶殘差信號子幀的增益,共計4個,使用單碼本量化。量化後的 LSP index 和 gain 使用下述函數寫入獨立碼流。
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,該段碼流位於窄帶碼流之後,和窄帶碼流中的第二組多描述碼流綁定在一起組包。
解碼端
解碼器可以看做編碼器的鏡像,解碼器收到碼流後,首先會通過下述函數解碼得到 0-8kHz 採樣率的低頻信息,這部分我們稍後會做詳細解讀。
SKP_int SKP_Silk_SDK_Decode(
void* decState, /* I/O: State */
SKP_SILK_SDK_DecControlStruct* decControl, /* I/O: Control structure */
SKP_int lostFlag, /* I: 0: no loss, 1 loss */
const SKP_uint8 *inData, /* I: Encoded input vector */
const SKP_int16 nBytesIn[], /* I: Number of input Bytes */
SKP_int16 *samplesOut, /* O: Decoded output */
SKP_int16 *nSamplesOut /* I/O: Number of samples */)
隨後,解碼器通過下述函數得到低頻殘差信息以用來解碼 8-16kHz 的高頻信息。
SKP_int SKP_Silk_SDK_Get_Decoder_Residue(void *decState, SKP_int32 *r)
同時,解碼器會使用以下函數來進行高頻信息的解碼。
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 */
)
該函數內的處理整體上可以分成兩種 case,第一種是沒有正常接收到包含高頻信息的多描述碼流,這種情況下會復用上一幀解碼出的 LSP index 和子幀增益;如果正常接收到了包含高頻信息的多描述碼流,則會從 4 bytes 的高頻信息中解碼、反量化出所需的 LPC 濾波器係數和 4 個子幀增益。
恢復高頻信號使用的殘差信號是乘上子幀增益後的低頻殘差信號。使用高頻殘差再加上高頻 LPC 係數,通過以下函數就可以解碼得到高頻信號。
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 */)
隨後,低頻信息和高頻信息會進入到以下函數中,進行高低頻的合成,函數輸出的是 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)
至此,解碼端就完成了將窄帶信號擴展成寬頻信號的操作。
- 窄帶編碼
編碼模塊
SOLO 的窄帶編碼入口函數是 SKP_Silk_SDK_Encode。
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 */
)
在該函數內,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_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_noise_shape_analysis_FLP 是用來進行噪聲整形分析的函數,噪聲整形可以通過調整量化增益,使得量化噪聲隨著原始信號能量一起起伏。這樣利用掩蔽效應,就難以感知到量化噪聲。在這個函數裡,除了 Silk 原有的增益控制,SOLO 還有著一套自己的增益計算系統,其邏輯和 Silk 原有增益控制相似,部分參數細節不同。因為在 SOLO 里是雙流編碼,所以 SOLO 重新進行了碼率分配,並根據所分配碼率,計算出當前各個碼流的理論 SNR。隨後,該 SNR 會用於後續增益的計算,該增益用來控制後續處理殘差信號時的殘差幅值分割比例。
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 */
)
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_NSQ 和 SKP_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_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 */
)
在該函數內,核心操作有以下幾步:
1)通過 Agora_Silk_DelDec_Rewhitening、Agora_Silk_DelDec_Rewhitening_Side 和 SKP_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 編碼新增參數所需要的機率密度函數是根據大量中英文語料計算出來的,在一定程度上是編碼效率較高的機率密度函數。
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 */
)
解碼模塊
在進行了高低頻碼流分離後,攜帶低頻信息的碼流被送到低頻解碼器,低頻解碼器可以看做是兩個並行的 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_intdesp_type,
SKP_int decBytes[] /* O Used bytes */
)
在該函數裡,通過 SKP_Silk_decode_parameters可以從碼流中解碼出增益、線性預測係數以及殘差信號等重建音頻所需要的信息。需要注意的是,在前兩種收包情況下,解碼出的殘差並不是完整的殘差,而是兩段互補殘差中的一段。但因為另一段互補碼流沒有按時到達解碼器,所以解碼器無法獲得另一段互補殘差。因此,解碼出當前殘差後,需要使用在編碼器中計算並傳輸到解碼器的特殊增益將該殘差恢復為完整的殘差信號;如果當前收包情況是第三種,那只需要將解碼出的兩條互補殘差相加,即可得到完整的殘差數據。得到殘差信號後,再結合其他參數就可以進行語音信號的重建;如果當前收包情況是第四種,那麼 SOLO 會去呼叫丟包補償模塊,使用上一幀的增益和線性預測係數,以及隨機的殘差信號進行補償幀的生成。
最後,在經過一些和 Silk 相同的後處理後,解碼器的流程就結束了。
如果有 SOLO 相關的疑問,歡迎大家在 RTC 開發者社區(https://rtcdeveloper.com/t/topic/18020)與作者交流,如遇到問題可以在 Github 提交 issue。