ARTICLE DETAIL

资讯详情

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

MATLAB语音分析实战:从手机录音到时频双杀全流程解析

MATLAB语音分析实战:从手机录音到时频双杀全流程解析 把手机录音直接扔进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。好消息是较新版本的MATLABR2016b及以后的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才能保持能量守恒。只有当频率为0DC或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秒fs44100Hz则Δ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个采样点在fs44100Hz下对应约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结果重新合成回声音体验一次频域到信号的逆向转换。这些我接下来打算一个个试试完再继续写下来。
返回列表