rk3568上解析webrtc音频降噪算法处理流程

前言:

大家好,在上一篇文章里面,我们已经把webrtc的apm降噪工程代码已经移植到rk3568上,今天就开始从最简单的音频降噪NS工程代码来学习音频降噪的原理。

噪声的分类:

在讲解代码实践之前,首先我们需要了解噪声的分类;虽然我们都知道噪声是啥,但是在不同场景的噪声,处理的手段会有差异。

噪声分两大类:

  • 1、稳态噪声(Stationary Noise)

  • 2、非稳态噪声(Non-stationary Noise)

稳态噪声:

稳态噪声就是"长期存在、变化缓慢的噪声",它的能量谱(频谱)随时间变化很小。常见例子,比如说:

噪声类型 是否稳态 解释
空调噪声 稳态 "呼呼"持续不变
风扇噪声 稳态 基本恒定频率
电脑电流声 稳态 高频持续"滋滋"
车内发动机怠速 稳态 频谱变化缓慢
服务器机房噪声 稳态 持续背景噪声

这些噪声通常:

频谱平滑

能量分布稳定

变化极小

非稳态噪声

指短时突发、变化快、频谱不稳定的噪声,它的能量和频谱随时间剧烈变化。 比如说:

噪声类型 是否非稳态 原因
键盘敲击声 非稳态 高瞬态能量
门关上的"砰" 非稳态 爆发式冲击
撞击、敲桌子 非稳态 极短时冲击波
塑料袋、纸张摩擦 非稳态 随机频率变化
讲话中的爆破音(P/K) 瞬态 高频瞬态声
儿童尖叫声 变化快 非周期不稳定

特点:

瞬态(transient)

不可预测

持续时间极短(几毫秒)

能量突发式增大

对于这种非稳态噪声处理,是比较难处理的,在后续的文章里面,我们再详细介绍这块。

常见降噪处理算法和处理流程介绍

噪声处理,一句话来说,就是把语音信号里面的噪声去除掉,留下纯净的语音信号即可。

常见的语音降噪算法有,我这里介绍的是单通道的处理算法,而且是大概概括一下,本篇文章暂时不做详细的介绍:

  • 1、谱减法

  • 2、维纳滤波法

  • 3、基于最大似然(ML)、最大后验(MAP)、最小均方估计(MMSE)的统计模型法

  • 4、贝叶斯估计法

  • 5、基于特征值和奇异值分解的子空间法

  • 6、音频机器学习模型,代表算法:

    go 复制代码
    RNNoise(实用、轻量级)
    
     DTLN(高质量)
    
     DeepFilterNet
    
     SEGAN
    
     DeepConv-TasNet
    
     Demucs

RNNoise(基于RNN,实时降噪)开源地址:

go 复制代码
https://github.com/xiph/rnnoise?utm_source=chatgpt.com

DTLN(基于双信号转换 LSTM 网络),开源地址:

go 复制代码
https://github.com/breizhn/DTLN?utm_source=chatgpt.com

DeepFilterNet(深度多帧过滤器,实时优化),开源地址:

go 复制代码
https://github.com/Rikorose/DeepFilterNet?utm_source=chatgpt.com

webrtc降噪处理流程:

在讲解webrtc中的降噪代码之前,我们要先了解一下webrtc中降噪的总体流程:

  • 1、帧分割(10 ms)

  • 2、Hann 窗函数

  • 3、FFT(分为几个 sub-band)

  • 4、噪声能量估计(Min-statistics 或 MCRA)

  • 5、计算 SNR(信噪比)

  • 6、计算 Wiener 增益:gain = SNR / (SNR + 1)

  • 7、应用增益: 低 SNR(吵噪)→ 小增益 高 SNR(有人声)→ 保留语音

  • 8、iFFT

  • 9、重建时域信号

go 复制代码
10ms PCM → FFT → 噪声估计 → Wiener 增益 → 降噪频谱 → iFFT → 重建 PCM

帧分割(10ms)Frame Splitting:

为什么要分帧?这个肯定是第一眼看到你很疑惑;原因如下:

音频信号是连续的(流),但降噪算法(尤其频域算法)必须一次处理一小段。

为什么 10ms?

因为:语音信号 10ms 内基本视为"平稳"(stationary)

  • FFT 需要固定窗口长度

  • 10ms 延迟几乎听不出来(实时处理用)

  • 分帧就是把"长波形"切成一页页的小段来处理。

WebRTC 规定,这个是原文,:

go 复制代码
// APM processes audio in chunks of about 10 ms. See GetFrameSize() for
  // details.
  static constexpr int kChunkSizeMs = 10;

  // Returns floor(sample_rate_hz/100): the number of samples per channel used
  // as input and output to the audio processing module in calls to
  // ProcessStream, ProcessReverseStream, AnalyzeReverseStream, and
  // GetLinearAecOutput.
  //
  // This is exactly 10 ms for sample rates divisible by 100. For example:
  //  - 48000 Hz (480 samples per channel),
  //  - 44100 Hz (441 samples per channel),
  //  - 16000 Hz (160 samples per channel).
  //
  // Sample rates not divisible by 100 are received/produced in frames of
  // approximately 10 ms. For example:
  //  - 22050 Hz (220 samples per channel, or ~9.98 ms per frame),
  //  - 11025 Hz (110 samples per channel, or ~9.98 ms per frame).
  // These nondivisible sample rates yield lower audio quality compared to
  // multiples of 100. Internal resampling to 10 ms frames causes a simulated
  // clock drift effect which impacts the performance of (for example) echo
  // cancellation.
  static int GetFrameSize(int sample_rate_hz) { return sample_rate_hz / 100; }

APM(Audio Processing Module,音频处理模块)以约 10 毫秒为一个音频块来进行处理。具体细节请参阅 GetFrameSize() 函数。kChunkSizeMs = 10:每个处理块的固定时长为 10 毫秒。GetFrameSize() 的作用: 返回 floor(sample_rate_hz / 100) 的结果。 这个值表示:在调用以下 APM 函数时,每个声道要输入 / 输出的采样点数量:

go 复制代码
ProcessStream()

ProcessReverseStream()

AnalyzeReverseStream()

GetLinearAecOutput()

如果采样率能够被 100 整除,那么计算出来的值就与 10ms 的音频长度完全一致。比如:

48000 Hz → 每声道 480 个采样点(等于 10ms)

44100 Hz → 每声道 441 个采样点(等于 10ms)

16000 Hz → 每声道 160 个采样点(等于 10ms)

如果采样率不能被 100 整除,那么得到的帧长度只是"接近" 10ms,而不是严格 10ms。比如:

22050 Hz → 每声道 220 个采样点(≈ 9.98ms)

11025 Hz → 每声道 110 个采样点(≈ 9.98ms)

这些不能整除 100 的采样率,相比那些整百采样率,会导致更低的音频处理质量。 因为 APM 内部必须将其重采样为严格 10ms 的帧长度,这会产生一种类似"时钟漂移(clock drift)"的效应,并且会影响某些算法的性能(例如回声消除 Echo Cancellation)。

GetFrameSize() 的具体实现非常简单: 只返回 sample_rate_hz / 100,也即采样率除以 100。这个结果就是 10 毫秒音频所包含的采样点数量。

go 复制代码
static int GetFrameSize(int sample_rate_hz) { 
    return sample_rate_hz / 100; 
}

其他环节我们详细来看源代码来解读,这里暂时介绍第一个。

webrtc降噪代码工程:

下面是一个最简单的降噪代码工程:

go 复制代码
// Build AudioProcessing
    rtc::scoped_refptr<webrtc::AudioProcessing> apm = webrtc::AudioProcessingBuilder().Create();
    webrtc::AudioProcessing::Config cfg = apm->GetConfig();
    cfg.high_pass_filter.enabled = opt.hpf;
    cfg.noise_suppression.enabled = true;
    // Map NS level
    if (opt.ns == "low") cfg.noise_suppression.level = webrtc::AudioProcessing::Config::NoiseSuppression::kLow;
    elseif (opt.ns == "moderate") cfg.noise_suppression.level = webrtc::AudioProcessing::Config::NoiseSuppression::kModerate;
    else cfg.noise_suppression.level = webrtc::AudioProcessing::Config::NoiseSuppression::kHigh;
    // AGC2 (recommended over AGC1 for most apps)
    cfg.gain_controller2.enabled = opt.agc2;
    cfg.gain_controller2.adaptive_digital.enabled = opt.agc2;
    // VAD
    //cfg.voice_detection.enabled = opt.vad;
    // AEC (disabled in this file-only demo; left here for reference)
    cfg.echo_canceller.enabled = false;

    // 瞬态抑制
    cfg.transient_suppression.enabled = true;

    // 语音检测
    //cfg.voice_detection.enabled = true;

    apm->ApplyConfig(cfg);

    // 1) 创建并配置 VAD
      VadInst* vad = nullptr;
      WebRtcVad_Create();
      WebRtcVad_Init(vad);
      // 模式:0~3,越大越"激进/敏感"
      WebRtcVad_set_mode(vad, 2);



    const int sample_rate = (int)in.sample_rate;
    const size_t channels = in.channels;
    const size_t frame_samples = static_cast<size_t>(sample_rate / 100); // 10 ms
    const size_t total_samples = in.samples.size();

    webrtc::StreamConfig input_cfg(sample_rate, channels);
    webrtc::StreamConfig output_cfg(sample_rate, channels);

    std::vector<int16_t> out;
    out.reserve(total_samples);

    size_t frames_with_voice = 0, total_frames = 0;

    for (size_t pos = 0; pos < total_samples; pos += frame_samples * channels) {
      size_t remaining = total_samples - pos;
      size_t this_frame = std::min(frame_samples * channels, remaining);
      std::vector<int16_t> frame(frame_samples * channels, 0);
      std::memcpy(frame.data(), in.samples.data() + pos, this_frame * sizeof(int16_t));

      // In-place process
      int err = apm->ProcessStream(frame.data(), input_cfg, output_cfg, frame.data());
      if (err != 0) throw std::runtime_error("AudioProcessing::ProcessStream failed");

      if (opt.vad) {
        auto stats = apm->GetStatistics();
        // voice_detected is optional in stats
        if (stats.voice_detected && *stats.voice_detected) frames_with_voice++;
        total_frames++;
      }
      out.insert(out.end(), frame.begin(), frame.end());
    }

官方也有给一个demo学习,但是他不是降噪的工程,我上面是代码基于这个改造得到的:

在讲解代码之前,我们先简单来看AudioProcessing降噪结构定义:

go 复制代码
// Enables background noise suppression.
    struct NoiseSuppression {
      bool enabled = false;
      enum Level { kLow, kModerate, kHigh, kVeryHigh };
      Level level = kModerate;
      bool analyze_linear_aec_output_when_available = false;
    } noise_suppression;

有了这个基础之后,我们开始介绍一下上面的代码,整个代码处理流程:

go 复制代码
原始 PCM
   ↓
分帧(10ms)
   ↓
APM 加工(包含:高通 + 降噪 + AGC2 + 瞬态抑制)
   ↓
WebRTC Spectral Noise Suppression(维纳滤波降噪)
   ↓
可选:VAD 检测是否有人声
   ↓
输出降噪后的 PCM
  • 1、AudioProcessing(APM)是 WebRTC 音频处理的总控模块
go 复制代码
rtc::scoped_refptr<webrtc::AudioProcessing> apm =
    webrtc::AudioProcessingBuilder().Create();

AudioProcessing内部包括模块有:

模块 功能
HPF 高通,去低频噪声(风噪、低频嗡声)
NS 噪声抑制(频域维纳滤波)
AEC 回声消除(你这里关闭了)
AGC1/AGC2 自动增益控制(响度一致)
VAD 语音检测
TS 瞬态噪声抑制(键盘敲击声)

目前代码开通的模块:

高通滤波

降噪(NS)

AGC2

瞬态抑制(击键/碰撞噪声处理)

  • 2、开启高通滤波器
go 复制代码
cfg.high_pass_filter.enabled = opt.hpf;//这里赋值为ture

高通滤波器的定义:

go 复制代码
/*
    高通滤波器在 APM 中是非常重要的基础模块,主要用来:
    去除低频轰鸣声(风声、空调声、道路噪声)
    去掉麦克风/电源带来的 50/60Hz 嗡声
    降低 DC offset(直流偏置)
    提升 Voice Activity Detection (VAD) 的准确性;WebRTC 的高通滤波器通常是 90 Hz(左右)截止频率。
    Input PCM
        ↓
    [HighPassFilter]  ← 你这段配置控制的就是这一步
        ↓
    Echo Cancellation (AEC)
        ↓
    Noise Suppression (NS)
        ↓
    Automatic Gain Control (AGC)
        ↓
    Output PCM
    */
    struct HighPassFilter {
      bool enabled = false;//是否启用高通滤波器
      /*
      决定高通滤波器作用在哪个频带。如果为 true(默认):高通滤波器对整段信号进行处理(full-band);也就是说:
      所有频率范围的输入信号都经过 HPF → 再进入 APM 其它模块。
      如果为 false:高通滤波只应用在音频的"底层带宽"(即低带),通常是 窄带(8kHz) 或 16kHz 的部分段
      apply_in_full_band 的含义是:
          true:在整个 full-band 上应用 HPF(即对原始全频率信号做一次高通,然后再做分带)
          false:只对某一个子带(通常是低频/主带)应用 HPF,其他高频带不单独再滤
      */
      bool apply_in_full_band = true;
    } high_pass_filter;
  • 3、开启降噪(Noise Suppression)
go 复制代码
cfg.noise_suppression.enabled = true;
  • 4、设置降噪强度(low / moderate / high)
go 复制代码
if (opt.ns == "low") cfg.noise_suppression.level = kLow;
else if (opt.ns == "moderate") cfg.noise_suppression.level = kModerate;
else cfg.noise_suppression.level = kHigh;
level 降噪力度 声音效果
low 最轻 保真度最高
moderate 中等(默认) 会议最佳
high 强降噪 有人声变"干"风险
very high 暴力降噪 强噪环境用
  • 5、开启 AGC2(自动增益控制 2)
go 复制代码
cfg.gain_controller2.enabled = opt.agc2;
cfg.gain_controller2.adaptive_digital.enabled = opt.agc2;

AGC2 特点:

完全数字化

不需要硬件支持

自动让音量保持稳定

适合嵌入式系统、录音文件处理

作用:

防止声音忽大忽小

增强较小的语音

提高降噪后音质

AGC2 与 NS 配合可以达到 专业会议级降噪音质。

  • 6、开启瞬态噪声抑制(键盘/撞击噪声)
go 复制代码
cfg.transient_suppression.enabled = true;

TS 模块专门处理:

键盘敲击

物件掉落

咔哒声

爆音

鼠标点按

它能去除这种 "瞬间,高能量" 的噪声。

不过这个未来可能是被抛弃,官方后期不会维护这个模块,估计对于处理非稳态噪声,处理效果不是很好,暂时没有测试,后面有测试再和大家说。

  • 7、ApplyConfig --- 把设置应用到 APM 内部
go 复制代码
apm->ApplyConfig(cfg);

执行这个之后:

HPF、NS、AGC2、TS 都被开启

APM 内部会创建对应模块

初始化 FFT、噪声估计器、滤波器等

这是开始降噪前必要的一步。

  • 8、准备分帧(WebRTC 统一使用 10ms)
go 复制代码
const size_t frame_samples = sample_rate / 100; // 10ms
  • 9、10ms 循环:核心处理过程
go 复制代码
for (pos=0; pos < total_samples; pos += frame_samples * channels)
{
    ...
    apm->ProcessStream(frame.data(), input_cfg, output_cfg, frame.data());
    ...
}

ProcessStream 内部执行哪些降噪算法?

go 复制代码
Raw PCM
 ↓
High Pass Filter(高通)
 ↓
Noise Suppression(频域降噪)
 ↓
Transient Suppression(瞬态噪声抑制)
 ↓
AGC2(自适应增益 + 限幅器)
 ↓
Output PCM

-10、可选:统计 VAD(语音检测)

go 复制代码
auto stats = apm->GetStatistics();
if (stats.voice_detected && *stats.voice_detected) frames_with_voice++;

WebRTC 内部会用:

能量判断

频谱判断

多带语音概率

来判断是否为语音帧。

可用于:

静音检测

录音分析

语音触发

自动剪辑无声片段

总结:

上面每一个流程会比较复杂,暂时我们没有看到里面具体是怎么实现,所以后期我们后面深入去追webrtc APM的源码来学习,下期内容,我们来放到板端进行测试一下效果是怎样的!

相关推荐
浅笑离愁12343 小时前
VI视频输入模块学习
linux·音视频
立志成为大牛的小牛3 小时前
数据结构——五十二、散列函数的构造(王道408)
数据结构·笔记·程序人生·考研·算法
EasyDSS3 小时前
视频推拉流平台EasyDSS平台如何赋能无人机推流直播新纪元
音视频·无人机
希望有朝一日能如愿以偿4 小时前
力扣每日一题:可被三整除的最大和
数据结构·算法·leetcode
闲猿类4 小时前
嵌入式第九天学习
linux·c语言·学习·算法·嵌入式
轻微的风格艾丝凡4 小时前
光伏 MPPT 算法介绍
人工智能·算法·光伏
无敌最俊朗@5 小时前
力扣hot100-环形链表(2)142
算法·leetcode·链表
nvd115 小时前
niri 音频图形界面工具
前端·chrome·音视频
lqj_本人5 小时前
鸿蒙Qt音频实战:解决QMediaPlayer的高延迟与杂音问题
qt·音视频·harmonyos