1. 为什么我要自己写语音增强代码语音增强这个方向很多人第一反应是找现成的工具箱或者调一个开源模型跑一跑。我一开始也是这么想的但实际用下来发现现成方案要么黑盒太重、参数改不动要么对特定场景的噪声根本不起作用。后来我干脆用 MATLAB 从零把整个链路写了一遍从分帧加窗到谱减法、维纳滤波再到后处理平滑全部自己控制。这么做最大的好处是每一个环节你都能看到数据长什么样出了问题能定位到具体哪一行而不是对着一个封装好的函数干瞪眼。这篇文章适合谁看如果你正在做语音相关的课程设计、毕业项目或者手头有一段带噪录音想搞清楚怎么把它弄干净又或者你已经会用 MATLAB 但不知道怎么把信号处理的理论落到代码上那这篇内容应该能帮到你。我会把整个思路、关键参数的计算过程、实际踩过的坑都摊开讲代码片段可以直接拿去改。核心关键词就几个语音信号增强、MATLAB自写代码、谱减法、维纳滤波、分帧加窗。下面我按实际开发的顺序来拆解。2. 整体方案设计与思路拆解2.1 为什么选谱减法作为主线语音增强的方法大致分几类基于谱减的、基于统计模型的、基于深度学习的。深度学习效果确实好但它需要大量配对数据训练而且推理阶段往往依赖额外的运行环境。对于我这种想在一个 MATLAB 脚本里跑通全流程的需求来说谱减法是最务实的选择。谱减法的核心逻辑其实很朴素带噪语音的功率谱约等于干净语音功率谱加上噪声功率谱。那我在静音段估计出噪声的功率谱然后从带噪谱里减掉它剩下的就近似是干净语音。用生活化的类比就像你知道房间里空调的底噪大概是什么样然后从录音里把这个底噪扣掉。它的优势是计算量小、原理直观、参数可解释。缺点也明显会引入音乐噪声就是那种听起来像水底下冒泡泡的残留噪声。所以我在谱减法之后又加了一级维纳滤波做平滑把残留的毛刺压下去。2.2 处理链路的分层设计整个链路我分成四层每一层职责单一方便单独调试预处理层预加重、分帧、加窗。目的是把连续信号切成短时平稳的小段方便后续频域处理。噪声估计层从带噪信号的前几帧假设是静音段估计噪声功率谱后续帧做平滑更新。增强层谱减法做主增强维纳滤波做二次修正。重建层逆变换、重叠相加、去加重把处理后的帧拼回连续信号。这样分层的好处是如果最终效果不对我可以逐层打印中间结果。比如先看分帧后的帧数对不对再看噪声估计的谱形是否合理最后看重建有没有拼接痕迹。如果全写在一个函数里排查起来会非常痛苦。2.3 参数选型的底层逻辑采样率我定在 16 kHz这是语音处理的常用档位既能覆盖语音主要频段300 Hz 到 3.4 kHz又不会让数据量太大。帧长选 25 ms对应 400 个采样点帧移选 10 ms对应 160 个采样点。为什么是这两个数25 ms 是经验值短时平稳假设在这个尺度上基本成立10 ms 的帧移保证帧与帧之间有 60% 的重叠重叠相加时不会出现明显的拼接断裂。窗函数用汉明窗。矩形窗频谱泄漏太严重汉宁窗主瓣又偏宽汉明窗在两者之间比较平衡是语音分帧的常规选择。这些参数不是拍脑袋定的每一个都有对应的信号处理依据后面实操部分我会把计算过程写清楚。3. 核心细节解析与实操要点3.1 预加重到底在加什么预加重就是让信号通过一个一阶高通滤波器传递函数是 H(z) 1 - 0.97 z⁻¹。为什么要做这一步因为语音信号的频谱在高频段会以大约每倍频程 6 dB 的速度衰减预加重把这个倾斜拉平让高频部分的能量不至于在后续处理中被淹没。系数 0.97 是常用值它对应的截止特性大约在几十赫兹附近。如果你处理的信号低频干扰特别重可以适当调高这个系数比如 0.95 到 0.98 之间微调。但注意预加重之后在重建阶段必须做去加重也就是用 1/(1 - 0.97 z⁻¹) 还原否则输出声音会变得尖锐刺耳。我第一次写的时候忘了去加重结果出来的音频像老式收音机跑调排查了半天才发现是这一步漏了。3.2 分帧与加窗的实操细节分帧的时候有个容易忽略的点帧数计算。假设信号长度是 N帧长是 frame_len帧移是 hop那么帧数大约是 floor((N - frame_len) / hop) 1。如果信号长度不能被整除最后一帧可能不完整我的做法是在信号末尾补零保证每一帧都是完整的 frame_len 长度。加窗这一步很多人直接对整帧乘窗函数就完事了。但要注意窗函数的作用是减小帧边缘的突变如果加窗后不做重叠相加重建信号会有周期性起伏。所以帧移必须小于帧长我用的 10 ms 帧移对应 25 ms 帧长重叠率 60%这个比例在重建时能有效平滑过渡。提示分帧前先做预加重顺序不要反。如果先分帧再预加重帧边缘的滤波瞬态会污染结果。3.3 噪声估计的平滑策略噪声估计的准确性直接决定增强效果。最简单的做法是取前 5 到 10 帧当作纯噪声求平均功率谱。但实际录音里前几帧未必是静音可能一开口就是语音。所以我加了一个基于能量的语音活动检测计算每帧的短时能量如果能量低于某个阈值就认为这帧是噪声帧用它来更新噪声谱。更新公式我用的是指数平滑noise_est alpha * noise_est (1 - alpha) * current_power。alpha 取 0.95 左右这样噪声估计会缓慢跟随环境变化又不会被某一帧的突发噪声带偏。这个 alpha 很关键太小了噪声估计抖动大太大了跟不上噪声变化。我实测下来 0.92 到 0.97 之间比较稳。3.4 谱减法的过减与下限控制朴素的谱减法就是带噪功率谱减去噪声功率谱然后开方。但这样会出问题当带噪谱小于噪声谱时减法结果是负数开方就出虚数了。所以必须加一个下限通常做法是取一个小的正数比如带噪功率谱的 1% 到 5%。另外为了压制音乐噪声我用了过减因子 alpha_sub 和谱下限 beta。过减因子让减法减得更狠一点谱下限保证残留噪声不会完全消失导致听觉上的空洞感。公式大致是增强谱 max(带噪谱 - alpha_sub * 噪声谱, beta * 噪声谱)。alpha_sub 我取 2 到 4beta 取 0.01 到 0.05。这两个参数需要根据实际噪声类型调白噪声可以小一点周期性噪声要大一点。4. 实操过程与核心环节实现4.1 完整代码框架与关键片段下面是我实际用的代码骨架按处理顺序排列。为了篇幅考虑我只贴关键部分但逻辑是完整的。% 参数设置 fs 16000; frame_len 400; % 25 ms hop 160; % 10 ms pre_emph 0.97; alpha_sub 3; beta 0.02; alpha_noise 0.95; % 读取信号 [x, fs] audioread(noisy_speech.wav); x x(:,1); % 取单声道 % 预加重 x_pre filter([1, -pre_emph], 1, x); % 分帧 num_frames floor((length(x_pre) - frame_len) / hop) 1; frames zeros(frame_len, num_frames); for i 1:num_frames start_idx (i-1)*hop 1; frames(:,i) x_pre(start_idx : start_idx frame_len - 1); end % 加窗 win hamming(frame_len); frames_win frames .* win; % 频域变换 fft_frames fft(frames_win, frame_len); mag abs(fft_frames); phase angle(fft_frames); power mag.^2;这段代码里filter([1, -pre_emph], 1, x)就是预加重hamming生成汉明窗fft做频域变换。注意fft的点数我直接用了 frame_len没有补零因为 400 点已经是 2 的幂次附近补零带来的频率分辨率提升对语音增强意义不大反而增加计算量。4.2 噪声估计与谱减的代码实现% 噪声估计初始化 noise_power mean(power(:, 1:5), 2); % 逐帧处理 enhanced_mag zeros(size(mag)); for i 1:num_frames % 语音活动检测短时能量 frame_energy sum(power(:,i)); noise_energy sum(noise_power); if frame_energy 1.5 * noise_energy % 认为是噪声帧更新噪声估计 noise_power alpha_noise * noise_power ... (1 - alpha_noise) * power(:,i); end % 谱减法 sub_power power(:,i) - alpha_sub * noise_power; sub_power max(sub_power, beta * noise_power); enhanced_mag(:,i) sqrt(sub_power); end这里的语音活动检测用的是能量比值1.5 这个系数是我试出来的。太小了会把弱语音误判成噪声太大了噪声更新不及时。你可以根据实际录音的信噪比调整信噪比低的时候可以降到 1.2 左右。4.3 维纳滤波做二次平滑谱减法之后残留的音乐噪声还是能听到。我加了一级维纳滤波它的核心是计算一个增益函数让增强谱在噪声占主导的频点衰减更多在语音占主导的频点保留更多。% 维纳滤波增益 snr_post max(power(:,i) ./ (noise_power eps) - 1, 0); wiener_gain snr_post ./ (1 snr_post); enhanced_mag(:,i) wiener_gain .* mag(:,i);这段代码里snr_post是后验信噪比wiener_gain就是维纳增益。它的取值范围在 0 到 1 之间噪声大的频点增益趋近 0语音强的频点增益趋近 1。实际用的时候我会把谱减法和维纳滤波的结果做一个加权平均比如 0.6 倍谱减加 0.4 倍维纳这样既保留了谱减法的力度又借了维纳滤波的平滑性。4.4 重建与去加重% 逆变换 enhanced_frames enhanced_mag .* exp(1j * phase); enhanced_time real(ifft(enhanced_frames, frame_len)); % 重叠相加 output zeros(length(x), 1); for i 1:num_frames start_idx (i-1)*hop 1; output(start_idx : start_idx frame_len - 1) ... output(start_idx : start_idx frame_len - 1) enhanced_time(:,i); end % 去加重 output filter(1, [1, -pre_emph], output); % 归一化 output output / max(abs(output)); audiowrite(enhanced_speech.wav, output, fs);重叠相加的时候因为加了汉明窗每帧的边缘被衰减了相邻帧叠加起来正好能补偿这个衰减。但如果你用的窗函数不是汉明窗或者帧移比例不同可能需要额外除以一个归一化因子。我实测汉明窗加 60% 重叠直接相加不需要额外归一化幅度基本一致。5. 常见问题与排查技巧实录5.1 增强后声音发闷怎么办这是最常见的问题原因通常是高频被过度衰减了。排查思路先看噪声估计的谱形如果高频段的噪声估计偏高谱减法会把高频语音也一起减掉。解决办法有两个一是降低过减因子 alpha_sub二是对高频段单独设置一个更小的减法系数。我一般会在代码里加一个频率加权向量低频段减法系数大一点高频段小一点。比如 0 到 1 kHz 用 alpha_sub1 kHz 以上用 alpha_sub * 0.7。这个加权可以根据你的噪声类型调如果是低频嗡嗡声就重点压低频如果是宽带白噪声加权可以平缓一些。5.2 音乐噪声太明显怎么压音乐噪声的本质是谱减法在随机频点上留下的孤立峰值。压制方法除了前面说的维纳滤波还可以在频域做平滑。具体做法是对增强后的幅度谱沿频率轴做一个滑动平均窗口大小取 3 到 5 个频点。这样孤立的峰值会被周围的值拉平听感上会柔和很多。但注意平滑窗口不能太大否则会把共振峰也抹掉语音会变得含糊。我一般用 3 点平滑也就是当前频点和左右各一个频点做加权平均权重是 [0.25, 0.5, 0.25]。5.3 帧数计算错误导致索引越界这个坑我踩过不止一次。当信号长度刚好接近帧长整数倍时floor((N - frame_len) / hop) 1算出来的帧数可能多一帧导致最后一帧的索引超出信号长度。解决办法是在分帧前先判断如果剩余长度不足 frame_len就补零到 frame_len。或者更简单在循环里加一个边界检查超出部分用零填充。5.4 常见问题速查表问题现象可能原因排查方法解决手段输出完全没声音去加重遗漏或增益全零检查去加重滤波器系数补上去加重检查维纳增益下限声音发闷高频过度衰减打印噪声谱高频段数值降低高频段过减因子音乐噪声重谱减下限太小听增强后静音段提高 beta加频域平滑帧边缘有咔哒声重叠相加不连续检查帧移和窗函数确保帧移小于帧长用汉明窗索引越界报错帧数计算偏差打印 num_frames 和信号长度末尾补零或加边界检查注意调试的时候不要一上来就听整个文件先截取前 1 秒看波形和频谱确认单帧处理没问题再跑全量。这样能省很多时间。6. 参数调优的实战经验6.1 过减因子和谱下限的联动调整这两个参数不是独立的。过减因子调大音乐噪声会减少但语音失真增加谱下限调大残留噪声变多但语音更自然。我的经验是先固定谱下限在 0.02然后从 2 开始逐步增大过减因子每次增加 0.5听感上找到一个失真和噪声的平衡点。然后再微调谱下限通常 0.01 到 0.05 之间就够了。如果你处理的噪声是非平稳的比如键盘敲击声、纸张翻动声过减因子可以取大一点3 到 4 之间。如果是平稳的背景噪声比如空调声、风扇声2 到 3 就够了。6.2 噪声更新系数的选择前面提到 alpha_noise 取 0.95 左右。但如果你处理的录音环境噪声变化很快比如从安静房间走到嘈杂走廊0.95 就太慢了噪声估计跟不上。这时候可以降到 0.85 到 0.9。反过来如果环境很稳定可以升到 0.97 到 0.98让噪声估计更平滑。判断标准很简单增强后的静音段如果还能听到残留噪声的起伏说明更新太慢如果语音起始部分被削掉了说明更新太快把语音当噪声吸收了。6.3 采样率和帧长的匹配16 kHz 采样率配 25 ms 帧长是常规组合。但如果你处理的信号是 8 kHz 采样帧长应该相应调整到 200 点保持 25 ms 的时长不变。帧移同理10 ms 对应 80 点。这个比例关系不要乱否则短时平稳假设不成立频域处理的效果会大打折扣。另外如果你的信号是 44.1 kHz 或 48 kHz建议先降采样到 16 kHz 再处理。语音的主要能量集中在 4 kHz 以下高频部分对增强效果贡献很小降采样能大幅减少计算量而且不会明显损失语音质量。7. 效果评估与后续扩展方向7.1 怎么判断增强效果好不好客观指标可以用信噪比提升和分段信噪比但这两个指标需要干净语音做参考实际场景往往拿不到。更实用的方法是主观听测加频谱对比。我会把带噪信号和增强信号的频谱图并排画出来看噪声底是否被压下去了同时看语音的共振峰是否还清晰。如果共振峰模糊了说明增强过度如果噪声底还在说明增强不足。这个判断不需要精确计算肉眼对比频谱图就能看出来。MATLAB 的spectrogram函数画出来很直观建议每调一次参数就画一次。7.2 可以继续加的东西这套代码目前是谱减法加维纳滤波的经典组合后续可以往几个方向扩展。一是加入基于最小均方误差的统计模型方法对非平稳噪声的处理会更好。二是把整个流程封装成函数支持批量处理多个文件。三是加一个简单的图形界面方便调参时实时听效果。如果你想把深度学习的方法也接进来可以用这套代码做数据预处理和结果对比把传统方法作为基线看看深度模型到底提升了多少。这样你的项目报告里既有传统方法的可解释性又有新方法的对比数据内容会充实很多。我个人在实际操作中的体会是语音增强没有一套参数能通吃所有场景。同一个算法换一段录音可能就要重新调参。所以与其追求一个万能配置不如把调参的逻辑和排查方法搞清楚遇到新问题能快速定位和调整。这套代码的价值不在于它本身有多完美而在于每一个环节你都能改、都能看懂这才是自己写代码最大的意义。
阅读完成 · 觉得有帮助?