Matlab低通滤波器语音去噪:时频图分析与参数调优实战
1. 先搞清楚低通滤波器在语音去噪里能干成什么事做语音信号处理的头一年我踩过最大的坑就是拿到一段带噪语音条件反射地打开滤波器设计工具随手拉一个低通截止频率往 4 kHz 一放就以为去噪完成了。结果播放出来人声是闷的齿音全没了丝次吃这类音听起来像被人捂住了嘴。后来才明白低通滤波器做语音去噪这件事本质上是在赌一件事噪声的能量集中在高频而人声的有用信息集中在低频。这个赌注在很多真实场景里只对了一半而我要做的就是把这一半的边界划清楚。这篇内容我想讲的是从时域图和频域图两个视角去看待一段带噪语音用Matlab设计低通滤波器把它处理干净并且用肉眼能看懂的方式验证去噪到底有没有效果。适合刚接触数字信号处理的同学也适合工作里偶尔要处理一段录音、但不想翻厚教材的工程师。前置知识只需要知道什么是采样率、什么是频率其余的我会在过程中补上。所有代码都可以直接复制进 Matlab 跑音频文件换成你自己的就行。先把话说透低通滤波器不是万能的它更像一把粗筛子。它解决的是高频嘶嘶声这一类问题比如录音设备底噪、风扇声、磁带高频噪声、某些场景下的电流声。它解决不了的是低频嗡嗡声比如 50 Hz 工频干扰、中频段的人声干扰、混响、以及宽带白噪声的低频部分。我在后面会专门拿一节讲怎么判断你手上这段音频到底该不该用低通这一步比写代码重要十倍。2. 从频谱出发为什么低通这个思路能成立2.1 人声的能量分布到底长什么样人说话的基频男声大概在 85 Hz 到 180 Hz女声在 165 Hz 到 255 Hz这个频段决定了声音的音高。但决定听清听不清的不是基频而是共振峰也就是声道形状对不同频率的放大作用。第一共振峰 F1 通常在 300 到 800 Hz第二共振峰 F2 在 800 到 2500 Hz这两个是区分元音的关键。而像sshfth这类清擦音能量大头在 4 kHz 以上甚至能延伸到 8 kHz 到 10 kHz。这带来一个非常现实的矛盾你如果把截止频率压到 3.4 kHz 以下元音基本不受影响但清擦音会被削掉一大块听感上就是闷糊大舌头。传统电话线路的带宽是 300 到 3400 Hz所以你用手机打电话时听到的声音本身就是被低通加高通处理过的那个音质就是低通的极限参考。你可以自己录一段话把截止频率分别设成 3 kHz、4 kHz、6 kHz、8 kHz 听一遍差异极其明显。所以在设计之前我一般会先看一眼原始信号的频谱长什么样。如果噪声集中在 6 kHz 以上那截止频率放 6 kHz 到 7 kHz 就够人声几乎无损如果噪声从 3 kHz 就开始铺开那我就得考虑这活儿低通干不了得换思路。2.2 三种滤波器路线我为什么默认选这两种Matlab 里做低通有太多入口fir1、butter、cheby1、ellip、designfilt、filterDesigner工具新手很容易在这里迷路。我自己的习惯是只保留两条主线方案核心函数相位特性阶数需求适用场景FIR 窗函数法fir1kaiser严格线性相位较高几十到上百阶需要保波形、做后续分析IIR 巴特沃斯butter非线性相位低4 到 8 阶只关心幅频、追求计算量小IIR 切比雪夫 Icheby1非线性相位更低允许通带波纹过渡带要陡IIR 椭圆ellip非线性相位最低极端追求陡峭能忍受相位畸变为什么默认是 FIR 和巴特沃斯这两个FIR 的线性相位意味着所有频率成分延迟相同滤波后的波形不会变形这对后面还要做时域分析比如做包络、做端点检测的场景非常关键。而巴特沃斯的好处是通带最大平坦没有波纹听感上最干净缺点是过渡带偏缓要达到同样的陡度需要更高阶。我一般这样决策如果这段音频只是拿去听选巴特沃斯 4 到 6 阶就够了如果后面还要接别的算法、要做波形对齐选 FIR 加凯泽窗。切比雪夫和椭圆我用得少因为通带波纹在语音上会引起轻微的金属感而且相位畸变严重时用filtfilt虽然能消掉延迟但瞬态响应会变差。提示不要用filterDesigner图形工具做最终交付。它生成的代码耦合了大量对象属性换个 Matlab 版本就可能报错而且参数藏在 GUI 里别人根本看不懂你当时是怎么想的。手写几行butter或fir1可读性和可维护性都高得多。2.3 什么情况下低通是错的方向这一点我必须单独拎出来讲因为太多教程默认去噪 低通这是误导。举几个反例工频干扰50 Hz 或 60 Hz 的嗡嗡声能量在极低频低通完全无效得用陷波器iirnotch。空调、风机噪声能量集中在 100 到 500 Hz低通无效反而要考虑高通或谱减法。多人说话的背景人声和人声频段完全重叠任何线性滤波器都无能为力。混响这是时域问题不是频域问题滤波只会让声音更糊。判断方法很简单画一张功率谱密度图pwelch看噪声的鼓包在哪。如果鼓包在 5 kHz 以上低通是对症的如果鼓包在低频那你需要的是高通或带通如果整个频段均匀抬升那是白噪声单靠一个低通只能去掉高频那一半剩下的得靠谱减法或者维纳滤波。3. 参数定生死采样率、截止频率、阶数怎么算3.1 采样率不是随便设的采样率决定了你能处理的最高频率也就是奈奎斯特频率 fs/2。如果采样率是 16 kHz那 8 kHz 以上的信息根本不存在于数字信号里你设计再高的截止频率也没意义。这一点在做滤波器设计时特别容易出错fir1和butter的频率参数都是归一化频率范围是 0 到 1其中 1 对应 fs/2。很多人直接把赫兹数填进去结果滤波器完全失效还以为是代码写错了。归一化公式是$$W_n \frac{f_c}{f_s / 2}$$举个实际例子录音采样率 16000 Hz我想把 3400 Hz 以上切掉那 $W_n 3400 / 8000 0.425$。如果采样率是 44100 Hz同样切 3400 Hz$W_n 3400 / 22050 \approx 0.1542$。同一个截止频率采样率不同归一化值差了三倍这就是为什么代码不能照抄。常用采样率的选择我整理了一下采样率奈奎斯特上限典型用途8000 Hz4000 Hz电话语音、窄带识别16000 Hz8000 Hz宽带语音、ASR 训练标配22050 Hz11025 Hz一般录音、播客44100 Hz22050 Hz音乐、高保真录音如果你手上的音频是 44.1 kHz 录的但只是想做人声分析我建议先用resample降到 16 kHz 再处理。好处有两个一是滤波器阶数需求大幅下降因为过渡带相对变宽了二是计算量小一个量级跑批量处理的时候差别非常明显。3.2 截止频率的确定得看噪声脸色截止频率没有标准答案但有一套可操作的流程。我的做法是先画原始信号的功率谱密度找到噪声开始明显高于信号地板的那个拐点再往上留 10% 到 20% 的余量。具体操作用pwelch算 PSD横轴是频率纵轴是 dB。人声的谱在 300 Hz 到 3 kHz 之间会有一个明显的隆起3 kHz 之后逐渐下降但不会断崖式掉到零。噪声的特征是如果是设备底噪它会在高频形成一个相对平缓的平台如果是嘶嘶声会在 6 kHz 以上抬高。我常用的经验值噪声类型建议截止频率fs16k说明轻微设备底噪7000 Hz几乎不损伤语音明显高频嘶声5000 Hz齿音略损失可接受强高频噪声4000 Hz开始明显发闷极端情况3400 Hz电话音质慎用注意截止频率不要设成整数后就不管了。butter的Wn必须严格小于 1如果算出来是 0.9999 这种值滤波器会接近不稳定。我一般会限制Wn不超过 0.95也就是截止频率不超过 0.95 × fs/2。3.3 阶数怎么估给一个能算的公式FIR 滤波器的阶数和过渡带宽是直接挂钩的。用凯泽窗设计时有个经典的估算式$$N \approx \frac{A - 8}{2.285 \cdot \Delta\omega}$$其中 A 是阻带衰减dB$\Delta\omega$ 是过渡带宽rad/sample等于 $2\pi \cdot \Delta f / f_s$。实际算一遍fs 16000 Hz通带截止 3400 Hz阻带起点 4000 Hz所以 $\Delta f 600$ Hz。想要 60 dB 的阻带衰减。$\Delta\omega 2\pi \times 600 / 16000 \approx 0.2356$ rad/sample。$N \approx (60 - 8) / (2.285 \times 0.2356) \approx 52 / 0.5384 \approx 96.6$取 97 阶。97 阶 FIR 的群延迟是 $(N-1)/2 48$ 个采样点换算成时间是 $48 / 16000 3$ 毫秒。这个延迟在实时通信里是可以接受的但如果延迟敏感就得考虑 IIR。如果放宽到 40 dB 衰减$N \approx 32 / 0.5384 \approx 59.4$取 60 阶延迟 1.84 毫秒。这就是我在大多数场景下的默认配置60 阶凯泽窗 FIRβ 640 dB 阻带衰减。听感和计算量之间比较平衡。巴特沃斯的阶数不能这么直接算但有个规律阶数每加 1过渡带的陡度大约增加 6 dB/倍频程。4 阶巴特沃斯在截止频率处是 -3 dB到 2 倍截止频率处衰减大约 24 dB。这意味着如果截止设在 3400 Hz那么 6800 Hz 处才衰减 24 dB中间这一大段噪声是压不住的。所以我用巴特沃斯时通常会把截止频率往下压一点用 6 阶实际听感才够干净。4. Matlab 完整实操从读文件到出图4.1 音频读取与预处理的几个细节第一步读文件这里有几个坑。audioread返回的矩阵如果是立体声就是 N×2直接送进滤波器会报错或者按列分别滤波得先降成单声道。另外不同来源的音频幅值范围可能差异很大有的是 int16 范围-32768 到 32767有的是 -1 到 1 的浮点归一化一下更稳妥。%% 1. 读取与预处理 [x, fs] audioread(noisy_speech.wav); % 立体声转单声道 if size(x, 2) 1 x mean(x, 2); end % 幅值归一化到 [-1, 1]防止后续计算溢出 x x / max(abs(x) eps); % 统一采样率到 16 kHz如果原始不是的话 if fs ~ 16000 x resample(x, 16000, fs); fs 16000; end fprintf(采样率: %d Hz, 时长: %.2f 秒, 样本数: %d\n, ... fs, length(x)/fs, length(x));eps这个小东西很有用。如果音频恰好是全零比如一段静音被裁出来max(abs(x))是 0除零会得到 NaN后面全崩。加个eps就规避了。还有一个容易被忽略的点音频开头往往有一段静音或者爆音。如果录音设备的启动瞬态很强滤波后会在开头产生明显的振荡。我的处理是在滤波前把首尾各 50 毫秒的样本做个淡入淡出或者干脆裁掉。4.2 FIR 低通从设计到滤波下面这段是我最常用的 FIR 方案参数按上面算的来。%% 2. FIR 低通滤波器设计 fc 3400; % 截止频率 Hz N 60; % 滤波器阶数 beta 6; % 凯泽窗参数对应约 40 dB 阻带衰减 Wn fc / (fs/2); % 归一化截止频率必须 1 win kaiser(N1, beta);% 窗长度 阶数 1 b fir1(N, Wn, low, win); % 查看滤波器幅频响应确认设计正确 figure; freqz(b, 1, 1024, fs); title(FIR 低通滤波器幅频响应);设计完之后我会先跑一次freqz看一眼通带是不是平的、阻带是不是够深、过渡带在哪里。这一步很多人跳过结果滤波器设计错了还以为是音频问题。我在一次项目里就遇到过Wn忘了归一化直接填了 3400Matlab 不报错但滤波器通带几乎是零输出全是静音排查了半天。滤波阶段有两个选择%% 3. 滤波 % 方案 A直接 filter保留真实延迟适合实时场景 y_a filter(b, 1, x); % 方案 Bfiltfilt 零相位前后各滤一遍抵消延迟 y_b filtfilt(b, 1, x);filter是因果滤波输出会有 $(N-1)/2$ 个采样的延迟波形在时间轴上整体右移做时域对齐分析时要记得补偿。filtfilt是正反各滤一次幅频响应变成原来的平方阻带衰减翻倍这是好事相位为严格零但代价是边界瞬态会变严重开头和结尾各有一段不可用的样本。实际操作中我推荐filtfilt然后在出图前裁掉首尾各3*(N-1)个样本避免边界振铃污染图形。%% 4. 裁掉边界瞬态 margin 3 * N; if length(y_b) 2*margin y_clean y_b(margin : end-margin); x_trim x(margin : end-margin); else y_clean y_b; x_trim x; end4.3 IIR 巴特沃斯方案给不想算阶数的人%% 5. 巴特沃斯低通 order 6; Wn 3400 / (fs/2); % 先检查稳定性极点必须在单位圆内 [z, p, k] butter(order, Wn, low); if max(abs(p)) 1 error(滤波器不稳定请降低阶数或提高截止频率); end [b, a] butter(order, Wn, low); y_iir filtfilt(b, a, x); % 对比零极点图 figure; zplane(b, a); title(巴特沃斯低通零极点分布);butter在阶数高、截止频率低的时候会数值不稳定尤其是[b,a]形式的直接型结构表现为极点跑到单位圆外。6 阶以内基本安全8 阶以上就可能出问题这时候得用zp2sos转成二阶节级联结构或者改用designfilt的对象形式。我听感上的结论是4 kHz 截止、6 阶巴特沃斯的声音和 60 阶 FIR 的声音非常接近但巴特沃斯的过渡带更缓4 kHz 到 6 kHz 之间压得不够狠。如果噪声正好落在这一段就得加到 8 阶或者改用切比雪夫。4.4 时域图和频域图画对才有说服力时域图相对简单横轴时间、纵轴幅值原始和处理后上下并排。频域图坑就多了我得拆开说。第一个坑是直接fft不加窗。不加窗意味着默认信号是周期延拓的首尾不连续会在频谱上产生泄漏表现为整条谱线抬起看起来哪儿都有能量判断噪声位置时会误判。正确做法是先乘汉宁窗。第二个坑是幅值计算。abs(fft(x))得到的是未归一化的结果数值大小和信号长度、幅值都相关不同长度两段音频没法直接比。正确做法是除以 N并且单边谱要把非直流和奈奎斯特分量的幅值乘 2。我把这套逻辑封装成一个函数function [f, P] spec_single_sided(x, fs) % 返回单边幅度谱已加汉宁窗并归一化 x x(:); N length(x); w hann(N); X fft(x .* w); half floor(N/2) 1; P abs(X(1:half)) / (N/2); % 归一化 P(2:end-1) P(2:end-1) * 2; % 单边谱补偿 P(1) P(1) / 2; f (0:half-1) * fs / N; end然后是完整的出图脚本%% 6. 时域 频域对比图 t (0:length(x_trim)-1) / fs; [f, P_raw] spec_single_sided(x_trim, fs); [~, P_clean] spec_single_sided(y_clean, fs); figure(Position, [100 100 1100 700], Color, w); % 上时域 subplot(2,1,1); plot(t, x_trim, Color, [0.6 0.6 0.6], LineWidth, 0.6); hold on; plot(t, y_clean, Color, [0.85 0.2 0.2], LineWidth, 0.8); xlabel(时间 (s)); ylabel(幅值); title(时域波形对比灰原始红去噪后); legend({原始信号, 去噪信号}, Location, northeast); grid on; xlim([0 min(3, t(end))]); % 下频域 subplot(2,1,2); plot(f, 20*log10(P_raw 1e-10), Color, [0.6 0.6 0.6], LineWidth, 0.8); hold on; plot(f, 20*log10(P_clean 1e-10), Color, [0.85 0.2 0.2], LineWidth, 1.0); xline(fc, --k, sprintf(截止频率 %d Hz, fc), ... LabelVerticalAlignment, bottom); xlabel(频率 (Hz)); ylabel(幅值 (dB)); title(频域幅度谱对比); legend({原始信号, 去噪信号}, Location, northeast); grid on; xlim([0 fs/2]); ylim([-100 -20]); % 保存 exportgraphics(gcf, denoise_compare.png, Resolution, 200);图出来以后要看什么时域上看两件事一是红线的整体幅度是不是比灰线小了很多如果是说明截止频率压太低了把有用信号也削了二是看有没有明显的振铃也就是信号包络附近出现高频振荡那是滤波器阶数太高或者边界处理不当的信号。频域上看的是截止频率之后的那段是不是真的被压下去了压下去了多少 dB以及截止频率之前的那段有没有被意外影响——理想情况下 3400 Hz 以下的两条曲线应该几乎重合如果红线的低频段明显低于灰线说明设计有问题。5. 效果到底好不好别光靠耳朵5.1 三个视角交叉验证只放一遍听主观判断很容易被心理暗示带偏。我的习惯是三样东西一起看时域波形、幅度谱、语谱图。时域波形看的是宏观的噪声压制效果。如果原始信号看起来像一根毛茸茸的粗线处理后变成纤细清晰的线那高频噪声确实被压住了。但如果处理后连语音的包络都变得平缓了那说明伤到信号了。幅度谱主要看截止频率前后的对比这个前面说了。语谱图是我最看重的因为它是时频联合表示能同时看到时间和频率的变化%% 7. 语谱图对比 figure(Position, [100 100 1000 600], Color, w); subplot(2,1,1); spectrogram(x_trim, 256, 200, 512, fs, yaxis); title(原始信号语谱图); colorbar; subplot(2,1,2); spectrogram(y_clean, 256, 200, 512, fs, yaxis); title(去噪后语谱图); colorbar;看语谱图的关键在于看高频区域的颜色。如果上半部分4 kHz 以上原本是一片浅蓝色到黄色处理后变成深蓝色那高频噪声被干掉了但如果你同时注意到 3 kHz 到 5 kHz 之间那些代表齿音的竖条纹也变暗了那就得警惕说明通带边缘削得太狠。spectrogram的参数值得一提窗长 256 在 16 kHz 采样下对应 16 毫秒是语音分析的经验值重叠 200 意味着 78% 的重叠谱图会更平滑FFT 点数 512 提供了更好的频率插值。这三个值不需要背但你要知道改它们会带来什么窗长加大会提高频率分辨率但损失时间分辨率反之亦然。这是时频分析的基本权衡。5.2 客观指标有参考和没参考两套打法如果有干净语音作为参考比如仿真时人为叠加噪声那 SNR 是最直观的function snr_db calc_snr(signal, noise_or_err) % signal: 干净参考信号noise_or_err: 噪声或误差信号 ps sum(signal.^2); pn sum(noise_or_err.^2); snr_db 10 * log10(ps / pn); end仿真流程是这样拿一段干净语音 s生成高斯白噪声 n按指定 SNR 混合得到 x s a*na 由目标 SNR 反推然后滤波得到 y再算calc_snr(s, y - s)。这个指标能直接量化滤波把多少噪声变成了残留误差。实际录音就没有参考了。我的替代方案是分段能量比把信号切成 20 毫秒的帧计算每帧能量然后看静音段能量最低的 10% 帧的平均能量和语音段能量最高的 30% 帧的平均能量之比。这个比值在去噪前后应该显著提高因为它反映的是噪声底相对语音被压低了多少。function ratio speech_noise_ratio(x, fs) frame_len round(0.02 * fs); n_frames floor(length(x) / frame_len); energy zeros(n_frames, 1); for i 1:n_frames seg x((i-1)*frame_len 1 : i*frame_len); energy(i) sum(seg.^2); end energy sort(energy); n_low max(1, round(0.1 * n_frames)); n_high max(1, round(0.3 * n_frames)); noise_floor mean(energy(1:n_low)) eps; speech_lvl mean(energy(end-n_high1:end)) eps; ratio 10 * log10(speech_lvl / noise_floor); end这个指标有个前提音频里确实存在静音段。如果全程都在说话切出来的低能量帧其实是发音的过渡段指标就失真了。遇到这种情况我会手动截取一段公认的静音比如录音开头的 0.5 秒作为噪声底参考。5.3 主观听感测试得有点章法客观指标好看但听感差的例子太多了所以耳朵这一关不能省。我的做法是 A/B 盲听把原始和处理后的音频随机排序不看文件名连听三轮记录下哪个更清楚哪个更自然有没有金属感。有几个听感特征是低通滤波特别容易引入的值得专门留意闷截止频率太低齿音消失声音像隔着门。金属感IIR 滤波器阶数太高或者用了椭圆滤波器通带波纹引起。开头咔哒声filtfilt的边界瞬态没裁干净。整体偏轻滤波后能量下降需要重新归一化或者做增益补偿。补一句实操经验滤波后一定要做一次响度对齐再比较。滤波本身会降低总能量如果直接播放人耳会觉得处理后的声音小误判成去噪效果好但音质差。用y y / max(abs(y)) * max(abs(x))拉到同一峰值再听结论才靠谱。6. 踩过的坑和常见问题速查6.1 那些让代码跑不通的低级错误这类问题占了新手报错的一大半我列个表方便对照查。报错或现象原因解决方式输出全是零或极小值Wn没归一化填了赫兹数改成fc/(fs/2)fir1报 Wn must be between 0 and 1归一化频率越界检查fc是否超过fs/2filtfilt报 input must be a vector传入的是 N×2 立体声矩阵先mean(x,2)转单声道滤波后完全没变化滤波器系数算错或没使用返回值用freqz检查幅频响应声音变调、速度不对采样率参数与实际文件不符用audioinfo确认真实 fs极点跑出单位圆IIR 阶数过高、截止频率过低降到 6 阶以内或转 SOS 结构audioread找不到文件相对路径问题用fullfile拼绝对路径其中输出全是零这个我见过太多次。fir1(60, 3400)这种写法Matlab 在较新版本里不报错但它会把 3400 当作超范围值处理结果得到一个几乎全零的滤波器。每次写完滤波器设计第一件事就是freqz看一眼这个习惯能省掉大量排查时间。6.2 边界振铃一个必须处理的细节filtfilt的边界效应比filter严重得多因为它是双向滤波正向和反向的瞬态会在两端叠加。表现是音频开头和结尾出现一段几百毫秒的嗡嗡声如果原音频开头是静音这段噪声会特别明显。除了前面说的裁剪法还有一个更优雅的方案镜像填充。在信号两端各镜像延拓一段数据再滤波滤完把延拓部分丢掉。function y filtfilt_mirror(b, a, x, pad_len) x x(:); L length(x); pad_len min(pad_len, L - 1); head x(pad_len1 : -1 : 2); % 前端镜像 tail x(end-1 : -1 : end-pad_len); % 后端镜像 x_pad [head; x; tail]; y_pad filtfilt(b, a, x_pad); y y_pad(pad_len1 : pad_lenL); endpad_len一般取3*(N-1)就够。这个方法的好处是不损失有效数据长度对短音频特别友好。我在处理几十毫秒的关键词音频时裁剪法会把信号裁掉一大半镜像法就没这个问题。6.3 高频削太狠了怎么补救有时候截止频率设完才发现声音太闷但又不想重新跑一遍。我的补救手段有三个按推荐度排序。第一是提高截止频率重跑这是最直接的。但要注意提到 4.5 kHz 以上时噪声可能又回来了需要权衡。我的经验是每次调整幅度不超过 500 Hz边听边调。第二是做通带增益补偿。低通滤波器在截止频率附近有轻微的幅度衰减巴特沃斯在截止点就是 -3 dB可以用一个反向的均衡曲线拉回来但实现复杂一般不值得。第三是只对高频段做衰减而不是硬切。这本质上是换了个思路不用砖墙式低通而是设计一个缓降的高频搁架滤波器high-shelf在 3 kHz 之后逐渐衰减但保留一点齿音。Matlab 里可以用designfilt(highpassiir)的反向思路或者直接手动设计一个 FIR让阻带不是 -60 dB 而是 -15 dB。这个方案在主观听感上往往比硬切更自然。6.4 批量处理时性能怎么优化单个文件怎么都好说几百个文件的批量处理就得考虑效率了。我总结了几条。一是滤波器系数只算一次。很多人把butter写在循环里每个文件重新算一遍系数白白浪费算力。系数只和 fs、fc、阶数有关全部文件采样率一致的话循环外算好就行。二是用dsp.FIRFilter对象替代函数调用。对于长音频对象形式避免了每次调用的状态初始化开销实测能快 20% 到 30%。但要注意对象是有状态的处理多个文件之间必须调reset。三是考虑resample降采样后再滤波。如果原始是 44.1 kHz 而只需要 8 kHz 带宽先降到 16 kHz 再滤波计算量直接降到三分之一而且滤波器阶数需求也更低。降采样本身也是低通一举两得。7. 把低通放进更大的方案里7.1 带通才是更常见的起点单一低通在实际项目里用得其实不多更常见的是带通。原因很简单人声的有效频段是 300 到 3400 Hz窄带或者 80 到 7000 Hz宽带低于 80 Hz 的部分基本是噪声风噪、桌面震动、工频谐波高于有效带宽的部分也是噪声。两头都切才是完整方案。%% 带通滤波80 - 7000 Hz fs_target 16000; bp_low 80 / (fs_target/2); bp_high 7000 / (fs_target/2); [b_bp, a_bp] butter(4, [bp_low bp_high], bandpass); y_bp filtfilt_mirror(b_bp, a_bp, x, 200);这里用 4 阶而不是 6 阶原因是带通的阶数在数学上是低通的两倍4 阶带通的实际复杂度相当于 8 阶低通再往上就容易数值不稳定了。高通侧切在 80 Hz 是为了保留男声基频如果只处理女声可以提到 120 Hz。带上高通之后你会发现音质改善比单纯低通明显得多因为低频的隆隆声对听感的干扰其实比高频嘶声更大——人耳对低频噪声更敏感而且低频能量高会掩盖语音细节。7.2 低通 谱减法的组合思路如果噪声是宽带的低通只能解决高频那一半。这时候我的常规打法是先带通把带外噪声干掉再用谱减法处理带内噪声最后用低通做一次平滑收尾。谱减法的核心假设是噪声在静音段可以估计出来然后在每帧频谱上把它减掉。Matlab 里没有现成的高层函数得自己写核心就几行%% 简化版谱减法核心逻辑 frame_len 256; hop 128; win hann(frame_len); n_frames floor((length(x) - frame_len) / hop) 1; % 用前 10 帧估计噪声谱 noise_mag zeros(frame_len/21, 1); for i 1:10 seg x((i-1)*hop1 : (i-1)*hopframe_len) .* win; noise_mag noise_mag abs(fft(seg, frame_len))(1:frame_len/21); end noise_mag noise_mag / 10; % 逐帧减去噪声幅度谱 y_out zeros(length(x), 1); for i 1:n_frames idx (i-1)*hop1 : (i-1)*hopframe_len; seg x(idx) .* win; X fft(seg, frame_len); mag abs(X(1:frame_len/21)); phase angle(X(1:frame_len/21)); mag_clean max(mag - 1.5 * noise_mag, 0.02 * mag); % 过减因子 谱下限 X_clean [mag_clean; conj(mag_clean(end-1:-1:2))] .* exp(1j * ... [phase; -phase(end-1:-1:2)]); y_out(idx) y_out(idx) real(ifft(X_clean, frame_len)) .* win; end里面的1.5是过减因子0.02是谱下限系数。这两个数没有理论最优值得根据噪声类型调。过减因子大了会有音乐噪声残留的随机音调小了噪声压不干净。谱下限是为了防止减成负数后产生随机相位噪声。这套组合跑下来比单纯低通的效果好一大截但代价是引入了明显的失真尤其是过减因子大的时候。我的经验是如果下游是给人听优先保证自然度过减因子不超过 1.2如果下游是给语音识别可以激进一点反正模型对失真不敏感。7.3 参数批量扫描让数据替你选最后分享一个我常用的技巧与其凭感觉调截止频率不如写个循环扫一遍把每个参数下的指标和音频都存下来然后一起听、一起看。%% 截止频率扫描 fc_list [2500 3000 3400 4000 5000 6000 7000]; results struct(fc, {}, snr, {}, ratio, {}); for k 1:length(fc_list) fc fc_list(k); Wn fc / (fs/2); b fir1(60, Wn, low, kaiser(61, 6)); y filtfilt_mirror(b, 1, x, 200); % 指标 results(k).fc fc; results(k).ratio speech_noise_ratio(y, fs); % 存音频方便盲听 audiowrite(sprintf(out_fc_%d.wav, fc), y / max(abs(y)) * 0.95, fs); end % 汇总表 T struct2table(results); disp(T);跑完你会得到一张表横着看哪个 fc 的语音噪声比最高再配合盲听定夺。我做过几次这样的扫描结论经常和直觉不一样比如在某种特定噪声下5 kHz 的综合表现反而比 4 kHz 好因为 4 kHz 削掉的齿音对可懂度的贡献大于它压掉的噪声。提示扫描时一定要把输出音频的峰值统一到 0.95 再保存否则响度不一致会严重干扰盲听判断。这个细节我在第一次做 A/B 测试时就吃过亏误判了两轮。8. 我在实际项目里的几点体会做了一段时间下来我对低通滤波器语音去噪这件事的看法变得更朴素了它是一个基本功但绝不是终点。真正决定效果上限的是你对噪声特性的判断而不是滤波器的阶数或者代码写得多漂亮。同一段音频截止频率差 500 Hz主观听感的差距可能比换个算法还大。另一个体会是可视化的重要性被严重低估。我见过不少人拿到代码就跑跑完直接听听完觉得不行就瞎调参数。如果你先把原始信号的时域图、频域图、语谱图摆出来很多问题一眼就能看出来噪声在哪个频段、语音的能量集中在哪、有没有削顶失真。这三张图基本决定了后面所有参数的选择方向。最后一个我想强调的实操习惯永远保留原始文件所有处理结果另存新文件文件名里带上关键参数。我早年的坏习惯是原地覆盖结果调了一晚上参数回头想找回某个中间版本没了。现在我的命名规则是orig_xxx.wav、bp80-7000_fir60.wav、bp80-7000_fir60_specsub.wav一看就知道每一步做了什么出了问题也能快速回退到某一步单独排查。这个习惯看起来琐碎但它能让你在几十次迭代之后依然清楚自己在干什么。