☰
Matlab信号分帧全解析:帧长、帧移与窗函数从原理到代码
2026/10/25 3:44:09 网站建设 项目流程

简介:本资源是一份面向计算机、电子信息工程及数学等专业本科生的Matlab信号处理实践材料,聚焦信号分帧(分割)这一语音与数字信号处理中的基础且关键操作,适用于课程设计、期末大作业及毕业设计等实践环节。压缩包共5个文件(78KB),含2个核心Matlab脚本(framing.m与example.m,实现参数化分帧逻辑与示例调用)、1个说明文档(txt)、1幅分帧效果示意图(png)及1段实测语音数据(wav),覆盖从理论理解、代码调试到结果可视化的完整学习链路。已有62人学习下载。代码采用清晰的参数化设计,帧长、帧移、窗函数等关键参数均独立可调,配合逐行中文注释,便于初学者快速掌握分帧原理与Matlab实现细节;同时支持Matlab 2014a至2024a多版本,开箱即用,无需额外配置。

1. 为什么好好的信号要切成一段段:分帧到底解决什么问题

我第一次接触信号分帧,是在做一个语音端点检测的小项目。当时想的特别简单:不就是把一段语音切成一小段一小段的吗?用数组切片不就行了?结果真动手才发现,事情远没有这么简单。信号分帧不是"切块",而是要在一段连续信号上"开一个固定长度的窗口,然后每次把窗口往后挪一段距离,再切一刀"。这个窗口滑动切片的操作,才是分帧的核心。

为什么非要多此一举地做滑动切片?因为绝大多数真实世界里的信号,比如语音、生物电信号、振动信号,都是非平稳的。你今天录一段"你好",和昨天录同一句话,波形看起来完全不一样;就算同一天录两遍,语速、音调、背景噪声也都有细微差别。但有一个重要性质:虽然整段信号不稳定,但在一个极短的时间范围内,信号可以近似看作是平稳的。这个"极短时间范围"在语音处理里通常是10到30毫秒。在这个尺度下,声道的形状、激励方式可以认为基本不变,信号的统计特性也是稳定的。

这个道理和看股票的K线图特别像。你要是看一只股票过去十年的走势,趋势千变万化,受各种宏观因素影响,根本没法用一套固定规律描述。但你取最近5分钟的走势,往往能看出相对稳定的震荡区间和量价关系。信号分帧就是干这件事:在长程不稳定中切出短程稳定片段,让后续的频谱分析、特征提取有可靠的数学前提。

所以分帧是几乎所有语音信号处理和很多振动信号分析的第一环。它直接决定了后面特征提取的质量。帧切得不对,后面不管用多么高级的算法,都是白搭。这也是为什么我要专门写一篇博客,把Matlab里的信号分帧从原理到代码、从参数选择到常见坑位讲透。

这篇内容适合谁?第一类是在做语音识别、说话人识别、语音情绪识别的人,分帧是你跑通整个pipeline的第一步。第二类是做机械振动分析、心电信号处理的人,同样需要把长信号切成短帧来提取特征。第三类是正在做Matlab课程设计、毕业设计涉及信号处理模块的学生。无论你是哪种情况,这篇博客都会给出可以直接复制的代码和参数参考值。

2. 分帧原理拆解:帧长、帧移、窗函数三件套

分帧这个操作,表面上看就是一个"切"字,但实际涉及三个紧密相关的参数:帧长、帧移、窗函数。这三个参数任何一个设置不合理,都会直接影响后续分析效果。

2.1 帧长(Frame Length):切出来的每一段要多长

帧长的单位通常是毫秒,但在代码里你要算成采样点数。比如采样率是16000Hz,那1秒里有16000个采样点,20毫秒就是320个点,25毫秒就是400个点,30毫秒就是480个点。换算公式是:

帧长(采样点数) = 采样率 × 帧长(秒)

语音处理里,20到30毫秒是最常用的帧长区间。为什么是这个区间?因为语音信号的"短时平稳性"在这个范围内最成立:太短了,帧内包含的样本太少,频谱分辨率不够,连基频都测不准;太长了,可能跨越了两个不同的音素,帧内信号就不再平稳了。举个实际例子:成年男性的基频大概在100到150Hz,周期是7到10毫秒;一个25毫秒的帧能包含2到3个完整的基频周期,足够估算基频了。

如果是非语音类的信号,帧长的选择要先看信号的频率分布。一般来说,帧长要至少包含信号最低关注频率的2到3个周期。比如心电信号关注的是0.5Hz到40Hz,帧长可以取到1到2秒;而机械振动信号如果关注的是1000Hz以上的高频成分,帧长取10到50毫秒就够了。

2.2 帧移(Hop Length / Frame Shift):窗口每步挪多远

帧移是相邻两帧起点之间的距离。为什么要让两帧之间重叠?因为如果每帧之间完全无缝衔接,会导致帧与帧在边界处出现突变,而且某些信号特征在帧边缘被截断后就很难恢复。通过重叠,可以让相邻帧之间平滑过渡,同时保证信号的信息不丢失。

帧移通常取帧长的三分之一到二分之一。比如帧长25毫秒,帧移10毫秒是语音识别里最经典的搭配,也就是相邻帧之间重叠15毫秒。帧移越小,帧数越多,时间分辨率越高,但计算量也越大;帧移太大,则可能漏掉信号的快速变化。做语谱图时,我通常取帧移5到10毫秒,这样时频图上的线条会更细腻;做特征提取时取10毫秒就够了,再小就是浪费算力。

2.3 窗函数:切片之外的另一道工序

理论上,分帧就是一个矩形窗的乘法——你切出第n帧,等效于把原始信号乘上一个只在帧内为1、帧外为0的矩形窗。但矩形窗有个致命问题:它在频域的主瓣和旁瓣特性很差,频谱泄漏严重。信号中的某一个频率成分会"泄漏"到其他频率上,尤其在做FFT时,这种泄漏会表现为频谱图上出现假峰。

解决办法是加窗。常用的是汉明窗(Hamming Window)和汉宁窗(Hann Window)。这两种窗的共同点是中间高、两端低,加窗后帧边缘的幅度被平滑地压到接近零,从而减少频谱泄漏。之所以汉明窗最常用,是因为它的旁瓣衰减比矩形窗好得多,而主瓣宽度又不像布莱克曼窗那样过宽,在频率分辨率和幅度精度之间取了不错的平衡。

在Matlab里加窗很简单:

w = hamming(frame_len, 'periodic'); % 注意用periodic而不是symmetric windowed_frame = signal_frame .* w;

这个操作看起来是"多此一举",但如果你做的是频谱分析或者任何需要FFT的任务,这步不做或者做错了,后果很直接:频谱图上会出现一些不应该存在的频率成分,你会误以为信号里含有这些频率,然后开始一段漫长的找Bug之旅。我见过不少新手在这个地方被坑过,所以提前说明白。

3. 可直接用的Matlab分帧函数:三种写法从入门到实用

接下来是核心部分:代码。我会给出三种不同复杂度的Matlab分帧实现,从最利于理解原理的循环写法,到最简洁的矩阵化写法,再到内置函数的快速调用。你可以根据自己的需求直接抄。

3.1 方法一:循环切片,最直观的原理呈现

这个方法适合第一次接触分帧、想彻底搞明白每一步在做什么的人:

function frames = frame_signal_loop(x, frame_len, hop_len) % x: 一维输入信号 % frame_len: 帧长(采样点数) % hop_len: 帧移(采样点数) len_x = length(x); % 计算总帧数 n_frames = floor((len_x - frame_len) / hop_len) + 1; % 预分配内存,避免循环里动态扩容 frames = zeros(frame_len, n_frames); for idx = 1:n_frames start_idx = (idx - 1) * hop_len + 1; end_idx = start_idx + frame_len - 1; frames(:, idx) = x(start_idx:end_idx); end end

这段代码的关键是标号对齐。第1帧从第1个点开始,取到第frame_len个点;第2帧从hop_len+1开始取;第k帧的起始位置是(k-1)hop_len+1。很多人容易在这里搞混,写成了khop_len+1,导致最后一帧越界或者起始位置错了一个点。

预分配frames矩阵这一步也值得强调。如果不预分配,Matlab会在每次循环时自动扩容,数据多时速度会慢好几倍。先zeros出来再填,是最基本的Matlab性能优化习惯。

3.2 方法二:buffer函数,最省事的官方内置方案

Matlab自带信号处理工具箱里有buffer函数,专门干分帧这件事,用法很简洁:

x = randn(1, 10000); % 模拟一段信号,长度10000 frame_len = 320; % 帧长320个采样点(20ms @ 16kHz) hop_len = 160; % 帧移160个采样点(10ms) % buffer的核心用法 frames = buffer(x, frame_len, frame_len - hop_len);

buffer函数的参数逻辑:第一个参数是信号,第二个是帧长,第三个是重叠长度。这里最关键的是第三个参数传的是"重叠的样本数",不是帧移。重叠样本数 = 帧长 - 帧移。所以frame_len=320、hop_len=160时,重叠就是160个样本。

buffer的输出也是一个矩阵,每一列是一帧。需要注意,如果信号长度不能刚好被分完,buffer会自动在最后一帧后面补零。这个行为有时候是好事,但有时候会给你埋雷——补零区域会被当成有效信号参与后续计算。我在后面会专门讲这个坑。

3.3 方法三:向量化索引,不加工具箱也能飞快的写法

有人嫌循环慢,又不想依赖Signal Processing Toolbox,那可以试试向量化索引:

function frames = frame_signal_vectorized(x, frame_len, hop_len) len_x = length(x); n_frames = floor((len_x - frame_len) / hop_len) + 1; % 生成起始索引向量:1, 1+hop_len, 1+2*hop_len, ... start_idx = (0:n_frames-1) * hop_len + 1; % 用隐式扩展生成每个样本的索引矩阵 idx = start_idx + (0:frame_len-1).'; % 直接用索引矩阵取值,这就是所有帧 frames = x(idx); end

这段代码的核心是用索引矩阵一次性取出所有帧。start_idx是每一帧的起点,加上(0:frame_len-1).'这个列向量后,利用Matlab R2016b之后的隐式扩展机制,自动生成一个frame_len×n_frames的索引矩阵。比如第3列的所有行,就是第3帧的所有采样点在原信号中的位置。

实测下来,这种方法比循环快3到5倍,尤其在信号很长、帧数很多的时候优势明显。而且它不需要额外工具箱,纯原生Matlab语法就能跑。

三种方法怎么选?我的建议是:初学用循环写法理解原理;在项目中追求简洁和可靠性,直接用buffer;如果对性能有极致要求又不想依赖工具箱,用向量化写法。

4. 帧长、帧移、窗函数怎么选:从经验值到具体测试

参数选择没有绝对标准,但有非常成熟的参考范围。下面这个表格来自我在语音处理、振动分析和生物信号处理里的实践经验:

应用场景采样率帧长帧移常用窗
语音识别/说话人识别16kHz25ms(400点)10ms(160点)汉明窗
语谱图绘制16kHz50ms(800点)5ms(80点)汉宁窗
语音基频提取16kHz30~50ms5~10ms汉宁窗
机械振动分析较高取决于关注频率帧长50%汉宁窗
心电信号分析250~1000Hz1~2s0.5s矩形窗或布莱克曼窗

4.1 采样率与帧长的桥接:先算点数再写代码

我把这一步单独拎出来说,是因为太多人栽在这里。在Matlab里做分帧时,所有参数都是以"采样点数"为单位,不是以毫秒为单位。你手上如果只有信号的采样率,怎么换算?

假设你的信号采样率是fs=44100Hz,你想做帧长50ms、帧移25ms的分帧,那代码里的参数应该是:

fs = 44100; frame_len = round(0.05 * fs); % 2205个采样点 hop_len = round(0.025 * fs); % 1102.5,取整为1103

注意帧移的计算结果可能是小数,需要四舍五入取整。这个取整操作会影响总帧数的计算,后面要小心核对。

4.2 如果帧长和帧移算出来没有整除关系

信号的总长度很少能恰好被分完。比如信号有10000个采样点,帧长320、帧移160,那最后一帧的起点是9601,终点是9920,刚好还有80个点落在最后一帧的窗口外。这部分剩余信号就直接丢掉了。

有些项目不能容忍丢掉尾部数据,比如要求处理的信号必须覆盖整个信号长度。那有两种处理方式:一是反向取帧,即最后一帧以信号末尾为终点反推起点;二是尾部补零,把信号补长到能恰好整除为止。

% 尾部补零的做法 len_x = length(x); remainder = mod(len_x - frame_len, hop_len); if remainder ~= 0 x_padded = [x, zeros(1, hop_len - remainder)]; else x_padded = x; end

补零的做法优点是简单,缺点是在尾部制造了非真实的数据点;如果后续要计算过零率或短时能量,这些补零区域会出现假性的"能量骤降"或"过零率异常"。所以我更推荐反向取帧的方法,或者干脆在分帧前就把尾部不足一帧的部分用条件判断跳过。

4.3 窗函数选择的进一步说明

前面提到了汉明窗最常用,但实际项目中窗函数的选择也要结合具体场景。

  • 汉宁窗(Hann):主瓣稍微宽一点,但旁瓣衰减快,频率分辨能力好。做语谱图时频谱更"干净",推荐。
  • 汉明窗(Hamming):主瓣窄,旁瓣衰减不如汉宁窗,但幅度精度好。语音特征提取里非常常用,尤其MFCC的计算,基本默认汉明窗。
  • 布莱克曼窗(Blackman):旁瓣衰减最大,但主瓣最宽。适合对幅度精度要求极高、不要求频率分辨率的场景。
  • 矩形窗(Rectangular):没加窗外加,频谱泄漏严重。只有当你确定要分析的内容正好落在窗函数主瓣内、且不太在意旁瓣时才用。

经验法则:做以频率估计为主的任务,优先汉宁窗;做以幅度提取为主的任务,优先汉明窗;做高精度幅度分析,可以试试布莱克曼窗。具体选择是否有明显影响,最好的办法是在同一段信号上分别加不同窗,看看FFT结果,你一眼就能看出来差异。

5. 分帧之后信号变短了:边界处理和输出尺寸校验

分帧的代码看起来短,但跑完你通常要花最多时间在"对不上数"这件事上。这一节我集中讲分帧之后最容易出现的边界问题和输出尺寸问题。

5.1 总帧数到底怎么算最稳

最稳妥的总帧数计算公式是:

n_frames = floor((len_x - frame_len) / hop_len) + 1;

这个公式的逻辑是:第一帧永远从第1个点开始,消耗frame_len个点;之后每多取一帧,信号起点往后挪hop_len个点,但最后一帧必须保证终点不超过len_x,所以最大可追加的帧数是floor((len_x - frame_len) / hop_len)。

如果你用的是buffer函数,buffer会自己算帧数,返回的矩阵列数就是帧数。但是要小心,buffer处理尾部数据时会在最后一帧自动补零,补零的列也在输出矩阵里。你要么接受这个补零帧,要么在算帧数时手动修正。最保险的办法是分帧前先自己用上面的公式算好帧数,再决定用buffer还是自己写循环。

5.2 检验输出维度的小技巧

分帧完成后,第一时间检查frames矩阵的维度:

assert(size(frames, 1) == frame_len, '帧长维度错误'); assert(size(frames, 2) == n_frames, '帧数维度错误');

这个assert如果没过,说明参数配置有误,或者信号长度计算有问题。别看这一步简单,它能在你进入后续复杂处理前就把隐患挡掉。

5.3 横轴到底是对应时间还是帧序号

分帧之后,很多人画图时发现横轴是帧序号,完全不知道每一帧对应什么时间。换算公式是:

  • 第k帧的起始时间:t_start(k) = (k-1) * hop_len / fs
  • 第k帧的中心时间:t_center(k) = ((k-1) * hop_len + frame_len/2) / fs

画语谱图或者逐帧特征曲线时,一定要把横轴换成时间轴,否则图上的横坐标会让人困惑:

frame_time = ((0:n_frames-1) * hop_len + frame_len/2) / fs; plot(frame_time, feature_curve);

5.4 混叠与首尾帧的坑

分帧的重叠等于引入了时间上的相关,相邻帧之间存在大量重复信息。这在特征提取时是好事(上下文信息保留),但如果你直接把每帧当成独立样本去做机器学习训练,要注意样本之间不独立,有潜在的数据泄漏风险。比如你要做训练集/测试集划分,不能简单按帧随机划分,而是应该按整个信号文件来划分,否则同一信号前后帧高度相似,会导致评估结果虚高。

首帧和尾帧有个特殊的坑:首帧前面没有上下文,所以常用做法是给首帧前面做对称延拓或补零;尾帧后面如果补了零,频谱和特征会和前面的真实帧有明显偏差。语音处理里的VAD(语音活动检测)经常会误判尾帧,就是因为补零导致了能量异常。

6. 分帧只做了一半:帧信号出来后的典型后续处理

分帧本身不是目的,它只是通往特征提取的中间站。这一节我展示三个最常见的分帧后续操作,让你理解分帧在整个流程中的位置。

6.1 计算短时能量

短时能量是最简单的语音时域特征,它衡量每一帧信号的总能量大小,常用于语音活动检测。分帧之后,计算每一帧的能量就是:

frame_energy = sum(frames.^2, 1); % 沿列方向求和

注意frames的每一列是一帧,所以沿第1维(行方向)求和,得到的是每一帧的总能量。如果不加窗,这个能量值等于信号的总功率乘以帧长,物理意义很直观;加窗后,由于窗函数把边缘压制了,能量值会偏小,但这并不影响帧间对比。

这个特征对噪声很敏感,实际项目中通常会在计算前先做带通滤波,只保留关注的频段,否则噪声的能量会把语音段的能量波动掩盖掉。

6.2 计算短时过零率

过零率统计的是每一帧内信号穿越零轴的次数,计算公式:

sign_changes = diff(sign(frames), 1, 1); % 沿每一列做差分 nonzero_count = sum(sign_changes ~= 0, 1); % 统计符号发生变化的次数 zcr = nonzero_count / frame_len; % 归一化过零率

过零率的意义在于:语音中的清音(如"s"、"t")过零率高,浊音(如"a"、"i")过零率低。结合短时能量,可以大致区分出有声段、无声段和噪声段。

我在实际项目中通常把短时能量和过零率同时画出曲线,判断语音端点,比单独看一条线靠谱得多。

6.3 拼接特征矩阵,通向机器学习

分帧的最终输出往往是特征矩阵:每一行是一个样本(帧),每一列是一个特征维度。比如MFCC特征就是先分帧加窗,然后对每一帧做FFT、Mel滤波器组、对数运算和DCT,最终输出每帧的MFCC系数。

num_mfcc = 13; mfcc_features = zeros(n_frames, num_mfcc); for idx = 1:n_frames frame = frames(:, idx); mfcc_features(idx, :) = compute_mfcc(frame, fs, num_mfcc); end

这就是标准的语音识别/说话人识别特征输入。整个pipeline里,分帧是成本最低但影响最大的一环:帧长决定频率分辨率,帧移决定时间分辨率,窗函数决定频谱质量。三者的组合直接决定了特征矩阵的信息含量。

7. 分帧常见的坑与效率优化:我踩过的都帮你趟平了

最后一节,我列举几个我在实际项目中反复踩过的坑和优化手段,这些在教科书里几乎不会写,但遇到时真的很浪费时间。

7.1 buffer函数的第三个参数传错

buffer第三个参数传的是重叠长度,不是帧移。我把这个单独拎出来说,是因为我也记错过,而且也帮别人改过好几次。frame_len=320、希望帧移160,那第三个参数应该传320-160=160。如果有人传了160以为这是帧移,那就正好是对的;但如果传了320,那相邻帧就完全不重叠了。frame_len=512、hop_len=256时,重叠=256;frame_len=400、hop_len=160时,重叠=240。一定要自己算清楚。

另外还有一个小区别:buffer的第二个参数传的是窗口长度,第三个传重叠,第四个参数可以传"nodelay"选项。默认情况下,buffer输出的第一列可能是零填充的初始状态,加"nodelay"可以去掉这个延迟列。

frames = buffer(x, frame_len, overlap, 'nodelay');

加了"nodelay"之后,第一列就是真正的第一帧,不用再手动删第一列,省得后面索引对不上。

7.2 frames矩阵的行列顺序搞反

frames矩阵的尺寸是frame_len×n_frames,列是帧,行是采样点索引。这个布局在Matlab里很自然,因为按列操作效率高。但有些从Python转过来的同学习惯了"行是样本",一过来就把时序特征计算写成了对行求和,结果得出的"每一帧能量"全是错的。如果你后面要传给机器学习模型,记得转置成n_frames×frame_len的格式,即一行一帧。

features_input = frames.'; % 转置为 n_frames × frame_len,适应常见的模型输入

7.3 计算复杂度优化:大信号分帧别用循环

分帧本身的数据搬运量不大,但后续特征提取的循环如果太慢,整体体验会很差。我有一个经验:遇到长信号(比如几十分钟的语音或者长时间的振动监测数据),不要直接用for循环逐帧处理,先考虑能不能用矩阵运算把特征计算向量化。

以短时能量为例,向量化写法:

frame_energy = sum(frames .* frames, 1); % 一次得到所有帧的能量

以FFT为例,Matlab的fft函数可以直接对矩阵按列运算:

spec = fft(frames, nfft, 1); % 每一列做FFT,得到 频率×帧 的复数矩阵

这样就把"循环n_frames次"变成了"一次矩阵运算",速度提升极其明显,尤其是几十万帧规模的时候能差出数量级。

7.4 加窗要乘在帧数据上,不是在原始信号上

有人图省事,先对整段信号加窗再分帧,这是错的。窗函数是对每一帧加的,不是对整段信号加的。如果是整段信号加一个汉明窗,那相当于给整段信号做了全局加权,和分帧后再逐帧加窗是完全不同的结果。逐帧加窗是为了降低"每一帧"在FFT时的频谱泄漏;全局加窗只会让信号开头和结尾的幅度被压低,完全达不到目的。

7.5 分帧在Matlab里的精度问题

Matlab的FFT是基于浮点运算的,分帧计算本身不会引入精度问题,但如果你反复在信号上做切片、补零、拼接,浮点误差会累积。我曾经在做一个信号拼接实验时,反序列化后重新分帧,发现第100帧和第101帧在重叠区域有约1e-12级别的幅度差异。这个差异本身不影响分析,但如果你的算法对精度极其敏感(比如数值求解某些微分方程),就要注意在分帧后立即保留原始帧,不要在后续过程中反复重建。

7.6 处理多通道信号时别忘记循环通道

很多实际信号是多通道的,比如心电的12导联、脑电的多电极、振动信号的三轴加速度。分帧时如果有多个通道,最简单的做法是用一个循环对每个通道分别分帧,或者用一个三维矩阵保存结果。

% 多通道信号 x: 通道数 × 采样点数 [num_ch, len_x] = size(x); for ch = 1:num_ch frames_all{ch} = buffer(x(ch, :), frame_len, overlap, 'nodelay'); end

用元胞数组保存各通道的分帧结果,方便后续按通道提取特征。注意不要直接用三维矩阵一次性分帧,因为buffer不支持对矩阵的每一行分别处理,会报错。

7.7 采样率不是整数时的时间轴换算

有些设备的实际采样率不是整数,比如44100Hz和48000Hz的设备在录音时可能会有微小的时钟偏移。如果你用采样率直接换算帧位置,可能会在长时间尺度的分析中出现明显的漂移。这种情况下,建议用时间戳来定位采样点位置,而不是简单地用"采样点数÷采样率"。这在处理长时间生物信号时尤其重要。

8. 一个完整的分帧示例:从导入信号到输出特征矩阵

讲到这里,我想用一段完整的Matlab脚本把整个流程串起来,从信号导入到分帧、加窗、提取特征,一步不落。你可以把这段代码当作项目模板直接改。

% 完整的信号分帧示例 % 场景:对一段语音信号做分帧,提取短时能量和过零率 %% 1. 读取信号 [x, fs] = audioread('speech.wav'); x = x(:, 1); % 如果是立体声只取一个通道 x = x / max(abs(x)); % 简单归一化,防止幅值过大 %% 2. 设置参数 frame_ms = 25; % 帧长25毫秒 hop_ms = 10; % 帧移10毫秒 frame_len = round(frame_ms / 1000 * fs); hop_len = round(hop_ms / 1000 * fs); w = hamming(frame_len, 'periodic'); % 加窗 %% 3. 分帧 n_frames = floor((length(x) - frame_len) / hop_len) + 1; frames = buffer(x, frame_len, frame_len - hop_len, 'nodelay'); frames = frames(:, 1:n_frames); % 去掉尾部补零帧 %% 4. 加窗 frames_win = frames .* w; % 每列乘上窗函数,隐式扩展 %% 5. 提取短时特征 frame_energy = sum(frames_win.^2, 1); % 短时能量 zcr = sum(diff(sign(frames), 1, 1) ~= 0, 1) / frame_len; % 过零率 %% 6. 绘制结果 t_axis = ((0:n_frames-1) * hop_len + frame_len/2) / fs; figure; subplot(3,1,1); plot((0:length(x)-1)/fs, x); title('原始信号'); xlabel('时间 / s'); subplot(3,1,2); plot(t_axis, frame_energy); title('短时能量'); xlabel('时间 / s'); subplot(3,1,3); plot(t_axis, zcr); title('过零率'); xlabel('时间 / s');

这段脚本里有两个细节值得注意。第一,buffer输出后用frames = frames(:, 1:n_frames)把自动补零的多余列裁掉,这是为了保险。第二,过零率的计算用的是原始frames而不是加窗后的frames_win,因为窗函数会改变信号的符号变化位置,导致过零率失真。过零率应该基于未加窗的帧信号计算。

9. 分帧的进阶话题:就算法层面聊几句后续扩展

分帧本身不复杂,但如果你要做更深入的工作,有几个方向可以进一步拓展。

9.1 自适应帧长:固定参数不适用于所有信号

固定帧长在大多数场景没问题,但有些信号变化剧烈,固定的时间窗口要么漏掉瞬态事件,要么在平稳段浪费了计算量。一种做法是自适应帧长:先检测信号的瞬态位置(比如语音的起始点、机械故障的冲击时刻),在瞬态附近用短帧、在平稳段用长帧。这个方法在语音信号和振动信号里都有应用,实现起来比较复杂,但效果比固定帧长好不少。

9.2 重叠相加(Overlapp-Add)与分帧的关系

分帧的反操作是重叠相加,在很多语音增强、时域修改算法里都会用到。思路是:每帧经过处理后,按帧移把各帧拼回去,重叠区域的数值按比例相加。Matlab里有overlapadd函数可以做这事。如果你在做语音增强、变调变速或时频掩码后重建信号,就会用到这个操作。分帧和重叠相加是一对镜像操作,理解一个就能理解另一个。

9.3 分帧与其他"分割"方式的区分

这里我想多说一句。最近各种"分割"概念很火:图像里的语义分割、实例分割,文本里的字符串分割,还有Matlab里的字符串处理函数如split、strsplit,它们和信号分帧是完全不同的概念。图像分割是把图像按语义区域划分,字符串分割是按分隔符切文本,而信号分帧是在时间轴上滑窗切帧。有时候搜索资料,关键词一混,很容易找到完全不相干的东西。在写代码之前,先明确你要处理的是哪一类数据,别把图像Segmentation的算法思路套到信号分帧上。

9.4 分帧在深度学习方法中的替代与保留

现在做语音处理,很多端到端的深度学习模型(比如用CNN或Transformer做语音识别)第一层往往直接吃原始波形,看起来不需要分帧。但其实模型内部常常内置了类似分帧的操作——比如把波形reshape成(帧长, 帧数)的格式,或者在数据增强时模拟帧移。分帧这个思想并没有消失,而是被内化到了数据预处理的一部分。

如果你在做深度学习语音任务,我的建议是:仍然先用本章介绍的方法把波形分帧、加窗,看得见摸得着的处理流程更可控,也方便中间结果的可视化和调试。等模型训练稳定了,再考虑是否让模型自己学习特征,把分帧环节交给网络。

10. 最后聊聊我个人的几个使用习惯

做了这么多年信号处理,分帧是我每天都要接触的操作,有几个习惯想分享给读者。

第一个习惯是做任何分帧任务之前先打印参数确认。不是装模作样,而是真的有用。我遇到太多次因为帧长、帧移单位不统一导致的结果对不上。比如某一次我拿到一批数据,文档里写采样率是1000Hz,但实际去看发现有的文件是1024Hz,我按1000Hz做的分帧,结果所有时间轴全部偏移。从那以后,我每次处理新数据源都会先检查实际采样率,而不是盲信文档。

第二个习惯是分帧后的结果我既保存frames矩阵,也保存原始信号的时间戳信息。听起来简单,但这样在画图、特征对齐、调试时非常方便。

第三个习惯是分帧前先做一次简单的去趋势或预加重,尤其是语音信号。预加重就是用一个一阶高通滤波器把高频分量抬起来,减少低频能量对后续处理的压制。在Matlab里:

preemph = 0.97; x = filter([1, -preemph], 1, x);

这一步通常是MFCC特征提取流程里的标准操作,放在分帧之前也不会影响分帧本身,但它会让频谱特征更均衡,后续提取的特征更有效。需要注意的是,如果你的任务是分析原始波形的绝对幅度,比如做语音增强或者检测冲击事件,预加重反而会引入偏差,这种情况下就别做。

最后一个建议是:不要迷信某一个参数组合就是最优解。25ms/10ms是语音识别的经典配置,但你的录音环境、说话人语速、麦克风特性都可能影响最优帧长。项目跑通之后,可以试着跑一个小的参数网格搜索,看看不同帧长、帧移对最终指标的影响有多大。很多时候你会发现,帧长在20到30ms之间变化对结果影响不大,真正影响大的反而是一些看起来不起眼的预处理步骤,比如滤波器的带宽、静音段的剔除、幅值归一化方式。

分帧这个操作,在信号处理里就像是建房子打地基。地基打得好不好,外观看不出来,但地面的每一层都会受影响。希望这篇博客能帮你把地基打扎实,省下后面大量的Debug时间。

本文还有配套的精品资源,点击获取

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

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

立即咨询