从远端接收的音频帧,经过解头部RTP后,会首先插入到抖动buff,然后统计延迟信息,绘制延迟直方图,根据直方图计算抖动延时的参数,后续dsp的处理根据这个参数以及其他参数,来决策何种策略处理音频数据。这部分根据webrtc源码详细讲解如何插入抖动buff以及统计延迟直方图。
在webrtc中,NetEQ插入音频到抖动buff的函数为InsertPacketInternal
,传入参数为音频数据的头部信息(包括时间戳或序列号等)和净荷数据。在此处会进行一个时间戳的转换,将外部时间戳转换为内部时间戳,外部时间戳即为RTP携带的时间戳,表示RTP报文发送的时钟频率,单位为样本数而非真正的时间单位秒等。在语音中,通常等于PCM语音的采样率,RTP携带Opus编码数据包时,时钟频率为固定的48kHz,但采样率可以有很多值;在视频中,无论是何种视频编码,外部时间戳(时钟频率)都设置为固定的90kHz。内部时间戳为WebRTC使用的时间戳。如接收到的音频格式采样率是16000HZ,内部按照48000Hz处理,则需要将时间戳转换到48000HZ下的时间戳单位处理。函数如下:
uint32_t TimestampScaler::ToInternal(uint32_t external_timestamp,
uint8_t rtp_payload_type) {//external_timestamp为rtp包打包的时间戳,rtp_payload_type包类型
const DecoderDatabase::DecoderInfo* info =
decoder_database_.GetDecoderInfo(rtp_payload_type);
if (!info) {
// Payload type is unknown. Do not scale.
return external_timestamp;
}
if (!(info->IsComfortNoise() || info->IsDtmf())) {//时间戳转换的前提是非舒适噪声和dtm包
// Do not change the timestamp scaling settings for DTMF or CNG.
numerator_ = info->SampleRateHz();
if (info->GetFormat().clockrate_hz == 0) {
// If the clockrate is invalid (i.e. with an old-style external codec)
// we cannot do any timestamp scaling.
denominator_ = numerator_;
} else {
denominator_ = info->GetFormat().clockrate_hz;
}
}
if (numerator_ != denominator_) {//如果内外部处理时钟不同,则进行事件戳转换
// We have a scale factor != 1.
if (!first_packet_received_) {
external_ref_ = external_timestamp;
internal_ref_ = external_timestamp;
first_packet_received_ = true;
}
const int64_t external_diff = int64_t{external_timestamp} - external_ref_;//先确定外部进入的包事件戳增量,如果采样率是16000hz,
//一个包长度10ms=160增量,如果本端处理时钟48000hz,则本段时间戳增量=480,=160*(48000/16000)
RTC_DCHECK_GT(denominator_, 0);
external_ref_ = external_timestamp;
internal_ref_ += (external_diff * numerator_) / denominator_;//转换后的事件戳
return internal_ref_;
} else {
// No scaling.
return external_timestamp;
}
}
如果收到的是第一个包或包的源SSRC更改了,则需要初始化NetEq,清空packet_buffer_和dtmf_buffer_,更新sync_buffer_的end_timestamp_
packet_buffer_->Flush();
dtmf_buffer_->Flush();
// Update audio buffer timestamp.
sync_buffer_->IncreaseEndTimestamp(main_timestamp - timestamp_);//更新第一个包的时间戳到sync_buffer_的end_timestamp_
// Update codecs.
timestamp_ = main_timestamp;//记录timestamp_为第一个到达音频包的事件戳
处理RED类型的包以及DTMF类型的包后,将包插入到packet_buffer_之前先保存当前rtp的包类型,然后插入到packet_buffer_,代码流程如下:
int PacketBuffer::InsertPacketList(
PacketList* packet_list,//接收的音频包,即待插入的音频包
const DecoderDatabase& decoder_database,
absl::optional<uint8_t>* current_rtp_payload_type,
absl::optional<uint8_t>* current_cng_rtp_payload_type,
StatisticsCalculator* stats) {
RTC_DCHECK(stats);
bool flushed = false;
//确定当前rtp类型
for (auto& packet : *packet_list) {
if (decoder_database.IsComfortNoise(packet.payload_type)) {
if (*current_cng_rtp_payload_type &&
**current_cng_rtp_payload_type != packet.payload_type) {
// 新的舒适噪声类型
*current_rtp_payload_type = absl::nullopt;
Flush();
flushed = true;
}
*current_cng_rtp_payload_type = packet.payload_type;
} else if (!decoder_database.IsDtmf(packet.payload_type)) {
if ((*current_rtp_payload_type &&
**current_rtp_payload_type != packet.payload_type) ||
(*current_cng_rtp_payload_type &&
!EqualSampleRates(packet.payload_type,
**current_cng_rtp_payload_type,
decoder_database))) {
*current_cng_rtp_payload_type = absl::nullopt;
Flush();
flushed = true;
}
*current_rtp_payload_type = packet.payload_type;
}
//插入packet_buffer_
int return_val = InsertPacket(std::move(packet), stats);
if (return_val == kFlushed) {
// The buffer flushed, but this is not an error. We can still continue.
flushed = true;
} else if (return_val != kOK) {
// An error occurred. Delete remaining packets in list and return.
packet_list->clear();
return return_val;
}
}
packet_list->clear();
return flushed ? kFlushed : kOK;
}
具体插入packet_buffer_的函数如下:
int PacketBuffer::InsertPacket(Packet&& packet, StatisticsCalculator* stats) {
if (packet.empty()) {
RTC_LOG(LS_WARNING) << "InsertPacket invalid packet";
return kInvalidPacket;
}
RTC_DCHECK_GE(packet.priority.codec_level, 0);
RTC_DCHECK_GE(packet.priority.red_level, 0);
int return_val = kOK;
packet.waiting_time = tick_timer_->GetNewStopwatch();//获取包入buff的时间
if (buffer_.size() >= max_number_of_packets_) {//如果插入包的数量超过了buff的最大包数,则丢掉所有包
// Buffer is full. Flush it.
Flush();
stats->FlushedPacketBuffer();
RTC_LOG(LS_WARNING) << "Packet buffer flushed";
return_val = kFlushed;
}
// 获取一个迭代器,指向缓冲区中应该插入新包的位置,从列表反向查找
//从后面搜索列表,因为最可能的情况是新包应该在列表的末尾
PacketList::reverse_iterator rit = std::find_if(
buffer_.rbegin(), buffer_.rend(), NewTimestampIsLarger(packet));//反向查找当前队列,包应该插入到队列中的包时间戳或者序列号大的位置
//如果找到了位置,新包会插入到迭代器rit的右边,如果与新包时间戳相同,优先级比新入的包高,不插入
if (rit != buffer_.rend() && packet.timestamp == rit->timestamp) {
LogPacketDiscarded(packet.priority.codec_level, stats);
return return_val;
}
PacketList::iterator it = rit.base();//如果没有找到位置,新包会插入到迭代器it的左边,如果与新包时间戳,优先级比新入的包低,移除掉,插入新收到的包
if (it != buffer_.end() && packet.timestamp == it->timestamp) {//
LogPacketDiscarded(it->priority.codec_level, stats);
it = buffer_.erase(it);
}
buffer_.insert(it, std::move(packet)); // 根据时间戳或序列号的顺序将包插入到适当的位置
return return_val;
}
新包插入packe_buff后,如果不是事件戳乱序包则更新延迟信息。由DelayManager
累统计包的延时,绘制延迟直方图,更新函数如下,解析标注在代码中。计算音频包的时间长度,即一个音频包的长度是10ms或者20ms或者更长等:
时间长度packet_len(ms)= 1000*(与上一包的index时间戳差/与上一包序列号差)/采样率;
IAT直方图
IAT直方图统计2000ms内延迟统计概率,直方图划分为 2000ms/20ms = 100 个槽,编号index范围0~99,每个槽记录的是延迟为 index * 20ms 的概率,比如相对延迟50ms,则对应第2个槽,在第二个槽上增加概率值,直方图横坐标为index,如下图所示。
更新延迟信息代码如下:
int DelayManager::Update(uint16_t sequence_number,
uint32_t timestamp,
int sample_rate_hz) {
if (sample_rate_hz <= 0) {
return -1;
}
if (!first_packet_received_) {
// 第一个包到达后,开始定时,并获取序列号和时间戳,准备接收下一个包
packet_iat_stopwatch_ = tick_timer_->GetNewStopwatch();
last_seq_no_ = sequence_number;
last_timestamp_ = timestamp;
first_packet_received_ = true;
return 0;
}
int packet_len_ms;
if (!IsNewerTimestamp(timestamp, last_timestamp_) ||
!IsNewerSequenceNumber(sequence_number, last_seq_no_)) {
// 如果事件戳或序列号乱序,则保存上一次的包长信息.
packet_len_ms = packet_len_ms_;
} else {
// 根据时间戳来计算包长度
int64_t packet_len_samp =
static_cast<uint32_t>(timestamp - last_timestamp_) /
static_cast<uint16_t>(sequence_number - last_seq_no_);
packet_len_ms =
rtc::saturated_cast<int>(1000 * packet_len_samp / sample_rate_hz);
}
bool reordered = false;
if (packet_len_ms > 0) {//只有存在包长才会更新统计信息,计算包间到达时间(IAT),然后添加到包间到达时间直方图(IAT直方图)
// Inter-arrival time (IAT) in integer "packet times" (rounding down). This
// is the value added to the inter-arrival time histogram.
int iat_ms = packet_iat_stopwatch_->ElapsedMs();
int iat_packets = iat_ms / packet_len_ms;
// Check for discontinuous packet sequence and re-ordering.
if (IsNewerSequenceNumber(sequence_number, last_seq_no_ + 1)) {
// Compensate for gap in the sequence numbers. Reduce IAT with the
// expected extra time due to lost packets.
int packet_offset =
static_cast<uint16_t>(sequence_number - last_seq_no_ - 1);
iat_packets -= packet_offset;
iat_ms -= packet_offset * packet_len_ms;
} else if (!IsNewerSequenceNumber(sequence_number, last_seq_no_)) {
int packet_offset =
static_cast<uint16_t>(last_seq_no_ + 1 - sequence_number);
iat_packets += packet_offset;
iat_ms += packet_offset * packet_len_ms;
reordered = true;
}
int iat_delay = iat_ms - packet_len_ms;
int relative_delay;
if (reordered) {
relative_delay = std::max(iat_delay, 0);
} else {
UpdateDelayHistory(iat_delay, timestamp, sample_rate_hz);
relative_delay = CalculateRelativePacketArrivalDelay();
}
statistics_->RelativePacketArrivalDelay(relative_delay);
switch (histogram_mode_) {
case RELATIVE_ARRIVAL_DELAY: {
const int index = relative_delay / kBucketSizeMs;
if (index < histogram_->NumBuckets()) {
// Maximum delay to register is 2000 ms.
histogram_->Add(index);
}
break;
}
case INTER_ARRIVAL_TIME: {
// Saturate IAT between 0 and maximum value.
iat_packets =
std::max(std::min(iat_packets, histogram_->NumBuckets() - 1), 0);
histogram_->Add(iat_packets);
break;
}
}
// Calculate new |target_level_| based on updated statistics.
target_level_ = CalculateTargetLevel(iat_packets, reordered);
LimitTargetLevel();
} // End if (packet_len_ms > 0).
if (enable_rtx_handling_ && reordered &&
num_reordered_packets_ < kMaxReorderedPackets) {
++num_reordered_packets_;
return 0;
}
num_reordered_packets_ = 0;
// Prepare for next packet arrival.
packet_iat_stopwatch_ = tick_timer_->GetNewStopwatch();
last_seq_no_ = sequence_number;
last_timestamp_ = timestamp;
return 0;
}
那么如何划分延迟信息呢?
如:当一个长度len=20ms的新包到来时,统计与上一个包到达的时间差iat_ms,则延迟时间iat_delay=iat_ms-包长len,理想状态是iat_delay=0,即包长度len=时间差iat_ms。但是实际网络延迟或抖动会导致iat_delay不为0。将iat_delay记录到最大2000ms的历史延迟队列delay_history_中,如果到达的新包与队列第一个包的时间戳之差超过2000ms,则移除最早入队列的iat_delay,代码如下:
//存储包的延时信息到历史延迟队列
void DelayManager::UpdateDelayHistory(int iat_delay_ms,
uint32_t timestamp,
int sample_rate_hz) {
PacketDelay delay;
delay.iat_delay_ms = iat_delay_ms;
delay.timestamp = timestamp;
delay_history_.push_back(delay);
while (timestamp - delay_history_.front().timestamp >
static_cast<uint32_t>(kMaxHistoryMs * sample_rate_hz / 1000)) {
delay_history_.pop_front();
}
}
计算delay_history_队列中相对延迟relative_delay,因为该队列中记录的是包与包之间的延迟,延迟包括正延迟(iat_delay>0)和负延迟(iat_delay<0),相对延迟relative_delay将队列中所有延迟时间依次叠加,如果叠加值不小于0,小于0的按0计算。
int DelayManager::CalculateRelativePacketArrivalDelay() const {
int relative_delay = 0;
for (const PacketDelay& delay : delay_history_) {
relative_delay += delay.iat_delay_ms;
relative_delay = std::max(relative_delay, 0);
}
return relative_delay;
}
直方图概率值采用定点Q15计算,在这里摘录下别人对定点运算的解释:
知识:Q格式DSP处理浮点数据转换成定点运算
许多DSP都是定点DSP,处理定点数据会相当快,但是处理浮点数据就会非常慢。可以利用Q格式进行浮点数据到定点的转化,节约CPU时间。实际应用中,浮点运算大都时候都是既有整数部分,也有小数部分的。所以要选择一个适当的定标格式才能更好的处理运算。
Q格式表示为:Qm.n,表示数据用m比特表示整数部分,n比特表示小数部分,共需要m+n+1位来表示这个数据,多余的一位用作符合位。假设小数点在n位的左边(从右向左数),从而确定小数的精度
例如Q15表示小数部分有15位,一个short型数据,占2个字节,最高位是符号位,后面15位是小数位,就假设小数点在第15位左边,表示的范围是:-1<X<0.9999695 。
浮点数据转化为Q15,将数据乘以215;Q15数据转化为浮点数据,将数据除以215。
例如:假设数据存储空间为2个字节,0.333×215=10911=0x2A9F,0.333的所有运算就可以用0x2A9F表示,同理10911×2(-15)=0.332977294921875,可以看出浮点数据通过Q格式转化后是有误差的。
例:两个小数相乘,0.333*0.414=0.137862
0.333*215=10911=0x2A9F,0.414*215=13565=0x34FD
short a = 0x2A9F;
short b = 0x34FD;
short c = a * b >> 15; // 两个Q15格式的数据相乘后为Q30格式数据,因此为了得到Q15的数据结果需要右移15位
这样c的结果是0x11A4=0001000110100100,这个数据同样是Q15格式的,它的小数点假设在第15位左边,即为0.001000110100100=0.1378173828125…和实际结果0.137862差距不大。或者0x11A4 / 2^15 = 0.1378173828125
Q格式的运算
1> 定点加减法:须转换成相同的Q格式才能加减
2> 定点乘法:不同Q格式的数据相乘,相当于Q值相加,即Q15数据乘以Q10数据后的结果是Q25格式的数据
3> 定点除法:不同Q格式的数据相除,相当于Q值相减
4> 定点左移:左移相当于Q值增加
5> 定点右移:右移相当于Q减少
如何统计到IAT直方图?
首先确定相对延迟iat_delay的索引index= iat_delay / 20,20为直方图宽度20ms,然后在直方图中槽中找到index,在保证必须所有槽所对应的概率和为1的前提下,在对应index上添加概率。
首先对原始直方图中的所有概率bucket(Q30)都与遗忘因子(Q15)相乘,并统计所有概率和vector_sum,为什么会乘遗忘因子,个人理解是,所有的概率和为1,如果再添加上去,打破了1的平衡,因此要将所有index对应的概率乘以一个小于1的因子,这样就可以叠加概率而维持概率总和为1的平衡了,叠加后的概率计算:
bucket = bucket遗忘因子forget_factor_+(1-遗忘因子forget_factor_)
vector_sum =vector_sum +(1-遗忘因子forget_factor_)
如果概率总和vector_sum !=1,则需要进行补偿使其为1,算法如下:
如果vector_sum>0,则需要减去一个纠正因子correction,
如果vector_sum<0,则需要加上一个纠正因子correction。
纠正因子correction =min((vector_sum -1),index对应概率的十六分之一)
遗忘因子forget_factor_不是固定不变的,而是变化的,每次接收到一个包都需要更新forget_factor_,并逐渐收敛于0.996(初始设定),收敛方程:
forget_factor_=0.996(1- 2/延迟信息更新次数 ),随着延迟信息更新次数的逐渐增加,forget_factor_逐渐收敛于设定的0.996。
代码分析如下:
void Histogram::Add(int value) {
RTC_DCHECK(value >= 0);
RTC_DCHECK(value < static_cast<int>(buckets_.size()));
int vector_sum = 0;
// 对原始直方图中的所有概率bucket(Q15)都与遗忘因子(Q15)相乘,并统计所有概率和vector_sum
for (int& bucket : buckets_) {
bucket = (static_cast<int64_t>(bucket) * forget_factor_) >> 15;
vector_sum += bucket;
}
// 在对应index上增加概率值,forget_factor_是Q15格式,而buckets_值是Q30格式,所以在此还必须左翼<<15来转化成Q30格式
buckets_[value] += (32768 - forget_factor_) << 15;
vector_sum += (32768 - forget_factor_) << 15; // 将(1-forget_factor_)叠加到概率总和上,vector_sum也是Q30格式
// vector_sum应该为1,如果不为1 就需要补偿
vector_sum -= 1 << 30;
if (vector_sum != 0) {
// 更改前面一段的bucket值
int flip_sign = vector_sum > 0 ? -1 : 1;
for (int& bucket : buckets_) {
int correction = flip_sign * std::min(std::abs(vector_sum), bucket >> 4);
bucket += correction;
vector_sum += correction;
if (std::abs(vector_sum) == 0) {
break;
}
}
}
RTC_DCHECK(vector_sum == 0); // Verify that the above is correct.
++add_count_;
if (start_forget_weight_) {
if (forget_factor_ != base_forget_factor_) { // 更新forget_factor_,随着add_count_越来越大,forget_factor_逐渐接近base_forget_factor_=0.996
int old_forget_factor = forget_factor_;
int forget_factor =
(1 << 15) * (1 - start_forget_weight_.value() / (add_count_ + 1));
forget_factor_ =
std::max(0, std::min(base_forget_factor_, forget_factor));
RTC_DCHECK_GE((1 << 15) - forget_factor_,
((1 << 15) - old_forget_factor) * forget_factor_ >> 15);
}
} else {
forget_factor_ += (base_forget_factor_ - forget_factor_ + 3) >> 2;
}
}
至此,当前包的插入buff并延迟统计直方图统计结束,最后,还需要从直方图中计算出最终的网络延迟target_level,它能反映网络延迟的情况,target_level的计算有理由后面音频DSP处理决策,因此比较重要。
计算思路是:所有槽的值加起来是 1。 接下来遍历直方图,依次累加每个槽的概率, 直到累计值 >= 0.97, 这意味着,当前 index 代表的延时,可以覆盖 97% 的数据包, 我们将找到的 index 记为 bucket_index ,target_level >=1,且初始值为1,Q8格式,则
target_level = target_level + (bucket_index * kBucketSizeMs) / packet_len_ms_ 其中kBucketSizeMs 为直方图宽度为20ms。
计算bucket_index代码如下:
int Histogram::Quantile(int probability) {//probability是查找概率之和统计上限,这里是0.97,Q30格式
int inverse_probability = (1 << 30) - probability;
size_t index = 0; // Start from the beginning of |buckets_|.
int sum = 1 << 30; // Assign to 1 in Q30.
sum -= buckets_[index];
// 思路:从index 0到99开始累加概率值,知道概率值总和大于等于probability,则返回指定index
while ((sum > inverse_probability) && (index < buckets_.size() - 1)) {
++index;
sum -= buckets_[index];
}
return static_cast<int>(index);
}
到此,音频NetEq模块之插入BUFF流程就结束了,每收到一个音频包都会执行这个流程。