ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

小波阈值去噪在语音信号处理中的应用与Matlab实现

小波阈值去噪在语音信号处理中的应用与Matlab实现 最近清理硬盘翻出几段压箱底的老录音底噪大到连人声都被盖了一半。正好手头在做语音信号处理相关的活儿索性把“小波阈值去噪”这套经典流程重新盘了一遍取一段干净人声在Matlab里把白噪声直接怼上去再用小波阈值把信号“洗干净”最后从波形和频谱两个角度对比前后差异。这篇文章就是完整实操记录代码放在文末拿去就能跑。这套流程特别适合几类人正在做语音降噪相关毕设课题的学生、想在信号预处理阶段把底噪压一压的工程师以及想搞清楚小波去噪到底做了什么、而不是只会点一下封装函数按钮的同学。我尽量把原理讲得通俗参数设置讲得具体踩过的坑也都列出来看完你至少能独立复现出一套“加噪—去噪—评估”的完整链路。1. 小波阈值去噪的思路与方案选型1.1 常规滤波为什么搞不定语音噪声先说一个很多人容易踩的误区一提到去噪第一反应就是低通滤波。低通滤波确实能把高频噪声压下去但它对语音这种非平稳信号相当粗暴。语音里面有大量信息藏在瞬态和高频段里比如清音“s”“sh”“f”这类摩擦音能量分布明显偏向高频。低通切狠了人声直接变闷像隔着一层棉被在说话低通切浅了噪声又压不下去两头不讨好。更麻烦的是频谱重叠问题。我们把语音和噪声各自的频谱画出来会发现白噪声在低频段同样有能量而语音谐波在高频段也有分量。两者在频域是叠在一起的单一频带的滤波器在物理上就很难把它们完全分开。这就像两杯沙子和糖混在一起用筛子只能按颗粒大小粗略分想要干干净净地分离得换一个维度去思考。小波去噪的思路跟传统滤波完全不同。它不再试图在频域里“切一刀”而是先把信号做多尺度分解让不同频率成分落到不同小波系数上再在系数域做“稀疏化”处理。这种操作方式对语音更友好因为它能在压制噪声的同时尽量保住那些瞬态细节。1.2 小波变换凭什么能打小波变换的核心能力叫多分辨率分析。低频部分用较粗的时间分辨率去看对应信号的“骨架”高频部分用较细的时间分辨率去抠对应信号的“细节”。语音信号本身就是低频轮廓加高频瞬态的组合这种分析方式天然契合。还有一个关键特性是稀疏性。干净语音经过小波分解后大部分小波系数接近零能量集中在少数几个大系数上。而高斯白噪声是一个完全不稀疏的东西它的能量均匀散布在各个尺度的小波系数上而且每个系数的幅度都不大。这样一来“去噪”就从“难以分离的频域问题”变成了“容易处理的系数域问题”把小系数当作噪声扔掉把大系数保留下来再重构回去。这个过程有个很经典的生活类比把一段语音想象成一张素描铅笔线条是语音主体纸上均匀分布的小点子是噪声。小波分解相当于把画面分成不同粗细的图层每个图层上有线条也有小点。去噪就是把这些小点擦掉但保留线条最后把图层重新叠回去。普通滤波器的做法则是拿橡皮一通乱擦线条变淡小点却未必擦干净。1.3 阈值去噪的完整流程弄清楚原理之后流程就很清晰了经典的Donoho阈值去噪框架就三步第一步分解。用wavedec把带噪信号做多层小波分解得到最后一层近似系数和每一层的细节系数。近似系数对应信号的低频骨架细节系数对应不同尺度的高频内容。第二步阈值处理。对每一层细节系数估计一个合适的阈值thr然后用软阈值或硬阈值函数把系数做收缩处理。小系数被压掉大系数保留下来。这一步是整个流程的核心阈值估计得准不准直接决定去噪效果。第三步重构。用处理后的系数和原始近似系数一起做waverec把信号重建回时域。很多教程喜欢直接调用wdencmp或者wdenoise一行搞定但对初学者来说那样做最大的问题是你完全不知道内部发生了什么。出问题的时候根本没法排查。我建议至少手动实现一次wavedec加wthresh加waverec的组合把系数结构打印出来看一遍再去偷懒用封装函数。2. 实验环境准备与带噪语音构造2.1 语音素材怎么选做语音处理实验素材选择直接影响结论的可靠性。我建议选一段采样率16kHz、时长3秒左右的中文男声或女声内容最好同时包含浊音和清音比如“我去学校门口等你”这种带“q”“x”“m”“n”的句子。如果整段都是元音去噪效果会好得离谱但不代表算法真的强如果整段都是清音高频细节一丢就能听出来对参数设置非常敏感。混合内容的语音最能暴露算法的真实水平。读入语音时注意一个坑老教程里写的wavread在新版Matlab里已经移除一律要用audioread替代。读进来之后取单声道再做一次峰值归一化让信号幅度落在-1到1之间。归一化不是为了好看而是为了后面加噪时能量计算方便避免不同录音设备导致的幅度差异干扰实验结论。clean audioread(voice.wav); clean clean(:,1); % 取单声道 clean clean / max(abs(clean)); % 峰值归一化 fs 16000;2.2 加噪声与信噪比控制加噪声不能图省事直接clean 0.1*randn(size(clean))那样做你根本不知道实际信噪比是多少。正确做法是先想清楚目标信噪比再反推噪声能量。信噪比的定义是信号功率与噪声功率之比用dB表示SNR_dB 10 * log10(Ps / Pn)所以给定输入SNR后目标噪声功率就是 Ps / 10^(SNR/10)。为了让随机噪声生成得稳定我一般先把randn标准化为单位能量再乘以sqrt(Pn)。这样每次跑出来的实际SNR和设定值基本一致不会出现“我设了5dB实际算出来只有2dB”的尴尬。SNR_in 5; % 输入信噪比单位dB noise_raw randn(size(clean)); % 高斯白噪声 noise_raw noise_raw / sqrt(mean(noise_raw.^2)); Ps mean(clean.^2); Pn Ps / (10^(SNR_in/10)); noise noise_raw * sqrt(Pn); noisy clean noise;实测建议至少测三组信噪比0dB、5dB、10dB。0dB意味着噪声功率和语音功率一样大极端但能看出算法抗压能力5dB是很多语音处理任务里比较常见的恶化水平10dB则接近轻度底噪场景适合观察算法是否会把细节误杀。三组对比下来你对阈值的“脾气”会摸得很准。2.3 波形与频谱怎么看带噪语音造好之后先别急着去噪把原始语音、带噪语音的波形和频谱各画一遍。波形图能直观看到幅度随时间的变化干净语音的波形在语音段有明显轮廓静音段接近零加了白噪声之后整个波形表面会长出一层均匀的“毛刺”尤其在静音段原本平直的地方变得起伏不定。频谱图看的是频率成分的分布。干净语音的能量集中在中低频段和谐波位置高频段能量很低加噪之后高频段被明显抬高整个频谱的底部像垫了一层厚厚的“地板”。这个“噪声地板”的高度如果降不下去说明去噪不到位。一个非常影响观看体验的细节是频谱泄露。直接对有限长信号做FFT主瓣能量会跑到旁瓣去频谱图看起来锯齿严重该尖的地方不尖该平的地方不平。建议画功率谱密度估计时用pwelch加窗平滑或者至少先用汉宁窗对信号做加窗处理再算FFT。有条件的话再画一张语谱图spectrogram它能同时展示时域和频域信息后面评估去噪效果时我会反复用到它。3. 小波阈值去噪实操与关键参数3.1 小波基和分解层数的选择小波基的选择没有绝对标准但经验上有大致规律。语音信号我用得最多的是db4和sym8。db4属于Daubechies系列中最常用的类型紧支撑、正交性好跟语音的短时突变匹配得不错计算量也小。sym8是symlets系列它比db4更对称重构出来的波形更平滑听感上更圆润一些但代价是对瞬态细节的捕捉略钝一点。换成大白话解释一下选小波基就像选画笔db4是一支硬毛笔画细节线条锐利但偶尔有毛刺sym8是一支软毛笔画轮廓干净顺滑但细节处不够凌厉。实际项目里如果只是压低底噪且不希望听感“塑料化”sym8很稳如果是做语音识别前端预处理要保住清音特征db4往往更好。分解层数同样需要权衡。语音信号在16kHz采样率下我一般取4层或者5层。层数太少高频噪声没有被充分分开去噪不干净层数太多最低频的近似系数也会被处理可能会把语音的基频信息一并削弱听感上声音会“变细”或者“变薄”。可以用wmaxlev查询信号在给定小波基下的最大允许分解层数但最大层数只是上限不代表最优。level wmaxlev(length(clean), db4); % 查看最大层数实际用4或53.2 阈值规则和软硬阈值阈值估计是整套流程的灵魂。Matlab的thselect提供了四种常用规则各有脾气规则特点适用场景sqtwolog固定阈值去噪力度最大噪声几乎被压死但细节也容易丢底噪极重、对语音细节要求不高的场景rigrsureStein无偏风险估计阈值偏保守细节保留好有时去噪不彻底语音质量优先噪声不算太严重heursure启发式规则综合前两者信噪比高时偏保守低时偏激进通用折中方案不知道选什么时先用它minimaxi极大极小准则类似保守版固定阈值噪声抑制弱一些信号高频细节非常珍贵不敢乱砍我需要额外说明一点thselect如果对整个系数向量打一个全局阈值在噪声能量分布不均时会不够精细。更推荐的做法是对每一层细节系数分别估计阈值因为不同分解层上的噪声能量并不相同。常用估计公式是Donoho提出的thr sigma * sqrt(2 * log(N))其中sigma用第一层细节系数的绝对中位差来估计sigma median(abs(detail)) / 0.6745。这个0.6745来自高斯分布的统计特性目的是让估计出的标准差对异常值不敏感。软阈值和硬阈值的区别则是另一个关键决策。硬阈值做法是系数绝对值小于阈值的置零大于阈值的保持不变。硬阈值保真度更高但处理后的系数在阈值点处不连续重构出的信号可能在局部产生不自然的振荡。软阈值做法是系数绝对值小于阈值的置零大于阈值的向零收缩一个阈值单位。软阈值处理连续性好重构波形更平滑听感上不容易出现“炸音”但代价是所有保留的系数都小了信号能量会有一定损失。语音去噪我建议先用软阈值。你可以想象硬阈值像把草丛里的杂草直接一刀切平地面会留下很多刀痕软阈值像把草丛整体修剪一遍结构更自然。当然如果你对清音的“气声”特别在意可以试试硬阈值对比着听。3.3 去噪核心代码实现下面这段代码是我实际使用中的核心处理段按层估计阈值并做软阈值收缩wname db4; level 4; [C, L] wavedec(noisy, level, wname); C_den C; detail_start L(1) 1; for k 1:level seg_len L(k1); idx detail_start : detail_start seg_len - 1; sigma_k median(abs(C(idx))) / 0.6745; if sigma_k 0 sigma_k eps; end thr_k sigma_k * sqrt(2 * log(length(C(idx)))); C_den(idx) wthresh(C(idx), s, thr_k); detail_start detail_start seg_len; end denoised waverec(C_den, L, wname); denoised denoised / max(abs(denoised));这里有个容易搞错的地方wavedec返回的L向量结构不是从第一层开始的。L(1)是最后一层近似系数的长度后面依次是最后一层细节、倒数第二层细节……一直到第一层细节的长度。所以循环里我从L(1)1开始取细节系数顺序无所谓只要把每段长度取对就行。最后一步的再归一化很多人会忘。waverec重构出来的信号幅度通常和原始信号有偏差如果不重新归一化后面算SNR时结果会很难看听起来声音大小也不对。4. 波形与频谱前后对比分析4.1 时域波形到底发生了什么去噪后的波形和带噪波形放在一起看最直观的变化是背景毛刺被压制了。静音段原本起伏的曲线变得平直说明噪声能量明显下降语音段的幅度轮廓保留得比较完整浊音段能看到清晰的周期波动。但仔细观察会发现一个微妙的问题波形在语音起始和结束的边缘会有轻微钝化。比如爆破音“t”或者“k”的起始脉冲原本是突然出现的尖峰去噪后尖峰的幅度略有下降、上升沿变缓。这是小波阈值处理对瞬态信息的固有损耗无法完全避免关键是控制在“听起来还能接受”的范围。如果波形钝化严重甚至出现语音段中间“塌方”式凹陷那多半是阈值设大了需要回头调参数。我的经验是要把波形分成三段看静音段看噪声是否压平、浊音段看周期结构是否保持、清音段看高频小波动是否还在。只看整体轮廓容易骗自己分段盯着看才知道算法动了哪些手脚。4.2 频谱图上的变化信号频谱图的对比更能说明问题。处理后的频谱里高频段的“噪声地板”明显下降这是去噪生效的最直接证据。与此同时语音本身的谐波峰值应该保留下来尤其是低频段的基频和前面的几个高次谐波它们在频谱图上应该依然清晰可见没有被抹平。如果阈值设得过大频谱会出现一种典型异常高次谐波集体消失能量高度集中在低频整个频谱像被“压扁”了。听感上对应着声音发闷、细节丢失、像在远处说话。如果阈值设得偏小高频噪声地板又降不下去频谱底部依然厚厚一层去噪等于白做。这里我强烈建议配合语谱图一起看。语谱图横轴是时间、纵轴是频率、颜色深浅表示能量大小它能告诉你噪声在哪些时间段残留严重。去噪效果好的语谱图背景亮度应该整体变暗但语音段的条纹清晰连续。只靠波形和频谱很难发现噪声是均匀分布还是集中在某个时间段。4.3 客观指标SNR与RMSE听感和肉眼判断之外还要有客观指标。最常用的两个指标是去噪后的信噪比SNR和均方根误差RMSE。SNR看的是信号相对噪声的提升幅度RMSE看的是重构信号和原始干净信号之间的整体偏离程度。SNR_out 10 * log10(sum(clean.^2) / sum((clean - denoised).^2)); RMSE_out sqrt(mean((clean - denoised).^2));我拿自己的一段16kHz语音测试时输入SNR5dB经过4层db4软阈值去噪后输出SNR大约能到12.4dBRMSE从0.08降到0.035左右。这个数字仅供参考换一段语音、换一个输入信噪比结果都会不同。但规律是稳定的输出SNR会比输入高6到10dBRMSE通常能下降一半以上。要特别提醒的是SNR提升不等于听感变好。有些参数设置能让SNR飞涨但语音细节已经面目全非。所以必须把客观指标和主观听感放在一起评价单纯的数字竞赛没有意义。5. 完整代码与参数调优建议5.1 可直接运行的完整Matlab代码把前面的内容串起来就是一份可以完整运行的脚本。代码里我加了完整的注释并把波形图、频谱图、语谱图、指标计算都放在一起%% 语音读取与预处理 clean audioread(voice.wav); clean clean(:,1); clean clean / max(abs(clean)); fs 16000; %% 加噪目标信噪比 SNR_in SNR_in 5; noise_raw randn(size(clean)); noise_raw noise_raw / sqrt(mean(noise_raw.^2)); Ps mean(clean.^2); Pn Ps / (10^(SNR_in/10)); noise noise_raw * sqrt(Pn); noisy clean noise; %% 小波阈值去噪按层估计阈值 wname db4; level 4; [C, L] wavedec(noisy, level, wname); C_den C; detail_start L(1) 1; for k 1:level seg_len L(k1); idx detail_start : detail_start seg_len - 1; sigma_k median(abs(C(idx))) / 0.6745; if sigma_k 0 sigma_k eps; end thr_k sigma_k * sqrt(2 * log(length(C(idx)))); C_den(idx) wthresh(C(idx), s, thr_k); detail_start detail_start seg_len; end denoised waverec(C_den, L, wname); denoised denoised / max(abs(denoised)); %% 客观指标 SNR_in_calc 10 * log10(sum(clean.^2) / sum((clean - noisy).^2)); SNR_out_calc 10 * log10(sum(clean.^2) / sum((clean - denoised).^2)); RMSE_out sqrt(mean((clean - denoised).^2)); fprintf(输入SNR: %.2f dB\n输出SNR: %.2f dB\nRMSE: %.4f\n, ... SNR_in_calc, SNR_out_calc, RMSE_out); %% 绘图波形、频谱、语谱图 figure(Position, [100 100 1200 900]); subplot(3,2,1); plot(clean); title(干净语音波形); xlim([1 length(clean)]); subplot(3,2,2); plot(noisy); title(带噪语音波形); xlim([1 length(clean)]); subplot(3,2,3); plot(denoised); title(去噪语音波形); xlim([1 length(clean)]); subplot(3,2,4); [pxx_noisy, f_noisy] pwelch(noisy, hann(1024), 512, 1024, fs); plot(f_noisy, 10*log10(pxx_noisy)); title(带噪语音功率谱); xlabel(Hz); ylabel(dB); subplot(3,2,5); [pxx_den, f_den] pwelch(denoised, hann(1024), 512, 1024, fs); plot(f_den, 10*log10(pxx_den)); title(去噪语音功率谱); xlabel(Hz); ylabel(dB); subplot(3,2,6); spectrogram(denoised, hann(256), 128, 256, fs, yaxis); title(去噪语音语谱图);我建议运行时把采样率fs改成你语音文件的实际采样率不要硬套16000。语谱图的窗长也可以根据内容调整语音内容较长时hann(512)比hann(256)看得更清楚。5.2 参数调优与不同噪声场景的推荐实测下来不同噪声场景对参数的需求差异很大我整理了一张经验表可以作为起点再微调噪声类型推荐小波基分解层数阈值规则备注高斯白噪声db4 或 sym84rigrsure 或 heursure基础场景先跑这一组有色噪声/低频隆隆声db85sqtwolog层数加深方便剥离低频干扰非平稳瞬态噪声coif53minimaxi浅层处理避免抹掉瞬态特征底噪很轻但要保音质sym84rigrsure阈值偏保守细节保留调参的顺序不要乱。先把分解层数固定为4小波基固定为db4只调阈值规则听出每个规则的区别然后固定阈值规则换小波基对比听感最后再动分解层数。一次只动一个变量否则出了问题根本分不清是谁的锅。6. 常见问题与排查技巧6.1 去噪后出现“咕噜咕噜”的可疑噪声这是玩小波去噪最容易遇到的诡异现象听感上像在水底说话或者声音断断续续带着气泡感。多半原因是用了硬阈值细节系数在阈值处被硬生生切掉重构时系数的跳变产生了伪迹也可能是分解层数太少噪声能量没有被充分拆分几层细节系数里还混着大量噪声成分。解决思路有两个一是换成软阈值二是增加分解层数。如果换了软阈值还是咕噜去看看语谱图如果噪声集中在中高频段且成片出现把层数从4提到5通常能缓解。6.2 声音变闷、细节丢失严重这种情况一般是阈值设太大把语音本身的细节系数也砍掉了。高频清音和齿音在小波系数里能量本身就不高它们和噪声之间的界限很模糊大阈值会把它们一并抹掉。耳朵听到的结果就是“声音圆润了但没了气声说话像含着东西”。对策是把固定阈值换成rigrsure或者减少分解层数。另一个细节不要对所有层用同一个阈值。高频层系数整体偏小用通用公式很容易砍过头低频层系数大又可能砍不动。按层估计阈值、分层处理是目前我实践下来比较稳妥的做法。6.3 波形两端出现畸变重构信号开头和结尾经常会出现不正常的起伏这是小波变换的边界效应。任何信号处理都怕“开刀”正好开在边缘小波滤波器的卷积在边界处会缺数据不同边界延拓方式会给出不同结果。如果发现两端畸变先试dwtmode(per)改成周期延拓再试dwtmode(sym)改成对称延拓。我看波形时习惯把两端各切掉几十个采样点再做指标计算避免边界畸变污染SNR数字。6.4 听感和指标“打架”有时候SNR提升非常高但戴上耳机一听声音完全“塑料化”了背景噪声没了语音也像机器人念稿。对语音信号处理来说这其实是过度去噪的典型表现。我个人的评估习惯是SNR和RMSE作为参考最终以语谱图和听感为准。语谱图上背景亮度明显变暗说明噪声压住了语音段的条纹清晰连续说明细节保住了。两个目标同时满足才算效果好单看任何一个指标都容易跑偏。6.5 频谱泄露与加窗的补充最后补一句频谱泄露的问题。很多人也会遇到画FFT时明明是个单频信号频谱上却是一片胖墩墩的谱峰旁边还带一堆旁瓣。这就是有限长截断导致的频谱泄露。解决办法就是在做FFT之前先加窗函数比如汉宁窗。它对信号两端做平滑衰减能显著压低旁瓣频谱图干净很多。pwelch内部已经帮你做了加窗和分段平均所以日常我画功率谱都优先用pwelch。我自己每次跑完小波去噪习惯把同一段带噪语音用不同阈值规则各生成一个wav文件然后戴上耳机来回切着听。经常出现的情况是听感最舒服的那一版不是SNR最高的而是语谱图上噪声压得干净、语音谐波也完整的那一版。所以做这个实验时记得把波形、频谱、语谱图和自己的耳朵都用上它们各有各的信号要告诉你。
返回列表