从远端接收的音频帧,经过解头部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_sumfor (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.996int 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,则返回指定indexwhile ((sum > inverse_probability) && (index < buckets_.size() - 1)) {++index;sum -= buckets_[index];}return static_cast<int>(index);
}

到此,音频NetEq模块之插入BUFF流程就结束了,每收到一个音频包都会执行这个流程。

webRTC音频NetEq之音频包插入缓冲抖动BUFF处理过程相关推荐

  1. 解读 WebRTC 音频 NetEQ 及优化实践

    简介:NetEQ 是 WebRTC 音视频核心技术之一,对于提高 VoIP 质量有明显的效果,本文将从更为宏观的视角,用通俗白话介绍 WebRTC 中音频 NetEQ 的相关概念背景和框架原理,以及相 ...

  2. webrtc音频QOS方法一(NetEQ之音频网络延时DelayManager计算)

    一.整体思路    时间点  A  B  C  D      发送 30 60 90 120      接收 40 90 100 130      延时 null 50 10 30 不像视频一帧数据那 ...

  3. WebRTC 中的基本音频处理操作

    在 RTC,即实时音视频通信中,要解决的音频相关的问题,主要包括如下这些: 音频数据的采集及播放. 音频数据的处理.主要是对采集录制的音频数据的处理,即所谓的 3A 处理,AEC (Acoustic ...

  4. WebRTC Native M96 回调音频裸数据IAudioFrameObserver--采集和播放语音混音后的数据(onMixedAudioFrame)

    此前已经说道,通过注册回调,给上层APP抛音频裸数据: <WebRTC Native M96 SDK接口封装–注册语音观测器对象获取原始音频数据registerAudioFrameObserve ...

  5. 【Android RTMP】安卓直播推流总结 ( 直播服务器搭建 | NV21 图像采集 | H.264 视频编码 | PCM 音频采集 | AAC 音频编码 | RTMP 包封装推流 )

    文章目录 一. 安卓直播推流专栏博客总结 二. 相关资源介绍 三. GitHub 源码地址 四. 整体 Android 直播推流数据到服务器并观看直播演示过程 Android 直播推流流程 : 手机采 ...

  6. United Plugins Total Bundle for Mac(联合音频插件合集包)

    Plugins Total Bundle是一款由多个音频插件厂商或团队共同发布的联合音频插件合集包,这款插件包含有19种来自不同团队的音频效果器.这些效果器能够满足大家各类风格的音频效果处理. ​ U ...

  7. Web网页设计作业记录:音频和视频文件的插入

    Web网页设计作业记录:音频和视频文件的插入 Task1:将mp4文件插入作为背景音频 问题记录和疑惑: Task2:插入mp3音频和封面,保留播放控件 Task3:插入mp4,要求两种播放方式,出现 ...

  8. 【音频分离】python包安装方法以及音频分离

    pydub库安装 https://www.php.cn/python-tutorials-424614.html pydub安装路径:https://github.com/jiaaro/pydub 报 ...

  9. ijkplayer播放器剖析(四)音频解码与音频输出机制分析

    ijkplayer播放器剖析系列文章: ijkplayer播放器剖析(一)从应用层分析至Jni层的流程分析 ijkplayer播放器剖析(二)消息机制分析 ijkplayer播放器剖析(三)音频解码与 ...

  10. 【Android RTMP】音频数据采集编码 ( 音频数据采集编码 | AAC 高级音频编码 | FAAC 编码器 | Ubuntu 交叉编译 FAAC 编码器 )

    文章目录 安卓直播推流专栏博客总结 一. 音频数据采集.编码 二. AAC 高级音频编码 三. FAAC 编码器 四. Ubuntu 18.04.4 交叉编译 FAAC 编码器 安卓直播推流专栏博客总 ...

最新文章

  1. C# WinForm 弹出模式窗口操作滚动条
  2. DS-5/RVDS4.0变量初始化错误
  3. MFC改变static text颜色
  4. redis的那种目录结构能新建么_Serverless 解惑——函数计算如何访问 Redis 数据库...
  5. MongoDB模糊查询-查询某月的数据
  6. demo10 关于JS Tree Shaking
  7. Android大图片裁剪解决方案
  8. python123反素数_初学python之路-day01
  9. Python实现好友管理系统
  10. (十一)国产密码算法
  11. 服务机器人工程师(ROS)要求汇总220331
  12. delphi 人脸比对_中控人脸/指纹机DEMO(delphi)
  13. 什么是值传递,什么是引用传递
  14. QT学习笔记-第三天
  15. 如何将「插件化」接入到项目之中?
  16. c语言图书管理系统注释,图书管理系统 C语言
  17. OCR--服务器端身份证识别系统的原理及应用
  18. 手动安装m4, autoconf, automake, libtool
  19. 大数据之电商系统基本概念
  20. CRMEB-知识付费系统程序配置—阿里云购买产品和和阿里云key配置

热门文章

  1. 精华蚂蚁系统(解决旅行商 TSP问题)
  2. java 动态表单设计
  3. 微信小程序使用sass
  4. 齐齐哈尔大学计算机调剂,齐齐哈尔大学2020年硕士研究生调剂信息
  5. 新疆上半年工业品价格总水平创十七年新低
  6. android手机rom物理存储器,手机ROM/RAM的区别
  7. C++ 十进制转换为十六进制 ,十进制转换为二进制,十六进制转换为十进制
  8. PSD是什么文件格式
  9. 1156 Sexy Primes (20 point(s)) PAT 素数
  10. 数据库实验3 表、ER图、索引和视图的基础操作