☰
MATLAB语音信号处理实战:从录音到语谱图的完整链路
2026/9/30 4:03:04 网站建设 项目流程

宿舍里录一句“Hello MATLAB”,手机录音App存下来导成wav,拖进MATLAB用audioread读进来,先画时域波形,再上FFT看频谱,最后叠一层语谱图——这套“时频双杀”操作,我昨晚刚完整跑了一遍。整个过程比想象中有意思,坑也比想象中多。

说实在的,很多学信号处理的人,课本上的正弦波、白噪声、理想滤波器背得滚瓜烂熟,但第一次把真实语音丢进MATLAB,十有八九会愣住:波形怎么长这样?频谱怎么全是毛刺?语谱图怎么跟教材上的完全对不上?这不是你的问题,是真实信号从来都不讲道理。这篇文章我就从这段宿舍录音出发,把从手机录音、格式转换、读入MATLAB,到画出时域波形、频谱和语谱图的完整链路走一遍,每一步的“为什么这么做”和“踩过的坑”都会写清楚。适合刚学MATLAB的、或者信号与系统理论学了不少但还没上手过真实信号的人。

1. 动手前的准备:录音格式、读入命令与版本坑

1.1 手机录音导出的文件,大概率不是wav

先说第一个坑:手机自带录音App,绝大多数默认存的是M4A或AAC格式,不是wav。只有少数手机的“语音备忘录”类应用可以在设置里直接选WAV输出。我手里这台手机还算争气,直接在设置里切到了wav,但如果你导出一个M4A文件,千万别直接扔给MATLAB,会报错或者读出来一团乱码。MATLAB对压缩音频格式的支持非常有限,老老实实先转成wav。

转换工具首选ffmpeg,命令行一条搞定:

ffmpeg -i hello.m4a -ar 44100 -ac 1 hello.wav

参数里面-ar 44100是把采样率重采样为44.1kHz,-ac 1是转成单声道。为什么指定这两个参数?语音分析的实验,单声道完全够用,双声道只会徒增数据量,后面处理还得分声道看,纯属给自己找麻烦。采样率选44.1kHz是CD音频标准,16bit位深、44.1kHz采样率已经完全覆盖人耳听力范围,语音信号的频率又大多集中在100Hz到4kHz之间,44.1kHz的奈奎斯特频率22.05kHz,余量非常充足。

当然,手机系统也可能默认按48kHz录音。这问题不大,关键是读进MATLAB之后用fs变量去拿采样率,别自己在代码里写死一个值。我还见过有人不看fs,直接在代码里写fs=44100,结果时间轴和频率轴全部对不上,图看起来没问题,一算实际时长就是错的。这种错特别隐蔽。

1.2 audioread读入与容易踩的版本坑

读取就一行:

[data, fs] = audioread('hello.wav');

data是采样点数组,范围一般已经归一化到[-1, 1]之间,这是MATLAB读wav的默认行为。如果源文件是16bit PCM——手机导出的wav基本都是——原始脉冲编码调制数值范围是-32768到32767,MATLAB会自动除以32768归一化。这一点对后面的频域幅值分析很重要,因为FFT之后纵轴数值能直接对应到原始波形的幅度,不用自己再除一遍。

fs就是采样率,44099也好、48000也好,后面画时间轴、频率轴都靠它。读完之后建议立刻size(data)看一眼,有时候返回的是N×2矩阵,说明是双声道交织存储,取data(:,1)当单声道用。

这里顺带提一个跟本文主题关系不大但很多人都会卡住的MATLAB版本问题:我身边不止一个人装了新版本,双击图标直接闪退,或者打开之后白屏不动。按照我自己的排查经验,大概率是显卡驱动太旧,MATLAB主窗口走OpenGL渲染,驱动跟不上就会闪退。先把显卡驱动更新到最新,然后再把MATLAB打开,问题基本能解决。个别情况是系统高DPI缩放惹的祸,右键MATLAB快捷方式,属性里把“高DPI缩放替代”改成“应用程序”就行。如果许可证报错,检查一下系统时间对不对、许可证服务有没有启动,然后用管理员身份运行MATLAB再试一次。这些都属于正版用户也会遇到的通用环境问题,跟版本新旧关系不大。

2. 时域分析:先把声音翻译成波形

2.1 时间轴的计算是第一步,也是很多人算错的第一步

读进来之后,先画时域波形:

t = (0:length(data)-1) / fs; plot(t, data); xlabel('时间/s'); ylabel('幅度');

核心就是这一行时间轴计算。为什么必须用length(data)/fs?因为data里面存的是采样点数,不是秒。采样率44.1kHz表示每秒44100个点,那么length(data)个点对应的时间长度就是length(data)/fs秒。举个例子,1.5秒的语音,采样率44.1kHz,算下来length(data)约等于66150个点。如果你直接拿采样点序号当横轴画图,波形形状虽然一样,但横轴的数字跟时间对不上,后面你想“从第0.3秒到第0.6秒这一段单独分析”,就完全没法定位——你还得反过去用0.3*fs算采样点位置,多一道换算就多一分出错的可能。

至于纵轴,我已经说过,手机wav文件读进MATLAB后范围是±1左右。如果画出来波形幅度只有±0.05,说明录音音量特别小,后面频谱分析会很难看。这时候先别急着归一化,回到第一步重新录,比什么处理都强。

2.2 从波形里能看出的发音结构

画出来之后别急着做FFT,先盯着波形看一会儿。真人的语音波形长什么样?“Hello MATLAB”这句话,在时域上能大致分成几段。开头的/h/是清辅音,声带不振动,波形幅度很小、看起来很杂乱,没什么规律;紧跟着的元音部分能量是最大的,波形呈现明显的周期性起伏,那是声带振动产生的准周期信号,波形幅度大、看起来是一坨一坨的;后面“MATLAB”里/m/是鼻音,波形包络比较平缓,低频分量重,表现为大幅度的缓慢起伏;句尾/b/是爆破音,会在波形上留下一个突然的尖峰。

这些特征光看波形就能判断个七八分。我建议配合耳朵验证:用sound(data, fs)回放整段,再用鼠标选中某一段区间,比如sound(data(5000:15000), fs)只播这一段,边听边对照波形。眼睛和耳朵对上了,后面分析才有底气。这一步特别适合新手,因为你会意识到,语音不是“看不见摸不着”的东西,它就是一条有形状的曲线,而且这个形状跟声音内容有直观对应关系。

如果想进一步看能量随时间的变化,可以写一个滑动RMS(均方根)包络:

win = 512; % 窗长度,约11.6ms for k = 1:win:length(data)-win seg = data(k:k+win-1); rms_env(k:k+win-1) = rms(seg); end hold on; plot(t, rms_env, 'r');

RMS包络线能清晰展示语音的“响亮段”和“安静段”,分清浊音的位置,也可以用来找语音的起止点。这一步的实用价值是:后面做语谱图或者训练语音模型时,定位音素边界全靠这种能量曲线。

3. 频域分析:FFT画频谱前必须想清楚的三件事

3.1 为什么直接plot(abs(fft(data)))是不对的

很多人第一次画频谱,都是这样一行代码:

plot(abs(fft(data)));

图能出来,但纵轴数值完全没有物理意义,横轴也不是“赫兹”。这是我在实际使用中见过最多的问题,必须拆开说清楚。FFT的结果是N个复数,每个数值的大小跟信号幅值和N都有关系。如果不做归一化,一个幅度为A的正弦分量,在频域里对应两个高度约为A*N/2的峰——这个数字随着N增大而增大,N是采样点数。所以直接画abs(fft(data)),纵轴会大得离谱,不同长度的信号之间也没法比较。

正确的打开方式是这样:

N = length(data); X = fft(data); P = abs(X) / N; % 归一化幅值 % 单边频谱 if mod(N, 2) == 0 P_single = P(1:N/2+1); P_single(2:end-1) = 2 * P_single(2:end-1); % DC和Nyquist不乘2 else P_single = P(1:(N+1)/2); P_single(2:end) = 2 * P_single(2:end); end f = fs * (0:(length(P_single)-1)) / N; plot(f, P_single);

为什么先除以N?因为fft是N个点的求和,不除N,幅值就带着N这个因子,归一到单位幅值之后,频域峰的高度才等于时域对应正弦分量的实际幅度。为什么单边谱要乘2?因为MATLAB的fft输出是双边谱,正负频率对称,实数信号的能量一半在负频,我们只画正频一边,幅值要翻倍才不丢能量。0频(直流)和奈奎斯特频率只有一个点,不乘2。

这段逻辑是频谱分析的骨架,建议手写一遍而不是直接抄pwelch,写一遍就懂为什么书上说“频谱幅值要×2/N”了。

3.2 频率分辨率、窗函数和功率谱密度估计

画完频谱你会发现,波形看起来好脏,全是毛刺。原因有两个:第一,语音是非平稳信号,整段做FFT相当于把不同音节平均混在一起,而“Hello MATLAB”里面/f/、/h/、/æ/各种音的频谱特性差异很大,混在一锅里当然看不出名堂;第二,整段FFT相当于给信号加了一个矩形窗,首尾不连续的地方会泄漏出大量高频能量。

这里就要提频率分辨率的概念。频率分辨率Δf = fs/N,以1.5秒语音为例,N≈66150,Δf≈0.67Hz,理论上已经很精细。但精细不等于清晰,因为泄漏和信号本身的时变性把细节打碎了。解决毛刺问题的标准做法是分帧加窗,用pwelch做功率谱密度估计:

[p, f] = pwelch(data, hann(1024), 512, 1024, fs); plot(f, 10*log10(p)); xlabel('频率/Hz'); ylabel('功率谱密度/dB');

hann(1024)是1024点的汉宁窗,512是相邻窗口的重叠点数,重叠50%是为了不丢信息,1024是FFT点数,fs是采样率。画成dB是因为线性坐标下频谱动态范围太大了,主峰很高、细节全被压到看不见,用对数坐标才能同时看清强峰和噪底。

为什么用汉宁窗?矩形窗频域旁瓣高,泄漏严重;汉宁窗旁瓣衰减快,主瓣虽然宽一点,但换来的是一张“干净”很多的光谱。这是标准的工程取舍,做完对比你就懂了。pwelch的核心思想是“分段加窗平均”——把长信号切段、分别做FFT、再把幅度平均,随机波动被压平,稳定的频率成分就浮现出来了。真实语音用整段FFT看不出来频率结构,用平均功率谱就能看到低频能量主体和高频衰落趋势。

4. 时频联合:语谱图才是语音信号的完全体

4.1 STFT的分帧思想与spectrogram参数选择

现在要进入重头戏了。单纯时域波形看不到频率成分随时间变化,单纯整段频谱又分不清哪个频率出现在哪个时刻。语音是典型的非平稳信号,特性和句子内容强相关,必须要“时间-频率”联合一起看,这就是短时傅里叶变换(STFT,Short-Time Fourier Transform)干的活。

STFT的核心思想其实特别朴素:把长信号切成很多小段,每段大概是20到40毫秒,对每一小段做FFT,得到一个频谱向量;然后把所有段的频谱按时间顺序竖着排列成一张图,横轴是时间,纵轴是频率,颜色代表能量强度。因为语音在一个很短的时间窗内可以近似看成平稳的,每一小段的FFT都有意义。

MATLAB里直接调库函数,一行出图:

spectrogram(data, hann(512), 256, 512, fs, 'yaxis');

我在实际使用时对参数的理解是:hann(512)是窗长512点,44.1kHz采样率下大约是11.6ms,属于偏短的时间窗;256是重叠长度,相邻帧更新步长约5.8ms;512是FFT点数;最后的'yaxis'让频率轴竖直向上,符合看图的直觉。窗长短,时间分辨率高,能分辨出快速变化的音节边界,但频率分辨率差,谐波线条会糊在一起;窗长加长到2048点(约46.4ms),频率分辨率好了,谐波看清了,但相邻音节在时间维度上被“摊开”了,边界模糊。这就是时频分析里的“不确定性原理”——跟量子力学没关系,但在信号处理里同样致命:鱼与熊掌不可兼得。你要用多长的时间窗,取决于你想看的是句子结构还是谐波细节。

4.2 “Hello MATLAB”在语谱图里长什么样

我录的这段“Hello MATLAB”大约1.5秒,语谱图出来非常有意思。开头“Hel”部分的元音段,低频区域有一排清晰的水平条纹,那是声带振动的基频及其谐波,基频大约在100到200Hz之间,往上是等间距排列的谐波分量——这是浊音的典型特征。元音的共振峰在语谱图上表现为几条特别亮的水平能量带,不同元音共振峰位置不同,这也是语音识别早期算法看共振峰来区分元音的原理。

到“MATLAB”这一段,开头的/m/是鼻音,低频能量非常集中,在语谱图上就是一大块低沉的亮斑;“T”这个清音爆破,在高频位置出现一小片杂乱的亮噪点;最后的/b/爆破音则表现为一条垂直方向的亮线。你把这几个特征跟时域波形对照一下,会发现完全对得上。这一步是我觉得整个实验里最有价值的部分——声音不再只是耳朵里的一串波动,而是变成眼睛能看到的形状。

如果你不想只依赖内置函数,想彻底搞懂STFT内部逻辑,可以手动实现一版:

frame_len = 512; hop = 256; win = hann(frame_len); nfft = 512; spec = []; for k = 1:hop:(length(data)-frame_len) seg = data(k:k+frame_len-1) .* win; spec(:, end+1) = abs(fft(seg, nfft)); %#ok<SAGROW> end freq = (0:nfft/2-1) * fs / nfft; time = (0:size(spec,2)-1) * hop / fs; imagesc(time, freq, spec(1:nfft/2, :)); set(gca, 'YDir', 'normal'); xlabel('时间/s'); ylabel('频率/Hz');

拿这段代码去跟spectrogram的结果对比,形状基本一致。我建议这版循环代码至少亲手写一次——写完你才能真正明白,那些一行调用的库函数,背后不过是一段循环加窗加FFT而已,没有任何魔法。

5. 现场排查:真实语音分析的问题速查与经验

5.1 六个高频问题的排查表

以下这些问题,是我在实际操作中反复遇到、也看到周围人反复踩的,整理成一张速查表,建议收藏:

问题现象常见原因处理办法
波形整体幅度极小录音音量低、距离远重新录音,而不是急着归一化
频谱纵轴数值巨大FFT结果没有除以N先abs(X)/N,画单边谱时乘2
频谱毛刺多、峰看不清未加窗或未分段平均改用pwelch,汉宁窗,50%重叠
语谱图糊成一团窗长选择不合适短窗看细节,长窗看频带,按需求调
读入后数据是N×2矩阵双声道wav取data(:,1),或转换时强制单声道
m4a/M4A文件读不进来压缩格式MATLAB不支持ffmpeg转wav,注意设置采样率

其中第一个问题我想多提醒一句:千万不要一看到波形幅度小,就随手data / max(abs(data))做整体归一化。归一化确实能把波形“拉大”,但也会把背景噪声同步放大。我见过有人录了一段语音,音量小到像在说悄悄话,归一化之后波形变漂亮了,结果一做频谱,低频底噪比语音本身都快大,后面所有分析全部失真。正确的优先级永远是:录音质量 > 后处理补救。

5.2 几个录音和分析的小习惯

经过这次实验,我总结下来几个实操习惯,打算以后一直沿用了。一是录音时手机离嘴十厘米左右,太近容易喷麦——波形上会多出明显的低频冲击尖峰,太远信噪比就会变差;二是录音环境尽量避开空调、风扇这类持续低频噪声源,哪怕人耳听着不明显,频谱上低频能量会莫名其妙高出一大截,还容易跟基频混淆;三是如果需要分析某个音,别看整段,用鼠标在波形图上框出那个音对应的时间范围,单独把这一段拿去分析,效果会好很多。

另外,我强烈建议每次做完一版分析,就把图、参数和代码备注保存下来。特别是语谱图这种结果,不同人录制、不同语速、不同环境,差别非常大。保存好原始参数(采样率、窗长、重叠率),以后调参或者写报告要回查的时候,能省掉大量重复劳动。这次我在宿舍录的这段“Hello MATLAB”,原始音频文件、三张图、脚本我都归档了,过段时间再回头看看,应该还能发现新的细节。

这套流程我后来又跑了一遍,换了句方言短句,语谱图格局完全不同——方言里那些特殊的声调曲线在语谱图上会表现为基频线条的连续弯曲,特别直观。说实话,学信号处理最忌讳只做教材里的仿真信号,真实数据带来的意外感,远比书上的理想模型更能让人长记性。下次你可以试着录一句自己说的话,跑一遍这个流程,你会在图里看到自己都没想到的东西。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询