NetEQ音频DSP处理命令决策

一、NetEq音频Dsp处理命令分类
NetEQ音频DSP处理主要命令有以下集中,还有其他的一些命令文中也会一一提出:
    Normal(正常播放):数据正好符合播放要求,无需做其他信号处理,按正常流程取数据播放就行了。
    加速播放(Accelerate):在抖动延迟过大时,在不丢包的情况下尽量减少抖动延迟。这时候数据包累计多,为了尽快消耗数据包,将数据包播放时长缩短。
   减速播放(Preemptive):在网络状况不好时,丢包较多,为了连续性,延长等待网络数据的时间,这时候数据包累计少或没有,为了争取等待新的网络数据包的时间,将数据包的播放时长拉长。
   丢包补偿PLC(Expand):在当前帧丢失时,丢包补偿会参考之前最新的一些样本,通过线性预测重构生成数据,并更新为下次补偿做参考。
   merge(融合):当上一次播放的帧与当前解码的帧不是连续的情况下,需要来衔接和平滑一下。会让两个数据包一部分播放时间重叠,让过度更自然。
二、处理命令决策原理及代码流程分析
webrtc对命令做出决策是由DecisionLogic模块负责,流程如下:
1 舒适噪声状态更新,如果前一个包的模式是舒适噪声处理kModeRfc3389Cng,则舒适噪声状态更新为kCngRfc3389On,如果前一个包的模式kModeCodecInternalCng,则舒适噪声状态更新为kCngInternalOn,否则保留上一个状态不更新。
2 首先通过BufferLevelFilter模块计算抖动延时水平参数filtered_current_level_,函数代码如下:

void DecisionLogic::FilterBufferLevel(size_t buffer_size_samples) {//函数参数为抖动buff中尚未处理的音频包样本个数,如一个包20ms,采样率48000HZ,buff中还有5个包没有处理,则buffer_size_samples=480*2*5=4800
  buffer_level_filter_->SetTargetBufferLevel(
      delay_manager_->base_target_level());//先将网络延迟计算模块计算出的target_level设置到BufferLevelFilter中,target_level是以包个数为单位的,表示了网络延迟包数,值越大表示网络延迟越大

  int time_stretched_samples = time_stretched_cn_samples_;//经过播放伸缩处理的样本数,如减速播放时间戳拉伸,加速播放时间戳压缩
  if (prev_time_scale_) {
    time_stretched_samples += sample_memory_;
    timescale_countdown_ = tick_timer_->GetNewCountdown(kMinTimescaleInterval);
  }

  buffer_level_filter_->Update(buffer_size_samples, time_stretched_samples);//更新buffer_level_filter_,传入参数为抖动buff未处理的剩余样本数buffer_size_samples,经过时间戳伸缩处理过的样本数
  prev_time_scale_ = false;
  time_stretched_cn_samples_ = 0;
}

抖动延迟的水平因素平均值filtered_current_level_计算如下:
      filtered_current_level_ = level_factor_*filtered_current_level_+(1-|level_factor_|)*buffer_size_samples
其中level_factor_为网络延迟计算的target_level,level_factor_越大表示需要更多的样本数来计算平均值,这是NetEq为了保证网络状况不好的时候仍然能得到更加准确的抖动延迟统计结果,要参考更多的样本信息
filtered_current_level_的值还受到前一个处理包时伸缩的样本数影响
      filtered_current_level = filtered_current_level -time_stretched_samples;time_stretched_samples表示加速或减速播放后数据长度的伸缩变化样本数。经过加速或减速播放后,filtered_current_level_会变化福度较大,如果加速播放time_stretched_samples为正,filtered_current_level_剧烈减少,如果减速播放,time_stretched_samples为负,filtered_current_level_剧烈减增加,webrtc这样设计的目的是保证了加速和加速不会长期持续,确保了音质的舒适度,后面从代码中详细解释这句话。

void BufferLevelFilter::Update(size_t buffer_size_samples,
                               int time_stretched_samples) {
  // |level_factor_| 和|filtered_current_level_| Q8格式.
  // |buffer_size_samples|是 Q0格式.
  const int64_t filtered_current_level =
      ((level_factor_ * int64_t{filtered_current_level_}) >> 8) +
      ((256 - level_factor_) * rtc::dchecked_cast<int>(buffer_size_samples));

  filtered_current_level_ = rtc::saturated_cast<int>(std::max<int64_t>(
      0,
      filtered_current_level - (int64_t{time_stretched_samples} * (1 << 8))));//(1 << 8)的意思是filtered_current_level是Q8格式,需要将参与加减法的数据也转成Q8格式
}

3 获取期望得到的包的时间戳target_timestamp,抖动缓冲packet_buff有两种状态:
没有音频包
     抖动缓冲packet_buff中没有音频包时,视作包丢失,是将丢失包用舒适噪声替代还是PLC构造一个包,取决于舒适噪声功能是否开启以及舒适噪声类型决定,如果舒适噪声状态为kCngRfc3389On,则按kRfc3389CngNoPacket处理此丢包,如果舒适噪声状态为kCngInternalOn,则返回kCodecInternalCng处理此丢包,如果舒适噪声状态kCngOff,表明不用舒适噪声替代丢包,如果需要播放DTMP,则返回kDtmf处理命令,其他情况按丢包处理,返回kExpand处理命令,但是不可能一直会expand,如果连续expand包数超过了100个,说明可能发送端重启,则需要将之前的所有neteq相关的都要重置。
有音频包
     抖动缓冲packet_buff中有音频包时则取出第一个包的时间戳available_timestamp,如果包是舒适噪声包,则输出舒适噪声命令kRfc3389Cng或kRfc3389CngNoPacket。有音频包也分两种情况target_timestamp=available_timestamp,和target_timestamp !=available_timestamp但是available_timestamp不是过时的时间戳包。
   target_timestamp=available_timestamp表示我期望收到的包时间戳target_timestamp,在抖动缓冲区队列中存在。如果前一个包的处理命令模式为kModeExpand,那么这个包的处理按正常播放。否则判断是否需要加速或减速播放,其中涉及到抖动延迟buffer_level_packets =filtered_current_level_/packet_length_samples_,单位为包个数。保证上一次加速或减速播放的状态到这一次超过100ms时,我们才考虑加速或减速,这样避免长时间连续加减速,影响音质效果。如果抖动延时超过网络延迟上限,表示抖动buff中包累计较多,需要加速播放快速消耗掉,因此返回kAccelerate。如果抖动延时地狱网络延时的下限,表示包抖动buff中包提供给播放模块不够,为了不影响播放卡顿,需要减速播放,因此返回kPreemptiveExpand。
代码分析如下:

Operations DecisionLogic::ExpectedPacketAvailable(Modes prev_mode,
                                                  bool play_dtmf) {
  if (!disallow_time_stretching_ && prev_mode != kModeExpand && !play_dtmf) {
    int low_limit, high_limit;
    delay_manager_->BufferLimits(&low_limit, &high_limit);//首先计算网络延迟的最低和最高延时范围
    int buffer_level_packets = 0;
    if (packet_length_samples_ > 0) {
      buffer_level_packets =
          ((1 << 8) * buffer_level_filter_->filtered_current_level()) /
          packet_length_samples_;//buffer_level_filter_原本为样本数,现在转换为包个数
    }
    if (buffer_level_packets >= high_limit << 2)//如果buffer_level_packets比网络延迟的最大延迟的1/4要高,则返回kFastAccelerate
      return kFastAccelerate;
    if (TimescaleAllowed()) {//如果从上一次时间戳伸缩开始过了5*20=100ms,再判断是否需要加速或减速,确保不要长时间加速或减速播放
      if (buffer_level_packets >= high_limit)//抖动延时超过了网络延迟的最大延迟,表示抖动buff中包累计较多,需要加速播放,快速消耗掉
        return kAccelerate;
      if (buffer_level_packets < low_limit)//抖动延时超过了网络延迟的最大延迟,表示抖动buff中包累计较多,需要加速播放,快速消耗掉
        return kPreemptiveExpand;
    }
  }
  return kNormal;
}
void DelayManager::BufferLimits(int target_level,
                                int* lower_limit,
                                int* higher_limit) const {
  if (!lower_limit || !higher_limit) {
    RTC_LOG_F(LS_ERROR) << "NULL pointers supplied as input";
    assert(false);
    return;
  }

  // lower_limit单位是包个数,将lower_limit设置为.target_level的3/4与target_level-85ms/2两者最大
  *lower_limit = (target_level * 3) / 4;

  if (packet_len_ms_ > 0) {
    *lower_limit =
        std::max(*lower_limit, target_level - kDecelerationTargetLevelOffsetMs /
                                                  packet_len_ms_);
  }
//最高延迟higher_limit设置为target_level与lower_limit +20ms/packet_len_ms_两者最大
  int window_20ms = 0x7FFF;  // D默认值
  if (packet_len_ms_ > 0) {
    window_20ms = (20 << 8) / packet_len_ms_;
  }
  // |higher_limit| 等于target_level, 但是应该至少比lower_limit高出20ms/packet_len_ms_的个数.
  *higher_limit = std::max(target_level, *lower_limit + window_20ms);
}

   target_timestamp !=available_timestamp但是available_timestamp不是过时的时间戳包,在抖动缓冲区队列中目前没有预期想得到的包的情况,此时视作丢失,如果前一个处理是kModeExpand,则本次处理kMerge,否则kExpand。如果前一次是处理是舒适噪声,则本次还是舒适噪声处理kRfc3389CngNoPacket或kCodecInternalCng。
代码如下:

Operations DecisionLogic::FuturePacketAvailable(
    size_t decoder_frame_length,
    Modes prev_mode,
    uint32_t target_timestamp,
    uint32_t available_timestamp,
    bool play_dtmf,
    size_t generated_noise_samples) {
  // Required packet is not available, but a future packet is.
  // Check if we should continue with an ongoing expand because the new packet
  // is too far into the future.
  uint32_t timestamp_leap = available_timestamp - target_timestamp;
  if ((prev_mode == kModeExpand || prev_mode == kModeCodecPlc) &&
      !ReinitAfterExpands(timestamp_leap) && !MaxWaitForPacket() &&
      PacketTooEarly(timestamp_leap) && UnderTargetLevel()) {
    if (play_dtmf) {
      // Still have DTMF to play, so do not do expand.
      return kDtmf;
    } else {
      // Nothing to play.
      return kExpand;
    }
  }

  if (prev_mode == kModeCodecPlc) {
    return kNormal;
  }

  // If previous was comfort noise, then no merge is needed.
  if (prev_mode == kModeRfc3389Cng || prev_mode == kModeCodecInternalCng) {
    size_t cur_size_samples =
        estimate_dtx_delay_
            ? cur_size_samples = packet_buffer_.GetSpanSamples(
                  decoder_frame_length, sample_rate_, true)
            : packet_buffer_.NumPacketsInBuffer() * decoder_frame_length;
    // Target level is in number of packets in Q8.
    const size_t target_level_samples =
        (delay_manager_->TargetLevel() * packet_length_samples_) >> 8;
    const bool generated_enough_noise =
        static_cast<uint32_t>(generated_noise_samples + target_timestamp) >=
        available_timestamp;

    if (time_stretch_cn_) {
      const size_t target_threshold_samples =
          target_level_window_ms_ / 2 * (sample_rate_ / 1000);
      const bool above_target_window =
          cur_size_samples > target_level_samples + target_threshold_samples;
      const bool below_target_window =
          target_level_samples > target_threshold_samples &&
          cur_size_samples < target_level_samples - target_threshold_samples;
      // Keep the delay same as before CNG, but make sure that it is within the
      // target window.
      if ((generated_enough_noise && !below_target_window) ||
          above_target_window) {
        time_stretched_cn_samples_ = timestamp_leap - generated_noise_samples;
        return kNormal;
      }
    } else {
      // Keep the same delay as before the CNG, but make sure that the number of
      // samples in buffer is no higher than 4 times the optimal level.
      if (generated_enough_noise ||
          cur_size_samples > target_level_samples * 4) {
        // Time to play this new packet.
        return kNormal;
      }
    }

    // 前一次采用什么模式处理,这一次仍然采用什么模式
    if (prev_mode == kModeRfc3389Cng) {
      return kRfc3389CngNoPacket;
    }
    return kCodecInternalCng;
  }

  // 除非前一次处理是expand,这一次merge。否则处理是expand
  if (prev_mode == kModeExpand) {
    return kMerge;
  } else if (play_dtmf) {
    return kDtmf;
  } else {
    return kExpand;
  }
}

   决策模块DecisionLogic输出操作命令后,并不是最终的决策,还需要进一步判定,如果是个DTX包,且DecisionLogic输出merge或加速kAccelerate或kFastAccelerate或减速kPreemptiveExpand时,操作应该输出kNormal,webrtc默认是不开启DTX功能的,DTX 是 Discontinuous Transmission的简称,这个特性是在用户不说话时不传输语音,这样可以节省点带宽。默认WebRTC是不开启这个特性的,要开启DTX,只需要在sdp中的a=ftmp这一行中加入usedtx=1就行。dtx数据是不允许时间伸缩的,因为决策可能是根据先前的缓冲区水平估计的,在DTX期间我们不更新缓冲区水平,当在DTX期间,如果我们有更好的方法来更新缓冲区水平,这就可以被丢弃。
代码如下:

 if (packet && packet->frame && packet->frame->IsDtxPacket() &&
      (*operation == kMerge || *operation == kAccelerate ||
       *operation == kFastAccelerate || *operation == kPreemptiveExpand)) {
    *operation = kNormal;
  }

   检测如果语音缓冲区sync_buffer_中有足够的样本数,这里的足够是指至少有一个完整的20ms包,如果是48000hz采样的话,就是960个样本,且决策模块输出不是merge或加速kAccelerate或kFastAccelerate或减速kPreemptiveExpand时则按正常kNormal播放。

if (samples_left >= rtc::dchecked_cast<int>(output_size_samples_) &&
      *operation != kMerge && *operation != kAccelerate &&
      *operation != kFastAccelerate && *operation != kPreemptiveExpand) {
    *operation = kNormal;
    return 0;
  }
   decision_logic_->ExpandDecision(*operation);//统计连续expand处理的次数

netEq中涉及到几个缓冲区,在此列举下其功能:
抖动缓冲区packet_buffer_:接受到音频后,解析RTP,然后将数据插入到抖动缓冲区packet_buffer_中;
解码缓冲区decoded_buffer_:packet_buffer_中取出的数据经过解码后,为PCM原始音频数据,暂存在解码decoded_buffer_中;
算法缓冲区algorithm_buffer_:从decoded_buffer_中取出数据后按照决策命令进行DSP处理后,暂存在算法algorithm_buffer_中;
语音缓冲区sync_buffer:算法缓冲区中的数据会copy语音缓冲区中,声卡每隔10ms会从语音缓冲区中提取长度为10ms的语音数据播放。
决策输出后,NetEq按照决策命令对解码后的PCM数据进行相应的DSP处理,将在其他文章中记录DSP处理算法。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱钻研技术的小羊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值