WebRTC 音訊抗弱網技術(下)

融雲RongCloud發表於2022-07-16

上週,我們針對音訊弱網對抗技術中的前向糾錯技術、後向糾錯技術及 OPUS 編解碼抗弱網特性進行了分享。文字分享 WebRTC 使用的抗抖動模組 NetEQ。關注【融雲全球網際網路通訊雲】瞭解更多

抖動的定義和消除原理

抖動是指由於網路原因,到達接收端的資料在不同時間段,表現出的不均衡;或者說接收端接收資料包的時間間隔有大有小。

WebRTC 通過包到達時間間隔的變化來評估抖動,公式如下:

Ji 定義為時刻 i 時測量的抖動,E(T) 表示包到達時間的間隔均值,Ti 表示時刻 i 收到的包距上一次收包的時間間隔。

Ji > 0 說明資料包提前到了,這樣抖動快取區資料將會出現堆積,容易造成緩衝區資料溢位,導致端到端時間延遲增大;Ji < 0 說明資料包晚到或丟失,會增大延時;不管早到還是晚到,都可能造成丟包,增加時延。

NetEQ 通過測量包達到時間間隔,來預測包在網路中的傳輸時間延時;根據收到的包但還未播放的緩衝語音資料的大小來評估抖動的大小。

原則上通過對網路延時的測量,以其最大延時來設定抖動緩衝區的大小,然後使每個包在網路中的延時加上該包在抖動緩衝區的延時之和保持相等,這樣可以消除抖動,就可以控制語音包從抖動緩衝區以一個相對平穩的速度播放音訊資料。

下圖[1]說明了抖動消除的核心思想:

(抖動消除的核心思路)

A、B、C、D 包在傳送端分別以 30ms 間隔傳送,即分別在 30ms、60ms、90ms、120ms 處傳送;接收端接收到這些包對應的時間為 40ms、90ms、100ms、130ms;這樣它們在網路中的延時分別是 10ms、30ms、10ms、10ms;包到達的間隔分別為 50ms、10ms、30ms,即抖動。

因此可以使包 A、C、D 在抖動快取中的延時 20ms 再播放,即 A、B、C、D 的播放時間為 60ms、90ms、120ms、150ms,這樣可以保持一個平穩的間隔進行播放。

NetEQ 通過估算網路傳輸延時,根據網路傳輸延時 95% 分位來調整抖動緩衝區;這使得 NetEQ 在消除抖動緩衝影響時,兼顧最小延時。

下圖[2]是官方對 NetEQ 和其它技術在消除抖動帶來的延時對比,可以看出 NetEQ 在消除抖動時,可以保持很低的時間延時。

(NetEQ 與其它技術消除抖動延時對比)


NetEQ 及其相關模組

下圖[1]概要描述了 WebRTC 的語音引擎架構。紅色區域為 NetEQ 部分,可以看出 NetEQ 位於接收端,包含了 jitter buffer 和解碼器、PLC 等相關模組。接收端從網路收到語音包後,語音包將先進入 NetEQ 模組進行抖動消除、丟包隱藏、解碼等操作,最後將音訊資料輸出到音效卡播放。

(WebRTC 的語音引擎架構圖)

NetEQ 包含的模組見下圖[1]:

(NetEQ 模組)

NetEQ 核心模組有 MCU 模組 DSP 模組,MCU 模組負責往 jitter buffer 快取中插入資料和取資料,以及給 DSP 的操作;DSP 模組負責語音資訊號的處理,包括解碼、加速、減速、融合、PLC 等。

同時 MCU 模組從 jitter buffer 中取包受到 DSP 模組相關反饋影響。
MCU 模組包括音訊包進入到 buffer,音訊包從 buffer 中取出,通過語音包到達間隔評估網路傳輸時間延時,以及對 DSP 模組執行何種操作(加速、減速、PLC、merge)等。

DSP 模組包括解碼,對解碼後的語音 PCM 資料進行相關操作,評估網路抖動 level,向 play buffer 傳遞可播放的資料等等。 下面詳細分析 MCU 和 DSP 各相關模組。

MCU

將收到的包插入 packet_buffer_
從網路收到音訊包後,將其插入到抖動快取 packet_buffer_中,最多快取 200 個包;當快取滿了,會重新整理快取中所有的包,同時該快取最多快取近 5 秒的音訊資料,太早的會被定時清除。

若收到的包是 red 包,會將 red 包中的每個獨⽴包解析出來存入到快取佇列中,包在快取佇列中按照 timestamp 的升序進行儲存,即最近的音訊包儲存在佇列後面,老的包儲存在佇列前面。

收到音訊包後,還會更新 NackTracker 模組,該模組主要是通過包的序列號來判斷是否存在丟包,若存在丟包且重傳請求開啟,需要向傳送端發起 nack 請求,請求傳送端重發丟失音訊包。

估算網路傳輸延時

在將包插入到抖動快取中時,根據包到達時間間隔對網路延時進行估算。這裡將 WebRTC 中根據包到達時間間隔來計算網路延時,WebRTC 計算音訊網路延時,主要考慮如下幾點:

統計音訊包到達時間間隔,取其 95% 分為作為網路時延估計。

以上公式通過計算當前包和上一個包的時間差,以及 seqnum 的差值,來計算每個 seq 包占有的時間戳範圍,即 packet_per_time,然後通過當前包和上一個包收包間隔 iat_ms,來計算收包時間間隔延時,用包的個數來度量。

當正常包到達時 iat = 1,iat = 0 表示提前到了,iat = 2,表示延時一個包, 最終用包個數來衡量延遲時間。

每算出一個 iat 後,會將其插入到對應的直方圖,直方圖記錄 0-64 個包,即記錄 0-64 個包的延遲概率分佈情況。所有延遲概率和為 1。

計算出當前包的到達間隔 iat,將其插入到直方圖,並更新直方圖每個延遲的概率,每個延遲分佈乘以一個遺忘因子 f,f 小於 1, 更新過程如下:

上面公式的意義是保證每次計算出一個包到達間隔延遲後,對應增加其概率,同時對其它延時分佈的概率進行遺忘,使整個和保持為 1。最後取其 95% 分為的延遲作為目標延時,即有 95% 的延遲都小於這個目標延時。

統計包到達時間間隔最大峰值

統計多個包到達間隔峰值,最多 8 個,在一定情況下,以峰值作為網路延時估計。每次計算到包到達時間間隔 iat 和按 95% 分位計算到的目標延時(記為target_level)後,來判斷峰值。

認為該 iat 是⼀個延時峰值的條件是:

iat > target_level + threshold, 或 iat > 2 * target_level, 這裡的 threshold 為 3
引用
且距上次峰值時間間隔小於閾值 5s

當判斷該 iat 為延時峰值時,將其新增到峰值容器中,峰值容器中每個元素記錄兩個值,一個是峰值 iat 值,另一個是當前峰值距離上一次峰值的時間間隔(即為 period_ms);

當峰值容器超過了兩個,且當前時間距離上一次發現峰值時已流逝的時間(記為 eplase_time)小於峰值容器元素中記錄的最大 period_ms 的兩倍時,認為該峰值是有效的,需要從峰值容器取最大的 iat(記為 max_iat),則目標時延取值:

當 target_level 取值是 max_iat 時,該峰值的理論有效時間可到達 40s 以上,此處存在優化空間。

最小延時限制

根據設定最低時延,調整目標延時估計,保證目標延時估計不低於最小時延;最小時延是音視訊同步計算的出來的結果。目標延時小於該最小延時,會導致音視訊不同步。

目標延時不能超過 0.75 * 最大抖動緩衝區大小

WebRTC 中預設最大抖動緩衝區為 200,所以目標延時為:

即 target_level 不大於 150 個包,也就是目標延時不大於 3s 延時。

從 packet_buffer_中取包

播放執行緒嘗試每次獲取 10ms 資料來播放,要是抖動緩衝中沒有資料,則播放靜音資料。

從 packet_buffer_ 中獲取的資料首先需要經過解碼器解碼,然後再根據反饋的操作進行對應的 DSP 操作(加速、減速、丟包隱藏、PLC、融合等)。最後將 DSP 處理完的資料填入到 sync_buffer_ 中待揚聲器播放,並從 sync_buffer_ 中取 10ms 音訊資料進行播放。

計算抖動延時

根據抖動緩衝區 packet_buffer_ 和 sync_buffer_ 中未播放完的音訊資料總延時 total_dealy,來更新計算抖動快取延時 filtered_level

若經過加減速操作,需要從 filtered_level 中消除加減速操作後引入的延時變化,如下所示:

獲取對應的操作

下圖中的 packet_buffer_ 和 sync_buffer_ 中未播放的資料大小可以理解為抖動緩衝區大小,從圖中可看出資料從 packet_buffer_ 取出後,經解碼器解碼、DSP 處理,最後進入 sync_buffer_中待播放。

low_limit = 3/4 *target_level
high_limit = max(target_level, low_limit + window_20ms


(抖動緩衝區)

但進行何種 DSP 操作處理呢?

NetEQ 將根據 packet_buffer_和 sync_buffer_中未播放的音訊資料總延時(記為 total_delay),以及 sync_buffer_ 中最後待播放的音訊資料的時間(記為 endtimestamp),和 packet_buffer_首包的時間(記為 avaibleTimestamp),以及 target_level 和 filtered_level 的關係,來綜合判斷接下來執行何種 DSP 操作。

下面將對幾種核心的 DSP 處理操作條件進行簡要說明:

norml 操作

滿足以下條件之一,將執行 normal 操作

  1. 若原本要做 expand 操作,但是 sync_buffer_ 待播放的資料量大於 10ms。
    引用
  2. 連續 expand 操作次數超過閾值。
    引用
    3.當前幀和前一幀都正常到達,且 fitlered_level 在 low_limit 和 higth_limit 之間。
    引用
  3. 上一次操作是 PLC,當前幀正常,但當前幀來得太早,執行 normal 操作。
    引用
  4. 當前幀和前一幀都正常到達, 原本通過 filtered_level > high_limit 判斷,要加速操作,但是 sync_buffer_ 待播放的音訊資料大於小於 30ms。

expand 操作條件

當前資料包丟失或者還未到達時,同時在 sync_buffer_ 待播放的音訊資料小於 10ms 時,則滿足下面 4 個條件中任何一個,都將執行 expand 操作。

  1. packet_buffer_ 沒有可獲取的音訊資料。
    引用
    2.上一次是 expand 操作,且當前 total_delay < 0.5* target_level。
    引用
  2. 上一次是 expand 操作,且當前包(即availbeTimestamp - endtimestamp 大於一定閾值)來的太早,同時當前 filtered_level 小於 target_level。
    引用
  3. 上一次包非 expand(加速、減速或 normal),且 availbeTimestamp - endtimestamp >0,即中間存在丟包。

加速操作

上一個包和當前包都正常到達,filtered_level 大於 hight_limit,且 sync_buffer_ 中待播放的資料量大於 30ms。

減速操作

上一個包和當前包都正常到達,filtered_level 小於 low_limit。

merge 操作

上一次為 expand 操作,當前包正常到達。

DSP

基音[3]

基音是指發濁音時聲帶振動所引起的週期性對應的訊號基本諧波,基音週期等於聲帶振動頻率的倒數。一般的聲音都是由發音體發出的一系列頻率、振幅各不相同的振動複合而成的。這些振動中有一個頻率最低的振動,由它發出的音就是基音,其餘為泛音。基音攜帶了大部分能量,決定了音高。

基音的週期提取,一般使用短時自相關函式,因為自相關函式一般在基於週期上表現出有極大值的特點。在 NetEQ 的 DSP 訊號處理中,基音的提取是一個至關重要的步驟。

語音的拉伸[4]

對語音的拉伸變速,有時域方法和頻域方法。時域方法計算量相對頻域方法少,適合 VoIP 這種場景;頻域方法適合頻率激烈變化的場景,如音樂。

NetEQ 中對語音的拉伸(加速或減速)使用的是 WSOLA 演算法,即通過波形相似疊加的方法來變速;該演算法在拉伸語音時能夠保證變速不變調。同時該演算法是時域演算法,對語音有比較好的效果,下圖是其大致原理和流程:

(WSOLA 演算法原理)

WSOLA 演算法大致流程:

解碼

從 packet_buffer_ 中獲取資料,正常解碼,解碼資料儲存到 decoded_buffer_ 中。

加速

NetEQ 中,當 sync_buffer_ 和 packet_buffer_ 中的待播放資料總延時堆積過多,且前一幀和當前幀都正常時,需要通過加速播放操作,來降低抖動緩衝區待播放的資料量,以達到降低時延的目的,否則容易導致抖動緩衝區溢位,出現丟包。而加速是要求變速不變調的,WSOLA 演算法通過尋找相似波形並對相似波形進行疊加來實現變速目的。

這裡對 NetEQ 中的加速過程做簡要描述:

⾸先做加速要求最少需要 30ms 資料,資料一般來源於從 decoder_buffer_, 當 decoded_buffer_中得的資料不足 30ms,需要從 sync_buffer_中借一部分資料,湊滿 30ms。

30ms 資料對應 240 個樣本,將這 240 個樣本下采樣到 110 個樣本;將下采樣 110 個樣本分成兩部分 seq1 和seq2,seq2 為固定末尾 50 個樣本,即[60,110]; seq1 也為 50 個樣本,滑動範圍[0,100],計算 seq1 和 seq2 的自相關性。

根據計算出來的自相關性結果,以拋物線擬合來找到自相關性峰值和位置,極大值出現的位置,即為上圖中的 offset,即 seq1 視窗向左滑動的距離,所以基音週期 T = offset + 10。

在要加速的 30ms 樣本中,取 15ms 和 15ms - T 這兩個基音週期訊號,分別記為 XY 計算這兩個基音週期訊號的匹配度。

當 best_correlation 大於 0.9 時,則執行將兩個基音週期訊號合併為⼀個基音週期訊號,起到加速作用

加速完的資料,如下圖的 output 將儲存到 algorithm_buffer_中,要是之前因為 decoded_buffer_中的資料量不夠,從 sync_buffer_中借了部分資料,則需要從 algorithm_buffer_中將借的資料 copu 回 sync_buffer_對應的位置(目的是保證音訊的平滑過渡),同時將 algorithm_buffer_中剩下的資料 copy 到 sync_buffer_末尾,如下圖所示:


減速

當 NetEQ 抖動緩衝中待播放的資料延時水平小於目標延時下限時,說明待播放的資料量小,為了達到播放端的最佳音質體驗,需要將現有資料拉伸,適當增加播放資料量;這和加速是相反的操作,底層技術相同。同樣下面對其做簡要分析:

前 4 步和加速基本上是一樣的,這裡不再贅述

最後一步,是將 15ms - T 和 15ms 兩個基音週期交叉漸變合併的混合基音插入到 15ms 後一個週期前。達到增加一個基音週期資料的目的,以此來增加播放資料量,同時需要從演算法緩衝區中將借的資料返還給 sync_buffer_。

具體如下圖所示:


丟包補償

當前包丟失時,在 NetEQ 中會觸發丟包補償來預測丟失的包;丟包補償有兩種方式,一種是通過編解碼器來預測重構丟失的包,另一種是通過 NetEQ 模組來預測重構丟失的包;對應分別是 kModeCodecPlc 和 kModeExpand。

不論是何種模式,前提要求是 sync_buffer_ 中待播放的音訊資料量小於當前請求要播放的資料量,才能執行該操作;丟包補償通過最近的歷史資料,來重構 PLC 相關引數,然後通過歷史資料和 PLC 相關引數進行線性預測,恢復丟失的包,最後再疊加一定的噪聲。

當連續多次 PLC 操作時,失真將會加大,所以多次操作後,將會降低 PLC 語音能量值。

下圖是 NetEQ expand 操作的核心步驟。

(構建 PLC 引數步驟,點選檢視大圖)


(構建 PLC 包,點選檢視大圖)

PLC 操作中,首要任務是計算基音週期,這裡使用了自相關計算訊號失真計算兩種方式來計算基音週期,可通過下圖簡要說明。

A 為末端 60 個樣本訊號

B 為滑動視窗,視窗包含 60 個樣本訊號,視窗起始位置滑動範圍[0, 54]

通過計算 A 和 B 的自相關係數,得到 54 個自相關結果,對這 54 個自相關結果使用拋物線擬合找到三個最大值的位置 peak1_idx1、peak_idx2、peak_idx3 為三個自相關係數最大的值的位置

認為基音是週期訊號,極值出現的位置為週期上

故基音週期 T
T1 = peak_idx3 + 10
T2 = peak_idx2 + 10
T3 = peak_idx1 + 10


根據上面通過自相關及拋物線擬合找到的三個極值,得到了三個基音週期

取末端 20 個訊號作為 A,滑動視窗 B 也是 20 個樣本(2.5ms),B 在距 A 一個基音週期前後 4 個樣本範圍內(0.5ms)滑動

故有三個視窗滑動範圍,計算三個範圍內的視窗 A 和視窗 B 的失真度

取最小失真度三個極值,這三個極值作為通過失真最小計算的基音週期 T'

1, T'2, T'3
最小失真的衡量是以 A 和 B 對應元素差值絕對值之和最小為依據的

在得到基於自相關計算出來的三個基⾳週期 T1,T2,T3 和基於失真度最小得到的三個基⾳週期 T'1,T'2,T'3

通過比較這三對的 ratio = 自相值/失真度,當 ratio 最大時,則認為這對基音週期最佳

PLC 操作中,expand_vector0 和 expand_vector1 的計算細節如下圖:


(從歷史資料中構造 expand_vector0、expand_vector1)

構造 AR 濾波器引數,通過獲取一組(7 個)自相關值,對這組自相關值通過 LevinsonDurbin 演算法計算,預測出 AR 濾波器引數。

AR 濾波器是線性預測器,通過歷史資料來預測當前資料;在構建 PLC 包時,通過對歷史資料進行 AR 濾波,來預測丟失的包資訊。

下圖是對這個過程的簡要說明:

AR 濾波器公式如下所示:

這裡 k = 7;e(n)為預測值和實際值的誤差;AR 濾波就是通過使用最近的歷史資料來預測當前時刻資料;LevinsonDurbin 演算法通過自相性值的計算來預測 ck,使得 e(n) 最小。

自相關性越大,說明 e(n) 越小;

LevinsonbDurbin 演算法通過使用自相關性值來預測 AR 濾波器引數,可以理解為使⽤ e(n)來預測。

融合

融合操作一般是上一幀丟失了,但當前幀正常;

上一幀是通過預測生成的 PLC 包,和當前幀需要做平滑處理,防止兩種銜接出現明顯變化;

融合就是完成該功能,融合的組要過程有:

需要 202 個擴充套件樣本訊號,主要是向 sync_buffer_ 未播放的訊號借用 sync_buffer_ 未播放的訊號不足 202,通過生成足夠 PLC 資料來湊滿

通過擴充套件樣本訊號和輸入訊號,計算期它們的自相關性值,及通過拋物線擬合來獲取基音週期

對 input 資料進行 Ramp 訊號變換
對擴充套件訊號和 input 訊號部分段進行混合
從快取演算法取歸還借用的訊號資料到 sync_buffer_
將演算法緩衝區剩餘的平滑處理的訊號追加到 sync_buffer_

下圖對 NetEQ 中融合過程的簡要說明:


(基音週期和 mute_factor,點選檢視大圖)


(混合 expand 和 input 訊號,點選檢視大圖)

normal

當前幀可以正常播放,也就是可以直接將這段訊號送⼊到 sync_buffer_ 中, 但是因為上一幀可能是 expand 等,需要進行相關的平滑操作。

主要步驟如下:

將輸入訊號拷貝到演算法緩衝區生成一段 PLC 資料包

根據背景噪聲和輸入語音訊號的能量比值,計算 mute_factor

根據 mute_factor 對演算法緩衝區資料按照能量從弱到強的趨勢修正

將 expand 和演算法緩衝區的頭 1ms 音訊資料進行平滑混合,結果存在演算法緩衝區;

相關混合平滑公式如下:

將最終的演算法緩衝區資料最加到 sync_buffer_ 後

下圖對 normal 的操作流程做補充說明:

(normal 操作流程,點選檢視大圖)

NetEQ 相關 buffer

NetEQ 為了消除抖動、解碼音訊資料、對解碼資料進行 DSP 處理(加減速、PLC、融合、平滑資料),以及平滑播放等,使用了多個快取 buffer,下面是對這些 buffer 的簡要說明:

packet_buffer_: 用來接收網路中收到的音訊包資料,也可稱為抖動快取,會定時刪除當前時間 5 秒前的包,同時最多快取 200 個包,即 4 秒(這⾥以每個包 20ms 計算)。
引用
decoded_buffer_: 當播放執行緒每次獲取音訊資料來播放時,會根據目標延時和抖動快取延時來判斷是否需要從 packet_buffer_中獲取音訊資料進行解碼,解碼後的資料存放到 decoded_buffer_中。最多可快取 120ms 資料。

algorithm_buffer_: decoded_buffer_中的資料經過 DSP 處理後,將存放到該快取,一般每處理一次會清空一次。

sync_buffer_: 一般是從演算法快取拷貝過來的資料,是待播放的資料;sync_buffer_中有兩個變數,一個是 next_index,表示當前播放的位置,next_index 前的資料表示已經播放,後面的資料表示待播放;另一個是 endtimestamp, 表示 sync_buffer_中最後一個待播放的資料,也就是最近的音訊資料量。

該 buffer 可最多快取 180ms 資料,是一個迴圈快取;播放執行緒每次會從 sync_buffer_中取 10ms 資料進行播放。

關於何時從 packet_buffer_中取資料解碼說明:

一般情況下每次從 packet_buffer_中取 10ms 資料進行解碼。

當要執行 expand 操作時,但 sync_buffer_中有 10ms 以上資料,不從 packet_buffer_中取資料解碼。

當做加速操作時,若 sync_buffer_中有 30ms 以上資料,不從 packet_buffer_中取資料;或若 sync_buffer_中有 10ms 以上待播放資料,同時上次解碼了 30ms 資料,則不從 packet_buffer_中獲取。

當加速操作,若 sync_buffer_待播放資料小於 20ms,同時上次解碼的資料小於 30ms,從 packet_buffer_中獲取 20ms 資料待解碼。

當要做減速操作時,若 sync_buffer_中待播放的資料大於 30ms;或待播放的資料小於 10ms,但上一次解碼的資料多餘 30ms, 則不從 packet_buffer_中獲取資料。

當要做減速操作,sycn_buffer_中待播放的資料量小於 20ms, 且上次解碼的資料量少於 30ms,獲取 20ms 資料進行解碼。


NetEQ 能很好地跟蹤網路抖動,同時在消除抖動時保證延時儘量小,對音訊體驗提升明顯;結合上一篇關於弱網對抗的一些技術,可明顯提升音訊在弱網環境下的體驗。

參考資料:

相關文章