
前几天有学弟拿课程设计来问我说用Matlab做语音合成对着教材代码一步步敲完运行出来的声音“嗡嗡”的像隔了层棉被共振峰也找不准问我到底哪一步出了问题。我当时让他把LPC阶数从8改到12重新检查预加重系数再把基音检测的搜索范围限定到70到400Hz他改完之后说声音一下子干净了不少。这个事让我觉得值得把“线性预测共振峰检测基音参数”这条完整的语音合成路径好好写一遍。因为大多数教材只停在公式推导和理论框图真正影响成败的工程细节比如阶数怎么选、帧长取多少、基音检测怎么防野点教材里几乎没有。这篇文章就是冲着这些“教材不写但代码必须有”的细节来的。我按实际项目从零搭建的顺序把预处理、LPC系数求解、共振峰提取、基音检测到最终合成的完整链路拆开讲每段都有对应的Matlab代码和参数依据。无论是正在做语音处理课设、准备数字信号处理大作业还是想把手头的项目落地成能跑通、能听出明显效果的成品这篇文章都能帮上忙。1. 先看清整体这条合成链路到底在模拟什么讲代码之前必须先说清楚一个根本问题语音合成要模拟的究竟是什么物理过程人的发声过程可以简化成两个主要模块的串联。第一个是声源声带周期性开合产生脉冲状气流这个周期性决定了音高也就是基频F0清音比如s、sh则是气流通过狭窄缝隙产生湍流相当于一个噪声源。第二个是声道从声带到嘴唇之间这段空气管道形状随时在变舌位、下颌、唇形它对声源信号起到滤波整形的作用把能量集中在某些频率区域这些能量集中的地方就是共振峰。元音音色之所以不同本质上就是共振峰频率位置不同。所以“合成语音”这件事本质上就是“构造激励源 构造声道滤波器”的模拟过程浊音段给一个周期脉冲序列清音段给一段白噪声声道滤波器用线性预测LPC模型来逼近它的系统函数是全极点形式极点位置对应共振峰频率与带宽。这里选线性预测而不是简单用FFT频谱搬移根本原因是LPC模型对“声道声学传递函数”的物理拟合更直接。全极点模型对应理想无损声管的共振特性又恰好能被线性方程组高效求解一套系数既能量化声道形状共振峰又能为合成提供逆滤波器1/A(z)天然的“分析-合成”闭环。这就是这个项目技术路线成立的核心原因。项目的完整流程可以概括为六个环节预加重→分帧加窗→LPC系数求解→共振峰检测与基音检测这两步平行→构造激励源→逆滤波合成→帧间重叠相加输出。下面从预处理开始逐个拆。2. 预处理环节预加重、分帧、加窗的参数为什么这么定预处理看似简单但它对后续LPC分析和基音检测的影响是决定性的。很多人合成的语音闷闷的、共振峰位置漂移往往就是预处理参数没到位。2.1 预加重别小看这个高通滤波语音信号的高频段能量衰减很快大约每倍频程下降6dB。如果不做预加重LPC模型的残差在低频段占比过大分析出来的频谱包络会偏向低频导致共振峰位置整体拉低听起来又闷又堵。预加重就是一阶高通滤波器差分方程是y[n] x[n] - α * x[n-1]α一般在0.9375到0.985之间。我习惯取0.97效果稳定。Matlab里一句实现% 预加重 preemph [1, -0.97]; x_pre filter(preemph, 1, x);注意预加重不是越强越好。α太小高频噪声会被放大α太大低频信息丢失。0.94到0.97是一个安全范围我实测0.97在大多数干净语音上表现最好。2.2 分帧参数帧长25ms帧移10ms语音信号是短时平稳的。声道形状和基频在10到30ms内可以视为基本不变所以需要分帧处理。参数是怎么定的采样率Fs8000Hz电话语音常用或Fs16000Hz宽带语音常用下帧长20到30ms取25ms比较中庸。帧太短一个周期内的采样点不足LPC自相关估计方差大太长帧内语音变化明显平稳性假设不成立。帧移10ms即帧间重叠60%左右。这是为了让帧间参数平滑过渡合成时不至于帧与帧之间产生明显跳变。Fs 16000; % 采样率 frame_len round(0.025 * Fs); % 25ms frame_shift round(0.010 * Fs); % 10ms n_frames floor((length(x_pre) - frame_len) / frame_shift) 1;2.3 加窗汉明窗为主为什么不用矩形窗分帧之后必须加窗。矩形窗的频谱旁瓣太大会在帧边缘产生截断泄露导致LPC分析的频谱包络出现虚假波纹。汉明窗主瓣宽度适中、旁瓣衰减大约-43dB是语音分析最常用的选择。win hamming(frame_len, periodic); x_framed zeros(n_frames, frame_len); for i 1:n_frames start_idx (i - 1) * frame_shift 1; x_framed(i, :) x_pre(start_idx : start_idx frame_len - 1) .* win; end这里有一个经验点我用过hamming、hann、blackman做对比对后续LPC系数的影响汉明窗和汉宁窗差别不大黑曼窗频谱包络更平滑但共振峰带宽会被拉宽不利于精准检测共振峰。所以共振峰检测类的任务汉明窗是首选。3. LPC系数求解从自相关到Levinson-Durbin递推线性预测的核心思想非常直观用过去p个采样点的线性组合预测当前采样点预测误差最小化后得到的预测系数就包含了声道传输函数的频谱形状信息。3.1 Yule-Walker方程和Levinson-Durbin算法推导过程教科书里都有这里直接说结论。设LPC阶数为p我们需要解一个Toeplitz矩阵方程R * a r其中R是自相关矩阵a是LPC系数向量r是自相关向量。这类方程不能直接用高斯消元——不是不能解而是复杂度太高O(p³)实际项目中不划算。Levinson-Durbin递推利用Toeplitz矩阵的对称结构把复杂度降到O(p²)而且递推过程还能顺带得到反射系数k用于判断系统稳定性。% 手写Levinson-Durbin求解LPC系数 function [a_hat, e, k] my_levinson_durbin(R, p) a_hat zeros(p, 1); k zeros(p, 1); e R(1); for i 1:p % 计算第i阶反射系数 k(i) -(R(i1:-1:2) * a_hat(1:i-1) R(i1)) / e; % 修正系数 a_new zeros(i, 1); a_new(1:i-1) a_hat(1:i-1) k(i) * a_hat(i-1:-1:1); a_new(i) k(i); a_hat a_new; % 更新预测误差能量 e e * (1 - k(i)^2); end end实际用Matlab自带的lpc函数直接也能算它内部走的就是Levinson-Durbin路线% 对每一帧求LPC系数 p 12; % 阶数 lpc_coeffs zeros(n_frames, p 1); for i 1:n_frames [a, ~] lpc(x_framed(i, :), p); lpc_coeffs(i, :) a; end我第一次做这个项目时直接用了Matlab自带的lpc函数总觉得不太踏实于是又手写了一遍Levinson-Durbin两者结果对比后心里才真正有底。Matlab自带的lpc内部还有一步额外的窗口处理对自相关函数加窗结果会比我手写的版本在边界上更平滑一点差异很小但值得知道。3.2 阶数p怎么选经验公式与频率分辨率阶数p是整个LPC分析中最重要的超参数。阶数太低频谱包络太平滑共振峰被糊在一起阶数太高包络开始追踪频谱细节的波纹产生虚假共振峰。行业里通用的经验公式是p ≈ Fs / 1000 (2 ~ 4)Fs 8000Hz → p 10~12Fs 16000Hz → p 14~18我实测下来16000Hz采样率取16效果很好取12以下频谱包络明显太粗取20以上会出现额外的窄带假峰。判断阶数是否合适还有一个实用的副作用指标预测残差的能量误差能量e。随着p增大e单调下降但下降速度会在某个点明显变缓这个拐点附近就是合适阶数。你可以跑一个阶数从4到30的循环画出误差能量曲线选拐点处的p值。这个方法在清音段尤其有用。4. 共振峰检测峰值搜索法和极点求根法怎么配合LPC系数求出来之后共振峰检测有两条主流路线峰值搜索LPC频谱包络或者对LPC多项式求根再映射到频率。两条路线各有优缺点实际项目中我建议都实现交叉验证。4.1 方法一LPC频谱包络峰值搜索把LPC系数的频谱画出来找局部极大值就是共振峰候选。频率响应计算公式% 计算LPC频谱包络采样点512 nfft 512; [H, w] freqz(1, a, nfft, Fs); % a为LPC系数 [1, a1, a2, ...] H_dB 20 * log10(abs(H)); % 找峰值 [pks, locs] findpeaks(H_dB, MinPeakHeight, -10, MinPeakDistance, round(0.01 * Fs)); formants w(locs); % 单位Hz这里两个参数值得注意。MinPeakHeight设置为-10dB是为了滤除那些幅度过低、不够格成为共振峰的微小小峰。MinPeakDistance设置为100Hz左右是物理上的限制两个共振峰在正常语音中不会靠得太近除非特殊语音如果峰值间隔小于100Hz多半是数值抖动造成的假峰。峰值搜索法的优点实现简单直接对应频谱包络的直观特性。缺点如果两个共振峰靠得近比如某些元音的F2和F3峰值合并成一个宽峰分辨率不足。4.2 方法二LPC多项式求根极点法这是更“物理”的做法。声道全极点模型的极点位置直接对应共振峰频率和带宽。对LPC多项式A(z)1a1·z^(-1)...ap·z^(-p)求根% 求LPC多项式根 roots_lpc roots(a); % 筛选单位圆内的根实际上LPC保证根在单位圆内但数值误差可能越界 roots_lpc roots_lpc(abs(roots_lpc) 1); % 角度转频率模长转带宽 angle_z angle(roots_lpc); freq_hz angle_z * (Fs / (2 * pi)); freq_hz abs(freq_hz); % 取正频率 bw_hz -log(abs(roots_lpc)) * (Fs / pi);模拟后得到每个极点的频率F和带宽BW。筛选共振峰的标准是频率在200~5000Hz之间带宽在50~500Hz之间。带宽过大说明该极点衰减太快构不成明显的频谱峰带宽过小说明极点过于接近单位圆很可能是数值边缘不稳定也可能是真实但极窄的共振峰。极点映射到共振峰频率有个细节如果极点是复共轭对会得到正负对称的两个频率取正频率即可。如果实轴上的极点对应0Hz或Nyquist频率要么对应频谱整体斜率要么是计算伪迹直接剔除。4.3 两种方法如何取舍以我实际测试的经验分四种情况场景峰值搜索法极点求根法F1/F2靠得很近峰合并误差大仍能区分较好高次阶数p虚增假峰保真度高好判断产生大量窄带假根难筛噪声背景频谱包络被抬高峰位漂移极点位置相对稳定代码复杂度低5行搞定中需做筛选逻辑我的做法是两路并行先跑极点求根法得到候选频率带宽列表再用峰值搜索法检核每个候选是否真的对应频谱包络上的极大值点。两者都通过的才保留这样能过滤掉绝大部分假峰。这个方法在信噪比不太高的录音样本上尤其有效。4.4 共振峰平滑数学上美观听感上更实帧间共振峰参数如果不做平滑合成的共振峰频率会逐帧抖跳听感上就是“毛糙”“不干净”。我的做法是对共振峰频率序列做五帧中值滤波再叠加一次三帧均值平滑。这个处理只改变帧间连续性不会改变单帧频谱轮廓非常安全。5. 基音参数估计自相关法、基音范围与清浊音判断基音参数决定合成语音的韵律感。如果基频不准合成出来的语音要么“机器人腔调”太重要么句调怪。基音检测在语音处理里是个常说常新的话题我这篇就讲项目里最实用的自相关法。5.1 短时自相关法的核心逻辑短时自相关函数是r(k) Σ x[n]·x[n-k]当滞后k等于基音周期T0时r(k)会出现明显的峰。因为基音周期对应的延迟波形和自身错开一个周期后高度相似。5.2 基音周期搜索范围如何约束这一步对准确率影响极大。一条很关键的常识人声基频范围大约在70~400Hz之间男女有别。男声偏低80~200Hz女声偏高180~350Hz。所以搜索范围要限制不能全网扫描全延迟范围F0_min 70; F0_max 400; tau_min round(Fs / F0_max); % 最小延迟 tau_max round(Fs / F0_min); % 最大延迟 % 计算自相关只算这个范围 [acf, lags] xcorr(frame, frame_len - 1, coeff); lags_target lags(lags tau_min lags tau_max); acf_target acf(lags tau_min lags tau_max); [max_val, idx] max(acf_target); F0 Fs / lags_target(idx);如果整个搜索范围内自相关峰值都很低比如最大值小于0.3说明该帧缺乏周期性应判定为清音段基频置0或NaN。阈值0.3是经验值我调过不同录音样本0.25到0.35之间影响不大但低于0.2会漏判很多浊音高于0.4会误判很多清音0.3是安全中线。5.3 三帧中值平滑去掉野点基音检测器在个别帧上容易出现“倍频/半频错误”比如把基频检测成了2倍频或0.5倍频。单纯依赖单帧自相关很难根治最实用的办法是帧间中值平滑F0_smooth medfilt1(F0_seq, 5, omitnan);中值滤波长度取5帧约50ms能有效剔除孤立野点。但注意不要用太大的窗否则基音变化快的语段比如四声降调会被压平听感变怪。5.4 一个容易忽略的坑基音对齐自相关法求出的基音周期T0是个浮点数但构造周期脉冲激励时脉冲间隔必须是整数采样点才能落位到采样网格。我一开始直接取round(T0)导致脉冲序列的累计相位误差越来越大合成语音越到后面越“跑调”。解决办法是用累积相位法。用一个小数相位累加器每走一个循环累加值≥1就放一个脉冲相当于脉冲间隔的整数部分和小数部分都考虑进去。这样平均基频严格等于检测值不会累积误差。phase 0; F0_frame F0_smooth(i); T0 Fs / F0_frame; % 在整个帧范围内生成脉冲 impulse_train zeros(1, frame_len); for n 1:frame_len phase phase 1 / T0; if phase 1 impulse_train(n) 1; phase phase - 1; end end这个细节是我跑完整个项目回头优化时才补上的补上之后听感改善非常明显。5.5 清浊音帧的激励分界线上的难题判定为清音的帧激励源直接给零均值高斯白噪声能量按帧RMS对齐。判定为浊音的帧激励源是周期脉冲序列。合成时清浊音边界如果处理不好会出现“咔咔”的破裂声原因是浊音的周期脉冲突然切换成白噪声波形在边界处剧烈跳变。我的处理方案对起始帧和结束帧各取半帧过渡区过渡区内激励源按线性比例混合% 混合长度取半帧 mix_len round(frame_len / 2); for n 1:mix_len alpha n / mix_len; % 0到1线性过渡 excitation(n) (1 - alpha) * exci_voiced(n) alpha * exci_unvoiced(n); end这样可以显著降低清浊音切换时的爆破噪声比在合成后做后滤波效果更好。6. 语音合成激励源驱动逆滤波器1/A(z)前面分析的目的是拿到两组参数帧维度的LPC系数a以及帧维度的基频F0和清浊音标签。合成阶段就是把参数“变回去”生成语音。6.1 合成方程声道滤波器模型是x[n] e[n] - Σ(a[k]·x[n-k]) (k1..p)其中e[n]是激励源a[k]是LPC系数。用Matlab一句话synthesized_frame filter(1, a_frame, excitation);这里filter(1, a, e)就是对激励信号做全极点滤波a向量是[1, a1, a2, ..., ap]。6.2 增益匹配最容易忽略的一步这里有个大坑。LPC分析得到的每一帧语音能量差别很大而激励源的幅度往往与原始语音幅度差了好几个数量级。如果不做增益补偿合成语音会忽大忽小甚至整段声音小到几乎听不见。标准的做法是从LPC分析阶段保存预测误差能量e就是Levinson-Durbin递推最后一步算出来的e值合成时用它来标定激励源的幅度gain sqrt(e); % e是预测误差能量 excitation excitation * gain;但仅仅这样还不够精确。我实测下来最稳妥的增益匹配方式是在合成后按帧计算RMS并把它对齐到原始帧的RMSrms_orig sqrt(mean(orig_frame.^2)); rms_syn sqrt(mean(synth_frame.^2)); if rms_syn 1e-6 synth_frame synth_frame * (rms_orig / rms_syn); end这个RMS对齐做在合成帧输出阶段短时能量包络和原语音基本一致在听感上改善非常明显。6.3 帧间重叠相加合成因为分帧时采用了10ms帧移、25ms帧长每帧之间有15ms的重叠区域。合成时不能直接把各帧首尾相连拼接否则重叠区的数据会重复产生严重的梳状滤波效应声音发“空”。用重叠相加overlap-add处理synth_out zeros(length(x), 1); for i 1:n_frames start_idx (i - 1) * frame_shift 1; end_idx start_idx frame_len - 1; % 合成帧叠加到输出序列 synth_out(start_idx : end_idx) synth_out(start_idx : end_idx) synth_frame; end % 因为重叠区被多次叠加需要归一化 weights zeros(length(x), 1); for i 1:n_frames start_idx (i - 1) * frame_shift 1; end_idx start_idx frame_len - 1; weights(start_idx : end_idx) weights(start_idx : end_idx) hamming(frame_len, periodic); end synth_out synth_out ./ max(weights, 1e-6);注意合成时叠加的是汉明窗的函数值窗函数本身如果不是加窗的帧而是带窗帧的合成结果叠加会引入幅度调制噪声。正确做法是先加窗再合成最终用窗函数之和归一化——也就是上面这样。这一步做完播放sound(synth_out, Fs)你应该能听到一个虽然带点机器人味但音节清晰、声调自然的合成语音。7. 参数调整的工程心得听感指标与常见问题排查整个流程跑通后剩下的就是调参和优化听感。我把自己反复踩过的坑和最终验证可行的参数组合整理出来下载代码回去就能直接用。7.1 不同采样率下的推荐参数组合采样率LPC阶数帧长帧移预加重系数基音范围8000Hz10~12200采样点(25ms)80采样点(10ms)0.9770~400Hz16000Hz14~18400采样点(25ms)160采样点(10ms)0.9770~400Hz44100Hz20~241103采样点(25ms)441采样点(10ms)0.9570~400Hz44100HzCD音质下我推荐降一点预加重系数因为自身录音的高频更足α太大会让声音发脆发尖。7.2 常见听感问题与排查对照表听感问题可能原因处理方案声音闷像隔墙预加重系数太小或省略LPC阶数偏低检查α升到0.97提高阶数持续“嗡嗡”低频噪声共振峰检测把低频假峰当成F1极点筛选限制F200Hz提高MinPeakHeight声音尖锐毛糙阶数过高出现窄带假峰降低阶数或加共振峰带宽筛选清浊交界有“咔咔”声激励源切换无过渡加半帧线性混合过渡整段音量忽大忽小增益匹配不到位用帧RMS对齐音调平无抑扬顿挫基音平滑过度中值滤波窗降到3帧或去掉尾音发“空”有金属感帧间重叠相加归一化不当检查窗函数叠加归一化步骤7.3 关于“机器人味”的一点说明LPC合成的“机器人味”是机理决定的不是工程缺陷。因为LPC全极点模型保留了声道形状的频谱包络但对激励源的模拟极其粗糙——周期脉冲序列和真实声带波形的差异以及缺少基频微扰jitter和幅度微扰shimmer这些就是电子音色的根本来源。如果你想让合成音色更自然可以考虑在周期脉冲激励源的每个脉冲幅度上加少量随机扰动±5%或者在基频序列上叠加±1Hz级别的缓慢随机起伏模拟真实声带的不完全周期性。这个技巧不加复杂算法模块改动极小但自然度提升很显著。7.4 我自己实测的一条优化路线最后分享一条我自己在十几次调参中总结出来的路线先保证共振峰走向正确再调基频自然度最后优化激励源细节。如果共振峰错了后续调什么都是白费如果基频抖得太凶音调起伏会盖过音色问题。顺序反过来调参往往越调越乱。把这段代码跑通之后你可以试着用它合成一段自己录的“a-o-e-i-u”序列或者把基频序列人为拉伸一倍听听音高变化再换个说话人的录音看看共振峰轨迹的区别——这些实验做完你对LPC语音合成这套系统的理解会比我写十篇文章都管用。