NetEQ使得WebRTC語音引擎能夠快速且高解析度地適應不斷變化的網絡環境,確保了音質優美且緩沖延遲最小,其集成了自適應抖動控制以及丟包隱藏算法。
WebRTC和NetEQ概述
WebRTC
WebRTC (Web Real-Time Communications) 是一項實時通訊技術,它允許網絡應用或者站點,在不借助中間媒介的情況下,建立瀏覽器之間點對點(Peer-to-Peer)的連接,實現視頻流和(或)音頻流或者其他任意數據的傳輸。WebRTC包含的這些標准使用戶在無需安裝任何插件或者第三方的軟件的情況下,創建點對點(Peer-to-Peer)的數據分享和電話會議成為可能。
WebRTC主要由語音引擎、視頻引擎和傳輸引擎組成:
- Voice Engine(音頻引擎)
- iSAC/iLBC Codec(音頻編解碼器,前者是針對寬帶和超寬帶,后者是針對窄帶)
- NetEQ for voice(處理網絡抖動和語音包丟失)
- Echo Canceler(回聲消除)/ Noise Reduction(噪聲抑制)
- Video Engine(視頻引擎)
- VP8 Codec(視頻圖像編解碼器)
- Video jitter buffer(視頻抖動緩沖器,處理視頻抖動和視頻信息包丟失)
- Image enhancements(圖像質量增強)
- Transport
- SRTP(安全的實時傳輸協議,用以音視頻流傳輸)
- Multiplexing(多路復用)
- P2P,STUN+TURN+ICE(用於NAT網絡和防火牆穿越)
- 除此之外,安全傳輸可能還會用到DTLS(數據報安全傳輸),用於加密傳輸和密鑰協商
- 整個WebRTC通信是基於UDP的
音頻引擎的工作流程:
- 發送端采集音頻信號,並進行回聲抑制、噪聲消除、自動增益控制等前處理;
- 對處理后的數據進行編碼,並封裝為數據包;
- 數據包在網絡上傳輸至接收端;
- 接收端解包,進行
NetEQ
中的抖動消除、丟包補償、解碼,另外噪聲抑制、自動增益控制等后處理,並將處理后的信號回傳到發送端以進行回聲抑制; - 處理后的音頻送入聲卡播放。
NetEQ
NetEQ主要作用:消除由於網絡環境變化造成的丟包和數據包到達間隔抖動。
目標函數:
其中,IAT
為數據包到達時間間隔。
抖動定義
接收端數據包\(i\)到達間隔與平均數據包到達間隔之差定義為抖動:
其中,\(T_i\)為第\(i\)個數據包到達間隔;\(E(T)\)為平均數據包到達間隔,當數據流為固定碼率時,\(E(T)\)應等於或接近於數據包發送間隔。
抖動是一組由時間間隔差組成的序列,假設發送端每30ms產生一個數據包,也即是數據包發送間隔為30ms,在理想情況下,接收端對於任意\(i\),\(E(T)=30ms,T_i=30ms,J_i=0\)。當\(J_i>0\)時,稱作正抖動,說明數據包提前到達,對應於數據包的堆積,雖然保證了語音的完整性,但容易造成接收端抖動緩沖區溢出並且增大了延遲。當\(J_i<0\),稱作負抖動,說明數據包延遲到達或丟包。無論是由於超時和緩存溢出均可導致數據包丟失,因此不管是何種抖動,都會增加丟包概率。
抖動通常使用抖動緩沖進行消除,待播放時再以平滑的速率從緩沖區中取出,經解壓后從聲卡中取出播放。抖動消除的理想情況是:在網絡上傳輸時延和緩沖時延相等。此時對每一個數據包的時延預判准確,既能夠按照時序播放每一個數據包,又能夠最大限度的減少緩沖時間從而減少整體延遲。抖動緩沖控制算法包括靜態抖動緩沖控制和自適應抖動緩沖控制算法。自適應抖動緩沖控制算法:緩沖區大小隨着實際網絡狀況而變化,接收端將當前收到的數據包延遲和算法中保存的延遲信息比較,而從調整當前緩沖區大小,NetEQ使用的就是自適應抖動控制。
丟包隱藏
丟包隱藏又稱丟包補償(Packet Loss Compensation, PLC)。所謂丟包隱藏就是試圖產生一個相似的音頻數據包或噪聲包以替代丟失的數據包,基於音頻的短時相似性,在丟包率低時(小於15%)盡可能提升音質。
- 基於插入。插入一個填充包來修復丟包,而填充包一般很簡單,比如插入靜音包、噪聲包或者簡單重復前面的包。優勢:實現簡單;缺點:恢復效果差,沒有利用其它語音信息重建信號。
- 基於重構。通過丟包前后的解碼信息重構一個數據包,重構修補技術使用壓縮算法來獲得編碼參數。優勢:合成的丟失包效果最好;缺點:計算量最大。
- 基於插值。通過模式匹配和插值技術創建與丟失包相似的數據包。該方法考慮了音頻的變化信息。比基於插入的方法效果要好,實現難度要大。
NetEQ模塊
NetEQ主要包含MCU和DSP兩個模塊:
- 微處理單元(Micro Control Unit, MCU),主要作用是根據當前情況做出相應動作,具體而言就是安排數據包的插入並控制數據包的輸出。數據包的插入主要是確定從網絡中到達的數據包在緩沖區的插入位置,而控制數據包的輸出則是需要考慮什么時候輸出數據以及輸出哪一個數據。
- 數字信號處理(Digital Signal Process, DSP),主要作用是將MCU中提取到的數據包進行信號處理, 包括解碼,加減速,丟包補償,融合等。
NetEQ基本輸入輸出
本單元主要介紹NetEQ輸入數據包和輸出音頻數據,以及4種數據緩沖區。
NetEQ存取數據包的接口
NetEQ有兩個比較重要對外的接口,一是向NetEQ模塊插入從網絡取得的RTP數據包interface/neteq.h/InsertPacket()
,二是從NetEQ模塊提取處理后的PCM音頻數據interface/neteq.h/GetAudio()
。
向NetEQ中送入數據包
在neteq_impl.cc
中,NetEQ存數據包的具體實現是InsertPacket()
,而大部分工作由InsertPacket()
調用的InsertPacketInternal()
完成。主要是初始化NetEQ(第一次時調用,如刷新緩沖區,重置timestamp_scaler
,更新編解碼器等),更新RTCP統計信息,解析RTP
數據包,插入到抖動緩沖區PacketBuffer
等。
InsertPacket()
函數簽名:
int NetEqImpl::InsertPacket(const RTPHeader& rtp_header,
rtc::ArrayView<const uint8_t> payload,
uint32_t receive_timestamp)
另外,在解析網絡的RTP數據包時,進行了時間戳的轉換。主要解決了RTP采用的外部時間戳和代碼內使用的內部時間戳不一致的情況,主要由TimeStampScaler
完成時間戳的轉換:
時間戳縮放TimeStampScaler
TimestampScaler
類負責將外部時間戳轉換為內部時間戳,或者將內部時間戳轉化為外部時間戳。
timestamp_scaler.h
:
// This class scales timestamps for codecs that need timestamp scaling.
// This is done for codecs where one RTP timestamp does not correspond to
// one sample.
class TimestampScaler {
public:
explicit TimestampScaler(const DecoderDatabase& decoder_database)
: first_packet_received_(false),
numerator_(1),
denominator_(1),
external_ref_(0),
internal_ref_(0),
decoder_database_(decoder_database) {}
...
};
- 外部時間戳:外部時間戳即為RTP攜帶的時間戳,表示RTP報文發送的時鍾頻率,單位為樣本數而非真正的時間單位秒等。
- 在語音中,通常等於PCM語音的采樣率,RTP攜帶Opus編碼數據包時,時鍾頻率為固定的48kHz,但采樣率可以有很多值。
- 在視頻中,無論是何種視頻編碼,外部時間戳(時鍾頻率)都設置為固定的90kHz。
- 內部時間戳:WebRTC使用的時間戳。
外部時間戳轉換為內部時間戳時就是將外部時間戳按照采樣率縮放。假設初始內部時間戳為0,則:
timestamp_scaler.cc
:
numerator_ = info->SampleRateHz();
...
denominator_ = info->GetFormat().clockrate_hz;
...
const int64_t external_diff = int64_t{external_timestamp} - external_ref_;
...
internal_ref_ += (external_diff * numerator_) / denominator_;
反之,將內部時間戳轉換為外部時間戳就是按照采樣率擴大。假設初始外部時間戳為0,則:
const int64_t internal_diff = int64_t{internal_timestamp} - internal_ref_;
return external_ref_ + (internal_diff * denominator_) / numerator_;
從NetEQ中取處理后的音頻數據
在neteq_impl.cc
中,NetEQ取音頻數據的具體實現由GetAudio()
提供,大部分工作由GetAudioInternal()
完成。主要是獲取MCU決策,解碼,VAD檢測,DSP信號處理,將處理完成后的音頻數據存入語音緩沖區SyncBuffer
,更新背噪參數,更新已播放時間戳等。
主要流程:
- 獲得下一步的操作operation;
- 根據operation提取數據到解碼緩沖區(
decoder_buffer_
); - 處理后的數據存入算法緩沖區(
algorithm_buffer_
); - 將算法緩沖區的數據復制到語音緩沖區(
sync_buffer_
); - 將語音緩沖區中的數據提取到
output
。
GetAudio()
函數簽名:
int NetEqImpl::GetAudio(AudioFrame* audio_frame, bool* muted)
NetEQ的4個緩沖區
-
抖動緩沖區
暫存從網絡獲得的音頻數據包。
packet.h
:// Struct for holding RTP packets. struct Packet { ... uint32_t timestamp; uint16_t sequence_number; uint8_t payload_type; // Datagram excluding RTP header and header extension. rtc::Buffer payload; Priority priority; std::unique_ptr<TickTimer::Stopwatch> waiting_time; std::unique_ptr<AudioDecoder::EncodedAudioFrame> frame; ... }; typedef std::list<Packet> PacketList;
packet_buffer.h
:// This is the actual buffer holding the packets before decoding. class PacketBuffer { public: enum BufferReturnCodes { kOK = 0, kFlushed, kNotFound, kBufferEmpty, kInvalidPacket, kInvalidPointer }; ... };
實現抖動緩沖區的類
PacketBuffer
使用一個整數型max_number_of_packets_
表示抖動緩沖區所容納的最多網絡包的數量,使用一個typedef std::list<Packet> PacketList
成員變量保存數據包。 -
解碼緩沖區
抖動緩沖區中的數據包通過解碼器解碼成為PCM原始音頻數據,暫存到解碼緩沖區。
neteq_impl.h
:std::unique_ptr<int16_t[]> decoded_buffer_ RTC_GUARDED_BY(crit_sect_);
size_t decoded_buffer_length_ RTC_GUARDED_BY(crit_sect_);
static const size_t kMaxFrameSize = 5760; // 120 ms @ 48 kHz.
decoded_buffer_(new int16_t[decoded_buffer_length_]);
解碼緩沖區的定義是一個帶符號的16位整型數組,固定長度5760
-
算法緩沖區
NetEQ將解碼緩沖區中的數據進行拉伸、平滑處理后將結果暫存到DSP算法緩沖區。
audio_multi_vector.h
:class AudioMultiVector { public: // Creates an empty AudioMultiVector with |N| audio channels. |N| must be // larger than 0. explicit AudioMultiVector(size_t N); // Creates an AudioMultiVector with |N| audio channels, each channel having // an initial size. |N| must be larger than 0. AudioMultiVector(size_t N, size_t initial_size); ... protected: std::vector<AudioVector*> channels_; size_t num_channels_; ... };
算法緩沖區由一個
AudioMultiVector
類實現,包含std::vector<AudioVector*> channels_
和size_t num_channels_
成員變量。AudioMultiVector
通過通道數num_channels_
創建AudioVector
,一個通道對應一個AudioVector
。audio_vector.h
:class AudioVector { public: // Creates an empty AudioVector. AudioVector(); // Creates an AudioVector with an initial size. explicit AudioVector(size_t initial_size); ... };
AudioVector
實際上是封裝的類似於標准庫vector
的類,只不過大小固定。size_t begin_index_
和size_t end_index_
相當於vector
的begin()
和end()
函數,返回開始和尾部下一個對應的迭代器。 -
語音緩沖區
算法緩沖區中的數據會被塞到語音緩沖區中,聲卡每隔10ms會從語音緩沖區中提取長度為10ms(80@8kHz個樣本點)的語音數據播放。
sync_buffer.h
:class SyncBuffer : public AudioMultiVector { public: SyncBuffer(size_t channels, size_t length) : AudioMultiVector(channels, length), next_index_(length), end_timestamp_(0), dtmf_index_(0) {} ... };
語音緩沖區的實現類
SyncBuffer
繼承自算法緩沖區的AudioMultiVector
,但是多了next_index_
和end_timestamp_
兩個成員變量。next_index_
指示待播放的第一個樣本點。end_timestamp_
指示SyncBuffer
中最后一個樣本點的時間戳。
MCU決策分析
網絡延遲統計算法(target_level
,即BLo
)
本部分參見類DelayManager
-
統計數據包到達時間間隔
Iat
(以數據包個數為單位)計算方法如下:\[packet\_len\_sample=\frac{timestamp-last\_timestamp}{sequence\_number-last\_seq\_no}\\ packet\_len\_ms=\frac{1000*packet\_len\_samp}{sample\_rate\_hz}\\ iat\_packets=\frac{packet\_iat\_count\_ms}{packet\_len\_ms} \\ iat\_packets=\left\{\begin{matrix} max(iat\_packets+(last\_seq\_no-sequence\_number+1),0)\quad 亂序 \\ iat\_packets+(last\_seq\_no-sequence\_number+1) \end{matrix}\right. \]其中,
timestamp
為當前數據包時間戳,last_timestamp
為上一個數據包的時間戳,sequence_number
為當前數據包序列號,last_seq_no
為上一個數據包序列號,sample_rate_hz
為頻率(每秒多少樣本點),packet_iat_count_ms
為自上一個數據包經歷的時間。
上式中,計算Iat
時亂序和正常情況方法一致,但是要確保iat_packets
始終為正。假設發送端每30ms產生一個數據包(每一個數據包長度為30ms),0s時該數據包被發送,在90ms時接收端接收到該數據包並計算Iat
(網絡傳輸等花費了90ms),則此時Iat=90/30=3
。 -
更新直方圖,即更新
Iat
從0到64的概率分布
代碼中使用iat_vector_
向量保存該直方圖,其中每一個下標對應一個Iat
值,下標對應的元素為相應發生的概率值。-
iat_vector_
向量中的每個元素首先乘遺忘因子\(f\)iat_factor_
,使用遺忘因子對概率遺忘:\[p'_i=p_i\times f\quad i=0,1,...,64 \] -
增大本次計算到的
Iat
的概率\(p_{Iat}=p_{Iat}+(1-f)\)
這樣,在iat_vector_
中,除了Iat
對應的概率增加,其余概率都會減少 -
歸一化
iat_vector_
-
更新遺忘因子\(f\),使\(f\)為遞增趨勢,即通話時間越長,包間隔的概率分布應該越穩定。該遺忘因子
iat_factor_
應趨近於kIatFactor_
。其中kIatFactor_
為0.9993(32745@Q15)。
-
-
計算
TargetLevel
-
統計直方圖上大於95%概率的最小
Iat
值,也就是說該Iat
值能夠覆蓋到至少95%的情況,並且Iat
值最小。 -
統計
Iat
峰值Iat
峰值滿足下述兩個條件之一:\[\left\{\begin{matrix} Iat\_peak=target\_level+peak\_detection\_threshold\_ \\ Iat\_peak=2\times target\_level \end{matrix}\right. \]當發生
Iat
峰值時的計時器小於等於kMaxPeakPeriodMs
時,存儲該峰值;否則如果峰值間隔小於2*kMaxPeakPeriodMs
時,認為這是不正確的峰值,重新獲取峰值計時器並尋找下一個峰值;如果峰值間隔大於2*kMaxPeakPeriodMs
時,這有可能是網絡環境已經發生了變化,直接重置整個統計(包括計時器,歷史峰值數組等)。之后檢查峰值數值有足夠歷史數據並且歷史計時器小於
2*MaxPeakPeriod()
,如果符合要求,返回Iat
峰值歷史數據中最大的Iat
值(max_peak
)。 -
計算
target_level_
-
如果沒有尋找到Iat峰值,則
target_level_
就是概率大於95%時的Iat
-
如果尋找到
Iat
峰值:\[target\_level=max(target\_level,max\_peak) \]
-
該算法基本思想:有一個到達間隔時間直方圖
iat_vector
,這個直方圖每一項代表一種延遲情況。比如花費一個包時間才到達的Iat對應iat_vector
下標為1,提前到達的數據包對應的Iat統一為0,iat_vector
下標表示Iat,里面的存儲內容表示該間隔的概率。每來一個數據包就更新一下這個直方圖,增大這個數據包的Iat對應的概率,減小其它Iat的概率,確保整個概率和為1。然后求sum(iat_vector[:index])>0.95
最小的index,更新Iat峰值,結合兩者求得target_level
(即BLo
)。網絡抖動延遲反映的是網絡抖動情況,如理想情況下,每隔30ms應該收到一個數據包,現在網絡突然發生抖動,下一個數據包與之前數據包到達間隔變為35ms,這部分算法就是為了獲得target_level
值以反映該情況。實際上使用TargetLevel和PacketBuffer匹配,最好情況是TargetLevel對應的SyncBuffer的播放速度和PacketBuffer收包速度一致,既降低了延遲又保證了通話質量。(如果不一致就加減速,增加噪聲包等)
這體現了抖動消除的核心思想,即通過加減速等來實現自適應抖動緩沖區的物理設計。
-
抖動延遲統計算法(filter_current_level
,即BLc
)
本部分參見BufferLevelFilter
抖動延遲統計的是抖動緩沖區的自適應平均值,計算方法如下:
其中,buffer_size_packets
為緩沖區中的數據包個數;filtered_current_level
為當前抖動緩沖區自適應平均值,單位為數據包個數;level_factor
根據當前統計到的覆蓋95%的延遲包個數(即上述的target_level
,這里稱作target_buffer_level
)計算方式如下:
由level_factor_
計算方式可以看到,NetEQ計算抖動延遲的規則為:target_buffer_level
越大,遺忘系數level_factor
越大,而遺忘系數越大表示需要更多的樣本來計算平均值,這也就是說NetEQ為了保證網絡狀況較差時仍然能得到更加准確的抖動延遲統計信息,需要參考更多的歷史信息。
MCU控制機制
本部分參見DecisionLogic
第一步將sync_buffer
的end_timestamp
賦值給target_timestamp
,第二步是查找抖動緩沖區中的available_timestamp
(下一個包的時間戳),根據這兩個參數決定MCU從抖動緩沖區中提取數據的准確性和順序性。
MCU決策
MCU首先檢測抖動緩沖區是否為空,如果為空,發出丟包隱藏的控制命令。
-
如果檢測到
target_timestamp == available_timestamp
,就先判斷上一播放模式。如果是丟包隱藏播放kModeExpand||play_dtmf
,就發出正常播放控制命令。否則參考抖動延遲filtered_current_level
和target_level
之間的關系,再決定加速或減速播放(網絡數據包到達間隔比抖動緩沖區包延遲還大,也就是說網絡數據包遲遲不來,要減速;網絡數據包到達間隔比抖動緩沖區包延遲小,網絡數據包來的快了,要積壓在抖動緩沖區了,加速播放) -
如果檢測到
target_timestamp < avaliable_timestamp
,假設上一個模式不是kModeExpand
且實際緩沖區大小小於理論緩沖區大小時,繼續進行丟包隱藏控制命令。如果上述條件不滿足,且buffer size大於20ms,則進行kMerge
融合,buffer size小於等於20ms時仍做丟包隱藏。
也就是說當數據包正常到來,緩沖區延遲(BLc
)大於網絡延遲(BLo
),說明此時緩沖區數據累積,需要加速;否則需要減速。當需要播放的數據包沒有到來,但是BLc>BLo
,則需要merge
融合,否則一直等待,並做丟包補償expand
;當BLc
與BLo
相差不大時,可以進行正常的播放。
綜上,MCU控制命令發生的條件:
- 正常播放的枚舉為
kNormal
,必要條件為target_timestamp == available_timestamp
,即當前幀接收正常,且滿足:
-
上一個模式是丟包隱藏或者是播放DTMF;
-
上一模式不是丟包隱藏且不播放DTMF
代碼里面,稱3/4*BLo
為lower_limit,3/4*BLo+20/packet_len_ms
為higher_limit;packet_len_ms
默認為30
- 加速播放的枚舉為
kAccelerate
,加速播放的原因是播放數據正常到達,但網絡延遲已經小於抖動緩沖區的延遲,因此要加速播放。加速播放的必要條件為target_timestamp == available_timestamp
並且上一個播放模式不是丟包隱藏,且滿足以下條件之一:
其中,timescale_hold_off
初始化為\(2^5\),每次加速或減速右移一位,該參數主要是為了防止連續的加速或減速對聽感有損傷。
- 減速播放的枚舉為
kPreemptiveExpand
,又稱優先擴展。減速播放的原因是要播放的數據正常到達,但抖動延遲小於網絡延遲,就需要進行減速播放。減速播放的必要條件仍然是target_timestamp == available_timestamp
並且上一個播放模式不是丟包隱藏,且滿足:
- 丟包隱藏發生條件
丟包隱藏的枚舉為kExpand
,又稱擴展。丟包隱藏的原因是要播放的數據包還沒有被接收。MCU做出丟包隱藏主要有兩種情況:第一種情況是VoIP剛剛建立階段,還沒有數據包到達NetEQ時,MCU均做出丟包隱藏的操作。第二種情況是要播放的數據還沒有到達,但抖動緩沖區中有其它數據在緩存中。
- 融合的枚舉為
kMerge
,主要用於丟包隱藏(expand)后的數據和從抖動緩沖區中提取到的數據相銜接的過程。融合發生的必要條件是avaliable_timestamp > target_timestamp
,上一次NetEQ的播放模式為丟包隱藏且抖動緩沖區不為空,且需要滿足下列條件之一:
1)丟包隱藏的限制次數沒有到,但是目前抖動緩沖區的時延已經過大;
2)在抖動緩沖區中可以播放的數據包之前的數據還沒有補償完,但丟包隱藏超過限制次數(10次);
3)抖動緩沖區中的可播放的數據包之前的數據已經補償完;
4)抖動緩沖區中可以播放的數據幀與需要播放的數據幀相差太遠(大於100ms)。
- 未定義的枚舉為
kUndefined
,主要用於重置。用於一些意外的情況,如之前的模式返回錯誤kModeError
但包頭packet_header
不為空;如語音緩沖區的時間戳大於抖動緩沖區的時間戳,也就是說語音緩沖區中的音頻數據產生在抖動緩沖區的未來,這有可能是切換了一個新的數據流或編解碼器出現的,也應重置。
DSP模塊
基音
基音
,即發出的振動頻率最低的聲音,其余為泛音
,攜帶着聲音的大部分能量,其對應的頻率稱作基頻
,對應的周期為基音周期(pitch)
。
基於自相關函數的基音周期檢測
由於語音是非平穩信號,所以語音一般采用短時自相關函數,即:
其中,\(X\)為語音信號,\(n\)為窗函數從第\(n\)個樣本點加入,\(m\)為窗長,\(\tau\)為移動距離。短時自相關函數有以下性質:
1)如果語音信號是周期函數,周期為\(P\),即:\(X(n)=X(n+P)\),那么自相關函數也是周期函數,且周期為\(P\)。
2)當\(\tau =...,-P,0,P,...\)周期處,信號的自相關函數處於極大值。
3)自相關函數為偶函數,即\(R_n(-\tau)=R_n(-\tau)\)。
短時自相關函數進行基音周期檢測的原理就是利用自相關函數在基音周期處取得極大值的特點。
WSOLA算法
語音時長調整,就是要在不改變語音音調並保證良好音質的前提下,使語音在時間軸上被拉伸或壓縮,即所謂的變速不變調。語音時長調整算法可分為時域調整和頻域調整,時域調整以波形相似疊加(Waveform Similarity-based OverLap-Add, WSOLA
)為代表。相對於頻域算法計算量較小,而頻域調整適用於頻譜變化劇烈的波形如音樂數據。參見:《A Review of Time-Scale Modification of Music Signals》
時長變換可分為三個步驟:將音頻按幀分解;將分解好的幀重新定位;合成最終音頻。WSOLA
采用的就是這種分解合成的思想:1)輸入原始信號\(x\)和調整分析幀(adjusted analysis frame)\(x'_m\),該幀已被加窗並且拷貝到輸出信號\(y\);2)在擴展幀域\(x_{m+1}^+\)中選擇與調整分析幀\(x_m'\)的\(\tilde{x}_m\)最相似的幀\(x'_{m+1}\);3)將調整分析幀加窗並拷貝到輸出信號\(y\)。
在原始的時間伸縮調整TSM
(Time-Scale Modification)的第一步就是將信號\(x\)分解為短的調整分析幀\(x_m, m\in Z\),每個調整分析幀有\(N\)個樣本點,每個分析幀幀移\(H_a\)個樣本點:
在WSOLA
中,第\(m\)次迭代的調整分析幀定義為:
其中,\(H_a\)為幀間距;\(\Delta _m\in [-\Delta_{max}:\Delta_{max}]\),而\(\Delta _{max}\in Z\)個樣本點;\(N\)為該幀長度。如上圖5紅色實線框所示。
\(\tilde {x}_m\)定義為:
\(H_s\)可認為是伸縮后變化的長度。如上圖5藍色虛線部分。
\(x_{m+1}^+\)定義為:
尋找目標的調整幀\(x'_{m+1}\)必須在擴展幀域\(x_{m+1}^+\)內。
因此整個算法的核心思想就是在\(x'_{m+1}\)尋找與\(\tilde{x}_m\)最相似的調整幀\(x'_{m+1}\)。衡量兩個幀的相似度可以使用互相關系數:
其中,\(p\)和\(q\)為偏移\(\Delta \in Z\)個樣本點的信號。目標就是選擇最優偏移值\(\Delta _{m+1}\)以最大化\(\tilde{x}_m\)和\(x_{m+1}^+\)的互相關系數:
其中,偏移值\(\Delta_{m+1}\)就指示了調整分析幀\(x'_{m+1}\)在擴展幀域\(x_{m+1}^+\)中的位置。
一言以蔽之,與普通的Time-scale modification(TSM)
相比,WSOLA
首先確定一個區域,在這個區域內選擇一個相似的幀拼接上去。
DSP處理流程
丟包處理
expand
丟包隱藏使用語音緩沖區中最新的256個樣本數作為丟包隱藏的參考數據源,使用audio_history
指向這些樣本點。用於丟包隱藏的一幀數據長度為256個樣本而不同於正常的一幀語音數據的240(30ms@8kHz)個樣本長度,其原因是不做丟包隱藏時,NetEQ可能會選擇加速、減速、融合等處理,因此會拉伸語音長度,所選樣本數需要大於240.
丟包隱藏時,根據上一語音幀的線性預測系數LPC
建模,重建語音信號然后加載一定的隨機噪聲;連續丟包隱藏時,均使用同一個線性預測系數LPC
重建語音信號,注意這里需要減少連續重建信號間的相關性,因此丟包隱藏產生的數據包能量遞減;最后為了語音連續,需要做平滑處理。當需要進行丟包補償時,從存儲最近70ms的語音緩沖區中取出最新的一幀數據並計算該幀的LPC
系數即可。
融合處理
merge
融合操作發生在上一幀和當前數據幀不是連續的情況下,需要融合操作的平滑。
-
首先使用丟包隱藏獲得長度為(120+80+2)個樣本的補償序列
expanded_[202]
。expanded_[202]
是由Sync Buffer
中剩余樣本sync_buffer_->next_index()
及其之后的所有樣本和expand操作之后的數據expand_temp
的數據拼接而成。該部分參見Merge::GetExpandedSignal()
-
確定該補償序列從第幾個樣本開始與解碼后的數據序列相關性最大,設相關性最大時,滑動窗口已經滑動了
best_correlation_index
個樣本。該部分參見
Merge::CorrelateAndPeakSearch()
-
平滑處理
之后需要對解碼端的數據進行平滑處理,由於merge操作發生在丟包補償PLC
之后,所以DSP
的平滑系數\(\alpha\)不應為1,因此解碼后的數據都要進行平滑處理。而解碼數據分為兩部分,前一部分用於與expanded_
的數據進行混合並平滑(該部分數據記作\(B\)),后一個部分僅僅進行平滑即可(該部分數據記作\(D\))。則平滑處理可以表述為:
其中,\(B'[i]\)是解碼數據的前半部分處理得到的,用於后續與expand_
混合;\(E[i]\)是解碼數據的后半部分處理得到的,可以直接輸出到算法緩沖區。
NetEQ采用每1ms讓平滑系數遞增0.032(每毫秒處理8個樣本點,8*0.004)的方式對解碼后的數據進行平滑處理。丟包補償時采用遞減的方法對數據進行平滑,而merge
采用遞增方法進行平滑處理。接下來處理相關性最大的兩段數據:丟包補償的數據和解碼的數據,將這兩段數據混合的依據是時間上的相關性。因為丟包補償的數據先於解碼數據,因此混合的方式是,混合后最開始的數據與丟包補償的相關性最大,后面的數據與解碼數據相關性最大,也就是說遞減丟包隱藏的數據樣本,遞增解碼的數據樣本,具體而言:
其中,\(P\)為上述中找到兩序列相關性最大的開始索引best_correlation_index
;\(A\)為丟包補償產生的序列\(expand\_\);\(B'\)為3中解碼數據平滑處理后的序列。該部分參見DSPHelper::CrossFade()。
由於merge
操作在最開始的丟包隱藏中,使用了語音緩存區sync_buffer
還未播放的sync_buffer_->next_index()
及其之后的樣本,在操作完成之后需要將這部分數據復制回sync_buffer_
並且從輸出中刪除該部分數據:
// Copy back the first part of the data to |sync_buffer_| and remove it from
// |output|.
sync_buffer_->ReplaceAtIndex(*output, old_length, sync_buffer_->next_index());
output->PopFront(old_length);
// Return new added length. |old_length| samples were borrowed from
// |sync_buffer_|.
return static_cast<int>(output_length) - old_length;
正常處理
normal
正常處理發生在提取得到的數據正好符合播放要求,所以可以直接將該網絡包解碼后送入語音緩沖區中去,但是由於NetEQ的DSP
有五種處理類型,因此正常處理時還需要考慮上一次的DSP
處理類型。
為了使經過PLC補償的幀與接下來沒有丟包的幀保持語音連續,需要進行平滑處理。因此當NetEQ的上一次處理模式為expand
時,需要求出一個平滑系數\(\alpha\),對當前要進行正常處理的這幀先進行平滑處理。NetEQ對64個樣本(即8ms的數據)采用移動平均法計算平滑系數:
上式中,\(\alpha\)為DSP
的平滑系數,一般情況下置為1,經過丟包補償(PLC)
后小於1;\(\alpha_e\)為丟包補償時用的用來減少連續丟包補償時相關性的系數;\(i\)為第\(i\)個樣本;\(BGN\)為背景噪聲;\(D\)為解碼后的樣本數據。
根據平滑系數\(\alpha\)更新解碼后的數據:
int32_t scaled_signal = (*output)[channel_ix][i] *
external_mute_factor_array[channel_ix];
// Shift 14 with proper rounding.
(*output)[channel_ix][i] =
static_cast<int16_t>((scaled_signal + 8192) >> 14);
// Increase mute_factor towards 16384.
external_mute_factor_array[channel_ix] = static_cast<int16_t>(std::min(
external_mute_factor_array[channel_ix] + increment, 16384));
其中\(len\)為解碼后的數據長度,即樣本數;\(D\)為解碼后的樣本數據。
NetEQ采用每1ms使平滑系數增加0.032(代碼中叫做muted increase by 0.64 for every 20 ms (NB/WB 0.0040/0.0020@Q14).
)的方式對解碼后的數據進行平滑,最后將解碼並處理后的數據全部放在算法緩存區中。
加速播放
accelerate
加速處理主要用於加速播放,使用時機是抖動延遲累積過大時,這時網絡上的數據源源不斷的過來,數據包被累積在NetEQ中,在不丟包的情況下,解決數據包在抖動緩沖區中的累積,減少抖動延遲的關鍵措施。使用WSOLA
算法在時域上壓縮語音信號。
處理流程:
1)判斷解碼緩沖區中是否有足夠的數據(輸入數據必須大於等於30ms,也就是多於240個樣本,30ms@8kHz),如果少於30ms的數據,直接把輸入復制到輸出;否則進行第2步。該部分參見Accelerate::Process()
2)根據短時自相關函數計算經解碼的一幀數據流的基音周期。一幀語音數據長30ms@8kHz,共240個樣本點。具體的,取50個樣本作為短時自相關函數中的\(X(n+m)\),長度相同的移動窗以移位距離\(\tau\)為10開始移動,\(\tau\)最大值為100,求出相關性最大的移位距離\(\tau\),在此基礎上加上20個樣本的誤差,就得到基音周期。該部分參見TimeStretch::Process()
3)計算數據流15ms前后的兩個基音周期的相關性best_correlation
,相關性的計算公式:
其中,\(A[i],B[i]\)為15ms前后的兩個樣本點區域(即上述理論部分的\(x'_m,x'_{m+1}\))。該部分位於TimeStretch::Process()
。
4)當相關性大於kCorrelationThreshold
(0.9, 14746@Q14
)時,將兩個基音周期交叉混合后輸出,否則直接將解碼緩沖區數據移動到輸出(算法緩沖區)。
當相關性符合要求時,語音幀15ms前后的兩個基音依據時間上的相關性進行交叉混合,即:
交叉混合部分參加AudioVector::CrossFade()
。
一言以蔽之,加速處理就是將兩個基音混合成一個並取代原始的兩個基音從而縮短了語音長度。因此經過加速的語音幀,其長度縮短。
減速
PreemptiveExpand
1)判斷解碼緩沖區是否有一個語音幀,也就是decoder_buffer
至少有30ms@8kHz的樣本,即判斷解碼緩沖區是否有數據。此外,應該為減速結果提供overlap_samples_
個樣本點的空間。如果上述結果不滿足,直接將解碼緩沖區的數據移動到算法緩沖區作為輸出。否則繼續下面步驟;
2)根據短時自相關函數計算一幀語音數據的基音周期;
3)計算該語音數據15ms前后的兩個基音周期的相關性best_correlation
;
4)當相關性大於kCorrelationThreshold
時,將兩個基音周期交叉混合后輸出,否則直接將解碼緩沖區的數據移動到輸出。
減速處理和加速處理的唯一不同之處就是將兩個基音混合成一個並插入到兩個基音之間從而延長語音長度。
DSP后續處理
WebRTC語音引擎每10ms從NetEQ取10ms處理完成的語音數據傳輸到聲卡播放。在NetEQ內部,這10ms的數據由語音緩沖區sync_buffer
提供。如果語音緩沖區中未播放的數據小於10ms,就從算法緩沖區取出一定量的樣本點湊夠10ms再輸出。輸出最先開始等待播放的10ms數據,語音緩沖區保留最新播放的舊數據和經過處理的等待播放的新數據。播放和未播放的分界點由成員變量next_index_
指示。由於每個數據包語音時長30ms,而NetEQ只輸出10ms,因此並不是每次從NetEQ提取數據都會執行解碼操作。
NetEQ改進
基於語音質量評估的NetEQ,對E-Model的改進
在E-Model的基礎上加入了抖動因子\(I_j\),量化公式變為:
其中,抖動因子\(I_j\)建模為:
其中,\(T=\mathop{ln}(1+t_j)\),\(t_j\)為抖動緩沖區的大小,單位為毫秒;\(S_0\)~\(S_6\)為常數,使其抖動緩沖小於20ms時抖動因子劇烈增大,大於30ms時對數增大。
吳江銳. WebRTC語音引擎中NetEQ技術的研究[D]. 西安電子科技大學, 2013.
自適用比特率控制算法
提出了一種基於帶寬估計的自適應比特率控制算法DBLA
,主要由兩部分組成:1)在接收端,基於延遲和緩沖的帶寬估計算法;2)在發送端,基於損失的帶寬估計和比特率控制方法。
X. Tian, S. Jia, P. Dong, T. Zheng and X. Yan, "An Adaptive Bitrate Control Algorithm for Real-Time Streaming Media Transmission in High-Speed Railway Networks," 2018 10th International Conference on Communication Software and Networks (ICCSN), Chengdu, 2018, pp. 328-333.