4. 峰值检测与包络跟踪算法:RMS检测、峰值保持、攻击与释放时间

限幅器的核心,说白了就是「怎么判断信号该不该压」。

你想想看,如果连信号多大都不知道,怎么决定要不要动手?

所以峰值检测和包络跟踪,就是限幅器的「眼睛」和「耳朵」。我做了这么多年音频算法,见过太多人在这块翻车——不是检测太慢导致爆音,就是跟踪太快把音色搞没了。

今天咱们就把这块彻底讲透。

4.1 为什么要做峰值检测?

音频信号是连续变化的。你没法直接拿原始波形去判断音量,因为波形本身在正负之间来回跳。

我们需要一个「平滑后的音量指示器」。

这个指示器,就是包络(Envelope)。

我习惯把包络理解成「信号的外壳」——它勾勒出信号幅度的变化趋势,而不是每个采样点的瞬时值。

核心目标:从原始音频信号中提取出「感知上的响度变化」,用于控制增益衰减量。

4.2 RMS检测:最常用的响度估计

RMS(Root Mean Square)检测,就是计算一段时间内信号的平均功率。

公式很简单:

RMS = sqrt( mean( x[n]^2 ) )

但实际实现时,我们通常用滑动窗口或者一阶低通滤波器来近似。

我个人项目中用得最多的是「滑动平均RMS」:

// 滑动窗口RMS检测
float rms_detect(float input, float buffer[], int *index, int window_size) {
    // 平方后存入环形缓冲区
    buffer[*index] = input * input;
    *index = (*index + 1) % window_size;
    
    // 计算窗口内平均值
    float sum = 0.0f;
    for (int i = 0; i < window_size; i++) {
        sum += buffer[i];
    }
    float mean = sum / window_size;
    
    // 开方得到RMS
    return sqrtf(mean);
}

嗯,这里要注意:窗口大小直接影响响应速度。

  • 窗口太小(比如2ms):RMS波动大,限幅器会「神经质」地乱动
  • 窗口太大(比如100ms):反应迟钝,瞬态信号会漏过去

我一般建议:

应用场景 推荐窗口长度 说明
人声处理 10-20ms 兼顾清晰度和平滑度
鼓组/打击乐 3-5ms 需要快速响应瞬态
母带处理 30-50ms 追求整体响度一致性

我的小技巧:如果你做的是实时处理,可以用一阶IIR滤波器代替滑动窗口,计算量小得多:

rms = alpha * rms + (1 - alpha) * input^2;

其中 alpha = exp(-1 / (Fs * T)),T是等效时间常数。

4.3 峰值保持:抓住瞬态不放手

RMS检测有个毛病——它太「平均」了。

一个短暂的鼓点,RMS可能只上升一点点,但峰值已经爆表了。如果只用RMS控制限幅器,这个鼓点就会「穿墙而过」,造成失真。

所以我们需要峰值保持(Peak Hold)。

峰值保持的逻辑很简单:

  1. 检测当前采样点的绝对值
  2. 如果大于当前保持值,立即更新
  3. 如果小于当前保持值,按一定速率衰减

我曾经在一个现场扩声项目里吃过亏——只用RMS检测,结果底鼓每一下都削出爆音。后来加上峰值保持,问题立刻解决。

// 峰值保持实现
float peak_hold(float input, float *hold_value, float decay_rate) {
    float abs_input = fabsf(input);
    
    // 如果当前输入更大,立即更新
    if (abs_input > *hold_value) {
        *hold_value = abs_input;
    } else {
        // 否则按速率衰减
        *hold_value *= decay_rate;
    }
    
    return *hold_value;
}

decay_rate 通常取 0.999 到 0.9999 之间,对应不同的保持时间。

注意:峰值保持的衰减速率不能太慢,否则限幅器会「卡住」——信号已经小了,增益还在压着。我见过有人设成0.99999,结果一个响雷让限幅器罢工了半秒钟。

4.4 攻击与释放时间:包络跟踪的灵魂

有了RMS和峰值检测,我们还需要控制「什么时候开始压」和「什么时候松开」。

这就是攻击时间(Attack)和释放时间(Release)。

攻击时间决定了:当信号超过阈值时,增益衰减需要多快达到目标值。

释放时间决定了:当信号回落到阈值以下时,增益需要多慢恢复。

我习惯用一阶平滑器来实现:

// 攻击/释放包络跟踪
float envelope_track(float input_level, float threshold, 
                     float attack_coeff, float release_coeff) {
    static float envelope = 0.0f;
    
    // 判断当前是攻击还是释放阶段
    if (input_level > envelope) {
        // 攻击阶段:快速上升
        envelope = attack_coeff * envelope + (1 - attack_coeff) * input_level;
    } else {
        // 释放阶段:慢速下降
        envelope = release_coeff * envelope + (1 - release_coeff) * input_level;
    }
    
    return envelope;
}

系数怎么算?

攻击系数:attack_coeff = exp(-1 / (Fs * attack_time))

释放系数:release_coeff = exp(-1 / (Fs * release_time))

举个例子:

  • Fs = 48000 Hz
  • 攻击时间 = 1 ms → attack_coeff ≈ 0.979
  • 释放时间 = 100 ms → release_coeff ≈ 0.99979

你看,攻击系数小很多,所以上升快;释放系数接近1,所以下降慢。

经验之谈:攻击时间一般设1-10ms,释放时间设50-500ms。但具体数值取决于素材类型。

我处理人声时喜欢攻击5ms、释放150ms;处理鼓组时攻击1ms、释放50ms。

4.5 完整的包络跟踪流程图

下面我把整个流程画出来,方便你理解各个模块的关系:

峰值检测与包络跟踪流程图 输入音频信号 RMS检测 峰值保持 攻击/释放平滑 输出包络信号 滑动窗口或IIR 保持+衰减 取最大值或加权融合

4.6 实际项目中的避坑指南

最后,分享几个我踩过的坑:

  • RMS和峰值要配合使用:只用RMS会漏瞬态,只用峰值会过度敏感。我一般RMS占70%、峰值占30%做加权融合。
  • 攻击时间不是越短越好:太短会导致「咔嗒」声,因为增益变化太快产生了可闻的调制。我建议下限设在0.5ms。
  • 释放时间要避免「泵浦效应」:释放太快,背景噪声会跟着信号一起「呼吸」,听起来很不自然。我曾经在播客处理中把释放设成300ms,效果就好多了。

一句话总结:RMS看整体,峰值看瞬态,攻击释放管节奏。三者配合好了,限幅器才能「该出手时就出手,该收手时就收手」。

专注资料整理