ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

语音降噪算法详解:MATLAB实现谱减、维纳滤波与小波去噪

语音降噪算法详解:MATLAB实现谱减、维纳滤波与小波去噪

简介:语音降噪是语音信号处理中的关键环节,直接影响语音识别、大数据分析及人工智能系统的鲁棒性。这套资源为《语音信号处理实验教程》第05章配套MATLAB源代码包,面向需要动手实践的高校学生、算法研究者与入门工程师。压缩包共35个文件,含26个.m源码、5张示意图和4个wav测试语音,整体仅409KB,结构紧凑、便于本地快速运行。内容覆盖5.1自适应滤波器法(LMS)、5.2谱减法、5.3维纳滤波法、5.4小波分解法,每个算法均配有可执行脚本,并包含分帧、重叠相加、端点检测、SNR计算等辅助函数,方便对比不同降噪策略的效果。包内文件目录清晰,按章节划分,适合从理论过渡到代码实践,也可作为语音处理课程实验或项目改造的基础。目前已有210人学习下载,对希望深入理解语音降噪原理并快速上手的读者很有帮助。

1. 语音信号处理实验包里最值得先跑的部分:一套能出听感的降噪链路

做语音信号处理实验,很多人第一反应是找深度学习模型,真打开 MATLAB 把 wav 读进来之后才发现,最卡人的反而是“把带噪语音变成干净语音”这条基础链路。这套语音信号处理实验教程(MATLAB 源代码)语音降噪资源,把读音频、分帧加窗、短时傅里叶变换、谱减法、维纳滤波和小波去噪整理成了一整套可运行的 MATLAB 源码,每一步都有对应的脚本和参数。对要交课程实验报告的在校生,或者要给语音识别前端做数据预处理的一线开发者,这是比直接跑黑盒模型更值得研究的参考实现。

2. 降噪实验的底层逻辑:从短时平稳假设到频域增益

2.1 为什么先分帧加窗,而不是直接对整个文件算 FFT

语音信号不是平稳信号,全段一次性 FFT 会把不同时刻的频谱混在一起,得到的是一坨没有时间分辨率的平均结果。降噪要做的是“每一小段时间里单独判断噪声长什么样”,这依赖短时平稳假设:在 20~40ms 量级的时间窗内,语音的频谱特性近似不变。

所以标准做法是对信号分帧。帧长取 25ms 时,16kHz 采样率对应 400 个采样点;帧移取 10ms 对应 160 点,相邻帧有 60% 重叠。这个配置是语音处理里最常见的默认值,既能保证频率分辨率够用,又不会让帧间变化被抹得太平。加窗的目的有两个:一是压制 FFT 的频谱泄漏,二是保证重叠相加重建时不会产生跳变。汉明窗和汉宁窗都常用,实验包里一般倾向汉明窗,因为它的旁瓣衰减比矩形窗好不少,做谱估计时噪声底更干净。

分帧之后,每一帧先乘窗函数再做 FFT,得到的就是这一帧的短时频谱。后面所有降噪算法,本质上都是在这个“帧-频点”二维矩阵里做增益调整,而不是在时域波形上直接动手。

2.2 三类降噪算法的数学模型与 MATLAB 函数对应

实验包里有三套核心算法:谱减法、维纳滤波、小波去噪。它们的假设和应用场景差别很大,写实验报告时最容易混淆。

谱减法是最直观的:假设噪声是加性的、平稳的,先用前几帧估算噪声功率谱,再从带噪语音功率谱里减掉它,剩下的就是语音功率谱。公式上写作|S|^2 = |Y|^2 - α|N|^2,α 叫过减因子,用来补偿噪声估计偏差。它的优点是计算量小、实时性好,缺点是处理非平稳噪声容易留下“音乐噪声”,也就是听感上那种吱吱呀呀的残留音。

维纳滤波的思路不同,它根据信噪比估计一个频域增益G = ξ/(1+ξ),ξ 是先验信噪比。语音强的频段增益接近 1,噪声强的频段增益压下去,这种平滑过渡让音乐噪声明显更少,但先验信噪比估计不准时,语音会被削得发闷。

小波去噪则从时频分解角度切入,把信号分解到不同尺度,对小波系数做阈值收缩,噪声对应的小波系数小,直接置零。它的优势是处理脉冲噪声和瞬态噪声时比谱减稳定,劣势是阈值选择有玄学成分,不同母小波和分解层数结果差异很大。

算法核心假设适用场景核心 MATLAB 相关函数
谱减噪声平稳、加性稳态噪声、实时处理fft、angle、abs、重叠相加
维纳滤波需要先验 SNR 估计非平稳性适中的环境fft、迭代增益估计
小波去噪语音与小波系数稀疏性瞬态噪声、脉冲噪声wdenoise、wthresh、wnoisest

2.3 只修幅值不动相位:从频域回时域的关键

大多数降噪算法只修改幅度谱,相位保留原带噪语音的相位。这是工程上的妥协:相位对语音可懂度影响小于幅度,但完全扔掉相位重构出来的声音有严重的金属感。所以标准流程是S = |S_hat| * exp(i * angle(Y)),带着原始相位做 ifft。

这里最大的坑在重叠相加。分帧时每一帧是被窗函数截断的,重建时必须把相邻帧的重叠部分按位置累加,否则音量会忽大忽小。我一般会在重建时额外记录每一点的窗函数累加和,最后做一次除法归一化,这套逻辑在用ifft恢复每一帧后尤其重要。实际代码里,idx = (1 + (i-1)*frameShift) : ((i-1)*frameShift + frameLen)这个下标计算写错一位,后面全塌。

3. 跑通第一个谱减降噪 Demo:加噪、分帧、谱减、重构的完整脚本

3.1 读入语音并构造可控的带噪数据

首先准备一段干净的 wav 文件,推荐 16kHz 采样率、16bit、单声道,这是语音处理的事实标准。用audioread读进来后先转成列向量,然后叠加高斯白噪声,通过调整噪声幅度控制信噪比 SNR。为了实验可复现,随机噪声要固定种子,否则每次运行结果都不一样,报告里对不上。

clear; rng(0); [x, fs] = audioread('speech.wav'); % 读入干净语音 x = x(:); % 生成与语音等长的白噪声 numSamples = length(x); snr_db = 5; % 目标 SNR 5dB,能明显听出噪声 cleanPower = sum(x.^2) / numSamples; noisePower = cleanPower / (10^(snr_db/10)); noise = sqrt(noisePower) * randn(numSamples, 1); y = x + noise; % 带噪语音 audiowrite('noisy.wav', y, fs);

逻辑说明:先计算干净语音的功率,再按照 SNR 公式SNR = 10log10(P_signal/P_noise)反推噪声功率,最后乘randn生成高斯白噪声。这里没有直接调用awgn函数,因为手算能让学生看明白 SNR 的定义,而不是把工具函数当黑匣子。

参数说明:snr_db = 5是一个折中值,0dB 时噪声几乎盖过语音,10dB 时降噪效果不明显,5dB 适合展示算法效果。rng(0)固定随机种子很关键,实验重复性全靠它。

3.2 谱减核心循环:噪声估计、过减因子与谱底限制

分帧、加窗、FFT 之后,先拿前几帧做噪声功率谱估计。语音通常开头有 100~200ms 静音,16kHz 下 10 帧约 160ms,取前 10 帧足够。噪声谱估错会直接导致整段降噪翻车——如果前几帧恰好是清音,估计出来的“噪声谱”会把语音一起减掉。

frameLen = floor(0.025 * fs); % 25ms 帧长,16kHz 下 = 400点 frameShift = floor(0.010 * fs); % 10ms 帧移,= 160点 numFrames = floor((length(y) - frameLen) / frameShift) + 1; win = hamming(frameLen); % 分帧并计算每帧频谱 frames = zeros(frameLen, numFrames); for i = 1:numFrames idx = (i-1)*frameShift + 1 : (i-1)*frameShift + frameLen; frames(:,i) = y(idx) .* win; end Y = fft(frames, frameLen, 1); % 每一列是一帧的复数频谱 % 用前 10 帧平均功率谱作为噪声谱 noiseFrames = 10; noisePSD = mean(abs(Y(:,1:noiseFrames)).^2, 2); % 谱减 alpha = 1.5; % 过减因子 beta = 0.1; % 谱底比例 S = zeros(frameLen, numFrames); for i = 1:numFrames P = abs(Y(:,i)).^2; P_est = max(P - alpha * noisePSD, beta * P); % 减去噪声谱并限制底 S(:,i) = sqrt(P_est) .* exp(1i * angle(Y(:,i))); % 保留原始相位 end

逻辑说明:频谱矩阵每一列是一帧,噪声谱从整段带噪语音的前 10 帧平均而来。谱减这一步里max(P - alpha*noisePSD, beta*P)是经典的过减法,β 乘当前帧功率谱作为下限,避免负功率导致相位翻转和难听的爆音。

参数说明:alpha = 1.5是经验值,偏高会让语音变干瘪,偏低会留下更多噪声。beta = 0.1表示噪声底最多压到原功率的 10%,太小会出现音乐噪声,太大则降噪不彻底。这两个参数是调听感最关键的旋钮。

3.3 重叠相加重建波形,用 spectrogram 和 SNR 验证

重建的核心是重叠相加。由于分帧时帧移小于帧长,每个采样点被多个帧覆盖,直接累加会导致边缘能量偏大,必须做窗函数归一化。

recLen = (numFrames - 1) * frameShift + frameLen; x_rec = zeros(recLen, 1); winAccum = zeros(recLen, 1); for i = 1:numFrames seg = real(ifft(S(:,i))); idx = (i-1)*frameShift + 1 : (i-1)*frameShift + frameLen; x_rec(idx) = x_rec(idx) + seg; winAccum(idx) = winAccum(idx) + win; end x_rec = x_rec ./ max(winAccum, 1e-6); % 用窗累加和归一化 audiowrite('denoised.wav', x_rec, fs); % 计算降噪前后 SNR snr_before = 10*log10(sum(x.^2) / sum((y - x).^2)); snr_after = 10*log10(sum(x.^2) / sum((x_rec - x).^2)); fprintf('SNR before = %.2f dB, after = %.2f dB\n', snr_before, snr_after);

逻辑说明:winAccum记录每个采样点被多少窗函数能量覆盖,最后用除法归一化,这是重叠相加最常见也最稳的写法。max 里的1e-6防止除零。

参数说明:验证环节不要只盯着 SNR。SNR提升 5~8dB 是谱减的正常表现,但更重要的判断标准是 spectrogram 里噪声底是否变平、音乐噪声是否明显。运行这段后建议立刻用sound(x_rec, fs)听一遍,再用spectrogram(x_rec, win, frameLen-frameShift, frameLen, fs, 'yaxis')看谱图。

4. 从谱减到维纳与小波:同一条数据上的横向对比

4.1 判决引导维纳滤波:先验 SNR 估计决定一切

维纳滤波效果好坏,九成取决于先验信噪比 ξ 的估计质量。工程里最常用的是判决引导法:用上一帧估计的语音谱和当前帧后验信噪比做一阶递归,这样既平滑又不会过度滞后。增益公式写作G(k,l) = ξ(k,l) / (1 + ξ(k,l)),每个频点单独计算。

noisePSD = mean(abs(Y(:,1:noiseFrames)).^2, 2); xi_prev = max(eps, ones(frameLen,1)); % 上一帧先验SNR,初始为1 alphaDD = 0.98; % 判决引导平滑系数 S = zeros(frameLen, numFrames); for i = 1:numFrames P = abs(Y(:,i)).^2; gamma = P ./ noisePSD; % 后验SNR xi = alphaDD * xi_prev + (1-alphaDD) * max(gamma - 1, 0); G = xi ./ (1 + xi); % 维纳增益 S(:,i) = G .* Y(:,i); % 直接作用于复数谱 xi_prev = xi; end

逻辑说明:gamma = P./noisePSD是后验信噪比,max(gamma-1, 0)是对后验信噪比的保守估计,避免负值。alphaDD = 0.98让先验 SNR 的更新很平稳,这是抑制音乐噪声的关键。

与谱减对比,维纳滤波没有用max做硬底限制,增益是连续的 0~1 之间的值,所以听感更干净。代价是如果alphaDD调太大,语音起始段会响应变慢,听起来像声音“软”了。A同学第一次跑这个实验时把 alphaDD 改成 0.995,结果每个字开头都被削掉一半,后来调回 0.98 才正常。

4.2 小波去噪:一行函数背后藏着三个参数

小波去噪是这几套里代码量最小的,MATLAB 里用wdenoise就能完成,但参数含义比谱减更难直觉把握。母小波、分解层数、阈值规则三个参数,任何一个选不好都会把语音细节抹成“水声”。

x_rec_w = wdenoise(y, 6, 'Wavelet', 'sym8', ... 'DenoisingMethod', 'Bayes', ... 'ThresholdRule', 'Soft', ... 'NoiseEstimate', 'LevelDependent');

逻辑说明:6是分解层数,语音信号采样率 16kHz 时 6 层可以覆盖大约 125Hz 以上的频段,低频部分保留语音基频。sym8是对称性较好的小波,不会引入过多相位失真。Bayes表示用贝叶斯自适应阈值,比固定阈值更适应不同噪声强度。

参数说明:分解层数超过 8 会把语音低频能量也当噪声滤掉;低于 4 层则对高频噪声无计可施。Soft阈值比Hard阈值平滑,但也会让清音变得更弱,如果在做语音识别数据预处理,优先用Soft。

4.3 横向对比结果与选型建议

在同一段带噪语音上跑三种方法,结果差异非常直观。下面这个表是我在自己的测试样本上记录下来的典型表现,不同设备录出来的语音结果会有浮动,但趋势一致。

算法输出 SNR 提升音乐噪声语音清晰度计算耗时
谱减6~8dB明显中,有机械感O(帧数*FFT长度)
维纳滤波7~9dB较少较高略高于谱减
小波去噪5~7dB几乎没有中高,高频略暗较长

选型时我的判断标准是:做实时处理或批量大数据量音频,先上谱减,速度快且够稳;做语音识别前端训练数据清洗,优先维纳滤波,因为它对语音失真更小;遇到电磁脉冲、键盘敲击这类瞬态噪声,用小波去噪反而更干净。如果你只是想交实验报告,把三种方法结果和 spectrogram 对比图贴在同一页,比单独跑一种深刻得多。

5. 避坑清单:谱减、窗函数和音频格式的五个常见问题

5.1 加完噪后听起来跟原声没区别

现象:明明写了y = x + noise,播放y时几乎听不到噪声,降噪前后 SNR 都是 20dB 上下。

原因:噪声幅度设置不对。很多人直接把randn乘一个小常数,导致实际 SNR 远高于预期;或者干脆用awgn(x, 5)时没注意函数默认按整个序列功率计算,如果你的语音里静音段特别长,噪声会偏小。

解决:先算cleanPower再反推噪声功率,像 3.1 节那样写。验证时打印snr_before,必须落在目标值 ±1dB 范围内再继续。

5.2 降噪后出现“咕嘟咕嘟”的音乐噪声

现象:谱减运行成功后,背景噪声变成一阵一阵的颤音,像开水壶在响,语音反而不明显了。

原因:谱减后某些频点的功率被减到接近零,这些频点在不同帧之间随机出现,人耳听到的就是断续的“音乐噪声”。核心是过减因子 α 太大或者没有 β 限制。

解决:把alpha从 2.0 降到 1.2~1.5,同时把beta调到 0.05~0.15,给功率谱留一个底。如果还不行,对P_est做一次时间域平滑:P_smooth = 0.8*P_est + 0.2*P_prev,能明显减少帧间抖动。

5.3 降噪后开头有爆音,第一帧尤其明显

现象:播放降噪结果时,前几十毫秒先“咔嚓”一声,后面才正常。

原因:重叠相加时帧索引计算错误,或者窗归一化没做。最常见的是idx = (i-1)*frameLen + 1 : i*frameLen,这是把帧移和帧长搞混了,导致重建波形开头有跳变。

解决:idx = (i-1)*frameShift + 1 : (i-1)*frameShift + frameLen,并保存winAccum做除法归一化。这个错误几乎每个第一次手写 ISTFT 的人都会犯,跑完先看x_rec开头 100 个点有没有突变。

5.4 降噪后声音发闷,像隔着被子说话

现象:SNR 提升很高,但语音听不清楚,高频细节全没了,识别准确率反而下降。

原因:谱减法把高频弱语音误判成噪声,或者维纳滤波在先验 SNR 估计不准时把高频增益压得太狠。语音能量集中在低频,高频噪声又多,降噪时很容易“宁可错杀”。

解决:加一阶预加重滤波器恢复高频,x_pre = filter([1 -0.97], 1, x_rec),0.97 这个系数在 16kHz 下常用;或者对高频频点单独降低过减因子,例如频率大于 4kHz 时alpha_eff = 0.8 * alpha。

5.5 spectrogram 图和听到的声音对不上

现象:画出的 spectrogram 时间轴长度和音频长度明显不符,横轴频率范围也怪怪的,比如 16kHz 采样率的语音只显示到 5kHz。

原因:调用spectrogram时没有统一传入采样率和窗口参数,函数默认用了 256 点 FFT 和 1Hz 假定时钟,横纵轴自然全错。

解决:与分帧窗口保持完全一致,spectrogram(x, hamming(frameLen), frameLen-frameShift, frameLen, fs, 'yaxis'),其中noverlap必须是frameLen - frameShift,不能顺手填 50% 之类的比例。四五个参数全对上,图才能还原你用的算法配置。

6. 把降噪接到语音识别前端:用端点检测和 MFCC 给结果打分

6.1 端点检测:先找到语音在哪里

降噪效果好不好,光看波形和 SNR 不够直观。更客观的方式是把降噪结果送进语音识别前端,先做端点检测,看语音段是否保留完整。用短时能量和过零率双门限法,可以判断语音起止点有没有被降噪吃掉。

frameEnergy = sum(frames.^2, 1) / frameLen; % 每帧能量 th_high = max(frameEnergy) * 0.35; % 高门限 th_low = max(frameEnergy) * 0.15; % 低门限 isSpeech = frameEnergy > th_high; % 边缘扩展:低门限把语音边界向两端补齐 for k = 2:numFrames-1 if frameEnergy(k) > th_high && ~isSpeech(k-1) isSpeech(k-1) = frameEnergy(k-1) > th_low; end end

逻辑说明:高门限负责“确认这是语音”,低门限负责“语音边界向外延伸”,防止把词头和词尾的弱音截断。过零率可以再加一道判断,用于区分清音和噪声,但降噪后的语音用能量双门限基本够用。

6.2 MFCC 特征对比:降噪前后各算一组

MFCC 是语音识别里最常用的特征。对降噪前后的音频分别提取 MFCC,观察前几帧系数分布,能直观看到降噪是否让特征更稳定。这里有 Audio Toolbox 的话直接用mfcc函数,没有也可以按公式手动走 filterbank + DCT 流程。

[coeff_before, ~] = mfcc(y, fs); % 带噪语音特征 [coeff_after, ~] = mfcc(x_rec, fs); % 降噪语音特征 % 比较第一帧 MFCC 的距离 dist = norm(coeff_before(1,:) - coeff_after(1,:)); fprintf('MFCC distance = %.3f\n', dist);

如果降噪真把噪声压住了,降噪后语音的 MFCC 应该更接近干净语音的 MFCC,同时带噪语音和干净语音的 MFCC 距离明显更大。这个指标比 SNR 更能反映“对识别是否友好”。

从那以后,我每次跑完降噪实验,都会强制做一遍“音频 → 端点检测 → MFCC 距离”的验证,而不再只盯着 SNR 数字,因为听感和特征稳定性才是决定前端能不能用的关键。希望这套流程能帮你少走弯路,下载后按这份教程跑一遍,再对照 3.3 节的 spectrogram 参数去调,你会比只按.m文件点运行的人更清楚每一步在干什么。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进