做语音算法的人大概率都有同一种感觉语谱图看了不少频谱也画了一大堆但真要解释“为什么这个人的元音比那个人更亮、更闷、更靠前”的时候往往会卡住。共振峰Formant就是捅破这层窗户纸的关键。我在处理机器听觉相关项目时发现不管做的是语音识别、语音合成、说话人聚类还是声音事件检测一旦理解了语音的共振峰结构再看音频数据就像从“看色块”升级到了“看轮廓”。这一篇是系列第04篇专门聊语音共振峰结构它到底是什么、为什么机器要从波形里把它找出来以及我用Python落地提取时踩过哪些坑。1. 先搞懂共振峰到底在描述什么1.1 声源-滤波器模型一套能让语音“解构”的框架想理解共振峰先得接受语音领域最经典的一个抽象声源-滤波器模型。人的发音过程可以拆成两段。第一段是声带振动它产生周期性的声门脉冲串这个“声源”天生带有一串谐波。第二段是声道过滤包括咽腔、口腔、鼻腔这条变化多端的管子。气流通过管子时某些频率被放大某些频率被衰减相当于一个随时间变化的滤波器。麦克风最终收到的语音就是这两个环节相乘的结果激励谐波叠加上了声道滤波后的包络。这就解释了一个重要现象为什么同一句“啊”不同的人发出来听感完全不同声带只决定基频高低而真正区分音色、区分元音、区分说话人特质的主要是声道形状形成的共振结构。机器听觉要做的事情就是从一段波形里反演出这个“声道滤波器”的样子。共振峰就是这个滤波器的天然共鸣频率也就是频谱包络上的几个峰。因为激励和滤波混在一起直接从波形里分离它们并不直观这便有了后续一系列算法。1.2 F1、F2、F3编号背后是声道几何共振峰不是随便抓几个频谱峰就叫共振峰。它们按频率从低到高排序叫第一共振峰F1、第二共振峰F2、第三共振峰F3一般语音学分析最多看到F4。每个编号对应声道里一种特定的“共鸣模态”不是简单的峰值个数。从发音生理来看F1和舌头的高低关系最大。舌头放低、口腔开大F1通常会升高嘴唇收圆、舌位靠后F2会明显下降。所以用F1和F2两个参数就能画出一张二维声学元音图。举三个最典型的元音元音F1典型范围(Hz)F2典型范围(Hz)啊 a600-10001000-1400衣 i250-4002000-2800乌 u250-450600-1200注意这是典型男声的中值区域女声和童声整体会更高一些。但F1和F2的相对位置关系不会扭转这正是共振峰作为语音“结构化特征”的稳定性来源。机器只要能从频谱里稳定提取这几个峰就可以非常可靠地判断当前发的是哪个元音、口型偏向哪里甚至能感知到说话人的声道长短。1.3 共振峰和频谱峰值是两件事初学者最容易踩的第一个坑就是把频谱图上那些一根根尖峰当成共振峰。实际上你看到的尖峰大部分是声带的谐波它们与基频F0紧密相关。女性或儿童基频高谐波间隔大频谱就像一排密集的簪子。共振峰是罩在这一排簪子外面的“包络帐篷”由于谐波太密直接用FFT搜局部最大值会搜到一堆谐波峰根本分不清哪个是真正的共振峰。这个区分是理解后面所有处理的关键。共振峰关心的是声道形状谐波则关心声带振动周期。要做机器听觉中的元音识别、发声状态分析真正有价值的是前者。所以经典做法不会直接取频谱峰而是用一个全极点模型去拟合频谱包络再从这个模型里解析共振峰。这就是下一章要聊的LPC方法。2. 提取共振峰的经典思路LPC与全极点模型2.1 为什么几乎所有的老牌工具都用LPC共振峰提取不是只有一种方案但线性预测编码LPC是最经典、最常用、也最适合机器听觉落地的方法。原因在于它的物理意义非常清晰声道对语音的作用可以近似的看成一种自回归过程。当前时刻的语音采样值大概率能由过去若干个采样值的线性组合预测出来。声道形状越是规则预测误差就越小预测多项式反映的就是声道传递函数的倒置。换句话说LPC在做一个“预测”任务用过去的声音样本预测当前样本让预测误差的平方和最小。误差最小化之后剩下的残差里主要就是声带激励和噪声而预测系数则打包了声道共振的全部信息。这正好把上一章说的问题解决了不需要直接和声带谐波纠缠模型会自动把谐波当作“不可预测”的部分剥离掉。而且LPC计算量小哪怕在低算力设备上也能实时跑所以工程价值一直没衰减。2.2 从自相关到Levinson-Durbin递推最简实现LPC的求解链路一般是预加重 - 分帧 - 加窗 - 计算自相关 - Levinson-Durbin递推 - 得到预测系数 - 求复根得到共振峰。中间的Levinson-Durbin递推并不复杂核心是利用自相关矩阵的托普利茨结构逐阶递推出预测系数。下面这段是我手写的一个基础版能直接用于后续共振峰估计import numpy as np def levinson_durbin(r, order): a np.zeros(order 1, dtypenp.float64) e r[0].real a[0] 1.0 for k in range(1, order 1): acc r[k].real for j in range(1, k): acc a[j] * r[k - j].real kappa -acc / e if np.abs(kappa) 1.0: # 出现不稳定时截断返回 return a[:k], e prev_a a[:k].copy() for j in range(1, k): a[j] prev_a[j] kappa * prev_a[k - j] a[k] kappa e * 1.0 - kappa * kappa return a, e代码里最容易忽略的是稳定性判断。如果递推过程中反射系数绝对值超过1说明当前帧信号可能突变甚至有问题硬算下去会出现幅度大于1的极点共振峰频率和带宽都会失真。实际项目中我见过不少诡异结果追到底都是这里没有做保护。截断返回能避免把噪声帧的错误参数传给后续模块。2.3 从多项式根到频率和带宽不是所有根都能当共振峰得到LPC系数a之后共振峰藏在多项式A(z)0的复数根里。具体来说声道等效滤波器是全极点模型共振峰对应极点位置。极点在z平面上表示为r * e^(jθ)其中共振峰频率f θ * fs / (2π)共振峰带宽B -fs / π * ln(r)如果一个根的幅度r非常接近1说明这个共振峰能量衰减慢带宽窄如果r偏小带宽就宽峰形圆钝。工程上一般要过滤掉三类不靠谱根实数根、负频率共轭副本、带宽过宽的峰。实数根对应非振荡模式和共振峰的定义不符负频率副本只是共轭对称产生的重复带宽超过四五百赫兹的峰多半是不稳定帧或者噪声造成的对识别没有正向贡献。def estimate_formants(frame, sr, orderNone): if order is None: order int(sr / 1000) 2 n len(frame) r np.correlate(frame, frame, modefull)[n - 1: n order] if r[0] 0: return [] a, _ levinson_durbin(r, order) roots np.roots(a) roots roots[np.abs(np.imag(roots)) 1e-5] roots roots[np.abs(roots) 0.9999] formants [] for root in roots: radius np.abs(root) frequency np.angle(root) * sr / (2 * np.pi) bandwidth -np.log(radius) * sr / np.pi if 80 frequency sr / 2 - 80 and bandwidth 500: if np.imag(root) 0: formants.append((frequency, bandwidth, radius)) formants.sort(keylambda x: x[0]) return formants[:3]这段代码返回每帧最多三个共振峰的频率、带宽和极点幅度。频率按升序排列方便后面画轨迹。带宽过滤那一步就是前面说的“剔除不靠谱根”的落地实现。3. 用Python从音频文件里提取共振峰轨迹3.1 准备音频、读取与预加重演示用的音频可以直接用手机录一段稳定的元音“啊——”时长两三秒就够了采样率设成16kHz单声道。如果实在不方便录音也可以用语音合成生成一个简单的元音重点是利用已知参数来验证代码。读取音频我推荐用librosa封装好的加载函数它能自动重采样到目标采样率import librosa def pre_emphasize(x, alpha0.97): return np.append(x[0], x[1:] - alpha * x[:-1]) samples, sr librosa.load(vowel_a.wav, sr16000, monoTrue) samples pre_emphasize(samples)这段代码里的预加重很关键。语音信号的高频能量天然比低频低而共振峰包含F2、F3这些高频信息。如果不做预加重LPC会把大部分预测能力花在低频段导致高频共振峰估计不稳定。预加重的本质是添加一阶高通因子把高频段在数值上抬起来让模型“公平”对待各个频段。alpha取0.97是常用值后面讲坑时会细说。3.2 分帧加窗后逐帧估计共振峰语音是时变信号声道形状一直在动。共振峰只能按短时帧来算一般取20到30毫秒一帧。太短频率分辨率不够太长随时间变化的共振峰会被平均模糊掉。帧移一般取5到10毫秒这样相邻帧有重叠轨迹更平滑。frame_len int(sr * 0.025) hop int(sr * 0.010) formant_tracks [] for start in range(0, len(samples) - frame_len, hop): frame samples[start:start frame_len] * np.hamming(frame_len) formants estimate_formants(frame, sr) formant_tracks.append((start / sr, formants))加窗在这里不是为了好看而是为了减轻分帧造成的频谱泄漏。矩形窗会产生旁瓣干扰峰形判断汉明窗旁瓣小频谱包络干净共振峰定位更稳。这一轮循环结束后formant_tracks里保存的是每一帧的前三个共振峰频率。如果某帧没有稳定共振峰返回空列表后续处理时要跳过不要硬塞默认值。3.3 把共振峰轨迹画在语谱图上光返回数值不够直观最好把共振峰轨迹叠加到语谱图上一眼就能看出提取是否合理。import matplotlib.pyplot as plt from scipy.signal import spectrogram f, t, Sxx spectrogram(samples, sr, npersegint(sr * 0.025), noverlapint(sr * 0.015)) plt.pcolormesh(t, f, 10 * np.log10(Sxx 1e-10), shadingauto) for idx in range(3): ys [track[1][idx][0] if len(track[1]) idx else np.nan for track in formant_tracks] plt.plot([track[0] for track in formant_tracks], ys, o, markersize2) plt.ylim(0, 5000) plt.xlabel(time (s)) plt.ylabel(frequency (Hz)) plt.show()这个图里语谱图的深色横带就是高能量区域圆点就是算法认为的共振峰位置。如果圆点都落在深色横带附近并且轨迹连续不跳变基本说明提取正确。如果很多点悬在空白区或者一帧内上下乱跳问题往往出在参数选择上。3.4 参数调优的经验速查实际项目里共振峰算法“调参”比重很大。下面这个表是按我自己的测试经验整理的推荐范围参数推荐范围主要影响帧长20-30ms短则频率分辨率不足长则平滑过度帧移5-10ms小则轨迹细大则丢失共振峰转折预加重系数0.95-0.98太小低频主导太大会放大高频噪声LPC阶数采样率/10002左右低则丢峰高则产生虚假峰带宽阈值保留400-500Hz过宽往往是噪声或非稳态帧LPC阶数是最容易纠结的参数。阶数太低模型没有足够的极点去表达F3、F4阶数太高模型开始过度拟合频谱细节把谐波抖动也解释成极点结果就是共振峰分裂。一个经验起步值是fs/1000 216kHz对应18阶8kHz对应10阶先用这个跑通再根据轨迹稳定性微调。4. 共振峰结构在机器听觉里的真实用途4.1 语音识别传统特征与现代模型之间的桥梁端到端语音识别模型这几年大行其道很少有人再显式地把共振峰作为主特征喂给网络。但共振峰在识别链路里并没有消失而是转成了数据增强和前端优化的工具。举例来说同一个说话人的声道长度不同共振峰位置也不同。如果想让识别模型更鲁棒可以对训练语音做共振峰拉伸模拟出不同身高、不同声道长度的人说同一句话的效果以此扩充训练数据。这种做法比简单的音调变换更符合发音物理因为音调变换只改变基频声道长度差异需要靠共振峰偏移来模拟。在小语种、方言等低资源场景共振峰还有辅助特征的价值。数据不足时模型很难从海量参数里自学到稳定的声道结构先验显式加入共振峰轨迹或者把共振峰离散化为音位级别的约束往往能让模型更快收敛。4.2 语音合成与音色控制拖一下F1和F2声音就变了语音合成领域特别需要理解共振峰。现在很多语音合成系统虽然直接输出波形但调节声音风格时还是要回到声学参数。共振峰和音色的关系非常直观把F1往下压、F2稍微上抬声音会变厚、变低沉像是“胸腔共鸣更多”把F2大幅度抬高声音立刻变扁、变亮有点卡通味道。这一点在调音工程中特别有用。比如给虚拟角色做“老年音”最有效的操作往往不是拉低基频而是让共振峰整体向低频偏移同时适当加宽带宽。反过来做“儿童音”就要整体抬高共振峰位置。单纯改基频只会让人觉得音调高不会产生年龄感。理解这个机理之后做语音合成效果调试会精准很多。4.3 情感识别、病理语音分析和低算力设备共振峰结构作为一种物理客观参数在语音的“表情”分析里也有价值。人在紧张、兴奋、压抑等情绪状态下喉部和声道肌肉的紧张程度会改变导致共振峰频率产生偏移。相比纯粹的文本语义这些声学变化不容易伪装也更容易跨语言迁移。在语音病理分析上共振峰轨迹的异常往往和构音障碍有关。某些发音肌肉控制失调会导致F1/F2过渡变慢、目标位置不到位机器可以量化成“共振峰斜率下降”这样的指标。再考虑低算力场景智能音箱的麦克风前端、嵌入式语音唤醒模块、助听器算法都不希望为提取共振峰跑一个大型神经网络。经典LPC方法只需要几十次乘加运算加上一次多项式求根总计算量很小非常适合小型处理器。这也是为什么这项“老技术”到今天仍没被完全淘汰。4.4 共振峰还能用于数据清洗与质量评估这个用途可能很多人没想到。给一批语音数据做训练前清洗时如果某段录音的共振峰轨迹乱飞、F1和F2几乎混在一起、带宽普遍超过500Hz那这段音频大概率有问题要么是削波失真要么是环境噪声过大要么是麦克风频响不平。共振峰提取结果可以当客观指标用。在实际项目中我通常会先写一个脚本批量提取每句音频的共振峰统计轨迹连续性、带宽分布、频率范围。凡是严重偏离正常语音分布的样本自动打上“可疑”标签再做人工复听。这个方法比单纯看信噪比更贴近发音层面的质量问题。5. 实操中的常见坑与排查技巧实录5.1 LPC阶数乱调共振峰“裂”了典型现象是提取结果里突然出现两三个频率非常接近、带宽非常窄的峰比如F2本该在2200Hz左右结果同时输出2100Hz和2280Hz两个峰。原因几乎都是LPC阶数太高模型疯狂拟合精细谐波结构。谐波本身是等间隔排列的也带有窄带宽模型容易把其中一个谐波当成极点。排查方法很简单打印每个候选峰的带宽。真实元音共振峰带宽通常在40到250Hz之间而谐波拟合出来的假峰带宽往往在10到30Hz窄得可怜。如果看到带宽小于30Hz的“峰”直接删除。另一个办法是降两阶再看轨迹若F2区域从两个峰合并成一个稳定峰那基本就是阶数问题。5.2 预加重做与不做的区别比想象中大我最早写共振峰提取时偷懒跳过了预加重结果在16kHz录音上F2甚至F3老是往上偏。原因前面提过不预加重时LPC会把主要预测能力集中到低频。既然低频能量占主导模型就不在乎高频拟合得好不好高频共振峰位置自然不稳定。但预加重也不是越大越好。alpha接近1时高频噪声被明显放大安静环境下还算可控背景有噪时就会在F3附近引入虚假共振峰。我的经验值是安静录音用0.97到0.98噪声较大时降到0.95到0.96然后做一轮梯度确认静态元音段的F1和F2应该基本平直如果还抖再微调阶数。5.3 清音、停顿和鼻音段没有共振峰怎么办共振峰模型的前提是声门激励有周期性所以清音段如s、f、sh还有停顿和吸气声提取出的“共振峰”基本是噪声激励下的随机极点没有语言学意义。鼻音段更特殊鼻腔耦合会产生反共振峰体现在频谱上不是峰而是凹槽LPC用全极点模型描述起来会很别扭输出可能乱跳。工程解决方案是先做清浊音判断或端点检测只对浊音段提取共振峰。最简单的判断可以用帧能量和过零率能量低的是静音过零率高的是清音。对提取结果再做一次中值滤波把偶发的单帧跳变拉平。不要为了追求“每一帧都有值”而填充无意义数据那只会污染后面的分类器。5.4 怎么定量验证共振峰算法准不准共振峰没有绝对真值但有一个非常实用的自检方法合成元音。用已知的共振峰参数生成一段元音音频几个峰该在什么位置自己是清楚的再把这段音频喂给估计算法比较输出误差。误差在几十赫兹以内算正常如果偏差上百赫兹那一定是预加重、阶数或带宽过滤出了问题。我建议把合成测试写成一个固定脚本每次调整参数后先跑一遍。这比拿真实语音反复调参要高效得多因为真实语音没有参照物你分不清是算法不稳还是录音本身不行。等合成测试稳定了再上真实数据这时如果轨迹还乱那大概率是录音环境的问题而不是算法的问题。这些年做语音项目踩过最大的坑就是总想用深度学习一次到位忽略了共振峰这种底层物理结构。后来调试音频分类特征怎么调都不收敛最后把共振峰轨迹作为辅助特征加进去稳了很多。如果你也想在机器听觉里快速建立一个“听音辨形”的直觉建议先从共振峰入手它看起来只是几个峰背后却串起了发声物理、信号处理和工程系统一整条链路。