用MATLAB拆解语音信号:从波形、频谱到去噪与基频提取

发布时间:2026/10/13 13:46:12
用MATLAB拆解语音信号:从波形、频谱到去噪与基频提取
打开Matlab的时候给自己整杯咖啡这大概是每个做信号处理的人最有仪式感的开场。今天不聊那些高深莫测的算法推导就实打实从硬盘里拽一个wav文件进来用最接地气的方式把语音信号从头到尾拆一遍。不管你是刚接触语音处理的初学者还是想换个角度把Matlab玩得更有趣的老手手头只要有一段录音——哪怕是手机备忘录里随手录的几句话——都能跟着把波形画出来、把频谱翻出来、试着把噪声滤掉、再把音高曲线拎出来看看。整个过程用到的都是最基础的操作但能把“声音在计算机里到底是什么”这件事讲得明明白白。我是习惯了先用audioread直接把音频文件怼进工作区这也是今天整套操作的第一步。后面要干的事其实就三件把语音看明白、把一个包含噪声的实际录音处理干净、再从里面提取出几个能用的特征。全程结合真实代码和踩坑记录来讲尽量让你看完就能照着跑一遍。1. 先把手头这个WAV文件搞定audioread的正确打开方式1.1 为什么不用老牌的wavread而用audioread玩过几年Matlab的人应该对wavread不陌生早期版本里它几乎是读取音频的唯一选择。但问题是wavread只支持wav格式而且官方早就把它标记为不推荐使用在新版本里甚至直接移除。现在统一推荐audioread它能读的格式范围广得多wav、mp3、flac、ogg都在支持列表里返回的数据格式也更规范。从实际使用的角度说audioread最大的好处是让你不用操心文件编码的差异。以前读wav还得小心是不是pcm编码、是不是IEEE float现在它内部帮你把这些都处理好了拿到手的就是归一化到[-1, 1]区间的double类型数据。这个归一化特别重要后面做FFT、滤波、特征提取时数据范围统一能少踩很多坑。1.2 一行代码把语音拉进工作区先看最核心的用法[y, Fs] audioread(myRecording.wav);这行代码做了两件事把文件里的音频数据读入变量y同时把采样率存入Fs。如果文件的采样率是44.1kHzFs就是44100如果是16kHzFs就是16000。变量的命名是个好习惯看到Fs就知道是采样率后面代码里不会再犯把采样率当普通数字用的低级错误。如果关心音频的位深可以再接收第三个返回值[y, Fs, nBits] audioread(myRecording.wav);nBits返回的是16、24这类数值表示每个采样点用多少bit存储。一般语音识别用的音频大多是16bit这算是比较通用的格式。y的形状也有讲究。单声道文件读出来是N×1的一列双声道文件则是N×2的矩阵每一列代表一个声道。处理语音时如果只想分析其中一个声道直接取一列就行y_mono y(:, 1);注意千万别把y和Fs的关系搞混。y的长度除以Fs才是音频的时长秒很多人刚上手时会误以为length(y)就是时间这个坑我一开始也踩过。1.3 采样率、通道数、位深决定你后面玩法上限的三个参数读进来之后第一步不是急着画图而是先盘一盘这三个参数因为它们直接决定后续处理方式。采样率决定了语音的频率上限。奈奎斯特定理告诉我们能无失真表示的最高频率是采样率的一半。CD音质44.1kHz能覆盖到22.05kHz人耳听觉上限差不多20kHz所以够用语音处理常用的16kHz采样率最高能表示8kHz这对说话声完全够因为语音的主要能量集中在300Hz到3400Hz之间。如果你手里的文件是48kHz录的做语音分析时反而建议先降采样到16kHz省内存也省计算量还可以用resample做这事。通道数影响计算复杂度。做语音特征提取时双声道并不会带来额外信息大多数场景下直接合并成单声道处理。我只是提醒一下实际音频里左右声道可能有细微差异如果你想分析声场、定位声源那另说普通语音内容分析用一个声道足够。位深影响动态范围。16bit能表示大约96dB的动态范围对话录音完全够用。读进来之后数据是归一化的double范围在[-1,1]之间这在后面做滤波和特征提取时非常友好。1.4 读进来第一件事先听听它到底响不响说完参数有个习惯我觉得值得培养读到音频后先在Matlab里播一遍确认内容跟自己预期一致。sound(y, Fs);就这么一行系统就会通过默认声卡播放这段音频。如果录音里有明显的底噪、爆音或者根本就不是你想要的那段内容这一步就能发现省得后面花半天处理一个没用的文件。播放的同时可以顺手看看数据的统计特征disp([时长: , num2str(length(y)/Fs), 秒]); disp([峰值幅度: , num2str(max(abs(y)))]); disp([平均幅度: , num2str(mean(abs(y)))]);峰值幅度接近但不超过1说明录音没有削波平均幅度如果只有0.01量级说明录音音量偏小后面做特征提取时可能要先把增益提上去。这些数字看着不起眼但能帮你快速判断录音质量。2. 可视化先行把看不见的语音画出来看2.1 时域波形图——最简单的语音身份证音频读进工作区之后最直观的第一步永远是画波形图。t (0:length(y)-1) / Fs; plot(t, y); xlabel(时间 (秒)); ylabel(幅度); title(语音时域波形);横轴算成真实时间而不是采样点序号这样看图时能直接读出“这段语音大概说了一秒半”这类信息。波形图能告诉你的东西很多整体音量起伏、哪儿是安静的停顿、哪儿是高幅度的爆发音。比如清辅音听起来轻、波形幅度小元音的波形则是幅度大且呈现明显的周期性。我第一次拿到真实录音画波形图时最直观的感受是“原来语音真的不是连续的”。中间有大量低幅度片段那就是停顿和清音部分浊音段才表现出规则的振荡。画完图之后有个常被忽略的小操作查看数据里有没有直流偏移。dc_offset mean(y);正常情况下语音信号的直流分量应该非常小接近零。如果算出来是0.1甚至更大说明录音环节引入了直流偏置后面不管是频谱分析还是特征提取都会被这个偏移干扰。处理的办法很简单用y y - mean(y)去掉均值就行。2.2 短时能量和过零率一眼看出哪里在说话、哪里在停顿语音是非平稳信号但在一段很短的时间内比如20到30毫秒可以看作平稳的。所以用短时能量来区分有声段和静音段是最常见的手段。短时能量的定义很简单每一帧内所有采样点的平方和frameLen 256; frameShift 128; energy []; for i 1:frameShift:length(y)-frameLen frame y(i:iframeLen-1); energy(end1) sum(frame.^2); endframeLen是帧长对应16kHz采样率下16毫秒frameShift是帧移128点对应8毫秒相邻帧有50%重叠。这样算出来后把energy画出来会看到明显的高低起伏。高能量段就是说话内容低能量段就是静音。比能量再多一个维度的是过零率统计一帧内信号穿越零点的次数zcr []; for i 1:frameShift:length(y)-frameLen frame y(i:iframeLen-1); zcr(end1) sum(abs(diff(sign(frame)))) / 2; end过零率对清音和浊音的区分很有用。浊音比如元音波形平滑过零率低清音比如很多辅音波形接近噪声过零率高。这两个特征联合起来就能把一段语音粗略地切成“说话段”和“静音段”这其实就是最简单的语音端点检测。2.3 玩点花的把整段语音的幅度包络描出来有时候看原始波形还是一团乱麻尤其是录音里噪声比较重的时候。这时候把幅度包络画出来会清爽很多。最简单的包络可以用hilbert变换来做这种方法在Hilbert变换被称作解析信号法本质是构造解析信号取模就是包络envelope abs(hilbert(y));如果你想避免直接用希尔伯特变换也可以用滑动窗口取局部最大值来近似包络。包络曲线能很清楚地展示语音的音量和节奏变化对于判断重音位置、分析语速都挺有用。画包络时有一点要注意hilbert对整段信号运算如果文件很长比如几十秒内存占用会明显上升。这种场景下建议先分段处理或者用分帧取极值的办法性能更好效果也足够呈现。3. 从时域钻进频域看看声音里藏着哪些频率3.1 FFT怎么用才不算白用——频谱泄露和窗函数画完波形接下来就该看频率成分了。直接对整个y做FFT是完全可以的N length(y); Y fft(y); P abs(Y(1:N/21)).^2 / N; f (0:N/2) * Fs / N; plot(f, P);这里只取前一半是因为FFT结果是共轭对称的后半部分对实信号来说没有额外信息。P算的是功率谱的大致形式横轴对应物理频率。画出来你会看到语音的能量并不是均匀分布的而是集中在某些频带。但直接对整段语音做FFT有个问题语音内容随时间变化整段平均会让频谱变得“面目模糊”看不出具体声音的特征。更关键的问题是频谱泄露——因为对有限长信号做FFT相当于隐含地给信号加了一个矩形窗这个窗的旁瓣会干扰频谱。解决频谱泄露的办法是加窗在分帧处理时常用汉宁窗或汉明窗win hann(frameLen, periodic); frame_windowed frame .* win;加窗的本质是让一帧信号在两端平滑过渡到零避免截断造成的频谱泄露。不过代价是主瓣变宽频率分辨率略有下降。这是语音处理里“鱼和熊掌”的经典权衡。3.2 功率谱、基频和共振峰识别元音的关键线索把分帧加窗后的每帧分别做FFT再平均起来就得到更有代表性的平均功率谱numFrames 0; avgP zeros(frameLen/21, 1); for i 1:frameShift:length(y)-frameLen frame y(i:iframeLen-1) .* win; Yf fft(frame); P_frame abs(Yf(1:frameLen/21)).^2; avgP avgP P_frame; numFrames numFrames 1; end avgP avgP / numFrames;从语音的功率谱里能看到几个关键结构。首先是基频它对应的谱峰是语音里最低的那个峰反映声带振动的快慢。男性说话基频一般在80到200Hz女性一般在180到400Hz。基频上面的几个较宽的峰是共振峰它们对应声道形状产生的共鸣频率不同元音的区别主要就在前两个共振峰的位置。举个例子读一个“啊”音第一共振峰大概在700到1000Hz第二共振峰在1100到1300Hz附近而读一个“衣”音第一共振峰比较低第二共振峰则明显偏高。看图时可以直接从功率谱上读出这些峰的位置。我自己刚开始做语音分析时就是靠画功率谱来验证“为什么不同元音听起来不一样”这个问题的那种恍然大悟的感觉确实挺爽。3.3 spectrogram语谱图一图看懂语音的“时间-频率”变化功率谱是整段语音的平均结果没法展现“某一个时刻在发什么音”。这时就该用spectrogram了它能把时间、频率、幅度三个维度展现在一张图上spectrogram(y, hann(512), 256, 512, Fs, yaxis);yaxis参数让纵轴显示频率坐标单位是Hz。横轴是时间纵轴是频率颜色深浅表示这个频率成分的能量强弱。一张语谱图出来语音的所有信息几乎都在里面了。读语谱图是有技巧的。浊音段会在低频部分出现明显的横向条纹那是基频的谐波元音段会出现几条颜色较深的水平带那是共振峰清音段在整个频带上颜色都比较浅而且均匀没有明显的结构。噪声在语谱图上一般表现为整片区域颜色普遍偏亮比如环境底噪会让所有频段都蒙上一层浅色。我处理录音时特别喜欢先看一眼语谱图再决定用什么滤波器。如果底噪主要在低频那就用高通滤波如果底噪是均匀的白噪声那得靠谱减这种降噪算法。语谱图相当于给信号作了一个“CT检查”先看病情再下药比盲目调参高效得多。4. 实际动手做一个去噪特征提取的小Demo4.1 设计一个滤波器把低频噪声去掉下面用一个真实场景来串一遍完整流程假设录音是在普通办公室环境录的有电脑风扇的低频哼声想把这段低频底噪去掉。用高通滤波器就是个很直接的办法。先根据需求确定截止频率。风扇噪声的基频往往在100Hz以下而语音的有效成分主要在300Hz以上。所以高通滤波器的截止频率设在100Hz左右比较合适fc 100; [b, a] butter(4, fc/(Fs/2), high); y_filtered filtfilt(b, a, y);这里有两个关键点值得说。第一是fc/(Fs/2)这是把模拟频率转换成归一化频率也就是以奈奎斯特频率为基准的相对值。很多初学者会把Fs/2漏掉导致滤波器完全没有产生预期效果就是因为归一化频率算错了。第二是用filtfilt而不是filter。filter是普通的因果滤波会产生相位延迟处理后波形在时间轴上会偏移filtfilt是零相位滤波它对信号做正反两次滤波相位失真相消了波形形状基本保持不变。这对于分析波形细节非常重要。滤波后可以用spectrogram对比看看低频的亮色是不是明显减弱了。filtfilt配合butter这个组合在离线处理里几乎是我的默认配置设计简单、相位不失真、效果稳定。4.2 谱减法削弱均匀背景噪声的思路和实现高通滤波只能对付低频噪声如果录音里混的是宽带背景噪声就需要更精细的降噪思路。谱减法是一个经典且有效的选择。谱减法的核心思路是假设噪声在短时间内是平稳的先找到一段纯噪声片段估计出噪声频谱把它从带噪语音的频谱里减去再重构信号。简单实现如下% 假设y_clean是带噪语音取前0.5秒作为噪声段估计 noise_samples 0.5 * Fs; noise_frame hann(256); noise_seg y(1:noise_samples); % 计算噪声的平均频谱 noise_spectrum zeros(129, 1); for i 1:256:noise_samples-256 seg noise_seg(i:i255) .* noise_frame; spec abs(fft(seg, 256)); noise_spectrum noise_spectrum spec(1:129).^2; end noise_spectrum noise_spectrum / floor((noise_samples-256)/256);对每一帧语音信号将其频谱减去估计的噪声频谱再进行相位恢复和逆FFT。这里有个实用技巧减完以后可能出现负值直接取负值会引入难听的“音乐噪声”。常规做法是加一个谱下限把所有负值压制到一个很小的正数避免听起来有断续感。谱减法实现容易、计算速度快但它对噪声的平稳性有要求。如果噪声本身在变化效果会打折扣。实际使用中我会把它和高通滤波叠加使用先高通去掉低频哼声再用谱减去除宽带本底噪声处理后的音频听起来干净很多。4.3 提取基频F0并画出音高曲线去完噪声高阶玩法是提取基频F0也就是声带振动的基音频率对应我们日常说的“音高”。提取基频的方法有很多种最稳妥、也不用深度学习模型的是基于自相关的检测。原理不复杂浊音段信号具有周期性把一帧信号与自身的延迟版本做相关当延迟刚好等于一个周期时相关性最大于是周期就能算出来frameLen 320; % 20ms at 16kHz frameShift 160; f0 []; for i 1:frameShift:length(y)-frameLen frame y(i:iframeLen-1) .* hann(frameLen, periodic); % 去掉均值再算自相关 frame frame - mean(frame); r xcorr(frame, frame); r r(frameLen:end); % 限制搜索范围在80~400Hz之间 minLag round(Fs/400); maxLag round(Fs/80); [~, idx] max(r(minLag:maxLag)); lag minLag idx - 1; if r(lag) 0.3 * r(1) f0(end1) Fs / lag; else f0(end1) 0; end end代码里设置了基频搜索范围80到400Hz这是语音比较常见的范围。自相关的峰值如果太弱说明这一帧可能是清音或静音直接把F0记为0。画音高曲线时把f0的值按时间轴点出来可以清楚看到一句话里的音调起伏。比如疑问句末尾语调上扬就是F0曲线在句尾明显升高。把语谱图和F0曲线叠加着看那种语音的“韵律感”一下就有画面了。这段代码我每次用都会做个小优化把所有连续的0值当作无音段直接不画否则图上有大段0值会把纵轴的显示范围拉得很奇怪。4.4 导出处理后的语音audiowrite保存处理完的语音总要保存下来这就轮到audiowrite上场y_out y_filtered / max(abs(y_filtered)); audiowrite(processed.wav, y_out, Fs);输出前做一次归一化是值得强调的好习惯。audiowrite会把数据范围映射到16bit整数的范围如果你的数据里有超出[-1, 1]的值会被强行截断听起来就变成明显的爆音。先除以峰值幅度就不存在这种隐患。如果想把位深也保存进去可以指定第三个参数audiowrite(processed16bit.wav, y_out, Fs, BitsPerSample, 16);保存后在文件夹里找到这个文件播放听听跟原文件对比一下降噪效果完整的工作流就算走通了。5. 我在实战中踩过的坑和常用排查清单5.1 常见问题速查表这些是我自己在处理语音信号时遇到的问题整理成表格供你对照排查。现象可能原因解决办法audioread报错说格式不支持文件扩展名与实际编码不符先右键查看文件属性或者试试用格式转换工具转成wav画出来的波形图是一条平线数据全是0或者接近0检查文件路径是否读对用max(abs(y))看峰值是否太小必要时放大增益播放时声音严重失真、破音数据范围超出[-1,1]被截断归一化后再播放确保abs(y) 1FFT后频谱图左右对称难看没有只取前半部分实信号只用1:N/21这段滤波后波形没有变化归一化频率算错了确认截止频率除以的是Fs/2不是Fs谱减法后出现“音乐噪声”谱相减负值没有做底限处理设置谱下限避免减到负值F0曲线大量零点画出来断断续续阈值设置过紧或者噪声未滤干净放宽自相关峰值的判定阈值或者先做降噪再提F0内存不足文件过长且一次性做FFT分帧处理逐帧运算避免把整个文件当作一个大矩阵操作5.2 几个容易忽略但很要命的细节第一个细节是数据类型。audioread读出来默认是double但很多音频处理库尤其是在外部函数交互时需要强制指定类型这时候要留意不要随手转成int16否则后续浮点运算会丢失精度。第二个细节是暂态现象。录音开始和结束的那几毫秒经常有咔哒声这个在波形上看就是幅度突然跳变。处理前把首尾各切掉20毫秒是个省心的做法cut round(0.02 * Fs); y y(cut:end-cut);这能避免很多莫名其妙的噪声出现在频谱边缘。第三个细节是大文件分帧的边界处理。如果文件长度不能被帧移整除最后会剩一截不够整帧的数据。之前给的循环里用length(y)-frameLen作为循环上界其实就是故意丢掉尾部不完整的残帧这是常见做法不用纠结那几个点的损失。第四个细节是Matlab的图形窗口显示问题。大音频文件画波形时直接plot会非常卡因为要绘制几十万个点。可以先用downsample或者用decimate降采样后再画只保留显示所需的点数图形刷新快得多。5.3 给你的下一步扩展建议到这步读wav、画波形、看频谱、分帧、加窗、滤波、提特征这条链路已经完整走了一遍已经具备了做更多花样实验的基础。我建议下一步可以试着做这几种扩展。第一个是性别识别。利用前面提取的F0统计整段语音的平均基频大于180Hz大概率是女声小于160Hz基本是男声。这是语音特征最简单的落地应用。第二个是端点检测强化版。把短时能量和过零率结合起来写一个能自动切分出每句话起止时刻的函数切出来的片段可以逐个播放试听。这个功能在语音数据集准备阶段特别有用。第三个是简单命令识别。提取几个简单词的声音特征比如MFCC然后用一个简单的统计模型进行分类就能做成一个基础的语音命令识别Demo。这里会说“统计模型”而不是直接推荐某个工具箱是因为掌握特征计算本身才是真正有价值的部分。这套流程我自己隔一段时间就会重新跑一遍。每次重新处理一批新录音总能发现以前没留意的细节。音频处理就是这样动手做永远比看文档强你把一个真实录音从读到画到滤到提特征完整走下来你对“声音数字化”这件事的理解就不再是课本上的抽象概念而是实实在在能看见、能听见、能改动的数据了。工具只是工具关键是思路今天这一套流程足够撑起一个小而完整的语音处理项目了。