简介:面向语音信号处理课程与数字信号处理初学者的短时时域分析实操包,以 MATLAB 源码和配套语音样本演示非平稳语音信号的分帧、加窗与短时参数提取过程。包内共 9 个文件,包括 8 个 .m 脚本和 1 个 .wav 测试音频;脚本分别实现短时幅度、短时能量、短时过零率、短时自相关、短时平均幅度差以及分帧、时间刻度映射等功能模块,结构清晰,便于按函数独立调用和二次开发。资源压缩后仅 28KB,体量小、针对性强,适合课程实验、课前预习或快速验证算法使用。目前已有 216 人学习下载。通过运行测试音频与脚本,学习者可直观对比窗长和帧移对短时特征曲线的影响,理解短时傅里叶分析在语音识别、端点检测和基音估计等应用中的基础作用,并可基于现有函数扩展自己的实验代码。
1. 短时平稳假设:为什么语音分析必须先“分帧”
语音信号最容易被忽视、又是最致命的特点是它不平稳。声带振动产生准周期脉冲、声道共鸣形成的谐振峰快速移动、辅音段气流噪声完全没有周期,这些因素叠加在一起,让语音在几十毫秒内发生剧烈变化。对整段语音直接计算功率谱或相关函数,得到的是统计平均结果,时间信息几乎完全丢失。解决这个问题的切入点就是短时时域分析。它的前提只有一个:把语音信号切分成 10~30ms 的小段,每一段内近似认为信号平稳。这个压缩包里 STEn.m、STAc.m、STAmdf.m 等文件,本质上都是对这个“短时平稳段”做不同侧面的测量。
这套资源适合两类人:一类是刚接触语音信号处理、需要把教材上的公式转成可运行代码的学习者;另一类是已经在做端点检测或基音提取、想快速对比不同短时参数效果的工程人员。attemptwi7这个标识说明它属于实验性版本,函数命名和调用方式都保留了教学代码的直白风格,容易改,也比较容易看出每个参数对结果的影响。接下来按分帧、加窗、参数提取、综合判断的顺序把这套代码拆开。
2. 分帧与加窗:enframe.m 和 FrameTimeC.m 的衔接
短时分析的第一步不是计算能量或过零率,而是把连续语音切成帧。需要注意,分帧并不是简单切割,而是让相邻帧之间有重叠,否则帧与帧之间的参数曲线会出现台阶状跳变。常见做法是帧长取 20~30ms,帧移取帧长的 1/2 或 1/3,这样既能保留时间分辨率,又能让短时参数曲线平滑。
2.1 enframe 的信号分段实现
MATLAB 信号处理工具箱里自带enframe,但这个资源包里的enframe.m是独立实现,不依赖额外工具箱。典型代码如下:
function frameMat = enframe(x, winLen, winShift) % x: 输入语音序列, 列向量 % winLen: 帧长(采样点) % winShift: 帧移(采样点) if size(x,1) == 1 x = x(:); end n = length(x); numFrames = floor((n - winLen) / winShift) + 1; frameMat = zeros(numFrames, winLen); for i = 1:numFrames startIdx = (i-1) * winShift + 1; frameMat(i, :) = x(startIdx : startIdx + winLen - 1); end end这段代码的关键逻辑是用行向量frameMat保存每一帧采样点。numFrames的计算方式保证了最后一帧的起始位置不超过信号总长度。winShift小于winLen时相邻帧存在重叠,重叠的目的是让滑动窗口对信号的加权更均匀,避免帧边界处参数突变。
实际使用时,采样率 8kHz 的语音,帧长取 160~240 点对应 20~30ms,帧移取 80 点对应 10ms。参数要按采样率换算,而不是按时间直接填。
2.2 时间刻度换算与坐标对应
分帧后得到一个二维矩阵,行是帧序号,列是帧内采样点序号。画图时如果直接把帧序号当横轴,时间语义不明确,且和原始语音波形无法对齐。FrameTimeC.m的作用就是完成这个换算:
function t = FrameTimeC(frameNum, winLen, winShift, fs) % frameNum: 帧序号(从1开始) % winLen: 帧长 % winShift: 帧移 % fs: 采样率 % 返回该帧中心位置对应的时间(秒) t = ((frameNum-1) * winShift + winLen/2) / fs; end这里取帧中心而不是帧起点作为时间刻度,是为了让短时参数曲线的取值点和语音波形上的事件对应。比如某个爆发音出现在第 1.2 秒,那能量和过零率曲线也应在该时刻附近出现尖峰,画图叠加对比时才不会有一帧的错位。做端点检测时这个细节直接影响到门限判决是否准确。
3. 短时能量与短时过零率:STEn.m 和 STZcr.m 的互补作用
分帧只是预处理。真正的语音参数提取从短时能量和短时过零率开始,这两个参数分别从“幅度大小”和“过零快慢”两个角度描述信号。清音段幅度小、过零率高,浊音段幅度大、过零率低,两者结合可以区分静音、清音和浊音。
3.1 短时能量定义与代码实现
短时能量的定义是每一帧内采样点平方求和:
function energy = STEn(frameMat) % frameMat: enframe 输出的帧矩阵, 每行一帧 numFrames = size(frameMat, 1); energy = zeros(numFrames, 1); for i = 1:numFrames energy(i) = sum(frameMat(i, :).^2); end end平方操作放大了大幅度采样点的贡献,因此短时能量对突发性语音事件很敏感。需要注意代码里没有除以帧长,也就是说这个能量值是累加值而不是平均值。帧长变化时能量绝对数值会随之变化,做阈值判决时要在同一帧长下比较。如果把sum换成mean,就得到短时平均能量,幅值范围和帧长解耦。
3.2 短时过零率实现与噪声陷阱
短时过零率统计每一帧内信号符号变化的次数。代码实现如下:
function zcr = STZcr(frameMat) % frameMat: 帧矩阵 % 返回每一帧的过零次数 numFrames = size(frameMat, 1); zcr = zeros(numFrames, 1); for i = 1:numFrames frame = frameMat(i, :); signChanges = diff(sign(frame)); zcr(i) = sum(signChanges ~= 0) / 2; end enddiff(sign(frame))计算相邻两个采样点符号的差值。符号从正变负得 -2,从负变正得 2,符号不变得 0,所以用~= 0计数后要除以 2,才能得到实际过零次数。zcr是真实次数,不是频率,如果要换算成过零率需要除以帧长再乘采样率。
这个指标对噪声极其敏感。背景噪声存在时,静音段也会出现大量过零,直接拿原始阈值做判决会把噪声误判为语音。常见处理手段是在计算前先做一次滑动平均降噪,或者为过零率设置一个最小幅度阈值,只统计幅度超过阈值的过零。调用方式如下:
zcr = STZcr(frameMat); vociedIdx = find(energy > 0.1 * max(energy) & zcr < median(zcr));3.3 双门限判决法如何串联两个参数
实际端点检测不会只用一个参数。经典做法是双门限法:用短时能量做一级判断,找到明显的语音段;再用过零率做二级判断,把清音段的边缘补回来。因为清音段的能量低、过零率高,单靠能量会把“s”“f”等音节的边缘切掉。
设置门限时不要用绝对值。语音信号幅度受麦克风增益影响很大,同一个人的同一句话在不同距离下能量差数倍。更稳妥的方法是以整段信号的最大能量为基准,取一个比例系数。比如高门限取0.3 * max(energy),低门限取0.1 * max(energy),过零率门限取整段平均过零率的 1.5 倍。这个比例可以参考,但不是固定值,要根据实际录音环境微调。
4. 短时自相关、平均幅度差与基音周期估计
能量和过零率能判断“有没有语音”,但要判断“音高是多少”就需要更精细的周期性分析。短时自相关函数(STAc)和短时平均幅度差函数(STAmdf)都是衡量帧内信号周期性的工具,只是计算方式和侧重点不同。资源包里同时给出这两个函数,方便对比各自在不同信噪比下的表现。
4.1 短时自相关的计算与周期峰值
短时自相关的定义是对每一帧做延时相关运算:
function ac = STAc(frameMat, maxLag) % frameMat: 帧矩阵 % maxLag: 最大延迟点数 numFrames = size(frameMat, 1); ac = zeros(numFrames, maxLag); for i = 1:numFrames frame = frameMat(i, :) - mean(frameMat(i, :)); for lag = 1:maxLag ac(i, lag) = sum(frame(1:end-lag) .* frame(lag+1:end)); end end end代码先减去了帧内均值,目的是消除直流分量。零延迟处的自相关值等于该帧的能量,是所有延迟中最大的;当延迟等于基音周期时,自相关会再次出现一个局部峰值。因此寻找第一个非零延迟的局部最大值,就能估计基音周期。
maxLag的选择直接决定可检测的最低基频。比如采样率 8kHz,要检测 100Hz 的基频,对应延迟 80 点;要检测 80Hz,就要至少 100 点。maxLag设得太小会漏检低音调,设得太大会把共振峰周期误判为基音周期。常见取法是让maxLag覆盖 50~500Hz 对应范围,即round(fs/50)到round(fs/500)之间的值要落在搜索范围内。
4.2 平均幅度差函数与抗噪优势
短时平均幅度差函数计算的是帧内信号和它延迟后信号的绝对差之和:
function amdf = STAmdf(frameMat, maxLag) % frameMat: 帧矩阵 % maxLag: 最大延迟点数 numFrames = size(frameMat, 1); amdf = zeros(numFrames, maxLag); for i = 1:numFrames frame = frameMat(i, :) - mean(frameMat(i, :)); for lag = 1:maxLag amdf(i, lag) = sum(abs(frame(1:end-lag) - frame(lag+1:end))); end end end和自相关不同,AMDF 是“越小越相关”。当延迟等于基音周期时,信号和自身延迟版本最相似,差值之和最小,形成谷值。寻找最小值的位置即可得到基音周期估计。
AMDF 比自相关对幅度突变更鲁棒,因为它用的是绝对值而不是平方值,不会放大噪声的影响。但它的缺点是谷底比自相关的峰值更平坦,尤其是在周期信号强弱交替时,容易出现“半频错误”或“倍频错误”,即估计结果是实际基频的一半或两倍。实际做基音提取时,我会同时计算STAc和STAmdf,让两个结果互相验证,一致时采纳,不一致时选自相关结果。
4.3 C3_2_y.m 中对浊音段的联合估计流程
主程序C3_2_y.m演示了如何用上述函数完成完整流程。核心思路是先分帧,再计算能量和过零率定位有效语音段,然后对有效段内的每一帧做自相关和 AMDF 分析,找到基音周期并换算成频率:
[x, fs] = audioread('C3_2_y.wav'); frameLen = round(0.025 * fs); % 25ms 帧长 frameShift = round(0.010 * fs); % 10ms 帧移 frameMat = enframe(x, frameLen, frameShift); energy = STEn(frameMat); zcr = STZcr(frameMat); maxLag = round(fs / 80); % 支持最低 80Hz ac = STAc(frameMat, maxLag); amdf = STAmdf(frameMat, maxLag); % 对每一帧找自相关峰值和 AMDF 谷值 for i = 1:size(frameMat, 1) if energy(i) > 0.1 * max(energy) [~, acLag] = max(ac(i, round(fs/500):end)); [~, amdfLag] = min(amdf(i, round(fs/500):end)); % 延迟换算为基频 f0Ac(i) = fs / (acLag + round(fs/500) - 1); f0Amdf(i) = fs / (amdfLag + round(fs/500) - 1); end end这里的核心技巧是搜索区间必须从round(fs/500)开始。500Hz 对应 16 点延迟,小于这个值的峰值通常不是基音周期,而是信号中的高频分量或共振峰干扰。如果从第 1 个延迟开始搜索,基频估计几乎必然出错。另外,audioread之后建议先做归一化x = x / max(abs(x)),这样能量阈值就可以用相对值判断,不受录音音量影响。
下面的表总结了两个估计方法的适用差异:
| 方法 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
| 短时自相关 (STAc) | 峰值尖锐,精度高 | 对噪声和幅度波动敏感 | 安静环境,录音质量较好 |
| 平均幅度差 (STAmdf) | 抗噪性好,计算快 | 谷底平坦,易出现倍频错误 | 有背景噪声,实时处理 |
5. 用 C3_2_y.m 快速验证帧长与窗型选择的影响
最后给出一个实用技巧。C3_2_y.wav是测试音频,C3_2_y.m是完整流程。拿到这套代码后,不必急着改算法,先用不同的帧长和窗型跑同一段语音,观察参数曲线变化,这是理解短时分析参数敏感性的最快路径。
具体做法是循环设置三组参数:帧长 20ms/帧移 10ms、帧长 30ms/帧移 15ms、帧长 10ms/帧移 5ms,分别计算STEn和STZcr,把结果画在同一张图上对比。运行下述代码:
figure; params = {0.020, 0.010; 0.030, 0.015; 0.010, 0.005}; for i = 1:3 fl = round(params{i,1} * fs); fs2 = round(params{i,2} * fs); fm = enframe(x, fl, fs2); e = STEn(fm); subplot(3,1,i); plot((0:length(e)-1) * params{i,2} + params{i,1}/2, e); end这个实验通常能直观看到:帧长越长,能量曲线越平滑,但时间分辨率越差,两个相邻音节之间的能量低谷可能被填平;帧长越短,曲线细节越丰富,但波动大,阈值判决不稳定。理解了这一点,就懂得为什么语音识别系统里帧长一般都定在 20~25ms,这个区间是时间分辨率和频率分辨率的折中点。
一个容易被忽略的坑是STZcr对高频噪声的放大效应。用同一帧长测试时,如果过零率曲线在静音段波动极大,先不要怀疑代码逻辑,用[b, a] = butter(4, 3000/(fs/2), 'low')做一个低通滤波后再算过零率,曲线会干净很多。这个操作不会明显影响浊音段的过零特征,但能大幅降低静音段的误判。整个资源包验证完毕后,把这套分帧加窗和参数提取流程封装成函数,就可以迁移到自己的语音预处理管线里,替换掉对工具箱函数的依赖。
本文还有配套的精品资源,点击获取