WebRTC AEC回声消除算法拆解

WebRTC AEC算法流程分析——时延估计(一)

其实,网上有很多类似资料,各个大厂研发不同应用场景设备的音频工程师基本都对其进行了拆解,有些闪烁其词,有些却很深奥,笔者随着对WebRTC了解的深入,逐渐感觉其算法构思巧夺天工,算法逻辑的设计也采用了很多技巧,这就导致没有一定的C语言基础,很难理解其精妙之处,笔者精读完之后不禁让拍案叫绝,作为交流,本系列文章有感而记之,并为来者谏。(笔者C代码水平有限,难免所有错误)


文章目录

  • WebRTC AEC算法流程分析——时延估计(一)
  • 前言
  • 一、far_end 远端信号的处理
  • 二、时延估计
    • 2.程序分析
    • 3.参数说明


前言

WebRTC AEC主要分为三大部分:延时估计,频域分块NLMS滤波器和非线性滤波,三部分数据处理相互关联,不可分割,且每一部分对整个语音信号处理来说至关重要,本篇文章主要针对时延估计模块进行分析,其他模块会陆续进行展开。


一、far_end 远端信号的处理

远端数据处理是一个单独的模块,主要包括对信号的分帧、加窗以及对应的傅里叶变换,并将处理之后的不同数据分配到不同的Buffer中,这里的数据块主要采用环形数组。
首先对一些参数进行初始化:

    WebRtcAec_Create(&aecmInst);
    WebRtcAec_Init(aecmInst, 16000, 16000);
    config.nlpMode = kAecNlpAggressive;
    WebRtcAec_set_config(aecmInst, config);

用fread(far_frame, sizeof(short), NN, fp_far)函数读入远端数据,并通过WebRtcAec_BufferFarend(aecmInst, far_frame, NN)函数对远端数据进行处理:

int32_t WebRtcAec_BufferFarend(void* aecInst,
                               const int16_t* farend,
                               int16_t nrOfSamples) {

这里的处理主要包括两部分:

  for (i = 0; i < newNrOfSamples; i++) {
    tmp_farend[i] = (float)farend_ptr[i];
  }
  WebRtc_WriteBuffer(aecpc->far_pre_buf, farend_float, (size_t)newNrOfSamples);

  // Transform to frequency domain if we have enough data.
  while (WebRtc_available_read(aecpc->far_pre_buf) >= PART_LEN2) {
    // We have enough data to pass to the FFT, hence read PART_LEN2 samples.
    WebRtc_ReadBuffer(
        aecpc->far_pre_buf, (void**)&farend_float, tmp_farend, PART_LEN2);

    WebRtcAec_BufferFarendPartition(aecpc->aec, farend_float);

    // Rewind |far_pre_buf| PART_LEN samples for overlap before continuing.
    WebRtc_MoveReadPtr(aecpc->far_pre_buf, -PART_LEN);

第一步:通过WebRtc_WriteBuffer函数将远端数据读入到环形数组中,环形数组的定义为:

RingBuffer* far_pre_buf;  // Time domain far-end pre-buffer.

第二步就是通过While循环对数组进行缓存,由于读入的是10ms的数据,但是算法处理的帧长为64,重叠率50%,因此对数据进行了缓存,使fft变换的数据长度为128,通过WebRtc_MoveReadPtr(aecpc->far_pre_buf, -PART_LEN)函数对缓存数据进行更新,保证重叠率为50%;
数组缓存之后,通过WebRtcAec_BufferFarendPartition函数接口需要对数组进行FFT:

void WebRtcAec_BufferFarendPartition(AecCore* aec, const float* farend) {
  float fft[PART_LEN2];
  float xf[2][PART_LEN1];

  // Check if the buffer is full, and in that case flush the oldest data.
  if (WebRtc_available_write(aec->far_buf) < 1) {
    WebRtcAec_MoveFarReadPtr(aec, 1);
  }
  // Convert far-end partition to the frequency domain without windowing.
  memcpy(fft, farend, sizeof(float) * PART_LEN2);
  TimeToFrequency(fft, xf, 0);
  WebRtc_WriteBuffer(aec->far_buf, &xf[0][0], 1);

  // Convert far-end partition to the frequency domain with windowing.
  memcpy(fft, farend, sizeof(float) * PART_LEN2);
  TimeToFrequency(fft, xf, 1);
  WebRtc_WriteBuffer(aec->far_buf_windowed, &xf[0][0], 1);
}

考虑到后续回声消除和非线性处理会用到加窗和不加窗两部分不同的数组,因此,这里对缓存的数组进行加窗和不加窗两种处理方式,并放置在不同的buffer中。
以上是对远端数据的一个基本操作,但这里还有一点需要注意,由于初始缓存是160个数据,也就是10ms的数据,因此导致系统存在一个延迟,通过:

  WebRtcAec_SetSystemDelay(aecpc->aec,
                           WebRtcAec_system_delay(aecpc->aec) + newNrOfSamples);

函数进行设置system_delay=160。这个在ProcessNormal函数中会用到。

  msInSndCardBuf =
      msInSndCardBuf > kMaxTrustedDelayMs ? kMaxTrustedDelayMs : msInSndCardBuf;
  // TODO(andrew): we need to investigate if this +10 is really wanted.
  msInSndCardBuf += 10;

二、时延估计

在网友博客webrtc aecd算法解析
中,罗列了导致延时的三种类型,可以作为参考:
在这里插入图片描述
此外,这里还说到,硬件方面的延迟很容易确定,倒是软件方面的延迟比较难以估计。至于为什么这里以后会补充。

2.程序分析

其实,但从数据来说,系统的延迟时间前期可以通过仿真确定,手机,会议机以及耳机等终端设备的不同会导致不同程度的信号延迟,在前期仿真中可以通过互相关函数确定延迟的具体值。在WebRTC中,有两种模式对延迟信号进行估计。
首先通过WebRtcAec_Process函数将近端数据进行输入:

 WebRtcAec_Process(aecmInst, near_frame, NULL, out_frame, NULL, NN, 40, 0);

这里的40就是前期估计的延迟时间,WebRtcAec_Process的函数为:

nt32_t WebRtcAec_Process(void* aecInst,
                          const int16_t* nearend,
                          const int16_t* nearendH,
                          int16_t* out,
                          int16_t* outH,
                          int16_t nrOfSamples,
                          int16_t msInSndCardBuf,
                          int32_t skew) 

这里关注以下msInSndCardBuf参数,WebRTC的给的解释是:

 * int16_t       msInSndCardBuf Delay estimate for sound card and
 *                              system buffers

这个参数就是前期代码仿真时通过互相关函数确定的延迟时间,在extended_filter_enabled=1的时候,以下会使能:

  if (WebRtcAec_delay_correction_enabled(aecpc->aec)) {
    ProcessExtended(
        aecpc, nearend, nearendH, out, outH, nrOfSamples, msInSndCardBuf, skew);

然后会根据输入的msInSndCardBuf计算目标延迟,通过仿真完全可以和延迟时间对得上:

   int target_delay = startup_size_ms  * self->rate_factor * 8;
   int overhead_elements = (WebRtcAec_system_delay(self->aec) - target_delay) / PART_LEN;
    WebRtcAec_MoveFarReadPtr(self->aec, overhead_elements);

并根据计算出的overhead_elements 对信号进行延迟补充,WebRtcAec_MoveFarReadPtr函数如下:

int WebRtcAec_MoveFarReadPtr(AecCore* aec, int elements) {
  int elements_moved = WebRtc_MoveReadPtr(aec->far_buf_windowed, elements);
  WebRtc_MoveReadPtr(aec->far_buf, elements);
#ifdef WEBRTC_AEC_DEBUG_DUMP
  WebRtc_MoveReadPtr(aec->far_time_buf, elements);
#endif
  aec->system_delay -= elements_moved * PART_LEN;
  return elements_moved;
}

这里其实对加窗和不加窗数据进行move。
以上是根据msInSndCardBuf参数进行的延迟计算,当然在线性滤波器阶段,本身还存一个延时估计:

if (aec->delayEstCtr == 0) {
    wfEnMax = 0;
    aec->delayIdx = 0;
    for (i = 0; i < aec->num_partitions; i++) {
      pos = i * PART_LEN1;
      wfEn = 0;
      for (j = 0; j < PART_LEN1; j++) {
        wfEn += aec->wfBuf[0][pos + j] * aec->wfBuf[0][pos + j] +
                aec->wfBuf[1][pos + j] * aec->wfBuf[1][pos + j];
      }

      if (wfEn > wfEnMax) {
        wfEnMax = wfEn;
        aec->delayIdx = i;
      }
    }
  }

这里会估计出一个aec->delayIdx,那么和moved element就构成所有的延迟时间。
第二种方式通过一端时间的迭代,Web给出6帧数据,判断延迟是否稳定,在ProcessNormal中进行处理。

      // Before we fill up the far-end buffer we require the system delay
      // to be stable (+/-8 ms) compared to the first value. This
      // comparison is made during the following 6 consecutive 10 ms
      // blocks. If it seems to be stable then we start to fill up the
      // far-end buffer.

待数据稳定之后就会通计算给出要移动的数据个数移动。

overhead_elements =
          WebRtcAec_system_delay(aecpc->aec) / PART_LEN - aecpc->bufSizeStart;
          WebRtcAec_MoveFarReadPtr(aecpc->aec, overhead_elements);

并在EstBufDelayNormal函数中进行一定条件的筛选。
至此,延迟主要的逻辑基本叙述完毕,其实Web真正深奥的倒是那些约束条件,这个不经过一定的实践和及深厚的理论基础是很难想得到,有机会再做补充。

3.参数说明

对于16k采用率,每帧处理的采样点数为64,对于的FFT变化的长度为128;

#define FRAME_LEN 80
#define PART_LEN 64               // Length of partition
#define PART_LEN1 (PART_LEN + 1)  // Unique fft coefficients
#define PART_LEN2 (PART_LEN * 2)  // Length of partition * 2

但是为了能够覆盖大部分延迟时间,数据开闭的buffer为1s的数据块:

static const size_t kBufSizePartitions = 250;  // 1 second of audio in 16 kHz.

由于一帧数为64/16=4ms,那么16块对应64ms,12块数据对应48ms数据,因此也基本上能够覆盖一般的穿戴设备。


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/236900.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

算法Day28 二进制差异序列(格雷码)

二进制差异序列&#xff08;格雷码&#xff09; Description n 位二进制差异序列是一个由2^n个整数组成的序列&#xff0c;其中&#xff1a; 每个整数都在范围[0, 2^n - 1]内&#xff08;含0和2^n - 1&#xff09; 第一个整数是0 一个整数在序列中出现不超过一次 每对相邻整数…

scripty妙用

在monorepo项目中&#xff0c;随着子模块增多&#xff0c; 每个子项目都需要配置各自的package.json,并且大同小异&#xff0c;为了进一步提高配置效率&#xff0c;引入了scripty&#xff0c;自己写脚本&#xff0c;直接就可以用哦 1、安装 npm install scripty --save-dev 2…

STM32MP157D-DK1开发板固件烧录

本篇介绍STM32MP157D-DK1开发板如何烧录官方固件。 1 开发板基础硬件介绍 1.1 常用接口 板子上的各种接口功如下&#xff0c;本篇固件烧录&#xff0c;主要用的接口包括&#xff1a; CN6&#xff1a;供电接口B2&#xff1a;复位按键CN11&#xff1a;ST-LINK USB&#xff08…

MYSQL索引和事务

Mysql 索引 事务 存储引擎 索引&#xff1a;索引是一个排序的列表&#xff0c;列表当中存储的是索引的值和包含这个值的数据所在行的物理地址 索引的作用加快查询速度 索引的作用&#xff1a; 利用索引数据库可以快速定位&#xff0c;大大加快查询速度&#xff0c;主要作用表…

LINUX:如何以树形结构显示文件目录结构

tree tree命令用于以树状图列出目录的内容。 第一步&#xff0c;先安装tree这个包 sudo apt-get install tree 第二步&#xff0c;在指定文件目录输入下面命令&#xff0c;7代表7级子目录 tree -L 7 第三步&#xff0c;效果图 第四步&#xff0c;拓展学习 颜色显示 tree -C显…

Go json 差异比较 json-diff(RFC6902)

Go json 差异比较 json-diff(RFC 6902) 毕业设计中过程中为了比较矢量图的差异而依据 RFC 6902 编写的一个包&#xff0c;现已开源&#xff1a; Json-diff 使用 go get -u github.com/520MianXiangDuiXiang520/json-diff序列化与反序列化 与官方 json 包的序列化和反序列化不…

川崎ZX-6R确定引进,636它真的来了,3C认证已过。

最新消息&#xff0c;兄弟们&#xff0c;你们期待已久的川崎ZX6R&#xff08;636&#xff09;基本已经确定引进了&#xff0c;官方的3C认证已经通过&#xff0c;那么从3C里面我们可以看到哪几个信息&#xff1f;产品代号ZX636J就是心心念念的ZX-6R了。 有些小伙伴不太清楚3C认…

数据结构:第13关:查找两个单词链表共同后缀的起始结点

任务描述编程要求 输入输出测试说明来源 任务描述 本关任务&#xff1a;假定采用带头结点的单链表保存单词&#xff0c;当两个单词有相同的后缀时&#xff0c;则可共享相同的后缀空间。 例如&#xff0c;“loading”和“being”的存储映像如下图所示&#xff1a; 设str1和str2…

detectron2中save_text_instance_predictions⭐

save_text_instance_predictions demo.py中修改关于路径os.path.join()函数用于路径拼接文件路径&#xff0c;可以传入多个路径os.path.basename(path)就是给定一串路径的最终找到的那个文件python官方文档链接 将 Python 对象序列化为 JSON 字符串with open 打开文件&#xff…

PostGIS学习教程十三:几何图形创建函数

PostGIS学习教程十三&#xff1a;几何图形创建函数 目前我们看到的所有函数都可以处理已有的几何图形并返回结果&#xff1a; 分析几何图形&#xff08;ST_Length(geometry), ST_Area(geometry)) 几何图形的序列化&#xff08;ST_AsText(geometry), ST_AsGML(geometry)) 选取…

动态规划_不同路径||

//一个机器人位于一个 // m x n 网格的左上角 &#xff08;起始点在下图中标记为 “Start” &#xff09;。 // // 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角&#xff08;在下图中标记为 “Finish”&#xff09;。 // // 现在考虑网格中有障碍物。那么…

坚守数字化创新烽火之地 百望云入选新华社“品牌信用建设典型案例”

潮起海之南&#xff0c;风好正扬帆。2023年12月2日-5日&#xff0c;南海之滨&#xff0c;由新华通讯社、海南省人民政府、中国品牌建设促进会主办的主题为“聚焦新质生产力&#xff0c;增强发展新动能”的2023中国企业家博鳌论坛在海南博鳌隆重举行。 群贤毕至&#xff0c;高朋…

vue零基础

vue 与其他框架的对比 框架设计模式数据绑定灵活度文件模式复杂性学习曲线生态VueMVVM双向灵活单文件小缓完善ReactMVC单向较灵活all in js大陡丰富AngularMVC双向固定多文件较大较陡&#xff08;Typescript&#xff09;独立 更多对比细节&#xff1a;vue 官网&#xff1a;ht…

银河麒麟v10系统SSH远程管理及切换root用户的操作方法

&#x1f4da;&#x1f4da; &#x1f3c5;我是默&#xff0c;一个在CSDN分享笔记的博主。&#x1f4da;&#x1f4da; ​​ &#x1f31f;在这里&#xff0c;我要推荐给大家我的专栏《Linux》。&#x1f3af;&#x1f3af; &#x1f680;无论你是编程小白&#xff0c;还是有一…

深入解析C++中的虚函数和虚继承:实现多态性与继承关系的高级特性

这里写目录标题 虚函数虚函数实现动态绑定虚继承抽象类 虚函数 虚函数是在C中用于实现多态性的一种特殊函数。它通过使用关键字"virtual"进行声明&#xff0c;在基类中定义&#xff0c;可在派生类中进行重写。虚函数允许在运行时根据对象的实际类型来调用相应的函数…

在HarmonyOS上使用ArkUI实现计步器应用

介绍 本篇Codelab使用ArkTS语言实现计步器应用&#xff0c;应用主要包括计步传感器、定位服务和后台任务功能&#xff1a; 通过订阅计步器传感器获取计步器数据&#xff0c;处理后显示。通过订阅位置服务获取位置数据&#xff0c;处理后显示。通过服务开发实现后台任务功能。…

MySQL使用教程

数据构成了我们日益数字化的社会基础。想象一下&#xff0c;从移动应用和银行系统到搜索引擎&#xff0c;再到如 ChatGPT 这样的先进人工智能聊天机器人&#xff0c;这些工具若没有数据支撑&#xff0c;将寸步难行。你有没有好奇过这些海量数据都存放在哪里呢&#xff1f;答案正…

基于JavaWeb+SpringBoot+Vue在线拍卖系统的设计和实现

基于JavaWebSpringBootVue在线拍卖系统系统的设计和实现 源码获取入口Lun文目录前言主要技术系统设计功能截图订阅经典源码专栏Java项目精品实战案例《500套》 源码获取 源码获取入口 Lun文目录 摘 要 1 Abstract 1 1 系统概述 4 1.1 概述 4 1.2课题意义 4 1.3 主要内容 4 2 …

Android--Jetpack--Databinding源码解析

慢品人间烟火色&#xff0c;闲观万事岁月长 一&#xff0c;基本使用 关于databinding的基本使用请看之前的文章 Android--Jetpack--Databinding详解-CSDN博客 二&#xff0c;xml布局解析 分析源码呢&#xff0c;主要就是从两方面入手&#xff0c;一个是使用&#xff0c;一个…

C语言-枚举

常量符号化 用符号而不是具体的数字来表示程序中的数字 枚举 用枚举而不是定义独立的const int变量 枚举是一种用户定义的数据类型&#xff0c;他用关键词enum以如下语法来声明&#xff1a; enum枚举类型名字{名字0&#xff0c;…&#xff0c;名字n}&#xff1b; 枚举类型名…