ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Matlab信号分帧实现详解:从原理到代码实战

Matlab信号分帧实现详解:从原理到代码实战 简介本资源是一份面向计算机、电子信息工程及数学等专业本科生的Matlab信号处理实践工具包聚焦语音/时序信号分帧分割这一基础但关键的预处理技术适用于课程设计、期末大作业与毕业设计等实践环节。压缩包共5个文件78KB含2个核心Matlab脚本framing.m实现参数化分帧逻辑example.m提供完整调用示例、1个说明文档txt、1幅分帧效果示意图png及1段实测语音样本wav覆盖从理论理解、代码调试到结果可视化的完整学习链路。已有62人下载学习代码采用清晰的参数化编程结构帧长、帧移、窗函数等关键参数均独立定义并配有中文注释便于初学者快速修改适配不同信号特性也支持进阶用户拓展特征提取或机器学习前处理流程。 最近有朋友问我信号分帧的Matlab代码怎么写得清楚、效率又高正好这段时间我在折腾语音特征提取和振动信号分析天天都在和分帧打交道。索性把这块内容完整整理出来从原理讲到代码再讲到我实际踩过的坑一篇说透。先说清楚这篇文章是干嘛的不管你是做语音识别、声纹识别还是分析振动信号、肌电信号只要涉及到“把一段长时间序列切成很多小段再逐段做分析”就需要分帧。本文会讲清楚帧长、帧移怎么选窗函数加不加的区别以及三种主流Matlab实现方式的优缺点最后给一个可以直接拿来改的完整示例。1. 信号分帧到底在解决什么问题1.1 为什么不能直接分析整段信号很多刚开始接触数字信号处理的同学会有一个疑问我拿到一段10秒的语音或者一段5秒的振动波形为什么不能直接对整个信号做FFT、提特征非要切成一小段一小段来分析关键在于一个基本假设信号在短时间尺度上具有平稳性。拿语音来说你发出“你好”这个词声带振动的频率、口腔形状、气流速度都在持续变化。整个“你好”信号从头到尾都是非平稳的。但如果你只截取其中20毫秒比如一个元音的稳定段这个区间内的声带振动频率基本不变声道形状也变化不大可以近似看成平稳信号。只有在这种“短时平稳”的前提下FFT、自相关分析、线性预测这些经典工具才有意义。另一个原因是分辨率折中。如果你把整段10秒语音拿来直接做FFT确实能获得很高的频率分辨率频率间隔 1/10 0.1 Hz但频谱全混在一起你根本看不出来哪个时刻说了什么。分帧之后每一帧代表一个短时间段你可以在时间轴上一帧一帧滑动观察频谱随时间的变化这就是语谱图的基本思想。更直白地说分帧让我们能在“时间分辨率”和“频率分辨率”之间自由调节。1.2 分帧和加窗是谁提出来的这套方法本质上来自语音信号处理和短时傅里叶变换STFT的经典框架。语音信号处理领域有个教科书级流程预加重 → 分帧 → 加窗 → 逐帧提取特征LPC、MFCC、PLP等。这个流程从上世纪六七十年代就定了型至今仍是绝大多数语音系统的底座。实际场景中不止语音震动信号分析里做包络谱分析前要分帧脑电/肌电做脑机接口时要分帧电力系统检测暂态扰动也得分帧。所以分帧不是某个特定领域的偏门操作而是所有时序信号处理的基本功。2. 分帧的三个关键参数帧长、帧移、窗函数2.1 帧长怎么定帧长就是每一帧包含多少个采样点通常用时间长度乘以采样率得到。比如采样率Fs 16000 Hz帧长取25ms那么frame_len round(0.025 * 16000) 400这一帧就是400个采样点。帧长选多长取决于你的信号特性和你要提取的特征类型。语音处理里常用20ms到30ms因为这个区间内语音的短时平稳性成立而且足够容纳几个基音周期成年男性基频约100Hz周期10ms25ms帧内就有2.5个周期。振动信号看你要分析的频率范围如果关心的是低频段帧可以适当长一些如果关心的是瞬态冲击帧就要短一些才能捕捉到突变。但帧太短也有问题频率分辨率会变差FFT点数少谱线稀疏低频处的细节就糊了。2.2 帧移和重叠率帧移是相邻两帧起始点之间的距离也叫hop size或step size。前面说了相邻帧通常会重叠这是为了避免信息丢失。还是用语音举例Fs16000帧长25ms400点帧移通常取10ms160点那么两帧之间重叠的采样点数是400 - 160 240重叠率60%。为什么非要重叠因为如果你把信号无重叠地切成独立片段帧与帧之间的边界处特征会突变后面做端点检测、VAD时很容易把一句话切成好几块。重叠之后帧与帧之间平滑过渡特征序列的连续性就好很多。重叠率一般取50%到75%之间。50%是最常见的默认选择既保证了连续性又不会产生过多的冗余计算。2.3 帧数的计算公式假设信号总采样点数为N帧长为frame_len帧移为hop那么总帧数按照下面公式计算num_frames floor((N - frame_len) / hop) 1注意这里有个细节用floor还是ceil取决于你如何处理结尾部分不足一帧的数据。floor意味着最后不足一帧的数据直接丢弃如果想保留这部分信息可以用ceil然后把最后一帧补零到frame_len。实际工程中谁也不会愿意白白丢掉末尾数据所以补零更常用。我做实时系统时特别喜欢把这个公式背下来因为帧数算不对后面索引全部会越界或者少一段数据。2.4 窗函数的作用与选择分帧之后紧接着是加窗。为什么要加窗因为截断本身就是对原始信号加了一个矩形窗而矩形窗的频谱旁瓣很高会导致严重的频谱泄漏——一个纯粹的50Hz正弦波切成有限长度后做FFT频谱上会出现很多额外的频率分量明明只有一处谱峰结果周围拖了一长条尾巴。加窗的目的就是压低旁瓣。常用的窗有汉明窗Hamming、汉宁窗Hann、布莱克曼窗Blackman。语音特征提取里汉明窗用得最多它的旁瓣衰减比矩形窗好很多同时主瓣宽度还不至于太宽。汉宁窗在谱分析里也很流行尤其是对噪声背景下的信号它比汉明窗更平滑。布莱克曼窗旁瓣衰减更彻底但主瓣更宽频率分辨率会打折扣。Matlab里生成这些窗非常简单win hamming(frame_len); % 汉明窗 win hann(frame_len); % 汉宁窗 win blackman(frame_len); % 布莱克曼窗 win rectwin(frame_len); % 矩形窗相当于不加窗这个窗要和每一帧信号做逐点相乘也就是frame_data .* win。注意加窗之后信号的能量会发生改变如果你关心的是信号的绝对幅度可能要做能量归一化补偿如果只是提取特征比如MFCC一般都不用补偿因为后续会有归一化步骤。3. Matlab实现分帧的三种方式及对比3.1 方式一循环逐帧切分这是最直观的写法我刚学Matlab时就是这么写的。基本思想就是根据帧数循环每次从原信号里切一段出来。function frames frame_by_loop(x, frame_len, hop) N length(x); num_frames floor((N - frame_len) / hop) 1; frames zeros(num_frames, frame_len); for i 1:num_frames start_idx (i - 1) * hop 1; end_idx start_idx frame_len - 1; frames(i, :) x(start_idx:end_idx); end end每帧作为矩阵的一行存储后面做特征提取时直接按行遍历。这种写法优点是逻辑非常清楚出了问题打日志容易排查缺点是循环在Matlab里慢如果信号很长、帧数很多比如10小时的录音循环一次可能要跑几分钟性能完全不够。3.2 方式二向量化切分向量化的思路是把所有帧的起始索引预先算好然后一次性取出来组成矩阵。这在Matlab里速度快的多。function frames frame_by_vector(x, frame_len, hop) N length(x); num_frames floor((N - frame_len) / hop) 1; idx (0:num_frames-1) * hop (1:frame_len); frames x(idx); end这里用的技巧是索引矩阵idx是一个num_frames × frame_len的矩阵第i行第j列对应第i帧的第j个采样点在原信号中的位置。然后x(idx)一次就能取出所有帧。这个写法我实测过在百万级采样点上比循环快几十倍不止。缺点是对新手来说可读性差一些第一次看到x(idx)这种写法可能会懵需要多花几分钟理解索引矩阵。3.3 方式三直接用buffer函数如果你装了Signal Processing Toolbox最省事的方案是用buffer函数。这个函数专门用来做信号分帧功能比手写更全。% 参数说明frame_len为帧长hop为帧移 N length(x); num_frames floor((N - frame_len) / hop) 1; frames buffer(x, frame_len, frame_len - hop, nodelay); frames frames(:, 1:num_frames).;buffer第一个参数是信号第二个是每帧长度第三个是重叠的采样点数重叠量 frame_len - hop第四个参数nodelay表示不从第一帧开始延迟直接覆盖完整的第一段数据。输出矩阵默认把每帧放成列我习惯转置成行。buffer的优点是别人维护你的代码时一眼就能看出你在做分帧语义清晰。缺点是它默认会在末尾自动补零如果你没指定nodelay可能会在开头多出几个零所以实际使用时要仔细核对输出帧数和你自己算出来的帧数是否一致。我因为这儿没核对吃过亏结果后面提特征长度对不上排查了好久。3.4 三种方式对比实现方式代码复杂度运行速度可读性边界处理循环低慢高自主控制向量化中极快中自主控制buffer低快高自动补零需留意我自己的建议如果是写一次性脚本、只跑一次用循环无所谓如果是做批量处理、实时系统或数据集很大一定要向量化如果代码写完要给别人维护优先用buffer配合注释说清楚参数含义。4. 完整实战语音信号分帧 加窗 短时能量特征提取4.1 数据准备下面用一个具体例子带大家走一遍完整流程。我先生成一个模拟信号前3秒是1000Hz正弦波后2秒是50Hz正弦波模拟一个“信号成分突变”的场景。这样方便我们验证分帧后能否定位到突变时刻。clear; close all; clc; % 参数设置 Fs 16000; % 采样率 16kHz t_total 5; % 总时长 5 秒 t (0:1/Fs:t_total-1/Fs).; N length(t); % 合成信号前 3 秒 1000Hz后 2 秒 50Hz x zeros(N, 1); x(1:3*Fs) 0.8 * sin(2*pi*1000*t(1:3*Fs)); x(3*Fs1:end) 0.8 * sin(2*pi*50*t(3*Fs1:end)); % 加一点噪声更像真实信号 x x 0.05 * randn(N, 1);4.2 分帧加窗接下来设置帧长25ms、帧移10ms用向量化方式分帧并加上汉明窗。frame_len round(0.025 * Fs); % 25ms 400点 hop round(0.010 * Fs); % 10ms 160点 num_frames floor((N - frame_len) / hop) 1; % 向量化分帧 idx (0:num_frames-1). * hop (1:frame_len); frames x(idx); % 生成汉明窗并加窗 win hamming(frame_len).; frames_win frames .* win;这里frames每一行就是原始信号的25ms片段frames_win是加窗后的结果。加窗后帧边缘的幅度被压到接近零这是正常现象不要以为信号丢了。4.3 查看分帧效果我们取第250帧附近看看这个位置应该正好处于信号成分突变前后。% 看第 249 帧0-based对应的时间位置 frame_idx 249; % 实际第250帧 start_time (frame_idx - 1) * hop / Fs; end_time (frame_idx - 1) * hop / Fs frame_len / Fs; fprintf(该帧时间范围: %.3fs ~ %.3fs\n, start_time, end_time); % 绘制原始信号和加窗后该帧的对比 figure; subplot(2,1,1); plot(t, x); hold on; plot([start_time end_time], [1 1], r-, LineWidth, 3); xlabel(时间(s)); ylabel(幅值); title(原始信号与选中帧位置); grid on; subplot(2,1,2); plot((0:frame_len-1)/Fs*1000, frames(frame_idx, :)); hold on; plot((0:frame_len-1)/Fs*1000, frames_win(frame_idx, :)); legend(加窗前, 加窗后); xlabel(时间(ms)); ylabel(幅值); title(第250帧加窗前/后对比); grid on;运行后可以看到原信号在3秒前后频率明显不同在第250帧附近时间约2.49s-2.515s信号刚好跨越突变点这帧里就混合了1000Hz和50Hz两种成分。加窗后帧两端幅值被压缩这就是汉明窗的效果。4.4 短时能量计算分帧之后的典型应用就是计算短时能量。短时能量的定义是$$E_i \sum_{n1}^{frame_len} x_i^2(n)$$也就是第i帧所有采样点的平方和。这个指标在语音端点检测里用来区分静音和发声段。实现对加窗后的帧计算即可。% 计算短时能量 energy sum(frames_win.^2, 2); % 绘制短时能量曲线 frame_time ((0:num_frames-1) * hop frame_len/2) / Fs; % 每帧中心时间 figure; plot(frame_time, energy, LineWidth, 1.5); xlabel(时间(s)); ylabel(短时能量); title(短时能量曲线); grid on;因为三段式信号前3秒和高频后2秒低频的幅度相同能量理论上应该差不多。不过加窗之后这个曲线并不是特别平稳因为加窗引入了幅度调制尤其是高频段会更明显。如果只算每一帧的能量再画出来你会发现3秒处附近有个小的跳变或不稳定这就是信号成分切换的痕迹。实际语音场景中静音段的短时能量显著低于发声段通过阈值就能切开。4.5 STFT可选的快速工具如果你只是想看时频图而不想手动分帧Matlab的spectrogram函数直接封装了分帧加窗FFTfigure; spectrogram(x, hamming(frame_len), frame_len - hop, frame_len, Fs, yaxis); title(语谱图);这个函数内部改动的就是帧长、重叠点数、FFT点数、窗类型。很多朋友来问分帧代码其实用spectrogram就够但等你需要自己控制每一帧做后续处理时还是要手动分帧。5. 常见问题与排查经验5.1 帧数总比预期少一帧这个是最高频的bug。公式是floor((N - frame_len)/hop) 1不少人写成了floor(N/hop)或者floor((N - frame_len)/hop)都会导致帧数不对。我一般推荐用一个辅助函数打印每帧的起止索引核对最后一帧的真实边界% 调试用打印前5帧和后5帧的索引范围 for i [1:5, num_frames-4:num_frames] start_idx (i-1)*hop 1; end_idx start_idx frame_len - 1; fprintf(帧 %4d: %d ~ %d\n, i, start_idx, end_idx); end看最后几帧就知道是不是越界或者漏了。5.2 末尾不足一帧怎么办信号长度不一定是frame_len k*hop那种刚刚好的整数关系末尾总会剩一部分。处理方式有两种一是直接丢弃不足一帧的数据简单粗暴但会丢信息二是补零把最后一段不足的部分补到frame_len。补零的代码% 补零到能被整除 remain mod(N - frame_len, hop); if remain ~ 0 x_padded [x; zeros(hop - remain, 1)]; else x_padded x; end补零后重新计算帧数就能把所有数据都包含进来。补零对频谱的影响是会让主瓣稍微变宽一般来说特征提取场景可以接受。5.3 加窗后幅度变小是正常的吗正常。以汉明窗为例窗内数值范围约0.08到1.0加窗后每帧能量平均会下降。如果你需要恢复信号原始幅度对每帧除以窗函数的均值% 能量补偿 scale (frame_len / sum(win.^2))^0.5; frames_win frames .* win .* scale;这种补偿在语音特征提取里不必要如果你想从分帧加窗后的信号重建回原始时域波形做OLAoverlap-add那就要用额外的方法不是简单乘个系数能解决的需要注意窗口满足恒重叠相加COLA约束。这个话题展开讲比较深先留个坑。5.4 帧边缘出现明显伪影如果加窗后帧边缘还是有突发大值可能是窗函数没有正确作用。检查一下你的窗是否被正确转置我曾经把列向量窗和行向量帧矩阵直接点乘Matlab报错维度不一致一改维度发现窗向量方向不对计算结果就完全错了。另一个陷阱是不要把窗用在帧矩阵的维度上搞混每帧是一个行向量窗也应当是行向量二者点乘才是逐元素乘。如果是列向量窗结果会把窗作用在帧与帧之间那就完全错了。5.5 性能太差怎么办如果帧数几十万循环写法慢到怀疑人生。我实测过对100000帧、每帧400点循环要0.3秒向量化只要0.001秒差了300倍。向量化分帧时要小心索引矩阵太大导致内存溢出num_frames × frame_len很大例如100万帧×400点矩阵就有4亿个元素兆级别内存差不多1.6GB有可能溢出。这种情况下选择逐块向量化比如每次处理5万帧分批次切分。5.6 流式数据怎么分帧在线处理实时信号和离线不一样你不可能知道整段信号有多长。做法是维护一个缓冲区每次输入新数据时先把缓冲区尾部和新数据拼接然后按帧转移走完整的帧剩下的残留在缓冲区里。% 伪代码流式分帧 buf []; while ~isempty(new_data) buf [buf(:); new_data(:)]; while length(buf) frame_len frame buf(1:frame_len); buf buf(frame_len - hop 1:end); % 保留重叠部分 % 对 frame 做后续处理 end end注意缓冲区更新时保留的是frame_len - hop个点而不是丢掉全部否则重叠就丢了。这里我踩过坑写成buf buf(frame_len:end)重叠区域直接没了后续特征就出问题。6. 封装成自己的工具箱建议把分帧加窗封装成一个函数返回帧矩阵和时间轴方便复用function [frames, frames_win, frame_time] my_frame(x, Fs, frame_ms, hop_ms, win_type) % x: 单通道信号列向量 % Fs: 采样率 % frame_ms: 帧长毫秒 % hop_ms: 帧移毫秒 % win_type: hamming, hann, rect frame_len round(frame_ms * Fs / 1000); hop round(hop_ms * Fs / 1000); N length(x); % 补零 remain mod(N - frame_len, hop); if remain ~ 0 x [x; zeros(hop - remain, 1)]; N length(x); end num_frames floor((N - frame_len) / hop) 1; % 向量化分帧 idx (0:num_frames-1) * hop (1:frame_len); frames x(idx); % 窗函数 switch lower(win_type) case hamming win hamming(frame_len); case hann win hann(frame_len); case rect win ones(1, frame_len); otherwise win hamming(frame_len); end frames_win frames .* win; % 每帧中心时间 frame_time ((0:num_frames-1) * hop frame_len/2) / Fs; end这样以后你就有一个类似buffer的封装还自带时间轴。我一般放在自己的dsptools文件夹里所有项目都引用它省得每个工程重新写一遍。7. 写在最后的小经验信号分帧看似是件小事但它是后续所有分析的地基。地基没打牢后面无论做MFCC、语谱图、短时能量还是过零率全都会出问题。我自己最深刻的教训是千万不要想当然地认为“Matlab自动帮我处理好了边界”凡是用到buffer、spectrogram或者自己写索引都要亲自打印几帧验证一下起止时刻和原始信号对不对得上。还有个小习惯分享给你们设计实验验证分帧代码是否正确不要直接用真实信号先用一个你能准确预判结果的合成信号比如两个不同频率正弦拼接分帧后盯着突变点附近看几帧数据。确认无误后再上真实信号。这个方法帮我节省了大量排查时间。另外如果你们拿到的信号是多通道的比如脑电、肌电阵列分帧时注意要把通道维和帧维区分开。我通常的做法是逐通道分帧或者把分帧后的结果存成一个num_frames × frame_len × num_channels的三维数组这样后面按帧遍历时逻辑不会乱。分帧这个操作本身不难但它串联起了时域和频域分析的所有环节。能把分帧写得又快又稳后面的特征提取几乎不会出幺蛾子。希望这篇文章能帮你省下排查的时间把精力放在真正要解决的问题上。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进