☰
MATLAB语音分析实战:从手机录音到时频双杀全流程解析
2026/9/30 4:02:59 网站建设 项目流程

把手机录音直接扔进MATLAB做语音分析,这事儿我干过一次之后就上瘾了。在宿舍对着手机说了句“Hello MATLAB”,导出wav文件,然后在MATLAB里把原始信号的时域波形和频域频谱同时安排上——这就是标题里说的“时频双杀”。看着屏幕上波形在时间轴上起伏、频谱图上能量峰一根一根立起来,那种“声音真的能被拆开看”的感觉,比看任何教材都来得直观。

这篇文章就把这套流程完整拆开讲一遍:从手机录音的格式坑、wav文件的读取方式,到时域波形、FFT频谱、以及最后能动态呈现声音变化的时频图(spectrogram),每一步都有代码、有参数说明、有踩坑记录。想用MATLAB分析语音、搞信号处理入门、或者做课程作业的朋友,这套流程可以直接照抄。

1. 项目思路拆解:为什么要做“时频双杀”

先说清楚这个项目到底在做什么。手机录音本质是一个随时间变化的电压信号,麦克风把空气振动变成电信号,再用模数转换器(ADC)按固定节奏采样,得到一串数字。把这串数字画出来,就是时域波形。而任何一个周期性的声音,都可以分解成不同频率正弦波的叠加——这就是频域分析要做的事。

1.1 “时频双杀”的本质:两个维度互补

时域波形能告诉你“声音在哪个时刻响、响了多久、幅度多大”,但它看不出来这段声音由哪些频率成分组成。比如一段低沉男声和一段明亮女声,时域波形可能都是锯齿状的振幅起伏,光靠波形很难区分。

频域频谱则相反,它能清晰展示信号包含哪些频率、各自能量多大,但代价是丢失了时间信息——整段信号被当作一个整体去分析,你不知道某个频率是在开头出现还是到结尾才出现。

“双杀”的意思就是把这两个维度都拿下来:先看波形,建立对信号的整体印象;再看频谱,搞清楚频率成分;最后用时频图把两者缝合起来,看到“频率随时间的变化”。对于分析“Hello MATLAB”这种语音信号,这个组合恰好覆盖了所有需要的信息。

1.2 为什么选“Hello MATLAB”作为语音素材

这句语音其实是精心挑选的,不是随手乱录。

“Hello MATLAB”一共是三个词,每个词的发音特点差异很大:

  • “Hello”的元音部分(/e/、/o/)是浊音,声带振动产生周期性脉冲,频谱上会出现明显的谐波峰,时频图上能看到横向的亮条纹。
  • “MATLAB”里有爆破音(/M/、/B/)、弹舌音(/T/)和元音组合,辅音部分是清音,靠气流摩擦产生,频谱是宽频噪声,时频图上会呈现竖直方向的宽带能量分布。
  • 整个短语进行了两次重音变化:“Hello”重音在第二音节,“MATLAB”重音在后半个词。这会让时域波形呈现出清晰的能量包络起伏。

所以仅仅分析这一句语音,就能同时演示浊音、清音、重音、静音段等几乎所有语音信号的核心特征。对入门者来说,这是性价比极高的素材。

2. 数据准备:手机录音怎么变成MATLAB能读的文件

这一步看似简单,但坑最多。标题说的是“手机导出的wav文件”,实际上很多手机默认录音格式不是wav。

2.1 文件格式与采样率的坑

iPhone的语音备忘录默认导出是m4a格式(AAC压缩),安卓各家也不一样,常见的是m4a、amr、opus。wav文件是微软定义的未压缩音频容器,里面通常直接装着PCM采样数据,所以MATLAB读取后能直接拿到原始数字信号。而m4a这类压缩格式,需要解码器才能还原成PCM。

好消息是,较新版本的MATLAB(R2016b及以后)的audioread函数原生支持读取m4a、mp3等格式,不需要手动转码。但如果你拿到的文件后缀是wav,也要确认它内部到底是不是PCM——因为有些工具会把压缩音频硬封装成wav壳子,一样能读,但采样点已经经过有损压缩,多少和原始信号有偏差。

我个人经验是:做严肃的信号分析,尽量拿到wav或flac这种无损格式;如果是m4a录的,先用格式工厂或ffmpeg转成wav,命令很简单:

ffmpeg -i input.m4a -ar 44100 -ac 1 -sample_fmt s16 output.wav

这条命令干了三件事:把采样率统一成44.1kHz,声道合并成单声道,采样位深设为16位。后面两个参数对MATLAB分析特别友好,单声道数据方便处理,16位是wav最常见的位深。

2.2 MATLAB读取wav的标准姿势

读取文件用的是audioread,它取代了老版本的wavread。两者的区别在于:wavread只能读wav格式,audioread能读几乎所有MATLAB支持的音频格式,而且返回的采样率是真实的文件采样率,不用自己猜。

% 读取音频文件 [y, fs] = audioread('hello_matlab.wav'); % y: 采样点数据,单声道是N×1向量,双声道是N×2矩阵 % fs: 采样率,单位Hz,比如44100、48000、16000 % 查看基本信息 disp(['采样率: ', num2str(fs), ' Hz']); disp(['采样点数量: ', num2str(length(y))]);

这里有个细节需要留意:audioread返回的采样值y是归一化后的浮点数,范围在-1到1之间。不管原始wav内部是16位还是24位整数存储,读进来都会被归一化。这意味着后续画图、计算幅值时,要注意y的数值不是原始ADC的整数,而是比例值——但这不影响分析,时域波形形态和频域相对幅值关系不会改变。

2.3 录音时的硬件与场景注意

宿舍环境其实不算理想录音环境,但也正因为如此,这个项目里有不少“真实信号”值得聊。

  • 麦克风距离嘴大约20-30厘米比较合适,太近会削波(声音过大导致波形顶部被削平),太远环境噪声占比变大。
  • 手机一般会自动增益控制(AGC),音量会自动调整,导致信号的绝对幅度不具备参考价值。这一点对时域分析影响不大,但如果后续要做语音响度对比或训练模型,就要手动禁用AGC或用专业设备。
  • 采样率是手机决定的,但好在MATLAB读取时会给出真实的fs值。要注意有些安卓机型会把采样率固定在48kHz,而部分录音软件可能记录为44.1kHz,代码里公开不要写死44100,直接使用fs变量即可。

读取完成后,这个项目正式开始。

3. 先看时域:原始波形的第一印象

把y画出来是最直观的一步。毕竟波形能看到整段语音的“形状”——从静音段到第一个音节爆发,再到重音处能量峰值,最后归于安静。

3.1 画波形图与时间轴生成

画图前先构造时间轴:假设采样率为fs,总采样点数为N,则第k个采样点对应的时刻为(k-1)/fs。

% 生成时间轴 N = length(y); t = (0:N-1) / fs; % 绘制时域波形 figure; plot(t, y, 'b'); xlabel('时间 (s)'); ylabel('幅度'); title('Hello MATLAB 语音波形'); grid on; xlim([0, t(end)]);

如果你有一整段安静的宿舍底噪,波形会呈现出中间高两头低、带锯齿毛刺的特征。整段语音大约1.5到3秒不等,取决于说话速度。观察波形时可以按这样的思路逐步深入。

3.2 从波形里能读出什么

首先是静音段与语音段的分界。波形中有一段几乎平直的基线,那是录音刚开始时环境噪声和手机底噪的混合。随后第一个振幅突然增大的点,就是“Hello”的第一个音节出现的位置。

然后是能量包络。把波形按每20毫秒分帧,计算每一帧的RMS(均方根)值,能画出一条平滑的能量曲线。这条曲线可以清晰显示重音位置:“Hello”和“MATLAB”各对应一个能量峰,而“MATLAB”中的“LAB”部分如果有更大的幅度,说明说这句话时重音落在了它身上。

最后是削波检测。如果录音时离麦克风太近,波形顶部的值会反复触到±1的极限值,表现为顶部平坦化。这时信号已经失真,后续频谱分析会出现大量非真实的谐波。

% 计算分帧RMS能量 frameLen = round(0.02 * fs); % 20ms帧长 hop = round(0.01 * fs); % 10ms帧移 numFrames = floor((N - frameLen) / hop) + 1; rmsEnerg = zeros(numFrames, 1); frameTimes = zeros(numFrames, 1); for idx = 1:numFrames frame = y((idx-1)*hop + 1 : (idx-1)*hop + frameLen); rmsEnerg(idx) = sqrt(mean(frame.^2)); frameTimes(idx) = ((idx-1)*hop + frameLen/2) / fs; end figure; plot(t, y, 'Color', [0.6 0.6 0.6]); hold on; plot(frameTimes, rmsEnerg, 'r', 'LineWidth', 1.5); xlabel('时间 (s)'); ylabel('幅度 / RMS能量'); legend({'原始波形', 'RMS能量包络'}); title('语音波形与能量包络');

3.3 时域分析的实操心得

我把这段分析做成常规操作后,最大的体会到是:不要只盯着一张整体波形图看,多做局部放大。

比如“Hello”的第一个音节起始处,波形从噪底瞬间抬升,这个“onset时刻”精确到采样点级别,是做语音切分的依据。放大到单音节后,还能看到元音部分波形有明显的周期性——这正是声带振动的表现,周期对应基频的音高。

还有一点,如果波形看起来左右不对称、整体向上偏移而不是围绕零轴波动,说明信号里存在直流分量。这在手机录音里很常见,原因是模数转换的偏置电压没有完全归零。直流分量会让FFT频谱在0Hz处出现一个巨大的能量峰,必须处理掉。

% 去除直流分量 y_ac = y - mean(y);

4. 频域分析:把一句话拆成频率成分

时域图看完,硬菜的第二道就是频谱。对一段离散信号做FFT,得到的是从0Hz到采样率fs之间的频率分布,但受奈奎斯特采样定理限制,有效频率范围只到fs/2。

4.1 FFT计算的完整流程与代码

MATLAB里做FFT极其简单,但要做对,有几个细节必须处理好。

% 去除直流分量后再做FFT y_ac = y - mean(y); Ns = length(y_ac); % FFT点数做2的幂次扩展,便于计算 NFFT = 2^nextpow2(Ns); Y = fft(y_ac, NFFT); % 单边频谱:只取前一半,幅度乘以2修正 halfN = NFFT/2 + 1; f = (0:halfN-1) * fs / NFFT; mag = abs(Y(1:halfN)); mag(2:end-1) = 2 * mag(2:end-1); % 除DC和Nyquist外,其余点翻倍 % 频谱幅度转换为dB,方便观察小信号成分 mag_dB = 20 * log10(mag + eps); figure; plot(f, mag_dB); xlabel('频率 (Hz)'); ylabel('幅度 (dB)'); title('Hello MATLAB 单边频谱'); grid on; xlim([0 fs/2]);

这段代码里有几个关键点需要展开讲。

第一,为什么去直流。如果不减均值,0Hz处会有巨大的能量峰,整个频谱都会被相对压低,看起来像一条贴着零轴的线。手机录音几乎都有直流偏置,所以这一步基本是必须的。

第二,为什么用nextpow2扩展FFT点数。FFT的计算速度在N为2的幂次时最优,但扩展点数不会提高真实频率分辨率——分辨率由原始信号长度Ns决定,公式是fs/Ns。扩展查询到的效果只是把频谱插值变密,看起来更平滑,并没有增加真正的细节。这个点很多新手搞混。

第三,为什么单边频谱要翻倍。对实信号做FFT后,能量对称分布,负频率部分是正频率部分的镜像。只看0到fs/2半段时,要把正频率的幅度乘以2,才能保持能量守恒。只有当频率为0(DC)或fs/2(奈奎斯特频率)时不需要翻倍,因为这两处没有镜像对应的分量。

4.2 从频谱里读取语音特征

画出来的频谱包含的信息密度相当高。最显眼的是低频部分,通常在80Hz到400Hz之间会出现一排间隔均匀的峰——这是声带振动产生的谐波序列,谐波间隔就是基频F0。男生基频典型范围是85-180Hz,女生是165-255Hz,从这个峰的间隔可以大致判断录音者的性别和语调高低。

在中高频段,1kHz到4kHz范围会出现一个能量较高的隆起,这是声道共鸣形成的共振峰(formant),是决定元音音色的关键。不同元音对应不同共振峰位置,是语音识别的重要特征。比如“Hello”中的“e”音共振峰偏高频,“o”音共振峰偏低频,频谱上就能看出差异。

高频段通常能量较低,呈现缓慢下降的趋势,这说明语音信号能量主要集中在3kHz以下。如果频谱在高频出现异常突出的宽带峰,那多半不是语音本身,而是宿舍里空调、风扇等设备的恒定噪声。

4.3 频率分辨率和窗函数的选择

频谱分析里最容易被忽略的是分辨率问题。两条相邻频率的峰能被区分开的最低条件,是它们的间距大于频率分辨率Δf = fs/Ns。如果信号只有0.3秒,fs=44100Hz,则Δf≈147Hz。这意味着基频如果是120Hz和150Hz的两个声音,在这个分析长度下根本分不开。

要提升分辨率,有两个路径:一是延长录音时间(让Ns变大)。二是对较短的信号段做处理时,换用频谱泄漏更小的窗函数。

FFT默认是对整段信号直接切开,这等于隐含使用了矩形窗。矩形窗的主瓣虽然窄,但旁瓣高,会带来严重的频谱泄漏——一个强的频率分量会在附近产生大量假的旁瓣峰,掩盖真实的小信号。解决办法是给信号乘一个窗函数,比如汉明窗(hann/hamming),让信号两端平滑过渡到零,旁瓣大幅衰减,但也付出主瓣变宽的代价。

% 加汉宁窗再FFT,观察泄漏抑制效果 w = hann(Ns); y_windowed = y_ac .* w; Yw = fft(y_windowed, NFFT); mag_w = abs(Yw(1:halfN)); mag_w = mag_w / sum(w) * 2; % 幅度修正 plot(f, mag_dB); hold on; plot(f, 20*log10(mag_w + eps), 'r'); legend({'矩形窗', '汉宁窗'});

多次对比后,我的经验是:对于语音这种谐波丰富的复杂信号,汉宁窗是最稳妥的默认选择。它对频谱泄漏的抑制足够好,主瓣展宽的影响对语音这种宽频信号不构成问题。

5. 时频联合分析:用spectrogram看声音的“动态频谱”

时域和频域分析做完,其实还缺一块拼图——你看到波形知道每个音节的响度变化,看到频谱知道整句话包含哪些频率,但两者对不上号:某个频率成分到底在哪个时刻出现的?这就需要时频图,也就是MATLAB里最常见的spectrogram函数,基于短时傅里叶变换(STFT)。

5.1 STFT原理与spectrogram用法

STFT的思路很朴素:把长信号切成一小段一小段,对每一段分别做FFT,然后把每一帧的频谱按时间顺序排列,就得到一张“频率-时间-幅度”的二维热力图。

% 直接使用MATLAB内置spectrogram figure; spectrogram(y_ac, hann(512), 256, 1024, fs, 'yaxis'); title('Hello MATLAB 时频图 (spectrogram)'); colormap jet;

这里四个参数值得逐一解释:

  • 窗长。选择了512个采样点,在fs=44100Hz下对应约11.6毫秒。窗长决定了频率分辨率和时间分辨率的双输平衡:窗越长,频率分辨越高(峰越瘦),但时间分辨越粗(看不到瞬态变化);窗越短,时间定位越准,但频率峰越胖,两个靠近的频率会糊成一团。
  • 重叠点数。256表示相邻窗之间有256个点的重叠(即每次前进256点)。重叠的目的不是增加信息量,而是为了让频谱随时间变化看起来更平滑、更像“连续电影”,否则帧与帧之间跳变太大。重叠比例一般取窗长的50%到75%比较常用。
  • FFT点数。1024大于窗长512,对每帧做了补零插值。效果是让每一帧的频谱曲线更细腻,但不会提高真实频率分辨率。
  • y轴方向。设置'yaxis'让频率轴纵置、时间轴横置,这是语音分析的标准显示方式,读图习惯和看谱纸一样。

5.2 从时频图里读懂“Hello MATLAB”

时频图是语音信号分析里信息量最大的一张图,用好了甚至能直接“看”出这句话说了什么。

读图时先从横向找水平条纹。在几百毫秒的元音段内,你会看到多条明亮、平行的水平线——它们就是基频及其各次谐波。水平线之间的垂直间距对应基频大小,间距大意味着音高较高。这条水平线在发“Hello”的“o”音时有轻微的上下起伏,对应音调的升降,这是语调变化在时频图上的直接反映。

再把眼光放到竖直方向。辅音段,比如“MATLAB”开头的“M”音和末尾的“B”音,会呈现为竖直的宽带状能量柱,覆盖从低频到中高频的整个范围,颜色看起来比邻近区域更亮但能量不如元音的谐波线集中。这就是清音段的白噪声特征。

如果读图时看到某些竖直条纹高频区域突然出现一道亮线,那是录音环境里的瞬间哼鸣或爆音。我的录音里就出现过两次,一次是宿舍门开关的声音,一次是我自己换气的气流声——这些非语音信号在时频图上很容易定位和识别。

5.3 窗长选择的实战参数调节

spectrogram这个函数最磨人的就是窗长选择。到底选多长合适,没有唯一答案,取决于你想突出什么。

如果主要关心元音的谐波结构——比如要看基频变化轨迹,那选较长的窗,比如1024或2048,能获得更瘦的谐波线条,看起来清晰得多。

如果更关心辅音的起始时刻——比如想精确标注每个音节的边界,那就选短窗,比如256或128,让时间分辨率更高,瞬态变化不至于被抻平模糊。

一个实操办法是对同一段信号分别用短窗(256)和中窗(1024)各画一张图,并排对比。在语音分析里,不少实际项目就是这样干的,用两张不同分辨率的时频图相互补盲区。

% 对比不同窗长的影响 subplot(2,1,1); spectrogram(y_ac, hann(256), 128, 1024, fs, 'yaxis'); title('窗长256点'); subplot(2,1,2); spectrogram(y_ac, hann(1024), 512, 2048, fs, 'yaxis'); title('窗长1024点');

对比之后你很快会发现,信息并不完全是“越多越好”——短窗图噪声颗粒感更强,长窗图更干净,但某个辅音的起始时刻在短窗图上看得更清晰。这种取舍不是参数调教层面的技术问题,而是分析目标与观察尺度的匹配问题。

5.4 用Colormap增强可读性的经验

默认的spectrogram通常使用jet配色,但以dB为单位看,jet有一个天然缺陷:低能量背景区域颜色很亮,容易让观看者关注噪声而忽略语音主体。我习惯手动设置动态范围,只显示语音能量集中区间。

常见做法是先用频谱数据求出最大值,然后把显示下限设为最大值以下80dB的位置——这样把极低的噪声掩蔽掉,语音主体更突出。也能选择更友好的配色,比如parula或者简单灰度,灰度图最接近学术期刊的风格,且打印时不会失真。

% 手动增强时频图可读性 [S, F, T] = spectrogram(y_ac, hann(512), 256, 1024, fs); S_dB = 20*log10(abs(S) + eps); maxdB = max(S_dB(:)); figure; imagesc(T, F, S_dB); axis xy; colormap parula; caxis([maxdB-60, maxdB-10]); % 动态范围收窄,凸显主体 xlabel('时间 (s)'); ylabel('频率 (Hz)'); title('Hello MATLAB 时频图(增强版)'); colorbar;

这段代码在imagesc里直接画的思路,其实就是把spectrogram的显示层拆开来了,好处是动态范围可随意调节。如果你要导出论文级图片,建议都采用这种手动绘图方式,而不是直接截屏spectrogram的图形窗口。

6. 常见问题与排查实录

做语音分析时遇见的报错和异常图,比想象中多得多。这里整理几个我实际踩过的坑,以及对应的排查思路。

现象可能原因排查与解决
audioread读取失败文件路径含中文或特殊字符把wav文件复制到MATLAB当前工作路径,文件名改英文小写无空格
波形整体偏移,频谱0Hz处有巨峰信号含直流分量做y_ac = y - mean(y)处理
频谱杂乱像“刺猬”,没有清晰峰结构信号本身失真(削波)检查波形是否触顶,重新录音距麦克风远20-30cm
频谱高频段全是噪声手机AGC增益过大接受现状,分析时降低动态范围,聚焦低频语音段
spectrogram图像模糊一片窗长过长或重叠太少尝试缩短窗函数,提升重叠比例
频谱峰位置不在预期频率采样率理解错误检查fs变量,可能录音时采样率不是44.1k而是48k
波形长度和录音时间对不上采样率单位写错时刻轴用t = (0:N-1)/fs,而不是直接除以固定值

6.1 频谱泄漏问题详解

FFT频谱中出现的“拖尾峰”——真实能量集中在某个频率,但两侧有一片衰减振荡的假峰——是频谱泄漏的典型表现。问题根源是:FFT隐含假设信号是周期的,而实际信号在截断边界处不连续,产生高频分量。

判断方法很简单:找到频谱里的主峰,看它底部是否干净。如果峰底两侧有规则的波纹状衰减、有几个间隔均匀的小峰,很可能就是泄漏副瓣。解决途径有两个方向,一是加窗(推荐),二是尽量让信号整周期截断(对随机语音信号几乎不可能做到)。

加了汉宁窗之后,多数情况下泄漏副瓣会降低40dB以上,视觉效果和数值分析都会清爽不少。

6.2 关于语音信号分析起始时刻标注的常见问题

很多人在分工切帧后问我,怎么自动找到“Hello”开始的确切时刻。我的实现是利用RMS包络加一个简单的阈值判断:先计算整段信号RMS的均值和中位数,把阈值设为中位数加若干倍标准差,然后找第一个超过阈值的帧,再往前回溯到波形刚越过噪声带的位置,作为语音起始点。这个方法虽然朴素,但对干净的宿舍环境录音,精度能达到毫秒级,够用。

% 简单VAD起始点判断思路 rms_mean = mean(rmsEnerg); rms_std = std(rmsEnerg); threshold = rms_mean + 2 * rms_std; onset_idx = find(rmsEnerg > threshold, 1, 'first'); onset_time = frameTimes(onset_idx); disp(['语音起始时刻: ', num2str(onset_time), ' s']);

如果录音里底噪比较大,或者宿舍里有持续的背景音乐,这个简单方法就会失效。对这种情况,更稳健的做法是先把信号做带通滤波(比如保300-3400Hz这个电话语音频段),再做VAD判断。

6.3 双声道文件带来的问题

部分安卓机型录音导出的wav是双声道的,y会是一个N×2的矩阵。如果在画图和FFT时没注意维度,会出现数组维度不匹配的报错。

处理方式:如果双声道确属同一路信号,直接取第一列或者把两列平均成单声道;如果两列差异明显(比如某些双麦降噪手机会记录参考噪声通道),就要搞清楚哪一路是有用信号再取舍。

% 双声道处理 if size(y, 2) == 2 y = mean(y, 2); % 按行取平均,合为单声道 end

经验上,做语音内容展示时我更推荐取平均,因为可以压制一定的非对称噪声。但如果是分析立体声录音的双声道差异,那数据结构和处理流程完全是另一套逻辑了。

一段自己实操之后的额外体会

做完这一整套“时频双杀”之后,我对“看懂声音”这件事有了完全不同的理解。以前听一段语音,只能主观说“这句话前面响后面轻、音调有点高”,现在再看,声音里几十毫秒的瞬态、细微的基频漂移、某段辅音恰好落在共振峰上的峰值增强,全都变成图上肉眼可辨的细节。这种感觉一旦有了就很难退回原来的状态。

建议你把这段录音的wav文件留好,不要画完频谱就删掉。后续不少方向都能在这个基础上继续玩下去:比如把“Hello”和“MATLAB”两段分别切出来做语音识别练习;用滤波器组把频谱拆开模拟声道共振峰;甚至把FFT结果重新合成回声音,体验一次频域到信号的逆向转换。这些我接下来打算一个个试,试完再继续写下来。

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

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

立即咨询