做语音信号处理的人,迟早会碰到这样的问题:音是录下来了,背景里却全是“沙沙”的底噪,传统的滤波器一上去,要么削掉音色,要么残留刺耳的高频毛刺。我刚开始搭语音前端的时候,被这个问题卡了很久,后来把常用的降噪手段过了一遍,真正觉得“用顺手”的还是小波分解法。与其说它是一种滤波,不如说它是一次“分层审查”——把含噪语音按频率尺度和时间位置同时切开,找出那些主要是噪声的小波系数,做阈值处理后再重构回去。这篇文章把我在 MATLAB 上完整跑通的小波分解语音降噪例程、参数选型逻辑和踩过的坑一次性写清楚,适合信号处理方向的学生、语音前端刚入门的工程师,以及任何需要在短代码内快速验证降噪效果的开发者。
之前我总觉得降噪嘛,就是滤个波或者用谱减法把噪声谱估出来减掉。后来做了一段带强弱变化的语音才发现,语音信号根本就不是平稳的,清音浊音交替变化,局部频谱特征差异很大。小波分解能在一个分解框架里同时保留“哪个时刻、哪个频段”的信息,而且实现起来不需要太多数据,得到的结果也比普通带通滤波器干净得多。
1. 为什么语音降噪绕不开小波分解
1.1 传统滤波器在语音降噪上的三个尴尬
先说说我最早用过的几种方法,后面你就能理解为什么小波分解这条路值得走。
第一种是低通或带通滤波器。语音能量大部分集中在低频区域,噪声如果在中高频,我们用低通把高频切掉确实能去掉一部分底噪。但问题是,宽带噪声遍布整个频带,而语音里的清音、摩擦音(比如“s”“sh”这类音)恰恰也在高频。一刀切的结果往往是:噪声少了,齿音、舌尖音这些细节也没了,听起来像隔着一层布在说话。
第二种是频谱减法。核心思路是先估计噪声频谱,再从带噪语音的频谱中减掉它。这个方法在没有参考信号的情况下非常依赖噪声估计算法,而且减完之后容易出现音乐噪声——那些忽高忽低、断断续续的伪频点,听着比原噪声还难受。尤其是平稳性较差的环境,噪声估计一更新,整段低频就跟着抖。
第三种是自适应滤波,比如 LMS、NLMS 这类。效果好,但多数场景需要一个额外的参考麦克风去采噪声通道,很多实验环境不具备这个条件,测试数据往往只有一路含噪语音。而且自适应滤波在语音段和非语音段之间切换时,收敛速度、步长这些参数又够折腾一阵。
小波分解和上面几种的底层逻辑不一样。谱减法是从频域“把噪声幅度减掉”,自适应滤波是从时域“迭代逼近期望信号”,而小波分解是把语音在时间和频率两个维度上切成不同的子带,然后针对每个子带内的系数做“保留”或“收缩”。这样既不会一刀切掉高频细节,也不用额外参考信号,天然适合非平稳的语音场景。
1.2 小波分解降噪的本质:把噪声“挑”出来再扔掉
小波分解的数学过程我尽量用大白话讲。信号每次经过一对滤波器,一个低通、一个高通,再下采样,得到两个部分:一个是“近似系数”,相当于信号的低频轮廓;一个是“细节系数”,相当于信号的高频变化。对近似系数继续重复这个操作,一层一层拆下去,就形成了多分辨率的分解结构,这也是 MALLAT 算法最经典的形式。
拿 5 层小波分解来说,最后你会得到:
- 第 1 层细节系数:对应原始信号最高频的一带;
- 第 2 层细节系数:次高频;
- 第 3、4、5 层细节系数:频率逐层递减;
- 第 5 层近似系数:最低频的主体轮廓。
语音信号本身的能量高度集中在低频部分,所以大部分“有用的东西”都在近似系数里。而白噪声、环境底噪这些,能量相对平均地分布在各层的细节系数中。也就是说,每一层细节系数里都混着真信号的高频细节和噪声,但我们能够通过统计噪声的水平,设定一个阈值:绝对值小于阈值的系数,很大程度上是噪声,置零或收缩;绝对值大于阈值的系数,基本是声音的瞬态或特定高频特征,保留下来。
整个过程就像摘菜叶上的虫害点,不是整棵菜不要,而是把坏叶挑出来处理。这里的阈值处理不碰近似系数,近似系数里是语音的主体能量,动了它整段声音就变闷变空了。这也是这个方法相对柔和的关键所在。
1.3 小波降噪的适用场景与边界
它不是万能的,用之前先确认场景是否合适。
| 场景 | 效果 | 说明 |
|---|---|---|
| 高斯白噪声叠加语音 | 很好 | 噪声在各层细节系数分布均匀,阈值规律性强 |
| 稳态环境底噪 | 好 | 空调声、风扇声、麦克风底噪这类相对平稳噪声 |
| 突发性强噪声 | 中等 | 门声、钥匙声、拍桌子声,小波对瞬态其实有天然优势,但要配合端点检测做分段阈值调整 |
| 多说话人重叠语音 | 一般 | 阈值处理容易误伤重叠部分的语音细节 |
| 强混响环境 | 一般 | 混响本质是语音自身的反射叠加,不完全是噪声,小波降噪帮不上大忙 |
实时性也要注意。小波分解本身复杂度不算高,但多层分解加阈值计算、重构,再加上延迟处理,真要在嵌入式设备上跑,还是要做一些裁剪和缓冲优化。离线分析、离线降噪、课堂实验、后端后处理这种场景,直接用现成代码就非常舒服。
2. 方案设计:基函数、分解层数与阈值决策
2.1 小波基函数怎么选:3 个关键参数
写 MATLAB 例程之前,第一个要拍板的是用哪一族小波基。很多人一开始不管三七二十一用db1(其实就是 Haar),跑出来降噪效果粗糙,就以为小波降噪不行。实际上,基函数的选择直接决定了分解后的稀疏程度,也就是“语音能量能否集中到少数大系数上”。
选基函数主要看三点:正交性、紧支撑性、消失矩。
- 正交性:正交小波可以保证分解与重构的能量保持,不易引入冗余。语音处理常用的是 Daubechies(db 族)和 Symlets(sym 族)。
- 紧支撑性:支撑长度意味着小波基在时域上覆盖多长的区间。支撑太短,频域分得不够细,噪声和语音难以分离;支撑过长,瞬态定位又变差。
- 消失矩:它决定了小波对平滑信号的“敏感度”,消失矩越高,对平滑区域的响应越弱,意味着光滑语音信号会被压成更小的系数,噪声和信号在系数分布上的区分度更高。
我在实际项目中习惯从db6开始试,然后再看sym6、sym8。这几组在语音场景下的表现普遍不错。再往高走,比如db10、db20,消失矩更高,但支撑区间也变长,某些瞬态细节反而会被抹得太平。
| 基函数 | 消失矩 | 滤波器长度 | 优缺点 |
|---|---|---|---|
| db1(Haar) | 1 | 2 | 最简单,频域局部性差,降噪粗糙 |
| db4 | 2 | 8 | 计算快,保留瞬态较好,但分频精度稍弱 |
| db6 | 3 | 12 | 语音降噪起点选择,平衡性好 |
| sym6 | 3 | 12 | 近似对称,相位畸变小,听感更自然 |
| sym8 | 4 | 16 | 更精细的分频,但计算量略增 |
注意:小波基不是越高级越好。你用
db10跑出来的 SNR 可能比db6高零点几个 dB,但听感上瞬态细节的损失往往更明显。测指标和听效果要同时看。
2.2 分解层数:从采样率反推频带
分解层数决定你把语音分到多细的频率刻度。层数太少,噪声和语音混在同一个子带里,阈值不好使;层数太多,计算量上去了,同时最低频的细节系数里也可能藏着不少语音的基频谐波,处理过度就会伤音质。
一个比较土但很有效的办法:直接按采样率算每一层对应的频带范围。采样率 fs,第 k 层细节系数的频带范围是:
- 下边界:fs / (2^(k+1))
- 上边界:fs / (2^k)
以 fs = 16000 Hz 为例:
| 分解层 | 细节系数频带 | 对应语音内容 |
|---|---|---|
| 1 层(cD1) | 4000 ~ 8000 Hz | 高频齿音、部分摩擦音、宽带噪声残留 |
| 2 层(cD2) | 2000 ~ 4000 Hz | 部分共振峰、清音过渡段 |
| 3 层(cD3) | 1000 ~ 2000 Hz | 中频共振峰、辅音过渡 |
| 4 层(cD4) | 500 ~ 1000 Hz | 低频共振峰、浊音边界 |
| 5 层(cD5) | 250 ~ 500 Hz | 浊音基频部分 |
| 5 层近似(cA5) | 0 ~ 250 Hz | 语音主体低频能量 |
从这个表就能看出为什么一般不推荐 3 层以内:cD1 和 cD2 还是会把大量语音细节和噪声混在一起。我建议常规语音降噪至少选 4 到 5 层,采样率更高的录音(比如 48 kHz)可以选 6 层。层数再往上,提升有限,但系数规模变大,处理时间线性上升。
2.3 阈值规则与软硬阈值的选择
阈值处理是整个算法的心脏。MATLAB 里thselect提供四种常见阈值规则:
| 规则 | 关键词 | 特点 |
|---|---|---|
| 通用阈值 | sqtwolog | 固定公式,噪声抑制强,但容易过度收缩 |
| 无偏风险估计 | rigrsure | 基于 Stein 无偏风险估计,自适应强,对弱信号更友好 |
| 启发式阈值 | heursure | 综合 sqtwolog 和 rigrsure,根据信噪比自动择高 |
| 极大极小准则 | minimaxi | 保守处理,最大均方误差最小化,保留细节多 |
其中通用阈值的公式是:
thr = sigma * sqrt(2 * log(N))这里 sigma 是噪声标准差,N 是信号长度。这个公式的出发点是在信号足够长、噪声满足高斯分布时,理论上能把噪声系数几乎清零。但语音信号长度通常不短,这个阈值算出来往往偏大,容易把语音高频细节也压掉。所以实际做语音时,我更常用rigrsure或heursure,它们在听感上更“软”。
然后是软阈值和硬阈值的选择。
- 硬阈值:系数绝对值大于阈值则保留原值,小于等于阈值则置零。优点:细节能量保留完整;缺点:在阈值点附近会产生不连续,重构后容易出现伪振铃,听感上像带了一点金属感。
- 软阈值:系数绝对值大于阈值时,向零方向收缩一个阈值量。优点:连续性更好,重构后信号平滑;缺点:会让系数的整体幅度略偏小,听感上高频细节稍微变柔。
我在语音降噪里绝大多数情况选软阈值。尤其当你后续还要再接一个均衡器或者编码器时,硬阈值带来的不连续伪迹会被后续模块放大。只有离线处理、对瞬态细节要求非常高的时候,我才会考虑硬阈值。
3. MATLAB 例程实现与关键细节拆解
3.1 完整可运行的降噪例程
下面是我在 MATLAB 上直接跑通过的例程。输入可以是一段你自己录的带噪 wav 文件,也可以用程序合成一段模拟语音。为了方便你直接复现,我在代码里做了兼容:如果当前目录下有speech.wav就读文件,否则自动生成一段仿真语音信号。
clear; clc; close all; %% ===== 1. 语音数据准备 ===== fs = 16000; if exist('speech.wav', 'file') [x, fs] = audioread('speech.wav'); x = x(:, 1); % 取单声道 else t = (0:2*fs-1)' / fs; % 生成 2 秒仿真语音 x = 0.6 * sin(2*pi*120*t) .* exp(-((t-0.6).^2)/0.05) + ... 0.4 * sin(2*pi*180*(t-1.0)) .* exp(-((t-1.0).^2)/0.08) + ... 0.3 * randn(size(t)) .* (t>1.5); end x = x / max(abs(x)); % 归一化到 [-1, 1] N = length(x); %% ===== 2. 叠加高斯白噪声模拟带噪语音 ===== rng(42); noise = 0.08 * randn(N, 1); y = x + noise; %% ===== 3. 小波分解 ===== wname = 'db6'; level = 5; [C, L] = wavedec(y, level, wname); %% ===== 4. 噪声标准差估计与阈值计算 ===== cD1 = detcoef(C, L, 1); sigma = median(abs(cD1)) / 0.6745; % MAD 估计噪声标准差 thr = sigma * sqrt(2 * log(N)); % 通用阈值 fprintf('估计噪声标准差: %.4f\n', sigma); fprintf('阈值 thr: %.4f\n', thr); %% ===== 5. 对前 level 层细节系数做软阈值处理 ===== [C_den, L_den] = wthcoef('d', C, L, 1:level, 's', thr); %% ===== 6. 小波重构 ===== x_den = waverec(C_den, L_den, wname); x_den = x_den / max(abs(x_den)); %% ===== 7. 信噪比评估 ===== SNR_in = 10 * log10(sum(x.^2) / sum((y - x).^2)); SNR_out = 10 * log10(sum(x.^2) / sum((x_den - x).^2)); fprintf('降噪前 SNR = %.2f dB\n', SNR_in); fprintf('降噪后 SNR = %.2f dB\n', SNR_out); %% ===== 8. 画图对比 ===== t_axis = (0:N-1) / fs; figure('Color', 'w', 'Position', [100 100 900 700]); subplot(3, 1, 1); plot(t_axis, x); xlabel('时间/s'); ylabel('幅值'); title('原始语音'); grid on; subplot(3, 1, 2); plot(t_axis, y); xlabel('时间/s'); ylabel('幅值'); title('带噪语音'); grid on; subplot(3, 1, 3); plot(t_axis, x_den); xlabel('时间/s'); ylabel('幅值'); title('小波降噪后语音'); grid on; %% ===== 9. 语谱图对比(可选)===== figure('Color', 'w', 'Position', [100 100 900 500]); subplot(1, 2, 1); spectrogram(y, 256, 128, 256, fs, 'yaxis'); title('降噪前语谱图'); subplot(1, 2, 2); spectrogram(x_den, 256, 128, 256, fs, 'yaxis'); title('降噪后语谱图');代码结构就是四步:分解、细节系数阈值处理、重构、评估。wthcoef是这里最关键的接口,它的'd'表示对细节系数操作,1:level表示哪些层,'s'表示软阈值,最后一个参数是阈值大小。重构时waverec会自动把处理后的系数合成语音。跑完这个例程,你会看到一个很直观的现象:带噪语音波形里的那些细小毛刺,在降噪后明显平滑,但整体的音包络还在。
3.2 核心函数参数拆解
很多文章只甩代码不讲函数参数,这里我把例程里几个关键调用的含义和注意事项说清楚。
wavedec(y, level, wname)的返回值是[C, L]。C是一个行向量,里面按顺序放着第level层的近似系数、第level层到第 1 层的细节系数。L则记录了每个部分占多长。千万不要自己手动去拆C,除非你对系数排列已经非常熟悉,否则很容易索引错位。正确做法是用detcoef、appcoef提取系数,用wthcoef统一做阈值操作。
detcoef(C, L, 1)提取第 1 层细节系数,也就是分解后的最高频部分。我们在用它估计噪声标准差时,假设这一层里噪声占主导,同时语音高频信号只是少数“尖峰”。这个假设在多数非强噪场景下是成立的。
wthcoef('d', C, L, 1:level, 's', thr)的参数含义是:
'd':操作对象是细节系数;1:level:指定处理第 1 层到第 level 层的细节系数;'s':使用软阈值;thr:阈值。
它返回一组新的[C_den, L_den],直接交给waverec。
3.3 除了 SNR,还有什么评估维度
SNR 是衡量降噪效果最直接的指标,但它有个问题:如果算法把语音细节也压没了,SNR 指标反而不差,因为误差小了,但可懂度和听感变差了。所以我的习惯是至少看三个东西:
- SNR 提升幅度:一般提升 5~10 dB 就算有效果。
- 波形对比:降噪后的波形背景是否更干净,语音段的快速变化是否保留。
- 语谱图对比:高频共振峰和清音乱区是否还在,底噪区域是否变暗。
如果条件允许,建议加一个PESQ或者STOI指标来分析。PESQ 分数范围在-0.5到4.5之间,语音通信常用;STOI 更偏向可懂度,范围在0到1之间。这两个指标比单一的 SNR 更接近人耳的主观感受。
3.4 我的运行效果参考值
用fs = 16000、db6、level = 5、软阈值时,我常用的那批测试音频,输入 SNR 在 8~12 dB 左右时,降噪后一般能到 17~20 dB。语谱图上高频噪点明显变稀疏,元音的共振峰横条纹更清晰。听感上,底噪变小,但摩擦音的舌尖感会弱一点点,这在软阈值下是正常现象,如果想补救,可以在降噪后做一个 4~8 kHz 频段的轻微高频补偿。
4. 常见问题与参数调优实录
4.1 噪声标准差估计的“sigma”陷阱
我第一次自己写阈值代码的时候踩过一个很典型的坑:直接用整段带噪信号估计方差,然后代入通用阈值公式,结果阈值大得离谱,降完的语音变成“闷罐”。后来才明白,噪声标准差的稳健估计要用最细节的那层系数,也就是cD1,而且要用中位数绝对偏差(MAD)方式:
sigma = median(abs(cD1)) / 0.6745分子是cD1绝对值的中位数。0.6745 是标准正态分布 75% 分位数对应的常数,这样估计出的 sigma 的高斯白噪声一致性非常好。用普通标准差函数std(cD1)替代是不行的,因为cD1里其实还有语音的高频细节,这些大系数会让std偏大,进而导致阈值偏大。
如果你发现自己阈值算出来之后,连清音的摩擦噪声都被压得没了,先检查这一行。
4.2 硬阈值带出来的“类音乐噪声”
某次实验里我换成了硬阈值,SNR 看着更高,但听感奇差,出现了类似频谱减法的音乐噪声。原因刚才讲过:硬阈值在阈值点附近不连续,重构时会在小波基支撑范围内产生振荡性伪迹。
如果你非要保留硬阈值的高细节度,有一个折中方案:把硬阈值判断后保留下来的系数,稍微做软收缩或者乘一个 0.8~0.95 的缩放因子。这样既保留了细节,又缓和了不连续性。我在离线后处理里用过这个方式,SNR 只低了大概 0.3 dB,但听感自然很多。
4.3 分解层数过多导致语音发“闷”
调参过程中最容易遇到的一个现象是:降噪后的语音不吵了,但音色明显变闷。这大概率是分解层数过高或者对低频细节系数也下了狠手。
再看一遍分频表:第 5 层细节系数通常对应 250~500 Hz,这里头有不少浊音的基频能量。如果你对cD4、cD5也用了很大的阈值,相当于把语音的“骨架”跟着削了。
我的做法:对cD1到cD3用正常阈值,对cD4、cD5把阈值乘上 0.5~0.7,或者干脆不对cD5做处理。这样降噪后的语音既有干净的背景,又不至于闷。你可以用wthcoef分别处理不同层,给它不同的阈值:
[C_den, L_den] = wthcoef('d', C, L, 1:3, 's', thr); [C_den, L_den] = wthcoef('d', C_den, L_den, 4, 's', 0.6 * thr);4.4 边界效应与分段过渡
小波分解的滤波器在信号端点处不可避免会引入边界效应。整段处理时,端点只影响开头和结尾几十个采样点,不大起眼。但如果你把长语音切成很多短段分别降噪再拼回去,段边界处会出现明显的“咔哒”或“鼓包”。
处理方案有两种:
一是用 MATLAB 内部默认的边界延拓方式,绝大多数情况够用。二是做分段处理时加重叠和交叉淡化:每段之间重叠 10%~20%,降噪后先做淡入淡出再加和。我的实现习惯是重叠 50 毫秒左右,用汉宁窗做交叉淡化,听起来完全无痕。
4.5 参数调优速查表
| 症状 | 可能原因 | 调整方向 |
|---|---|---|
| 降噪后语音发闷 | 阈值过大 / cD4 cD5 处理过度 | 改用 heursure 阈值,或降低低频层阈值 |
| 背景噪声清了但声音变尖 | 高频细节被过度保留,噪声残留在 cD1 | 适当提高 cD1 阈值 |
| 出现音乐性伪差 | 硬阈值导致的不连续 | 换软阈值,或用硬阈值+系数缩放折中 |
| 波形端点有尖刺 | 边界效应 | 换延拓方式,或分段交叉淡化 |
| SNR 提升不明显 | 分解层数太少 / 阈值太小 | 增加层数,重新检查 sigma 估计 |
| 清音和齿音听感丢失 | cD1 cD2 被压得太多 | 单独降低 cD1、cD2 的阈值系数 |
5. 后续方向:从小波分解到小波包
如果你已经能在 MATLAB 里跑通上面的例程,下一步可以试试小波包分解。小波包和普通小波分解的区别在于:普通小波只对近似系数继续分解,细节系数不再展开;而小波包对近似和细节都会继续分解,相当于把整个频带分得更细,尤其是中高频区域也能做更精细的阈值调控。
语音里的非平稳噪声,有时在某一个子带里特别集中,普通小波分解对这个子带控制得不够细。小波包则可以把对应频段的叶子系数单独拿出来处理。MATLAB 里对应函数是wpdec、wprcoef和wpthcoef,你可以把第 3.1 节的例程改成小波包版本对比一下效果。
还有一条路是把小波降噪和谱减法结合:先用小波降噪去除宽带白噪声,再用谱减法处理残留的窄带噪声,能做到红白噪声混合场景下的更好效果。我后来在某个音频后处理项目里就是两层叠加,换来的是大约 1~2 dB 的额外提升和更干净的听感。
我个人的体会是:小波降噪这套流程,最难的部分不是代码,而是“理解你动的是哪些系数”。每次调参数前,先在脑海里过一遍:这一层细节系数现在对应的频段是什么,语音在这个频段有多少成分,噪声又有多少成分。想清楚了再动手改阈值,基本不会跑偏。
最后再分享一个小技巧:离线处理时,不要上来就用全局阈值。先用第 1 层细节系数算出 sigma,画出cD1系数的直方图,看看它是不是近似钟形分布。如果分布明显多出一个拖尾的大系数群,说明这一层里语音细节占比不低,阈值可以适当放宽。这个习惯帮我避开了很多“指标好看但听感劣化”的情况,建议你也试试。