淺談 WebRTC NetEQ

WebRTC Native 代碼裏面有很多值得學習的寶藏,其中一個就是 WebRTC 的 NetEQ 模塊。根據 WebRTC 術語表 對 NetEQ 的解釋:

A dynamic jitter buffer and error concealment algorithm used for concealing the negative effects of network jitter and packet loss. Keeps latency as low as possible while maintaining the highest voice quality.

一種動態抖動緩衝區和錯誤隱藏(丟包補償)算法,用於去除網絡抖動和數據包丟失的負面影響。在保持最高語音質量的同時,保持儘可能低的延遲。

NetEQ 其實就是音視頻處理中的 Jitter Buffer 模塊,在 WebRTC 的語音引擎中使用。這個模塊很重要,會影響播放時的體驗,同時也相當複雜。

本文源碼參考 WebRTC Native M78 版本。

抖動消除

同碼率下:抖動(J) = 平均到達間隔(接近發送間隔) - 單次到達間隔

  • J > 0:正抖動,數據包提前到達,包堆積,接收端溢出

  • J < 0 :負抖動,數據包延遲或丟包

由於網絡包的到達有快有慢,導致間隔不一致,這樣聽感就不順暢。而抖動消除就是使不統一的延遲變爲統一的延遲,所有數據包在網絡傳輸的延遲之和與抖動緩衝區處理後的延遲之和相等。

 

 

 

時間點 A B C D
發送 30 60 90 120
到達 40 90 100 130
處理後 60 90 120 150

通過處理,A、B、C、D 的播放間隔一致,播放端就聽感就會感受到延遲,但不會有卡頓。

常見的抖動緩衝控制算法有兩種:

  • 靜態抖動緩衝控制算法:緩衝區大小固定,容易實現,網絡抖動大時,丟包率高,抖動小時,延遲大。

  • 自適應抖動緩衝控制算法:計算目前最大抖動,調整緩衝區大小,實現複雜,網絡抖動大時,丟包率低,抖動小時,延遲小。

好的算法自然是追求低丟包率和低延遲。

丟包補償

丟包補償(PLC,Packet Loss Concealment)顧名思義,就是在丟包發生時,做的應對措施。主要分爲發送端的接受端的丟包補償。

發送端

  • 主動重傳:通過信令,讓發送端重新補發。

  • 被動通道編碼:在組包時做一些特殊處理,丟包時可以作依據。

    • 前向差錯糾正(FEC,Forward error correction):根據丟包前面的包信息來進行處理。
      • 媒體相關:雙發,數據包中第二個包一般用較低碼率和音質編碼的包。
      • 媒體無關:每 n 個數據包生成一個(多個)新的校驗包,校驗包能還原出這 n 個包的信息。
    • 交織:對數據包分割重排,減少單次丟包的數據量大小。

接收端

  • 插入:用固定的包進行補償
    • 靜音包
    • 噪音包
    • 重複包
  • 插值:模式匹配及插值技術生成相似的包,算法不會理解數據包具體內容,而只是從數據特徵上進行處理
  • 重構:根據編碼參數和壓縮參數生成包,與插值不同,算法使用更多數據包的信息,效果更好

整體架構

 

 

 

在 WebRTC 源碼中,NetEQ 位於語音引擎中。其他的,還有包括編解碼器,3A 算法等也很經典和通用的模塊。

 

 

 

而從聲音的處理流程中,NetEQ 在接受端靠前位置,用於處理收到的網絡數據包,並傳輸給下面的具體的音頻處理算法。

 

 

 

在 NetEQ 模塊中,又被大致分爲 MCU(Micro Control Unit,微控單元) 模塊和 DSP 模塊。MCU 主要負責做延時及抖動的計算統計,並生成對應的控制命令。而 DSP 模塊負責接收並根據 MCU 的控制命令進行對應的數據包處理,並傳輸給下一個環節。

MCU 模塊

MCU 模塊就像指揮部一樣。它在接收到數據包後,根據數據包的信息進行統計計算並分析,作出命令決策。主要包括:

網絡延時統計算法

這塊算法位於 neteq/delay_manager.cc。在接收到包時,會調用

int DelayManager::Update(uint16_t sequence_number,
                         uint32_t timestamp,
                         int sample_rate_hz)
複製代碼

將數據包的信息傳入,然後更新統計信息。主要流程如下:

  1. 計算從隊列中拉取包的時間到現在的間隔
  2. 根據包序號,包時間戳,計算延遲 iat_packets 間隔的數量
    • 正常到達:iat_packets=1
    • 亂序提前到達:iat_packets=0
    • 延遲到達的 n 個間隔:iat_packets=n
  3. 調用 CalculateTargetLevel 更新間隔(根據計算最近一段時間的延遲間隔概率,延遲峯值,進行推算)
int DelayManager::CalculateTargetLevel(int iat_packets, bool reordered)
複製代碼

抖動延遲統計算法

這塊算法位於 neteq/buffer_level_filter.cc。在取包時,將調用:

void BufferLevelFilter::Update(size_t buffer_size_samples,
                               int time_stretched_samples)
複製代碼

將當前抖動緩衝區剩餘包的數量和加減速處理過的包量傳入,然後更新統計信息。主要流程如下:

  1. 通過動態的遺忘因子(根據網絡延遲值計算),計算平滑延遲
  2. 計算加減速的影響(time_stretched_samples)

控制命令決策判定

這塊算法位於 neteq/decision_logic.cc。在取包時,將調用:

Operations DecisionLogic::GetDecision(const SyncBuffer& sync_buffer,
                                      const Expand& expand,
                                      size_t decoder_frame_length,
                                      const Packet* next_packet,
                                      Modes prev_mode,
                                      bool play_dtmf,
                                      size_t generated_noise_samples,
                                      bool* reset_decoder)
複製代碼

會根據包和前一個包之間的關係,進行判定,給出決策。主要判斷條件如下:

  • 當前幀正常+前一幀正常:根據網絡延時統計值判斷,給出正常/加速/減速決策
  • 當前幀正常+前一幀丟失:前一幀是補償產生的,所以要做平滑處理,給出正常/融合決策
  • 當前幀丟失+前一幀正常:啓用丟包補償
  • 當前幀丟失+前一幀丟失:連續丟包補償

DSP 處理

變速不變調

代碼位於 neteq/time_stretch.cc 中:

TimeStretch::ReturnCodes TimeStretch::Process(const int16_t* input,
                                              size_t input_len,
                                              bool fast_mode,
                                              AudioMultiVector* output,
                                              size_t* length_change_samples) 
複製代碼

實現變速不變調,進行語音時長調整,是能進行加減速控制的基礎。WebRTC NetEQ 中使用了 WSOLA 算法,但由於此算法過於複雜,以筆者的專業知識無法完全理解,感興趣見 文章

正常

代碼位於 neteq/normal.cc 中:

int Normal::Process(const int16_t* input,
                    size_t length,
                    Modes last_mode,
                    AudioMultiVector* output) 
複製代碼

數據正好符合播放要求,沒有什麼額外處理,但要考慮上一次包是否爲補償的包,若是則進行平滑處理。

加速

代碼位於 neteq/accelerate.cc 中:

Accelerate::ReturnCodes Accelerate::Process(const int16_t* input,
                                            size_t input_length,
                                            bool fast_accelerate,
                                            AudioMultiVector* output,
                                            size_t* length_change_samples) 
複製代碼

在抖動延遲過大時,在不丟包的情況下儘量減少抖動延遲。因爲這時候數據包累計多,爲了儘快消耗數據包,將數據包播放時長縮短。

減速

代碼位於 neteq/preemptive_expand.cc 中:

PreemptiveExpand::ReturnCodes PreemptiveExpand::Process(
    const int16_t* input,
    size_t input_length,
    size_t old_data_length,
    AudioMultiVector* output,
    size_t* length_change_samples) 
複製代碼

減速則相反,在網絡狀況不好時,丟包較多,爲了連續性,延長等待網絡數據的時間。因爲這時候數據包累計少或沒有,爲了爭取等待新的網絡數據包的時間,將數據包的播放時長拉長。

融合

代碼位於 neteq/expand.cc 中:

Expand::Expand(BackgroundNoise* background_noise,
               SyncBuffer* sync_buffer,
               RandomVector* random_vector,
               StatisticsCalculator* statistics,
               int fs,
               size_t num_channels)
複製代碼

當上一次播放的幀與當前解碼的幀不是連續的情況下,需要來銜接和平滑一下。會讓兩個數據包一部分播放時間重疊,讓過度更自然。

丟包補償

代碼位於 neteq/expand.cc 中:

Expand::Expand(BackgroundNoise* background_noise,
               SyncBuffer* sync_buffer,
               RandomVector* random_vector,
               StatisticsCalculator* statistics,
               int fs,
               size_t num_channels)
複製代碼

在當前幀丟失時,丟包補償會參考之前最新的一些樣本,通過線性預測重構生成數據,並更新爲下次補償做參考。但由於此算法過於複雜,以筆者的專業知識無法完全理解,感興趣見 文章.

緩衝區

整個 NetEQ 模塊處理過程中,有以下幾個緩衝區:

抖動緩衝區

位於 neteq/neteq_impl.h 的 NetEqImpl 類中的 Dependencies 結構體中。

std::unique_ptr<PacketBuffer> packet_buffer
複製代碼

用於存儲網絡的音頻數據包。

解碼緩衝區

位於 neteq/neteq_impl.h 的 NetEqImpl 類中。

std::unique_ptr<int16_t[]> decoded_buffer_ RTC_GUARDED_BY(crit_sect_);
複製代碼

用於存儲解碼後 PCM 數據。

算法緩衝區

位於 neteq/neteq_impl.h 的 NetEqImpl 類中。

std::unique_ptr<AudioMultiVector> algorithm_buffer_ RTC_GUARDED_BY(crit_sect_);
複製代碼

用於存儲 DSP 處理後的數據。

語音緩衝區

位於 neteq/neteq_impl.h 的 NetEqImpl 類中。

std::unique_ptr<SyncBuffer> sync_buffer_ RTC_GUARDED_BY(crit_sect_);
複製代碼

其實就是算法緩衝區的數據複製,增加了已播放位置分割標識。

進包處理

進包處理流程代碼位於 neteq/neteq_impl.cc 中的 InsertPacket 方法中,此方法調用了真正處理的內部方法:

int NetEqImpl::InsertPacketInternal(const RTPHeader& rtp_header,
                                    rtc::ArrayView<const uint8_t> payload,
                                    uint32_t receive_timestamp)
複製代碼

總體流程如下:

  1. 將數據放入局部變量 PacketList 中
  2. 處理 RTP 包邏輯
    • 轉換內外部時間戳
    • NACK(否定應答) 處理
    • 判斷冗餘大包(RED)並解爲小包
    • 檢查包類型
    • 判斷並處理 DTMF(雙音多頻) 包
    • 帶寬估算
  3. 分析包
    • 去除噪音包
    • 解包頭獲取包的信息
    • 計算除糾錯包和冗餘包以外的正常語音包數量
  4. 將語音包插入 PacketBuffer(抖動緩衝區)

出包處理

出包處理流程代碼位於 neteq/neteq_impl.cc 中的 GetAudio 方法中,此方法調用了真正處理的內部方法:

int NetEqImpl::GetAudioInternal(AudioFrame* audio_frame,
                                bool* muted,
                                absl::optional<Operations> action_override)
複製代碼

總體流程如下:

  1. 檢查靜音狀態,直接返回靜音包
  2. 根據當前幀和前一幀的接收情況獲取控制命令決策
  3. 若非丟包補償,進行解碼,放入解碼緩衝區
  4. 進行靜音檢測(Vad)
  5. 根據命令決策,將解碼緩衝區進行處理,放到算法緩衝區(AudioMultiVector)
  6. 將算法緩衝區的數據拷貝到語音緩衝區(SyncBuffer)
  7. 處理並取出 10ms 的語音數據輸出

拓展 - 如何設計 Jitter Buffer

根據 WebRTC 的 NetEQ 模塊,筆者總結了以下設計 Jitter Buffer 需要注意的幾點:

  • 接口 - putPacket/getPacket
  • 包處理 - 類型判斷/分割爲播放幀
  • 亂序接收處理 - Sequence/Timestamp
  • 抖動/延時統計值計算
  • 取幀位置/容量變化 - 根據包接收的狀況
  • 音頻參數切換 - Reset
  • 丟包補償 - 參考歷史包進行重構

最後

WebRTC NetEQ 模塊大概介紹完畢了。正是因爲 Google 將其開源,才能一窺究竟,而大部分自研的語音引擎,或多或少都參考過 NetEQ 裏的策略。不得不說,WebRTC 真是音視頻領域的值得學習的好源碼。

本文轉載自
作者:Nemocdz
鏈接:https://juejin.im/post/5e1074546fb9a048131aa111

發佈了22 篇原創文章 · 獲贊 23 · 訪問量 13萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章