乐于分享
好东西不私藏

webrtc降噪模块NS源码解析(1)

webrtc降噪模块NS源码解析(1)

前言:

大家好,在上一篇文章里面,我们已经用webrtc的降噪模块,对噪声做了一个降噪处理,算是在rk3568上跑通了webrtc的ns模块,今天我们继续来学习webrtc的ns模块里面的细节实现,所以我们需要深度研究里面的源码学习。

webrtc的ns模块:

先来看一下数据流到ns处理的流程:

那么我们先不管增益和回声消除,核心来看一下降噪模块:

// 用于对输入信号做降噪(Noise Suppression, NS)的核心类。
// 典型位置:WebRTC 音频处理链路(APM)里,通常在 AEC(回声消除) 前后之一执行:
// - Analyze() 常用于“只分析统计量”,可在 AEC 前做,避免分析到 AEC 的 comfort noise。
// - Process() 才会真正修改音频数据,执行降噪。
class NoiseSuppressor {
 public:
  // 构造函数:
  // config          : NS 的配置(强度、模式、是否启用某些策略等)
  // sample_rate_hz  : 采样率(8k/16k/32k/48k... 会影响频带数、上频带处理等)
  // num_channels    : 通道数(1=mono, 2=stereo...)
  NoiseSuppressor(const NsConfig& config,
                  size_t sample_rate_hz,
                  size_t num_channels);

  // 禁止拷贝:因为内部持有大量状态/堆内存,拷贝成本大且容易引入状态错误。
  NoiseSuppressor(const NoiseSuppressor&) = delete;
  NoiseSuppressor& operator=(const NoiseSuppressor&) = delete;

  // 只分析输入信号(不修改音频内容)。
  // 注释里提到:通常在 AEC 前调用,避免把 AEC 产生的 comfort noise 当成噪声/语音去统计。
  //
  // Analyze() 的典型职责:
  // 1) 计算频谱/能量特征
  // 2) 更新噪声估计(NoiseEstimator)
  // 3) 更新语音存在概率(SpeechProbabilityEstimator)
  // 4) 更新上一帧频谱等历史状态
  void Analyze(const AudioBuffer& audio);

  // 对音频做真正的降噪处理(会就地修改 audio)。
  //
  // Process() 的典型职责:
  // 1) FFT(含 overlap 拼帧)
  // 2) 根据噪声估计 + 语音概率计算 Wiener 增益(或其他增益曲线)
  // 3) 频域乘增益抑制噪声
  // 4) IFFT + overlap-add 合成输出
  void Process(AudioBuffer* audio);

  // 指定“采集端输出是否会被使用”。
  // 目的:当输出不会被用到(例如静音、端点 mute),NS 可以关闭/简化部分耗时步骤降低 CPU。
  // 注意:一般仍要维护部分状态,避免恢复输出时瞬态不稳定(爆音、增益突变等)。
  void SetCaptureOutputUsage(bool capture_output_used) {
    capture_output_used_ = capture_output_used;
  }

 private:
  // ======= 维度与配置参数 =======

  // 频带数(band 的概念通常与采样率相关):
  // - 在 WebRTC 中,高采样率(32k/48k)往往会拆“低频带 + 高频带(upper band)”进行处理/对齐。
  // - num_bands_ 用于控制滤波器组状态、上频带增益数组等的大小。
  const size_t num_bands_;

  // 通道数:每个通道都有独立的噪声估计、语音概率估计、Wiener滤波状态等。
  const size_t num_channels_;

  // 抑制参数(由 NsConfig 转换/预计算得到):
  // 通常包含:
  // - 不同频段的目标抑制量(dB)
  // - 最小增益限制(避免“全静音/水下音”)
  // - 语音存在时的保真策略(减少语音失真)
  // - 过渡平滑、攻击/释放时间常数等
  const SuppressionParams suppression_params_;

  // 已分析的帧计数:
  // - 初始 -1 表示“尚未开始/尚未初始化到稳定状态”
  // - 某些算法会在前 N 帧做 warm-up(例如先估噪,再逐步启用强抑制)
  int32_t num_analyzed_frames_ = -1;

  // FFT 工具对象(NS 的核心是频域处理):
  // - 内部会用固定 FFT 大小 kFftSize
  // - 以及 kFftSizeBy2Plus1(实信号 FFT 的半谱大小 N/2+1)
  NrFft fft_;

  // 输出是否被使用(用于降耗/跳过部分处理)
  bool capture_output_used_ = true;

  // ======= 每通道状态(独立状态机) =======
  struct ChannelState {
    // 每通道的状态依赖 suppression_params 和 num_bands(例如不同 band 的延迟线长度等)
    ChannelState(const SuppressionParams& suppression_params, size_t num_bands);

    // 语音概率估计器:
    // - 输出每个频点/每个 band 的“像语音的概率”
    // - 常用特征:SNR、谱平坦度、谱变化率、历史平滑等
    // - 用于避免把语音也当噪声压得太狠
    SpeechProbabilityEstimator speech_probability_estimator;

    // Wiener 滤波器状态:
    // - Wiener 增益常见形式:G(k) = SNR / (SNR + 1)
    // - 实际会结合语音概率做保真(比如语音概率高 -> 增益更接近1)
    // - wiener_filter 通常保存平滑状态、上一帧增益等
    WienerFilter wiener_filter;

    // 噪声估计器:
    // - 跟踪每个频点的噪声功率谱密度(noise PSD)
    // - 在无语音段快速更新,在语音段慢更新或冻结,避免把语音统计进噪声
    NoiseEstimator noise_estimator;

    // 上一帧“分析用”的频谱(半谱大小 N/2+1):
    // - 用于平滑、计算谱变化、语音概率特征、突变检测等
    std::array<float, kFftSizeBy2Plus1> prev_analysis_signal_spectrum;

    // Analyze() 的分析端 overlap 记忆:
    // - 因为 NS 通常是 10ms 一帧输入(kNsFrameSize),
    //   但 FFT 窗口 kFftSize 往往更大,需要把上一帧尾巴与当前帧拼成 extended_frame。
    // - 这个数组保存“上一帧剩余那部分”。
    std::array<float, kFftSize - kNsFrameSize> analyze_analysis_memory;

    // Process() 的分析端 overlap 记忆(与 Analyze 分开存):
    // - Analyze 和 Process 可能在 pipeline 的不同位置被调用,
    //   两条路径的状态不能互相踩。
    std::array<float, kOverlapSize> process_analysis_memory;

    // Process() 的合成端 overlap 记忆:
    // - IFFT 回来的时域信号,需要 overlap-add 合成连续输出,
    //   这里保存合成时的重叠部分。
    std::array<float, kOverlapSize> process_synthesis_memory;

    // 处理延迟线(可能是:每个 band 一条或若干条):
    // - 在多 band、上频带对齐、或某些平滑/决策需要 lookback 时使用
    // - vector<array<kOverlapSize>> 表明:延迟线长度可变(由 num_bands 或配置决定)
    std::vector<std::array<float, kOverlapSize>> process_delay_memory;
  };

  // ======= 滤波器组/FFT 临时工作区 =======
  struct FilterBankState {
    // FFT 复数频谱的实部缓存
    std::array<float, kFftSize> real;

    // FFT 复数频谱的虚部缓存
    std::array<float, kFftSize> imag;

    // 拼好的扩展时域帧(窗函数/overlap 后的输入),用于 FFT
    std::array<float, kFftSize> extended_frame;
  };

  // ======= 堆上缓存(避免实时处理中频繁分配) =======

  // 滤波器组状态(可能按:通道×band 或 band 数分配,具体看实现)
  std::vector<FilterBankState> filter_bank_states_heap_;

  // 上频带增益缓存:
  // - 当采样率较高时(如 32k/48k),NS 可能对高频段采用独立增益或从低频外推。
  // - 这里存每个频点/每个 band 的 upper band 增益。
  std::vector<float> upper_band_gains_heap_;

  // 滤波前能量/功率缓存:
  // - 用于计算 SNR、语音概率、做能量门限、或用于调节增益/抑制策略
  std::vector<float> energies_before_filtering_heap_;

  // 增益调整项缓存:
  // - Wiener gain 计算出来后,可能还会做额外修正:
  //   * 最小增益限制
  //   * 语音存在时减轻抑制(保真)
  //   * 不同频段不同曲线(低频保留更多、噪声集中频段压得更狠)
  std::vector<float> gain_adjustments_heap_;

  // 每个通道一个 ChannelState(unique_ptr:避免拷贝大对象,明确所有权)
  std::vector<std::unique_ptr<ChannelState>> channels_;

  // ======= 关键私有方法:多通道 Wiener filter 聚合 =======
  // 将多个通道的 Wiener filter 合并为“一个最终滤波器”:
  // - 多通道(立体声)时常用来保持左右一致性(避免左右声道音色/噪声底不同)
  // - 也可能用于某些共享处理路径(降低计算或稳定决策)
  //
  // filter 是输出参数:长度 kFftSizeBy2Plus1 的半谱增益曲线
  void AggregateWienerFilters(
      rtc::ArrayView<float, kFftSizeBy2Plus1> filter) const;
};

从上面我们可以总结一下:NoiseSuppressor 是 WebRTC 的实时频域降噪器:它对每个通道维护噪声估计、语音概率和 Wiener 增益等状态,提供 Analyze(只统计)与 Process(真正降噪)两阶段接口,并支持多通道滤波器聚合和静音降算力。

下面为了更加详细的学习,我们先从NoiseSuppressor构造函数来学习源码开始:

作用:根据采样率和通道数,提前分配并初始化所有降噪所需的状态、缓存和子模块,避免在实时处理过程中动态分配内存:

NoiseSuppressor::NoiseSuppressor(const NsConfig& config,
                                 size_t sample_rate_hz,
                                 size_t num_channels)
    // ======= 1. 根据采样率计算频带数 =======
    // WebRTC 在高采样率(32k/48k)时,会把信号拆成多个 band
    //(低频带 + 上频带)分别处理
    : num_bands_(NumBandsForRate(sample_rate_hz)),

      // ======= 2. 保存通道数 =======
      num_channels_(num_channels),

      // ======= 3. 从配置生成抑制参数 =======
      // suppression_params_ 是 NS 实际算法用的参数集合
      // 通常包含:
      //   - target_level(目标抑制量)
      //   - 最小增益
      //   - 平滑系数
      //   - 语音存在时的保真策略
      suppression_params_(config.target_level),

      // ======= 4. 预分配滤波器组/FFT 工作区 =======
      // NumChannelsOnHeap() 通常返回:
      //   num_channels * num_bands
      // 或
      //   num_channels
      // 具体取决于实现
      // 目的:每个通道/频带都有独立 FFT 缓存
      filter_bank_states_heap_(NumChannelsOnHeap(num_channels_)),

      // ======= 5. 预分配上频带增益缓存 =======
      // 高频段可能采用独立增益或从低频外推
      // 这里提前分配,避免 Process() 里 new
      upper_band_gains_heap_(NumChannelsOnHeap(num_channels_)),

      // ======= 6. 预分配滤波前能量缓存 =======
      // 用于:
      //   - 计算 SNR
      //   - 语音概率估计
      //   - 决策是否冻结噪声估计
      energies_before_filtering_heap_(NumChannelsOnHeap(num_channels_)),

      // ======= 7. 预分配增益调整缓存 =======
      // Wiener 增益计算完后,还会做额外修正
      // 这里提前准备好数组
      gain_adjustments_heap_(NumChannelsOnHeap(num_channels_)),

      // ======= 8. 为每个通道预留 ChannelState 指针槽位 =======
      // 注意:这里只是 resize vector,还没 new ChannelState
      channels_(num_channels_) {
          // ======= 9. 为每个通道创建独立的 ChannelState =======
for (size_t ch = 0; ch < num_channels_; ++ch) {
    channels_[ch] =
        std::make_unique<ChannelState>(suppression_params_, num_bands_);
  }
}

上面代码中有一个函数NumBandsForRate,他是一个“采样率 → band 数量”的映射函数;band(频带)是 WebRTC NS 内部的处理单元数,不是指真正的滤波器组频段,而是逻辑处理分块,也就是这段代码的核心作用:

  • NumBandsForRate() 根据采样率把音频拆成每 16kHz 一个逻辑 band(16k=1,32k=2,48k=3),让 WebRTC NS 对不同频段采用不同的降噪策略,兼顾音质与性能。
sample_rate_hz
num_bands
含义
16000
1
只有低频带
32000
2
低频 + 高频
48000
3
低频 + 中频 + 高频
  • 16k:只处理 0~8k(语音主频)

  • 32k:band0:0~8k ; band1:8~16k

  • 48k:band0:0~8k;band1:8~16k;band2:16~24k

注意: 每 16kHz 采样率增加一个 band,所以这里对于采样率为44.1kHz就不行,无法整除16,如果要处理的话,只能先重采样来进行处理了。

这里还有一个配置降噪等级的参数:

// Config struct for the noise suppressor
struct NsConfig {
  enum class SuppressionLevel { k6dB, k12dB, k18dB, k21dB };
  SuppressionLevel target_level = SuppressionLevel::k12dB;
};
枚举值
含义
实际效果
k6dB
轻度降噪
几乎不损音质,噪声残留多
k12dB
中等降噪(默认)
平衡点
k18dB
强降噪
噪声明显减少,但可能轻微失真
k21dB
极强降噪
最狠,容易水下音

我们再看一下函数NumChannelsOnHeap:

// Maximum number of channels forwhich the channel data is stored on
// the stack. If the number of channels are larger than this, they are stored
// using scratch memory that is pre-allocated on the heap. The reason for this
// partitioning is not to waste heap space for handling the more common numbers
// of channels, while at the same time not limiting the support for higher
// numbers of channels by enforcing the channel data to be stored on the
// stack using a fixed maximum value.
constexpr size_t kMaxNumChannelsOnStack = 2;

// Chooses the number of channels to store on the heap when that is required due
// to the number of channels being larger than the pre-defined number
// of channels to store on the stack.
size_t NumChannelsOnHeap(size_t num_channels) {
return num_channels > kMaxNumChannelsOnStack ? num_channels : 0;
}

作用:通过“最多 2 通道用栈、超过 2 通道才用堆”的方式,兼顾了实时性能、内存效率和通道扩展性,是 WebRTC 实时音频模块的经典内存管理设计。

好了,构造函数解析完,我们现在来看一下NoiseSuppressor::Analyze内部实现细节:

void NoiseSuppressor::Analyze(const AudioBuffer& audio) {
  // ============================
  // [阶段 0] 每帧分析开始:让噪声估计器进入“分析阶段准备”状态
  // ============================
  // 作用:为本帧噪声估计更新做准备(例如清空/复位一些临时变量,切换状态机阶段等)
for (size_t ch = 0; ch < num_channels_; ++ch) {
    channels_[ch]->noise_estimator.PrepareAnalysis();
  }

  // ============================
  // [阶段 1] 检测“全零帧”(zero frame)
  // ============================
  // 为什么要检测?
  // - 实际系统里,麦克风 mute / 通道未连接 / 上游直接置零,会出现连续很多“全 0”帧。
  // - 如果在全 0 帧时继续更新“噪声统计/语音阈值”,统计量会被拉向“零信号”,
  //   导致后续一旦有真实信号进来,算法会误判“一切都是语音”,从而降噪失效,
  //   并且需要很长时间重新学习噪声底。
  bool zero_frame = true;

for (size_t ch = 0; ch < num_channels_; ++ch) {
    // 取出本通道的 band0(第 0 个频带)的 10ms 时域数据。
    // split_bands_const(ch)[0] 是 band0 的 float 数组(长度 kNsFrameSize)
    rtc::ArrayView<const float, kNsFrameSize> y_band0(
        &audio.split_bands_const(ch)[0][0], kNsFrameSize);

    // 计算“扩展帧”的能量:当前 10ms + 上一帧遗留的 overlap memory(analyze_analysis_memory)
    // 注意:这里不是只算当前 y_band0 能量,而是算“拼接后的 extended frame 能量”
    // 这样可以更可靠地判断是否真的是“全静音/全0”
float energy = ComputeEnergyOfExtendedFrame(
        y_band0, channels_[ch]->analyze_analysis_memory);

    // 只要任何一个通道的能量 > 0,就不是全零帧
if (energy > 0.f) {
      zero_frame = false;
break;
    }
  }

if (zero_frame) {
    // ============================
    // 全零帧:直接返回,不更新任何统计量
    // ============================
    // 注释解释的核心点:
    // - 若用全0信号更新统计,会把各种阈值/均值拉向 0
    // - 一旦信号恢复,算法会把一切当作语音(speech)
    // - 降噪效果消失,并且需要较长时间重新“学会噪声是什么”
return;
  }

  // ============================
  // [阶段 2] 更新分析帧计数 num_analyzed_frames_
  // ============================
  // 只有“真正分析过的帧”才计数:zero_frame 会被跳过。
  // 初始值可能是 -1,这里确保递增后不会一直为负。
if (++num_analyzed_frames_ < 0) {
    num_analyzed_frames_ = 0;
  }

  // ============================
  // [阶段 3] 对每个通道做频域分析与统计更新(不修改音频)
  // ============================
for (size_t ch = 0; ch < num_channels_; ++ch) {
    // ch_p:当前通道的状态(噪声估计/语音概率/维纳滤波/历史频谱/overlap memory 等)
    std::unique_ptr<ChannelState>& ch_p = channels_[ch];

    // 取本通道 band0 的 10ms 输入帧
    rtc::ArrayView<const float, kNsFrameSize> y_band0(
        &audio.split_bands_const(ch)[0][0], kNsFrameSize);

    // ----------------------------
    // 3.1 构造 extended frame(扩展帧)并加窗
    // ----------------------------
    // 为什么要 extended frame?
    // - NS 以 10ms 为“处理步长”,但 FFT 往往需要更长窗口 kFftSize(例如 256/512/1024)
    // - 因此必须把上一帧尾巴(overlap)与当前帧拼接成 kFftSize 长度的 FFT 输入
    // - analyze_analysis_memory 保存的是上一帧保留下来的那一段,用于 overlap 拼接
    std::array<float, kFftSize> extended_frame;
    FormExtendedFrame(y_band0, ch_p->analyze_analysis_memory, extended_frame);

    // ApplyFilterBankWindow:对时域帧乘窗函数(如 Hann/Hamming 等)
    // 作用:
    // - 降低频谱泄漏(spectral leakage)
    // - 让频域幅度更稳定,便于做噪声估计/语音概率估计
    ApplyFilterBankWindow(extended_frame);

    // ----------------------------
    // 3.2 FFT -> 频谱(real/imag),再计算幅度谱 |X(k)|
    // ----------------------------
    // real/imag:FFT 输出复数频谱的实部和虚部
    std::array<float, kFftSize> real;
    std::array<float, kFftSize> imag;
    fft_.Fft(extended_frame, real, imag);

    // signal_spectrum:幅度谱(通常只取半谱 N/2+1,因为输入是实信号)
    // kFftSizeBy2Plus1 = kFftSize/2 + 1
    std::array<float, kFftSizeBy2Plus1> signal_spectrum;
    ComputeMagnitudeSpectrum(real, imag, signal_spectrum);

    // ----------------------------
    // 3.3 计算一些统计特征(能量、谱总和)
    // ----------------------------
    // signal_energy:这里通过频域功率(real^2 + imag^2)求平均,作为整体能量特征
float signal_energy = 0.f;
for (size_t i = 0; i < kFftSizeBy2Plus1; ++i) {
      signal_energy += real[i] * real[i] + imag[i] * imag[i];
    }
    signal_energy /= kFftSizeBy2Plus1;

    // signal_spectral_sum:幅度谱的总和(一个粗粒度的谱强度特征)
float signal_spectral_sum = 0.f;
for (size_t i = 0; i < kFftSizeBy2Plus1; ++i) {
      signal_spectral_sum += signal_spectrum[i];
    }

    // ----------------------------
    // 3.4 噪声估计器:PreUpdate(更新前)
    // ----------------------------
    // PreUpdate 常用于:
    // - 根据当前观测谱做预测/预处理
    // - 更新一些内部平滑统计量
    // - 为后续 SNR 计算提供“当前噪声谱候选”
    ch_p->noise_estimator.PreUpdate(num_analyzed_frames_, signal_spectrum,
                                    signal_spectral_sum);

    // ----------------------------
    // 3.5 计算 SNR(先验/后验)
    // ----------------------------
    // post_snr:后验 SNR,通常基于当前观测谱与噪声谱直接计算(当前帧的即时SNR)
    // prior_snr:先验 SNR,通常用“决策导向(decision-directed)”方法平滑:
    //            prior_snr ≈ α * (上一帧增益^2 * 上一帧SNR) + (1-α) * max(post_snr-1, 0)
    // 这样做的好处:
    // - prior_snr 更平滑,避免增益抖动导致“音乐噪声(musical noise)”
    // - 对语音概率估计也更稳定
    std::array<float, kFftSizeBy2Plus1> post_snr;
    std::array<float, kFftSizeBy2Plus1> prior_snr;

    ComputeSnr(
        // 上一帧/当前的 Wiener filter(或增益曲线),用于 decision-directed 的 prior SNR 估计
        ch_p->wiener_filter.get_filter(),

        // 上一帧分析阶段存下来的幅度谱(历史观测)
        ch_p->prev_analysis_signal_spectrum,

        // 当前帧幅度谱(当前观测)
        signal_spectrum,

        // 上一帧噪声谱估计
        ch_p->noise_estimator.get_prev_noise_spectrum(),

        // 当前帧噪声谱估计
        ch_p->noise_estimator.get_noise_spectrum(),

        // 输出:先验/后验 SNR
        prior_snr, post_snr);

    // ----------------------------
    // 3.6 语音概率估计器更新(SpeechProbabilityEstimator)
    // ----------------------------
    // 目的:估计“这一帧/这一频点像语音的概率”
    // - 语音概率高:后续会减少噪声估计更新(避免把语音统计进噪声)
    // - 语音概率高:后续 Process 里也通常会更保真(增益更接近 1)
    //
    // conservative_noise_spectrum:更保守的噪声谱(常用于防止低估噪声)
    // 低估噪声会导致 SNR 假性偏高,从而抑制不足/判语音不准。
    ch_p->speech_probability_estimator.Update(
        num_analyzed_frames_, prior_snr, post_snr,
        ch_p->noise_estimator.get_conservative_noise_spectrum(),
        signal_spectrum, signal_spectral_sum, signal_energy);

    // ----------------------------
    // 3.7 噪声估计器:PostUpdate(更新后)
    // ----------------------------
    // 根据语音概率来决定噪声谱如何更新:
    // - 语音概率低:快速更新噪声谱(环境噪声变化能跟上)
    // - 语音概率高:慢更新或冻结(避免把语音当噪声学进去)
    ch_p->noise_estimator.PostUpdate(
        ch_p->speech_probability_estimator.get_probability(), signal_spectrum);

    // ----------------------------
    // 3.8 保存当前幅度谱(供下一帧 Analyze 或 Process 使用)
    // ----------------------------
    // 注释说:让 Process() 可用。
    // 常见用法:
    // - Process 阶段可能用这个“最近分析谱”来做平滑、估计 prior SNR、或者辅助决策。
    std::copy(signal_spectrum.begin(), signal_spectrum.end(),
              ch_p->prev_analysis_signal_spectrum.begin());
  }
}

channels_[ch]->noise_estimator.PrepareAnalysis();这个是噪声估计器,对应在NoiseSuppressor里面的ChannelState里面:

对应刚才的NoiseEstimator::PrepareAnalysis():

void NoiseEstimator::PrepareAnalysis() {
  std::copy(noise_spectrum_.begin(), noise_spectrum_.end(),
            prev_noise_spectrum_.begin());
}

PrepareAnalysis() 的作用是把“上一帧噪声谱”保存下来,作为本帧噪声估计、SNR 计算和语音概率估计的历史参考,这是保证降噪算法稳定与平滑的关键一步。

上面这里会涉及到一个什么是 “zero frame(全零帧)”,为什么要跳过?

zero frame 指这帧输入全是 0(或几乎全 0),常见原因:

  • 设备 mute

  • 上游没数据,填 0

  • 通道断开

  • AEC/AGC pipeline 某阶段“关断输出”

  • 如果继续更新统计量,会让:

  • 噪声底 → 逼近 0

  • 里面还涉及到其他相关类方法的源码,我们在下一期内容继续详细解读。

结果:当信号恢复时,算法会误判“这肯定是语音”,噪声估计不敢更新,降噪失效,恢复需要很久。

什么是 “extended frame(扩展帧)” / overlap memory?

NS 通常每次拿 10ms 帧(kNsFrameSize),但 FFT 需要更长窗口 kFftSize。 所以要拼:

extended_frame = [上一帧尾巴 (memory)] + [当前帧 (10ms)]

analyze_analysis_memory 保存“上一帧尾巴”,每帧都滚动更新。

整个流程如下:

里面还涉及到其他相关类方法的源码,我们在下一期内容继续详细。

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » webrtc降噪模块NS源码解析(1)

评论 抢沙发

6 + 5 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮