首页 / 资讯中心 / 文章详情

语音信号短时时域分析入门:分帧、加窗与三大特征详解

语音信号短时时域分析入门:分帧、加窗与三大特征详解 ★ FEATURED ARTICLE
最近在帮一个项目调语音交互的触发效果朋友给了一段现场录音需求看着很简单“你先帮我看看这段声音从哪儿开始算有效语音又从哪儿结束。”但真正动手以后我才发现这个问题远不是看一眼波形就能回答的。想比较靠谱地切出语音段把噪声、静音和说话内容分开最基础也最绕不开的方法就是先把信号按短时时域分析这一套走一遍。所谓语音信号短时时域分析简单说就是把一段几十毫秒到几百毫秒不等的连续语音按照固定长度切成一个个“短时帧”再在每一帧上计算能量、过零率、自相关函数这类时域特征。它解决的核心问题只有一个语音在宏观上是剧烈变化的非平稳信号没法用整段的统计特性去描述但只要把它切成10到30毫秒的小段每一段内部又能近似看成平稳信号。这套思路是语音唤醒、端点检测、基音估计、清浊音判别乃至很多音频特征工程的地基。适合正在入门语音算法的人也适合那些做音频内容分析但始终搞不清“窗口和帧移到底该怎么选”的工程师。所以这篇文章不讲虚的直接把这套流程从原理讲到参数再把代码给你写出来顺便把我在实际项目里踩过的几个坑一起列出来。1. 为什么语音信号必须“切成一段一段”来分析1.1 “非平稳”三个字决定了语音分析的底层逻辑你随便打开一段录音看波形会看到它有的大、有的小有的地方密、有的地方疏。这是因为语音信号的产生过程本身就在不断变化声带在振动口腔和鼻腔的形状在动嘴唇和舌头的位置在变有时还夹杂着停顿和摩擦噪声。所有这些因素叠加在一起导致语音信号的统计特性随时间剧烈变化专业说法就是“非平稳”。非平稳意味着什么意味着你不能拿整段信号去算一个均值、做一个傅里叶变换然后指望这个结果代表“这段语音的特征”。整段信号既包含元音又包含辅音既包含高能量的浊音段又包含低能量的静音段你把它们揉在一起得到的结果既不完整也没有实际参考价值。打个比方就像把一整部电影的几万帧画面叠成一张图最后看到的只是一团模糊的噪影既不是某个镜头也看不出剧情。所以语音处理的第一步思路就非常清晰了不能整体分析必须按帧切分。切出来的每一帧单独计算特征最后再把这些特征按时间顺序拼成一条曲线这条曲线才是我们真正要用的语音特征轨迹。1.2 短时平稳假设语音处理最重要的前提这里有一个关键问题既然语音是非平稳的凭什么切成小段就平稳了答案藏在语音产生的物理过程里。声音的快速变化来源是声带振动它的频率通常在80到500赫兹之间也就是说每秒钟振动几十到几百次。但发声器官的形态变化也就是舌头、嘴唇、下颌这些“慢动作”它们的移动速度远没有声带振动那么快。在10到30毫秒这么短的时间里声道形状基本可以看成是固定的声带振动的特性也相对稳定。这个假设在语音学里非常重要几乎所有经典语音特征都建立在它上面。因为只有承认了“短时内信号近似平稳”我们才能在一帧信号上用传统的能量统计、过零判断、自相关计算等工具得到有物理意义的特征值。比如一个“啊”音它持续可能100毫秒你把它切成五六个25毫秒的帧每一帧的短时能量和过零率都差不多这说明短时平稳假设在实践中是成立的。帧长取多少合理本质上也由这个假设决定。取得太长声道形态可能已经变了平稳假设不成立取得太短一帧里包含的振动周期太少很多特征算不准。常见的20到30毫秒就是在这两个约束之间折中的结果。2. 做好短时分析的三板斧预加重、分帧、加窗2.1 预加重给高频信号“找平”很多人一开始接触语音预处理时经常想不明白一个问题好好的波形为什么非要先过一遍高通滤波器原因在于语音信号在高频段衰减得很厉害。人的发声机制决定了语音的能量主要集中在低频和中低频高频部分天然就弱。如果不对它做处理后续分析中高频分量的存在感会被严重压制很多细节就丢了。预加重的做法很简单通常用一阶高通滤波器y[n] x[n] - a * x[n-1]其中 a 一般取 0.95 到 0.98 之间。它做的事情本质上就是把信号的高频分量抬起来让频谱在高低频范围内更平坦一些。这个处理并不改变语音的信息内容但它能显著改善后续特征提取的稳定性。我在实际项目中还发现预加重要放在分帧之前做而不是在每一帧上单独做。如果先分帧再预加重帧边界的跳变会在滤波后产生额外的高频噪声特征曲线会出现很多不必要的毛刺。正确顺序是原始信号进来先做预加重再转帧再逐帧加窗。2.2 分帧与帧移所有语音特征的“最小工作单元”分帧的操作看起来很简单就是从信号里按固定长度取一段。但有两个参数必须定好帧长和帧移。帧长就是每一帧包含多少个采样点换算成时间就是这一帧覆盖多长。比如采样率是8000赫兹帧长取25毫秒那一帧就是 8000 × 0.025 200 个采样点。帧移是相邻两帧起始位置之间的间隔通常取帧长的三分之一到二分之一。比如帧长200点帧移取80点对应时间就是10毫秒这样相邻帧之间会有明显重叠。帧移为什么不能等于帧长因为语音信号在帧边界处本来就不齐整如果两帧完全不重叠特征曲线会呈现锯齿状跳变很多连续变化的信息会被丢掉。这里可以算一笔简单的账一段8000点的一秒语音帧长200点、帧移80点时总帧数为n_frames 1 (8000 - 200) // 80 98也就是说一秒语音能产出约98个特征点这个密度已经足够捕捉绝大多数语音变化了。如果你想要更高时间分辨率的特征可以减少帧移比如改成40点特征点密度会变成每秒约198个但计算量也会同步上升。2.3 加窗的讲究矩形窗和汉明窗怎么选分帧本身是从信号中直接截取一段这个“直接截取”在数学上等价于把连续信号乘以一个矩形窗。矩形窗的最大问题在于它在帧边界上发生了从1到0的突变截断处会产生严重的不连续后续做频域分析时会把能量泄漏到旁瓣去。这就是经典的频谱泄漏问题。解决办法就是给每一帧乘上另一个窗函数让信号在边界处平滑过渡到零。语音处理里最常用的窗函数是汉明窗公式如下w[n] 0.54 - 0.46 * cos(2 * π * n / (N - 1)), 0 ≤ n ≤ N-1它跟矩形窗相比主瓣宽度会稍大一些高频分辨能力略有下降但旁瓣衰减会好得多能把频谱泄漏控制在可接受范围内。各种窗的基本特性可以看这张表窗型主瓣宽度旁瓣衰减量频率分辨率频谱泄漏抑制矩形窗最窄约-13dB最好最差汉宁窗较宽约-31dB中等较好汉明窗较宽约-41dB中等较好布莱克曼窗最宽约-57dB较差最好我个人的习惯是只要不是专门做频率分辨率很敏感的任务默认都用汉明窗。它最均衡既能把频谱泄漏压住又不至于像布莱克曼窗那样把主瓣拉得过宽。还有一点需要注意加窗之后每一帧两端的采样点会被乘到很小的值相当于被“弱化”了。这也是为什么我们需要让相邻帧有一定程度的重叠否则帧两端的信息就彻底浪费了。3. 三大经典时域特征从原理到应用能量、过零率、自相关3.1 短时能量语音端点检测的第一把尺子短时能量可能是所有语音特征里理解门槛最低的一个。对某一帧信号 x[n]它的能量就是这一帧所有采样点的平方和E Σ x[n]^2为什么要做平方因为采样点有正有负不平方直接累加正负会互相抵消什么都看不出来。平方之后大信号和小信号之间的差距被进一步拉开更有利于区分语音段和非语音段。在实际工程里我也经常用“短时平均幅度”代替短时能量即对每一帧所有采样点取绝对值再求和。差别在于平方会放大信号间差异对音量变化敏感绝对值的线性特性则温和一些在某些低信噪比场景下更稳。短时能量最经典的应用是语音端点检测。静音段的能量非常低一进入说话能量立刻抬升这个反差非常明显。你可以用短时能量曲线先画一版肉眼就能看出语音段大概在哪。我处理很多项目时都是先看能量曲线对数据有个整体判断再决定要不要做更精细的过零率分析。3.2 短时过零率清浊音判别的“过零计数器”短时过零率统计的是在一帧信号里波形穿越零轴的次数。计算方式是对相邻两个采样点判断符号是否变化如果从正变负或从负变正就计一次过零。公式可以写成这样ZCR 0.5 * Σ |sgn(x[n]) - sgn(x[n-1])|这里 sgn 是符号函数取值为1、0或-1。最后除以帧长或不做归一化都可以关键是看你要的是绝对次数还是单位时间内的频率。过零率的物理意义很直观信号越“高频”穿越零轴的次数就越多。浊音的基频较低波形也比较有规律过零率就低清音的频谱更分散高频成分多过零率明显高静音段虽然幅度很小但如果存在背景噪声它依然会频繁过零不过幅度往往不稳定。实际项目中过零率经常用来做两件事一是清浊音判别二是端点检测里的修补。比如一个语音段开始之前往往有很轻的清音摩擦声这时候短时能量还不够高单纯用能量判断可能会把这一小段漏掉。配合过零率就可以把这种“能量低但过零率高”的段识别出来拼回语音段的前后边界。3.3 短时自相关函数基音周期的“寻峰器”短时自相关函数的核心作用是在一帧信号内部寻找周期性。它的定义是R(k) Σ x[n] * x[nk]自相关做的事情可以理解为把信号和它自己错开 k 个点之后比较相似度。如果信号本身有周期性当 k 等于基音周期的整数倍时波形会又一次“对得很齐”R(k) 就会出现峰值。R(0) 永远是最大的因为它等于这一帧信号自身的能量其他峰都要比它小。实际操作中有两个细节必须处理。第一是归一化把R(k)除以R(0)这样结果不再受音量大小影响不同帧之间可以直接比较。第二是限定搜索范围。人声基频大约在60到500赫兹在8000赫兹采样率下对应的周期范围是8000/50016个采样点到8000/60≈133个采样点。所以你只需要在延迟 k 为16到133这个区间里找最大峰超出这个范围的值直接忽略否则很容易找到倍频或乱七八糟的噪声峰。我在项目里用自相关做基音检测时还加了一个约束只准在峰值位置两侧加一个平滑窗口后二次确认防止单个毛刺把峰值带偏。原因很简单自相关函数受帧内噪声影响很大一个帧的波形如果带了一点脉冲噪声峰值位置可能直接被误导。4. 参数选型避坑帧长、窗型、重叠率背后的硬逻辑4.1 不同任务对帧长的要求不一样先定任务再定参数很多新手喜欢上来就问“帧长到底多少合适”。这个问题的正确回答是取决于你的任务。如果只是做端点检测用短时能量区分语音和静音20毫秒帧长就够用。因为端点检测关心的是粗粒度变化不需要太高频率分辨率。如果你在做基音检测帧长就不能太短因为一帧里面至少要包含两到三个基音周期才能看到明显的自相关峰。男声最低基频约80赫兹一个周期就是12.5毫秒要容纳两个完整周期帧长至少25毫秒。这正好解释了为什么语音处理领域的默认帧长基本都是25毫秒左右。如果要提取梅尔频率倒谱系数这类频域特征帧长同样不能太短因为频域分辨率不够会导致谱失真。25到30毫秒是工程上的常用区间。时间分辨率和频率分辨率是一对矛盾帧长越短时间定位越准但频率分辨率越低帧长越长频率分得越清但帧内可能已经混入了多种不同语音状态。4.2 窗型与频谱泄漏为什么工程里默认用汉明窗有些读者可能会想时域分析不是不做傅里叶变换吗怎么也在意频谱泄漏其实这是很多人的误解。分帧加窗是所有后续特征计算的前置步骤不管你现在算的是能量还是过零率最后只要进一步做频域分析窗函数的影响就会暴露出来。我早期图省事直接用矩形窗切帧然后做频域分析结果谱图里出现很多奇怪的旁瓣。换用汉明窗之后那些假峰明显消失了。原因就是矩形窗的截断不连续造成了高频泄漏汉明窗通过让帧边界平滑归零把这种泄漏抑制住了。所以窗函数选择的核心逻辑是在频率分辨率和泄漏抑制之间找平衡。矩形窗主瓣最窄理论上频率分辨率最优但旁瓣衰减太差汉明窗旁瓣衰减不错主瓣也不算太宽是最常用的折中点布莱克曼窗旁瓣衰减最好适合给那些对动态范围要求极高的分析场景但主瓣宽度代价也不小。4.3 帧移多少合适特征平滑与计算量的权衡帧移这个参数经常被忽略但它直接影响特征曲线的平滑度和计算开销。帧移越小相邻帧时间重叠越多特征值变化越连续曲线越平滑。帧移等于帧长的一半重叠率是50%帧移等于帧长的四分之一重叠率是75%。常见配置8000赫兹采样率下帧长200点、帧移80点重叠率就是60%。这个设置在工程里非常常见既能让特征曲线足够平滑又不会让计算量翻倍。如果你想让特征点每秒更多可以把帧移降到40点一秒语音会产生约198个特征点。但你要想清楚是否真的需要这么高的时间分辨率因为计算自相关或频域特征时帧数翻倍意味着计算负载明显上升。调参时我的习惯是先用默认参数跑通全流程把特征曲线画出来看毛刺多不多再决定要不要减少帧移。不要一开始就追求极致密度那样调试成本会很高。5. 手写一个短时时域分析流程从分帧到特征可视化5.1 实验环境与数据准备这里我用 Python 和 numpy 手动实现一套完整的短时时域分析不依赖任何高级语音库目的是把底层逻辑讲清楚。需要的环境就是 Python 3 和 numpy再加一个 matplotlib 用于可视化。准备一段 8000 赫兹采样率的语音时长大约 1 秒。你可以用自己录的也可以用某个开源数据集里的样例。参数先按经典配置来帧长200点25毫秒帧移80点10毫秒预加重系数0.97窗函数用汉明窗。5.2 分帧与加窗的实现分帧函数我直接写一个循环版本逻辑清楚方便一步步调试。真正追求性能时再用滑动窗口视图优化也不迟。import numpy as np def enframe(x, win_len, hop_len): # 如果信号长度不足一帧先补零 if len(x) win_len: x np.pad(x, (0, win_len - len(x))) n_frames 1 (len(x) - win_len) // hop_len frames np.zeros((n_frames, win_len)) for i in range(n_frames): start i * hop_len end start win_len if end len(x): frame np.pad(x[start:], (0, end - len(x))) else: frame x[start:end] frames[i] frame return frames注意这个函数处理了信号末尾不足一帧的情况用补零补满。为什么补零而不直接丢弃因为最后这几个点的语音可能包含重要的结尾信息尤其是端点检测的终止边界往往就藏在那里。补零的代价是最后一帧能量会偏低所以在后续特征序列里最好对末尾帧单独标记或者把它删掉。加窗直接对每一帧乘汉明窗def apply_window(frames, win_typehamming): N frames.shape[1] if win_type hamming: w 0.54 - 0.46 * np.cos(2 * np.pi * np.arange(N) / (N - 1)) elif win_type rect: w np.ones(N) return frames * w5.3 短时能量和过零率的计算短时能量的实现非常直接就是每帧平方求和def short_time_energy(frames): return np.sum(frames ** 2, axis1)过零率的实现同样简单def zero_crossing_rate(frames): signs np.sign(frames) return 0.5 * np.sum(np.abs(np.diff(signs, axis1)), axis1)把整段语音处理完之后绘制特征曲线你会看到很有规律的现象静音段短时能量基本贴近零过零率则不稳定可能因为环境噪声而上下乱跳开始说话的瞬间短时能量骤然升高浊音段过零率低清音段过零率高这在时间轴上形成清晰的区分带。这里有一个容易犯的错误计算过零率之前没有对信号做直流分量去除。如果录音设备存在直流偏移波形整体往正或负方向偏移过零次数会明显减少特征就失真了。我在实验中都会先做一次去直流也就是x x - np.mean(x)这是一个花费几乎为零但收益很大的预处理步骤。5.4 用短时自相关估计基音周期自相关计算我用 numpy 的互相关函数实现取中间部分对应不同延迟的 R(k)def compute_autocorr(frame, max_lag): full_corr np.correlate(frame, frame, modefull) center len(frame) - 1 return full_corr[center:center max_lag 1]对某一帧语音计算自相关并归一化然后在16到133个采样点的范围内找峰值def estimate_pitch(frame, fs8000, fmin60, fmax500): lag_min int(fs / fmax) lag_max int(fs / fmin) max_lag min(len(frame) - 1, lag_max) R compute_autocorr(frame, max_lag) R R / R[0] # 归一化 search_region R[lag_min:lag_max 1] lag np.argmax(search_region) lag_min f0 fs / lag return f0, lag, R[lag] / R[0]返回的第三个值是峰值归一化幅度它能反映一帧信号的周期性强度。浊音帧的峰值幅度往往接近0.5以上清音帧或者噪声帧就没有这么明显的峰。实际使用中我会把这个值当置信度低于某个门限的直接标记为无基音避免在静音或清音段强行输出一个不靠谱的基频值。6. 工程里常见的坑和排查方法6.1 信号末尾不足一帧补零还是丢弃每次写分帧函数都有人问最后剩的那几十个点到底怎么办。我的答案分两种情况如果语音很长比如几十秒以上末尾不足一帧的直接丢弃即可因为影响微乎其微如果语音很短只有几百毫秒末尾可能还有有效内容这时候必须补零。补零的隐藏问题在于它制造的最后一帧能量会比真实值低而且自相关函数可能会因为这个零填充段产生一些假峰值。排查方法很简单看特征曲线最后几个点是不是出现了一个断崖式下跌如果是多半就是补零造成的可以在特征序列里删掉这个伪造帧。6.2 特征曲线毛刺太多先看窗再看帧移最后看噪声我在项目里遇到过短时能量曲线抖动非常厉害的情况最初以为是算法写错了后来逐个排查发现是矩形窗导致的边界不连续在作祟。换汉明窗之后毛刺明显减少。如果换窗还没解决问题就检查帧移是不是太激进。帧移太大意味着相邻帧之间重叠少特征的连续性就差。还有一个常见隐患是信号里存在低频噪声或者工频干扰。这类干扰会让过零率特征在静音段居高不下影响端点检测。解决办法是先对原信号做一次带通滤波比如保留300到3400赫兹再做短时特征分析。滤波会滤掉一部分有效语音的高频或者低频成分吗会但带内语音主要成分还在对于端点检测这类任务影响很小。6.3 端点检测失败率高双门限法才是稳的单独用短时能量做端点检测最容易出的问题是一个轻微的咳嗽或者关门声在能量上和语音很像于是被误判成语音段。我在项目中比较稳的做法是双门限法核心思路分三步。第一步计算整段信号的短时能量设置一个相对较高的门限和一个相对较低的门限。高于高门限的帧可以确定是语音低于低门限的帧先标成非语音落在两者之间的帧待定。第二步用短时过零率修正待定帧因为语音的清音头尾往往能量不高但过零率偏高如果待定帧过零率也高可以把它拉回语音段。第三步做最短时长抑制把那些长度不足50毫秒、孤立存在的“语音段”直接去掉因为它们多半是噪声或瞬时干扰。这套流程跑下来端点检测的可靠性会明显提升。但也提醒一句门限参数跟录音信噪比强相关换成另一个场景最好重新标定别指望一组参数走天下。我个人实际调这些特征的最大感受是先画图再调参。很多看似玄学的参数问题把波形、短时能量、过零率三条曲线叠在一张图上立刻就能看出问题出在哪个环节。最后再分享一个小技巧无论做什么短时分析先去掉直流分量再预加重再分帧加窗三个动作顺序不要记反很多后续问题都能从源头避免。
阅读完成 · 觉得有帮助?
咨询建站