24、音频处理实战:音频降噪、回声消除、自动增益控制,实现一个音频预处理工具。
说实话,做音视频开发这么多年,我踩过最多的坑反而不是编解码,而是音频预处理。你想想看,一个视频通话产品,画面再清晰,如果对方那边传来呼呼的风扇声、刺耳的回声,或者声音忽大忽小,用户立马就挂断了。音频预处理,说白了就是给原始音频信号“洗个澡”,让它干干净净地送出去。
今天我们就来手撸一个音频预处理工具,把降噪、回声消除、自动增益控制这三个核心模块串起来。我个人习惯把它们叫做“音频三件套”,缺一个都不行。
24.1 音频预处理的核心流程
先看整体架构。我画了一张流程图,帮你理清数据是怎么流转的。
数据从左到右依次经过三个模块。注意看,回声消除模块多了一个“远端参考信号”输入。这是关键——没有参考信号,回声消除就是瞎搞。我在项目中遇到过有人把AEC当降噪用,结果声音全乱了。
24.2 降噪模块:把背景噪声按下去
降噪的本质是什么?说白了就是区分“人声”和“噪声”。人声有谐波结构,噪声通常是平稳的。我常用的方法是谱减法,简单粗暴但有效。
核心思路:在频域里,把噪声的幅度谱从信号中减掉。噪声谱怎么估计?用静音段来建模。
来看一段实现代码。我用的是WebRTC的降噪算法,但为了让你理解原理,我写了一个简化版:
// 简化版谱减法降噪
void NoiseSuppression::Process(float* input, int samples) {
// 1. 分帧 + 加窗
// 2. FFT到频域
// 3. 估计噪声谱(前几帧作为噪声基底)
// 4. 计算增益:G = max(1 - alpha * noise_spectrum / signal_spectrum, floor)
// 5. 应用增益
// 6. IFFT回时域
for (int i = 0; i < samples; ++i) {
// 实际项目中用重叠保留法
float gain = ComputeGain(input[i]);
output[i] = input[i] * gain;
}
}
我的经验:alpha值一般取2~3,floor取0.01~0.1。alpha太大会有音乐噪声,太小降噪不干净。我曾经调了一整天,最后发现是窗函数选错了——用汉明窗比矩形窗好很多。
24.3 回声消除:别让喇叭的声音再回来
回声消除是音频处理里最头疼的模块。为什么?因为它要处理的是“非线性”问题。喇叭出来的声音经过空气传播、麦克风采集,中间有各种失真。
AEC的基本原理是自适应滤波。你想想看,我们已知远端播放的信号,也采集到了近端麦克风的信号。如果能估计出回声路径的冲激响应,就能从麦克风信号中减去回声。
| 参数 | 典型值 | 说明 |
|---|---|---|
| 滤波器长度 | 512 ~ 2048 | 取决于房间混响时间,我一般用1024 |
| 步长因子 | 0.1 ~ 0.5 | 太大发散,太小收敛慢 |
| 双讲检测阈值 | 0.3 ~ 0.6 | 双方同时说话时停止更新滤波器 |
注意:双讲检测(Double Talk Detection)是AEC的命门。如果双方同时说话时还继续更新滤波器,滤波器会发散,回声反而变大。我曾经在一个项目中没做双讲检测,结果测试时客户说“你们这个产品会自己说话”……
代码实现上,我推荐用NLMS(归一化最小均方)算法:
void AcousticEchoCanceller::Process(
float* mic_signal, // 麦克风采集信号
float* ref_signal, // 远端参考信号
int samples)
{
// 1. 双讲检测
if (IsDoubleTalk(mic_signal, ref_signal)) {
// 不更新滤波器系数
} else {
// 2. 自适应滤波
float error = mic_signal[i] - FilterOutput(ref_signal);
// 3. 更新滤波器系数
for (int j = 0; j < filter_len; ++j) {
h[j] += mu * error * ref_signal[i - j] / (power + epsilon);
}
}
// 4. 输出 = 误差信号
output[i] = error;
}
24.4 自动增益控制:让音量稳如老狗
AGC其实是最容易被忽视的模块。很多人觉得“音量调大点不就行了”?但实际场景中,人离麦克风远一点声音就小,近一点就爆音。AGC要做的就是动态调整增益,让输出音量保持在一个目标水平。
我常用的AGC策略是:
- 目标电平: -26 dBFS(语音通信标准)
- 增益范围: -20 dB ~ +20 dB
- 攻击时间: 10 ms(音量突然变大时快速压下来)
- 释放时间: 200 ms(音量变小时慢慢提上去)
关键点:攻击要快,释放要慢。为什么?因为人耳对音量突然变大很敏感,但对慢慢变小不太敏感。这是心理声学的基本原理。
void AutomaticGainControl::Process(float* input, int samples) {
float target_level = 0.05f; // -26 dBFS
float current_level = ComputeRMS(input, samples);
float gain = target_level / (current_level + 1e-6f);
// 限制增益变化速度
gain = Clamp(gain, prev_gain * 0.9f, prev_gain * 1.1f);
prev_gain = gain;
// 应用增益
for (int i = 0; i < samples; ++i) {
output[i] = input[i] * gain;
// 防止削波
if (output[i] > 1.0f) output[i] = 1.0f;
if (output[i] < -1.0f) output[i] = -1.0f;
}
}
避坑指南:我曾经把AGC放在降噪前面,结果噪声被放大了,降噪模块压力巨大。正确的顺序是:降噪 → 回声消除 → AGC。AGC放在最后,确保输出音量稳定。
24.5 整合成一个预处理工具
好了,三个模块都讲完了。现在把它们串起来。我习惯用链式调用的方式:
class AudioPreprocessor {
public:
void Process(float* mic_in, float* ref_in, float* out, int samples) {
// 1. 降噪
ns_.Process(mic_in, samples);
// 2. 回声消除
aec_.Process(mic_in, ref_in, samples);
// 3. 自动增益
agc_.Process(mic_in, samples);
// 4. 拷贝输出
memcpy(out, mic_in, samples * sizeof(float));
}
private:
NoiseSuppression ns_;
AcousticEchoCanceller aec_;
AutomaticGainControl agc_;
};
这个工具我已经在多个项目中用过。说实话,调参才是最花时间的。每个房间的声学环境不一样,麦克风硬件也不一样。我建议你准备一套默认参数,然后根据实际场景微调。
嗯,最后提醒一句:音频预处理没有银弹。降噪太狠会损伤语音,AGC太激进会引入呼吸声。你需要根据产品定位做取舍。做语音通话,保语音清晰度优先;做录音,保自然度优先。
公众号:蓝海资料掘金营,微信deep3321