1. 先把问题拆开:语音去噪到底在去什么
我处理过不少语音降噪的活儿,从会议录音、客服通话到设备采集的现场声,最开始大家都会去找各种"高级算法",但真正落到工程里,很多时候一个设计得当的低通滤波器就能把问题解决掉一大半。原因很朴素:绝大多数让人烦躁的噪声,能量恰好压在语音主要频段之外,尤其是高频那一段。齿音、电流底噪、设备自噪声、录音笔的本底嘶声,这些玩意儿在 4kHz 以上堆得满满当当,而人说话的能量重心基本落在 300Hz 到 3400Hz 之间。你把 4kHz 以上切掉,噪声里的"沙沙"感立刻下去一大截,而语音的可懂度几乎不受影响。
这篇内容讲的就是这件事:用低通滤波器做语音去噪,配合时域波形图和频域频谱图看清楚"去掉了什么、留下了什么",并且给出可以直接跑的 Matlab 代码。适合三类人看——刚接触数字信号处理的同学,需要快速交一个课程设计或者实验报告;做嵌入式音频采集的工程师,想在前端加一级最省算力的预处理;还有已经会调filter函数但画出来的频谱图自己都看不懂的实践者。我不打算只贴一段代码就完事,重点是那些参数为什么这么定、阶数怎么算出来的、图画成什么样才算"有信息量",以及我踩过的那些坑。
先明确一个前提,免得后面走偏:低通滤波去噪是有适用边界的。它对付的是高频段噪声,对付不了低频噪声。空调嗡嗡声、电源工频干扰、机器低频振动,这些能量在 50Hz 到 300Hz 之间,低通滤波器对它们完全无能为力,甚至会因为通带增益把它们一起放大。所以动手之前,一定先看一眼含噪语音的频谱,确认噪声到底在哪一段,这一步比写代码重要十倍。
1.1 语音信号与噪声在频谱上的分布差异
人声的产生机制决定了它的频谱形状。声带振动给出基频,成年男声大概 80Hz 到 180Hz,女声 160Hz 到 300Hz,这个基频经过声道共鸣腔的调制,形成一系列谐波,谐波的能量包络就是我们说的共振峰。元音的第一共振峰通常在 300Hz 到 900Hz,第二共振峰在 900Hz 到 2500Hz,第三共振峰往上到 3500Hz 左右。再往上,能量衰减得非常快。也就是说,语音的"有效信息"高度集中在 3.5kHz 以内,这也是为什么电话系统从几十年前就把带宽定在 300Hz 到 3400Hz——足够听懂,又最省线路资源。
噪声就不一样了。白噪声的功率谱是平的,在整个频带上均匀分布;热噪声、量化噪声也是类似特征。录音设备的前置放大器自噪声、ADC 的量化底噪,这些在高频段往往比语音的残余能量还高。所以当你在频谱图上看到 5kHz 以上还有一片"草地",而语音的谐波结构在 3.5kHz 之后已经基本消失,那这片草地就是纯噪声,可以放心切掉。
这里有个容易忽略的点:语音的清音(比如"s"、"sh"、"f"、"c")本身就是宽带噪声性质的,频谱能延伸到 8kHz 以上。如果你把截止频率压到 3kHz,这些清音会被削掉,听感上就是"大舌头""含着东西说话"。这就是低通滤波去噪的第一个trade-off:降噪量和清晰度之间的平衡。我的经验是,截止频率不要低于 3.4kHz,除非你明确知道这段语音不需要辨听细节。
另一个必须提前判断的因素是噪声的时变性。低通滤波器的系数是固定的,它对整段信号一视同仁。如果噪声只出现在某一段(比如中间有人咳嗽、有键盘敲击),固定滤波器管不了这个,那是谱减法或者维纳滤波的活儿。低通滤波擅长的是稳态的、频谱位置明确的宽带噪声,这类问题用固定滤波器既稳又便宜。
1.2 为什么低通滤波器是入门首选,也是容易被误用的方案
选低通滤波器的理由很实在:计算量小、因果性好、参数少、行为可预测。一个 8 阶的 IIR 低通,每采样点只需要十几次乘加,跑在几十兆主频的单片机上毫无压力;相比之下,谱减法要做 FFT、估计噪声谱、做减法、再 IFFT,中间还有一堆需要调的超参数,稍不注意就引入"音乐噪声"——那种忽隐忽现的、像水泡破裂一样的声音,比原来的底噪还难听。
但"容易上手"恰恰是它最大的风险。我见过太多人直接写一句b = fir1(20, 0.2),然后就开始调截止频率,调到频谱图看起来"干净"为止,从来不问这个 0.2 对应多少赫兹、20 阶够不够、过渡带有多宽。这样的结果往往有两种:一种是截止频率定得太低,语音闷成一团;另一种是阶数太低,过渡带拖得极长,本该切掉的 5kHz 附近只衰减了 3dB,等于没滤。
提示:归一化频率是一个高频错误点。Matlab 里
fir1、butter的Wn参数是相对于奈奎斯特频率归一化的,也就是Wn = f_c / (f_s/2)。采样率 16kHz 时,截止 4kHz 对应Wn = 0.5,不是 0.25。差这一步,滤波器就整体跑偏了。
还有一个误区是把低通当万能药。有人拿着一段带电流声的录音来问,为什么滤完还是嗡嗡的。电流声是 50Hz 及其谐波,落在通带正中间,低通滤波器对它零衰减。这种情况应该用带阻(陷波)滤波器,或者先做高通把 100Hz 以下切掉。判断逻辑很简单:看频谱,噪声在通带内,低通没用;噪声在阻带内,低通有效。
2. 方案与参数选型:截止频率不是拍脑袋定的
参数选型这一节是我最想讲透的部分,因为大部分教程直接给一组数字就完事,读者换个采样率就懵了。实际上,截止频率、滤波器阶数、滤波器类型这三个量是互相牵制的,必须结合采样率一起算。
2.1 采样率、奈奎斯特频率与截止频率的约束关系
数字滤波器能处理的频率范围上限是奈奎斯特频率,即采样率的一半。这是硬约束:采样率 8kHz,你最多能处理到 4kHz,截止频率必须严格小于 4kHz,否则滤波器设计直接失效,Matlab 会报错或者给出一个无意义的响应。
实际工程里,语音采样的常见配置有这么几档,对应的截止频率选择我列了一个表:
| 采样率 | 奈奎斯特频率 | 语音有效上限 | 推荐截止频率 | 典型场景 |
|---|---|---|---|---|
| 8 kHz | 4.0 kHz | 3.4 kHz | 3.4 ~ 3.6 kHz | 电话语音、窄带对讲 |
| 16 kHz | 8.0 kHz | 6.5 kHz | 3.6 ~ 4.2 kHz | 通用语音采集、会议录音 |
| 32 kHz | 16.0 kHz | 12 kHz | 4.0 ~ 5.0 kHz | 需要保留清音细节 |
| 44.1 kHz | 22.05 kHz | 16 kHz | 4.5 ~ 6.0 kHz | 音乐兼语音、后期处理 |
注意 16kHz 采样这一档,我给出的截止频率范围比"语音上限 6.5kHz"低不少,这是故意的。因为去噪的主要收益来自切掉 4kHz 以上的噪声堆积区,而 4kHz 到 6.5kHz 这一段保留的清音信息对可懂度贡献有限,却塞满了噪声。如果你做的不是音乐性内容,只是要"听得清说的什么",把截止压到 4kHz 左右是性价比最高的位置。
还有一点常被忽略:抗混叠滤波。如果原始采集环节没做好抗混叠,2kHz 以上的信号会镜像折叠到低频,那种噪声是滤不掉的,因为它已经混进语音频段了。这种情况下任何后处理都是白费力气,只能回去改采集前端。
2.2 FIR 还是 IIR:两类低通滤波器的取舍
这是绕不开的选择题,我把实际对比列出来:
| 对比维度 | FIR(有限冲激响应) | IIR(无限冲激响应) |
|---|---|---|
| 相位特性 | 可以做到严格线性相位 | 非线性相位,通带边缘相位失真明显 |
| 达到相同衰减所需阶数 | 高,通常几十到上百阶 | 低,通常 4 到 12 阶 |
| 计算量 | 与阶数成正比,较大 | 小,适合嵌入式 |
| 稳定性 | 无条件稳定 | 阶数高或截止频率极低时可能不稳定 |
| 群延迟 | 大且固定(N/2 个采样点) | 小,但随频率变化 |
| 设计难度 | 窗函数法直观,参数好估 | 需要理解通带纹波、阻带衰减 |
| 适用场景 | 离线处理、需要保相位、需要严格指标 | 实时处理、算力受限、指标宽松 |
我的实际做法是:离线处理优先 FIR,因为可以用filtfilt做零相位滤波,完全避开延迟问题,而且线性相位保证了语音的瞬态结构不被拉歪;实时嵌入式场合优先 IIR,因为阶数低,一个 6 阶巴特沃斯就能顶 FIR 的几十阶,内存和算力都省。
有一种情况必须用 FIR:你要做波束成形、要做多路信号的相关分析、要做语音识别前端。这些场合对相位一致性有要求,IIR 的非线性相位会破坏通道间关系。反过来,如果只是"把噪声压下去,人耳听着舒服",IIR 完全够用。
2.3 阶数怎么算:把过渡带指标翻译成阶数
这是本节的核心,也是最容易被跳过的一步。阶数不是"越大越好",阶数大了计算量大、IIR 还可能不稳定、数值精度问题也会冒出来(尤其是直接型结构在高阶时极点容易跑出单位圆)。阶数由什么决定?由你要求的阻带衰减量和过渡带宽度共同决定。
先说 IIR 里的巴特沃斯。它的幅频响应满足:
|H(jΩ)|² = 1 / (1 + (Ω/Ω_c)^(2N))
其中 Ω_c 是截止频率(-3dB 点),N 是阶数。要求在某个频率 f_s 处衰减至少 A dB,推导出来:
N ≥ log10(10^(A/10) − 1) / (2 · log10(f_s / f_c))
拿一个具体例子算。假设采样率 16kHz,截止频率 f_c = 3400Hz,希望在 f_s = 4000Hz 处衰减 40dB。代入:
分子:log10(10^4 − 1) = log10(9999) ≈ 4.000 分母:2 × log10(4000/3400) = 2 × log10(1.1765) = 2 × 0.0706 ≈ 0.1412 N ≥ 4.000 / 0.1412 ≈ 28.3,取整 29 阶。
29 阶的巴特沃斯,在 16kHz 采样下做直接型实现,极点密集在单位圆附近,双精度都未必稳,更别说定点实现了。这说明什么?说明过渡带太窄了,要求不合理。把阻带起点放宽到 5000Hz 重新算:
分母:2 × log10(5000/3400) = 2 × log10(1.4706) = 2 × 0.1675 ≈ 0.335 N ≥ 4.000 / 0.335 ≈ 11.9,取整 12 阶。
12 阶的巴特沃斯就现实多了,可以用二阶节级联(SOS)结构实现,数值稳定性好。如果再放宽到 5600Hz,分母变成 2×log10(1.647)=0.4334,N ≥ 9.2,9 阶即可。
同样的问题在 FIR 上怎么算?用窗函数法时,过渡带宽度和阶数的关系近似为:
Δf ≈ k · f_s / N
k 是窗函数相关的常数,矩形窗约 0.9,汉宁窗约 3.1,汉明窗约 3.3,布莱克曼窗约 5.5。假设采样率 16kHz,希望过渡带 Δf = 800Hz,用汉明窗:
N ≈ 3.3 × 16000 / 800 = 66
66 阶的 FIR,每个采样点要 67 次乘加。对离线处理完全无所谓,对实时嵌入式就要掂量了。如果放宽过渡带到 1600Hz,N ≈ 33,就舒服多了。
这里我想强调一个观念:过渡带宽度是你自己定的指标,不是物理约束。大部分人其实并不真的需要"3.4kHz 以上全部杀掉",需要的只是"4kHz 以上明显压低"。把指标从"陡"改成"够用",阶数能降一个数量级,这就是工程上的取舍。
3. Matlab 实现全过程:从读音频到画出时域频域图
这一节给完整可跑的流程。我按实际调试顺序来写,每一步附上意图说明和容易出错的地方。代码在 R2018b 之后的版本都能直接跑,不需要额外工具箱(butter、fir1、filtfilt属于 Signal Processing Toolbox,如果没装,只能手写卷积)。
3.1 环境准备与音频读取的几个坑
先解决数据入口。audioread返回的可能是单声道列向量,也可能是双声道矩阵,采样率也可能不是你以为的那个值。这三件事必须在继续之前确认清楚。
% 读取音频文件 [x, fs] = audioread('noisy_speech.wav'); % 关键检查一:通道数 if size(x, 2) > 1 x = mean(x, 2); % 多通道混成单声道 end % 关键检查二:采样率 fprintf('采样率: %d Hz, 奈奎斯特频率: %d Hz\n', fs, fs/2); fprintf('时长: %.2f 秒, 样点数: %d\n', length(x)/fs, length(x)); % 关键检查三:幅度范围与直流偏置 fprintf('幅度范围: [%.4f, %.4f]\n', min(x), max(x)); x = x - mean(x); % 去直流,避免 0Hz 处出现一根巨柱第一个坑:多声道直接取平均是最省事的做法,但如果两路信号相位有差异(比如双麦克风阵列),取平均会产生梳状滤波效应,某些频率被抵消,频谱图上会出现规律的凹坑。遇到这种情况应该先检查通道间相关性,不是无脑平均。
第二个坑:mean(x)去直流这一步很多人省略。一旦音频里有直流偏置(很常见,尤其是某些采集卡),fft出来的 0Hz 分量会极大,画频谱时把整个纵轴压平,其他频率的细节全看不见了。去直流之后频谱才可读。
第三个坑:如果音频幅度很小(比如 0.02 满量程),画出来的时域图就是一条平线,看不出包络。这时候可以在绘图时单独缩放,但不要改原始数据,因为后面滤波和播放都按原始幅度走。归一化只在输出保存时做一次。
3.2 滤波器设计与滤波执行
我同时给出 FIR 和 IIR 两个版本,实际用哪个按前面 2.2 的判断来。这里都用filtfilt做零相位滤波,代价是会引入3*(N-1)个采样点的边界瞬态,两端要丢弃或者加窗淡化。
%% 方案 A:FIR 低通(推荐离线使用) fc_fir = 4000; % 截止频率 4000 Hz Wn_fir = fc_fir / (fs/2); % 归一化到奈奎斯特频率 N_fir = 66; % 阶数,按 3.3*fs/(2*Δf) 估算 b_fir = fir1(N_fir, Wn_fir, 'low', hamming(N_fir+1)); y_fir = filtfilt(b_fir, 1, x); % 零相位滤波 %% 方案 B:IIR 低通(推荐实时或算力受限使用) fc_iir = 4000; Wn_iir = fc_iir / (fs/2); N_iir = 6; % 巴特沃斯 6 阶 [z, p, k] = butter(N_iir, Wn_iir, 'low'); [sos, g] = zp2sos(z, p, k); % 转成二阶节,数值更稳 y_iir = filtfilt(sos, g, x);这里有几个细节值得说。fir1的第一个参数是阶数,生成的系数长度是N+1,所以窗函数也要给N+1点,长度对不上会报错或者悄悄出错。这一点新手特别容易翻车。
butter直接给[b, a]在高阶时(比如 12 阶以上)会出现数值问题,因为多项式系数的动态范围太大,极点位置算不准。正确做法是先拿到零极点再用zp2sos转成二阶节级联,filtfilt支持sos, g形式调用。我做过对比,同样的 12 阶巴特沃斯,直接型在 3400Hz 截止下幅频响应开始出现畸变,SOS 结构则是干净的。
filtfilt和filter的区别必须拎清。filter是单向滤波,会引入群延迟,FIR 情况下延迟就是N/2个采样点。66 阶 FIR 在 16kHz 下延迟约 2ms,听起来不明显,但如果你要把滤波后的信号和原始信号做减法看"去掉了什么",不对齐就会看到完全错乱的差值。filtfilt正反各滤一次,相位正好抵消,延迟为零,代价是幅频响应变成原来的平方——也就是说,原本 40dB 的阻带衰减会变成 80dB,截止点附近的过渡带也更陡。这个副作用通常是我们想要的,但要知道它存在,别拿它去验证"我的滤波器是不是 40dB"。
3.3 时域图和频域图怎么画才有信息量
这是我见过最多人做不好的地方。很多人画频谱就是plot(abs(fft(x))),出来一堆对称的、看不出任何结构的线,然后自己也不知道该看什么。正确的画法需要处理四件事:取单边谱、加窗、转 dB、对齐频率轴。
function plot_tf(x, y, fs, tag) N = 2^nextpow2(length(x)); % FFT 点数取 2 的幂,加速计算 w = hann(length(x)); % 汉宁窗,抑制频谱泄漏 f = (0:N/2-1) * fs / N; % 单边频率轴 % 时域对比 t = (0:length(x)-1) / fs; figure('Name', tag); subplot(2,2,1); plot(t, x); grid on; xlabel('时间 (s)'); ylabel('幅度'); title([tag ' 去噪前 时域']); xlim([0 min(0.2, t(end))]); % 只显示前 0.2 秒 subplot(2,2,2); plot(t, y); grid on; xlabel('时间 (s)'); ylabel('幅度'); title([tag ' 去噪后 时域']); xlim([0 min(0.2, t(end))]); % 频域对比(单边谱,dB) X = fft(x .* w, N); X = X(1:N/2); Y = fft(y .* w, N); Y = Y(1:N/2); Px = 20*log10(abs(X) + eps); % 加 eps 防止 log10(0) 出 -Inf Py = 20*log10(abs(Y) + eps); subplot(2,2,3); plot(f, Px); grid on; xlabel('频率 (Hz)'); ylabel('幅度 (dB)'); title('去噪前 频谱'); xlim([0 fs/2]); ylim([max(Px)-80, max(Px)+5]); subplot(2,2,4); plot(f, Py); grid on; xlabel('频率 (Hz)'); ylabel('幅度 (dB)'); title('去噪后 频谱'); xlim([0 fs/2]); ylim([max(Py)-80, max(Py)+5]); % 叠加对比,看出被削掉的部分 figure('Name', [tag ' 频谱叠加']); plot(f, Px, 'Color', [0.5 0.5 0.5]); hold on; plot(f, Py, 'r', 'LineWidth', 1); grid on; xlabel('频率 (Hz)'); ylabel('幅度 (dB)'); legend('去噪前', '去噪后'); xlim([0 fs/2]); ylim([max(Px)-80, max(Px)+5]); end为什么取前 0.2 秒?因为整段语音的时域包络是起伏的,缩放到全长之后就成了一团色块,看不出波形细节。0.1 到 0.3 秒之间通常能包含一个完整的音节,能同时看到基音周期和噪声毛刺,对比效果最直观。
为什么加汉宁窗?FFT 隐含假设信号是周期的,首尾不连续就会产生频谱泄漏,表现为本底"抬升"、谱线变胖。加窗能把泄漏压下去 30dB 以上。代价是主瓣变宽,频率分辨率降低——如果你要看基频的谐波间隔(男声可能只有 80Hz),窗长就要足够;如果只看整体包络,牺牲一点分辨率没关系。这是一个典型的时间-频率分辨率权衡,没有标准答案。
为什么转 dB?语音的动态范围巨大,最强的基频可能比高频噪声高 50dB 以上。线性坐标下,噪声那一部分被压成一条贴地的线,完全看不出结构。转成 dB 之后,噪声的频谱形状、截止频率处的衰减斜率、阻带里的残余,全都清晰可见。纵轴范围我一般取[峰值-80, 峰值+5],80dB 的动态范围刚好能把语音和噪声的区别显示出来。
nextpow2这一步是为了让 FFT 走基 2 快速算法。虽然现代 Matlab 的 FFT 对任意长度也能算,但 2 的幂次还是最快的,而且频率轴点数整齐好对齐。
3.4 结果怎么读:三张图各自说明什么
跑完之后你手上至少有四张子图加一张叠加图。别只是看一眼"哦,干净了",要读出具体信息。
时域图看的是三件事:整体包络有没有变、毛刺有没有减少、语音起始段有没有被削掉。如果去噪后的波形在静音段变成了完全平直的一条线,说明你切得挺狠;如果语音开头的爆破音位置被削了个凹陷,那是filtfilt的边界瞬态影响了起始部分,需要在两端各丢弃几百个采样点。
频谱图看的是截止频率那个位置。正确的效果应该是:在f_c之前,两条曲线(去噪前后)基本重合;在f_c之后,红色曲线以明显斜率往下走,和灰线拉开距离。这个"拉开的斜率"就是你的滤波器阶数决定的过渡带陡峭度。如果你发现 5kHz 处只拉开 10dB,而期望是 40dB,那要么阶数不够,要么归一化频率算错了。
叠加图看的是"我到底牺牲了什么"。把两条曲线画在一张图里,落差最大的区域就是你切掉的部分。如果这个区域明显低于 3.5kHz,那你要警惕语音已经被削了;如果这个区域正好从 4kHz 开始,那就是干净利落。
还有一种情况:频谱图上完全看不出区别。这通常不是滤波器没生效,而是信号里本来就没有多少高频噪声,或者ylim设得太宽把差异压扁了。先max(abs(y-x))看看差值的量级,再回去调ylim。
4. 效果评估:别只看图好看
图好看不等于效果好,这是我在项目里吃过亏的地方。有一次我把一段录音滤得频谱图非常漂亮,4kHz 以上干干净净,结果客户一听就说"人像是捂着嘴在说话"。问题就在于截止频率压到了 2.8kHz,第二、第三共振峰全被削了,可懂度直接掉了一个档次。
4.1 主观听感永远优先
任何降噪处理,第一评判标准是听。我的习惯是做 A/B 盲听:把原始和去噪后的版本随机顺序放出来,让不熟悉这个项目的人听,问三个问题——听清了吗、累不累、有没有奇怪的声音。这三个问题对应可懂度、听觉舒适度和伪影。
低通滤波最容易引入的伪影是"闷"。判断方法很简单:如果去噪后需要把音量调大才能听清,说明你切掉了太多东西,能量损失严重。另一个伪影是"振铃",主要出现在高阶 IIR 上,尤其是在语音的爆破音位置,会听到"叮"的一声。这是因为 IIR 的通带边缘相位非线性,对瞬态信号产生了拖尾。降低阶数或者换 FIR 就能解决。
4.2 客观指标怎么用才不误导
常用的客观指标有三个,我列一下它们的适用性和局限:
| 指标 | 计算方式要点 | 能说明什么 | 容易误判的地方 |
|---|---|---|---|
| 信噪比 SNR | 需要干净参考信号 | 噪声整体被压了多少 | 滤波同时削语音时,SNR 反而可能变好 |
| 分段信噪比 segSNR | 分帧计算 SNR 再平均 | 反映语音段与静音段的差异 | 帧长选择影响大,需固定 20~30ms |
| 对数谱距离 LSD | 两谱对数差的均方根 | 频谱形状的偏离程度 | 对幅度整体缩放敏感 |
这里有个陷阱:如果你拿不到干净参考信号,就没法算真正的 SNR。很多教程里"滤波前后 SNR 提升了多少"的算法是拿滤波后的信号当参考的,这是循环论证,数字毫无意义。我在没有参考的情况下,更愿意用"静音段能量下降量"这个指标——挑一段明确没人说话的区间,比较滤波前后这段的 RMS。这个数字直接反映降噪效果,且不依赖参考。
有参考信号时,我更关注 LSD 而不是 SNR。因为低通滤波会把语音本身的一部分也削掉,这会拉低 SNR 的表现(语音能量也变小了),但 LSD 能反映"频谱形状是否被破坏",更贴近听感。
5. 常见问题与排查实录
这一节是我攒下来的真实问题清单,按出现频率排序。遇到现象先查表,能省掉大半调试时间。
5.1 问题速查表
| 现象 | 最可能的原因 | 排查动作 | 解决方式 |
|---|---|---|---|
| 滤波后声音明显发闷 | 截止频率过低 | 查fc是否低于 3.4kHz | 提到 3.6~4.2kHz 重试 |
| 噪声基本没变化 | 噪声在低频段,不在阻带 | 看原始频谱的能量分布 | 改用高通或带阻 |
| 出现金属声、振铃 | IIR 阶数过高或通带纹波大 | 降低阶数试听 | 降到 4~6 阶,或换 FIR |
| 波形整体右移 | 用了filter而非filtfilt | 检查函数调用 | 改filtfilt或补偿延迟 |
| 频谱图看不出差别 | 没加窗、没转 dB、ylim太宽 | 逐项检查绘图代码 | 按 3.3 节重画 |
| 首尾出现异常大波形 | 零相位滤波的边界瞬态 | 看信号两端 | 两端各丢弃3*(N-1)点 |
| 输出播放有削波 | 滤波后未检查峰值 | 查max(abs(y)) | 归一化到 0.95 再保存 |
butter报错或响应畸变 | 归一化频率 ≥ 1 或阶数过高 | 查Wn和N | Wn<1,改 SOS 结构 |
| 双声道处理后变单声道 | 混声道时取了平均 | 查size(x,2) | 分通道独立滤波再合并 |
5.2 三个我踩过最深的坑
第一个坑是归一化频率算错。有次换了个 44.1kHz 的素材,代码里Wn还是按 16kHz 写的 0.5,实际相当于截止 11kHz,等于没滤。后来我养成了一个习惯:所有涉及频率的地方,变量名都带上单位后缀,比如fc_hz、fs_hz,算归一化时写Wn = fc_hz / (fs_hz/2);,一眼能看出关系。
第二个坑是filtfilt的边界效应被忽略。66 阶 FIR 做零相位滤波,实际上相当于正反各滤一次,边界影响范围是3*(N-1)个采样点,66 阶就是 195 点,16kHz 下约 12ms。如果这段语音开头就是一个爆破音,正好落在这 12ms 里,就会被削掉。我的处理是:在原始信号前后各补一段静音(补零或者镜像延拓),滤完之后再裁掉,这样语音本体不受影响。
第三个坑是滤波器系数精度的隐性损失。用butter(10, 0.4)拿到的b, a是双精度,但如果要移植到定点 DSP 上,系数要量化成 16 位,这时候高阶直接型滤波器就会崩——极点位置偏移导致频率响应完全变形。所以从设计阶段就建议用zp2sos转二阶节,二阶节结构对系数量化不敏感,这是数字信号处理里非常成熟的经验。
注意:不要用"听不出差别"来判断参数对不对。人耳对 4kHz 以上的衰减不敏感,但对 3kHz 附近的削减弱非常敏感。参数验证要靠频谱图加客观指标,听感只做最终确认。
5.3 一个容易被忽略的前置检查
做滤波之前,先看一眼原始信号的频谱,判断噪声类型。这个步骤花不了两分钟,能避免后面所有的无用功。具体看三点:噪声在哪个频段集中、是稳态还是突发、有没有明显的窄带峰(比如某根特别高的尖峰,那是单频干扰,该用陷波)。
如果频谱上 5kHz 以上是一片均匀的草地,说明是宽带噪声,低通有效。如果频谱上从 0Hz 开始整体抬升,而且低频比高频高,那是低频噪声为主,低通只会让它更明显。如果频谱上有几根孤立的尖峰(比如 50Hz、100Hz、150Hz),那是谐波干扰,带阻滤波器才是正确工具。这三种情况的处理方案完全不同,选错方向再优化参数也是白搭。
6. 想再往前走一步可以试的方向
低通滤波做完,如果效果还不够,有几个低成本的升级路线。第一个是分帧处理,语音在 20 到 30ms 内可以认为短时平稳,把信号切帧,对每帧判断是不是语音段,静音段用更激进的截止频率,语音段用保守的,这样能避免"一刀切"带来的语音损伤。第二个是高通加低通组合,先切掉 80Hz 以下的低频噪声,再做低通,形成带通,成本几乎没增加,但对录音设备的低频隆隆声效果明显。第三个是自适应截止频率,先对整段信号做一次频谱分析,用一个简单的能量阈值找到噪声开始明显超过语音包络的频率点,把这个点作为截止频率,比固定值更省心。
不过说句实在话,如果你的噪声不是宽带的、位置还在语音频段内部,低通再怎么优化也就那样了。这时候该转向谱减法或者基于统计模型的方法,那是另一个话题。低通滤波器在整条降噪链路里最合适的位置,其实是最前面那一级预处理——把明显在带外的噪声清掉,后面做任何复杂算法,输入都会干净很多,迭代起来也更快。
我自己的习惯是,任何新的录音素材进来,先跑一遍这个低通脚本,看时域和频域两张图,用两分钟判断噪声分布,再决定要不要上更重的方法。这个流程下来,真正需要复杂算法的场景,其实比想象中少得多。