ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PSOLA语音算法实战:基频检测、音高标记与重叠相加合成

PSOLA语音算法实战:基频检测、音高标记与重叠相加合成 简介一份完整的PSOLA语音处理算法MATLAB实现与演示资源面向语音信号处理方向的学生、研究人员及开发者用于理解音高检测、语音分段与重叠相加合成的完整流程。压缩包共62个文件包括61个功能齐全的.m脚本和1个Voice.wav示例音频体积仅78KB涵盖PitchDetection、VoicedSegmentMarking、FindPeakCandidates、computeSMarks等核心模块并提供Synthesis_weighted、Synthesis_general、Synthesis等多种合成策略及主控脚本Script_principal便于按流程复现和二次开发。已有超过170人浏览学习资源通过分级函数与注释清晰的代码结构演示了音高估计、有声/无声段划分、峰值候选定位、分割点计算等关键环节同时包含平滑加权处理与过渡概率等细节适合用于课程实验、算法研究或工程实践参考。通过运行示例音频与各阶段可视化函数可深入观察中间结果快速掌握PSOLA的工作原理与调参方法为进一步优化算法提供坚实基础。1. 为什么还要拆开 PSOLA一个“把周期切成段再粘回去”的语音算法三秒的朗读音频想在保留语义和音色的前提下把基频提高 20%、语速放慢一成直接重采样会连音色一起变相位声码器又容易出金属味。PSOLA 换了个思路先找到每个基音周期的起点把波形切成小段再按目标音高和时长重新排列、重叠相加。这套PSOLA.zip里的 MATLAB 实现比网上只讲公式的 PSOLA 网站资料更接近工程PitchDetection.m、VoicedSegmentMarking.m、FindPeakCandidates.m、computeSMarks_*.m、Synthesis_*.m覆盖了从 wav 到结果的全流程。适合做语音合成实验、TTS 前端改造或课程设计复现的人。它不是一个黑盒需要你把每个模块的参数调开但调开之后你对 TD-PSOLA 的理解会比看十篇论文都深。2. 基频先验PitchDetection 与 VoicedSegmentMarking 的基频估计与清浊判决2.1 帧化参数25ms 分析窗与 10ms 帧移为什么够用解压PSOLA.zip之后不要先点开script_principal.m我建议先看PitchDetection.m与PitchEstimation.m。这一步定下整条链路的基频曲线。语音信号在 20~40ms 内近似平稳25ms 的分析窗能装下两到三个基音周期10ms 帧移保证帧间有足够重叠后续 HMM 平滑不至于太跳。代码调用如下% Voice.wav 是这个包里附带的样例音频单声道即可 [x, fs] audioread(Voice.wav); x x(:, 1); % 取单声道避免双声道影响自相关峰值 frameLen round(0.025 * fs); % 25ms折合 400 点16k 采样率 frameShift round(0.010 * fs); % 10ms 帧移 [f0raw, vuvRaw] PitchDetection(x, fs, frameLen, frameShift, ... CenterClipping, true, MedFilt, 5);CenterClipping开关决定是否在自相关前做中心削波MedFilt是帧级基频的中值滤波阶数设成 5 表示取前后各两帧一起做中值能去掉个别野点。frameLen太小清音段容易乱跳太大则基频在帧内已经变化峰值会变钝。若采样率是 8kframeLen对应 200 点此时建议把MedFilt降到 3减少平滑过度。如果你手里没有Voice.wav任意一段 16k 采样率、长度 3~5 秒的中文朗读录音都可以只要信噪比不低于 20dB。2.2 CenterClipping 的作用为什么削波后自相关更可靠CenterClipping.m看起来简单却是这套代码里很关键的一步。它把低于阈值的采样点直接置零让自相关函数更突出基音周期的脉冲性。共振峰引起的 500Hz 左右的振荡幅度通常低于声门脉冲削波之后这些干扰被压掉PitchEstimation.m在滞后范围内的峰值搜索就不容易被次峰带走。function y CenterClipping(x, clipLevel) % x: 单帧语音; clipLevel: 削波阈值 y sign(x) .* max(abs(x) - clipLevel, 0); endclipLevel一般按当前帧最大绝对值算取 0.6~0.8 倍。取 0.9 以上会把弱浊音帧整个削掉导致 HMM 判成清音取 0.3 以下等于没削共振峰次峰照样进自相关。常见做法是在PitchDetection.m内部按帧实时计算clipLevel 0.7 * max(abs(frame))。如果是气息声明显的女声这 0.7 可以放到 0.8因为低声门脉冲会被噪声淹没削得狠一点反而让主峰更明显。2.3 PitchEstimation 与 xcorr滞后范围约束到人声基频PitchEstimation.m是这个包里的另一个估计器。它和PitchDetection.m的分工通常是前者输出帧级基频的精细估计后者负责帧参数、削波和初步清浊标记。自相关的实现可以直接用包里的xcorr.m也可以调用 MATLAB 信号处理工具箱的xcorr。代码思路如下% 假设 frame 是当前 25ms 帧 [acf, lags] xcorr(frame, normalized); minLag round(fs / 500); % F0 上限 500Hz再高不常见 maxLag round(fs / 60); % F0 下限 60Hz防止低频漂移 validRange lags minLag lags maxLag; [~, idx] max(acf(validRange)); f0 fs / lags(idx);500和60是人声基频的常用上下限若是女声可以把下限改成 80若是低沉男声把上限降到 350。注意xcorr的结果在零滞后处最大所以必须把搜索范围限制在minLag到maxLag之间否则永远会拿到一个接近直流的错误基频。这里最容易犯的错误是选错峰如果实际 F0 是 150Hz自相关在 2 倍周期处也经常出现一个接近的峰所以后面才需要medfilt1和 HMM 平滑。2.4 VoicedSegmentMarking 的 HMM 平滑StateProb 与 TransitionProbVoicedSegmentMarking.m在这套代码里负责清浊音判决。很多课程设计只用能量阈值但能量阈值的毛病是清音爆破音如 /t/能量也很大会被误标成浊音。这个包里StateProb.m和TransitionProb.m写了一个两状态 HMM状态 0 清音状态 1 浊音。StateProb根据当前帧的自相关峰值强度、基频值是否落在合理区间给出观测概率TransitionProb则惩罚频繁的浊清切换。解码常用 Viterbi得到整句的vuv序列。% 对 2.1 得到的 f0raw 做 Viterbi 平滑 [vuv, f0sm] VoicedSegmentMarking(f0raw, [], fs, ... MinRun, 3, TransitionPenalty, 10);MinRun限制一段浊音最少包含 3 帧短于 3 帧的孤点被合并到邻近状态TransitionPenalty越大状态切换越困难曲线越平滑。如果发现词尾的鼻音被切碎就把TransitionPenalty从 10 往上调如果浊音段里仍有大量清音毛刺可能是StateProb的阈值在VoicedSegmentMarking.m内部写死需要先调CenterClipping而不是直接改转移概率。下表是这一阶段最常用的几个调参入口模块输入依赖典型参数调参方向PitchDetection原始 wav帧长 25ms、帧移 10ms、MedFilt5基频跳变严重时加大 MedFiltPitchEstimation单帧信号F0 范围 60~500Hz女士/儿童上浮下限CenterClipping单帧信号削波阈值 0.6~0.8弱浊音识别差时降低VoicedSegmentMarkingf0rawMinRun3, Penalty10清浊切换太碎时加大 Penalty3. 音高标记实战FindPeakCandidates 与 S-Marks 的生成、修正和补点3.1 峰值候选为什么不能用普通局部极大值基频检测拿到了连续的 F0 曲线下一步要在每个基音周期里找“真正的声门脉冲位置”也就是音高标记。如果直接在原始波形上找局部极大值一个周期内会因为共振峰叠加出现三四个候选峰。FindPeakCandidates.m的主要工作就是把候选峰限制在f0sm给出的周期邻域里。常见做法是对每一帧按当前 F0 计算周期T0 fs / f0在[start, start T0]窗口内找最大峰值并要求峰值幅度至少是当前帧最大峰值的 50%否则视为无关峰。代码调用如下% f0sm 和 vuv 来自第 2 章vuv1 的帧才搜索峰值 [peakPos, peakAmp] FindPeakCandidates(x, f0sm, vuv, fs, ... SearchWindow, 0.8, AmpThreshold, 0.5);SearchWindow设为 0.8 表示搜索窗长是当前周期长度的 80%不给旁边周期的次峰机会AmpThreshold则是相对幅度门限设太低会把共振峰小凸起也算进来设太高则漏检弱浊音段。注意这里peakPos是样本序号不是时间。如果按时间记定位误差会被放大到几乎一个周期。3.2 从峰值到 S-MarkscomputeSMarks_simple 与 computeSMarks_generalFindPeakCandidates的输出是候选峰还需要整理成合成阶段真正使用的 S-Markssynthesis marks。computeSMarks_simple.m、computeSMarks_general.m、computeSMarks_weighted.m三个文件对应三种策略computeSMarks_simple每个候选峰只保留一个最强峰作为标记适合音高稳定、噪声小的录音。computeSMarks_general在简单版基础上补插值。如果某个周期内没有候选峰按上一个周期间隔外推如果两个标记间距突然翻倍会尝试在中间补一个点。computeSMarks_weighted除了返回标记位置还返回每个标记的权重权重与峰值幅度、相邻周期相似度相关给后一级Synthesis_weighted.m使用。代码调用smarks computeSMarks_general(peakPos, f0sm, vuv, fs, ... MaxGap, 2.0, MaxJitter, 0.5);MaxGap允许的最大标记间隔倍率超过 2 倍说明中间少了一个周期函数会补点MaxJitter是标记相对理想网格的最大抖动超过阈值就认为峰值定位不可靠用中值位置替代。我一般先用simple版本跑一遍看标记图确认峰值质量再切到general。如果语音本身带轻颤音MaxJitter反而要设大一点否则真实抖动会被当错误去掉。3.3 selectCorrectPos 与 IncreaseMarking标记纠错与补点的边界条件即使有 HMM 平滑局部倍频错误还是可能让标记间隔出现 2:1 的偏差。selectCorrectPos.m的思路很简单逐个取出相邻标记间隔和期望周期T0比较如果d 0.6 * T0认为标记过密如果d 1.6 * T0认为标记过疏则用左右相邻间隔的中值修正当前标记。这个 0.6/1.6 是经验值比值太小会破坏正常 jitter太大失去纠错意义。smarksCorr selectCorrectPos(smarks, T0, Threshold, [0.6 1.6]); smarksFull AddUnvoicedMarks(smarksCorr, vuv, fs, IntervalMs, 10);Threshold的上下界控制纠错灵敏度AddUnvoicedMarks专门处理清音段。清音段没有稳定的峰值但重叠相加仍然需要均匀的标记点IntervalMs设置清音段的标记间隔一般取 10ms 或当前浊音段平均周期的一半。太密会产生明显的金属声太疏会让清音段丢失声母的动态。这三个参数对听感的影响比合成窗还明显我通常先调它们再动窗函数。3.4 PlotPitchMarks 与标记合法性检查PlotPitchMarks.m可以帮你在进入合成前做一次人工检查。运行这一行PlotPitchMarks(x, fs, smarksFull, f0sm, vuv);对照图看三点标记是否落在周期最大的正峰附近相邻标记间隔是否在 0.8~1.2 倍平均周期内清音段的标记是否规则但不过密。如果连续出现两个标记间隔只有半个周期是倍频错误回去调FindPeakCandidates的AmpThreshold如果整段标记都偏向波形上升沿是削波阈值太低导致主峰前移。这个检查步骤不能省因为合成阶段的错误大多是标记错误的表现形式不是合成本身的问题。相关参数行为如下表参数默认经验值过低表现过高表现MaxGap2.0漏补周期标记在正确位置插入伪标记MaxJitter0.5标记位置太死板峰值错位IntervalMs10清音颗粒感清音段丢失动态4. 合成阶段Synthesis 家族的重叠相加、加权窗与时长映射4.1 分析标记与合成标记Pitch2Duration 与 matching 的角色前两章得到的smarksFull是分析标记它标出了原始语音每个周期的位置。合成阶段的关键是构造一组“合成标记”目标是变调那么合成标记的间距要按比例缩放目标是变速那么合成标记的间距保持不变但总时长变化。Pitch2Duration.m负责把目标 F0 曲线映射成位置序列。matching.m则把每个合成标记映射回最近的分析标记解决“从哪个周期取音高”的问题。% 把原始 F0 整体抬高 20% targetF0 f0sm * 1.2; synMarks Pitch2Duration(targetF0, smarksFull, fs, ... Mapping, linear, MinF0, 60); anaIdx matching(smarksFull, synMarks);Mapping决定合成标记间距是按当前 F0 反比线性映射还是用对数映射避免过低的男声跳频MinF0保底防止映射出超过一个帧长的标记间距。matching输出的是分析标记索引数组长度为合成标记数之后在get_frame.m里直接按索引切窗。注意这里直接乘1.2是最粗的变调方式实际语音的高频谐波不会等比缩放会带一点“童声感”这是 TD-PSOLA 的固有特点。4.2 Synthesis.m 到 Synthesis_weighted.m 的差异这个包里合成函数有三个许多初读代码的人会混淆。Synthesis.m是最简单版本按分析索引切窗、加窗、叠加不做能量补偿Synthesis_general.m增加了双窗交叉淡化在相邻标记之间做 50% 重叠减少拼接痕迹Synthesis_weighted.m在 general 的基础上把第 3 章得到的权重乘到每个窗上并对重叠区域做归一化。表格函数核心机制适用场景典型产物Synthesis.m单窗直接叠加快速验证流程粗糙但可闻的变调Synthesis_general.m双窗交叉淡化正常语音合成无明显断点但音量不稳Synthesis_weighted.m加权重叠归一课程设计/演示音色接近原始语音合成主循环的简化实现如下% 对每个合成标记从分析位置取一段加窗信号 for k 1:length(synMarks) anaPos smarksFull(anaIdx(k)); % 匹配到的分析标记 winLen 2 * round(0.5 * median(diff(smarksFull))); % 两倍周期 seg get_frame(x, anaPos, winLen); seg seg .* hann(winLen, periodic); start synMarks(k); y(start:startwinLen-1) y(start:startwinLen-1) seg; endwinLen取两倍平均周期保证前后两个窗各有 50% 的重叠Hann 窗首尾接近零拼接处平滑get_frame.m实现带边界检查的取帧超出信号范围的部分补零。如果不先除重叠窗的累加和音量会随着标记密度变化所以Synthesis_weighted.m的最后一步是对y做逐点归一化。4.3 加权与重叠相加的数值问题重叠相加的一个经典坑是所有窗叠加后中间部分的增益在 1.5 到 2.0 之间浮动。若不对结果做除法听感就是一阵一阵的“呼吸声”。归一化要在合成后做不能在某一个窗上做否则窗与窗的交叠处仍然不对。常见做法是winSum zeros(size(y)); for k 1:length(synMarks) start synMarks(k); winSum(start:startwinLen-1) winSum(start:startwinLen-1) hannWin; end y y ./ max(winSum, 1e-6); % 避免除零极小值保护max(winSum, 1e-6)保证所有样本点都能被除输出不出现 NaN。若最后几句有明显杂音先检查winSum在信号尾部是否急剧下降因为最后一个合成标记附近可能没有完整窗这时裁剪输出长度比强行补窗更自然。另一个容易被忽略的问题hann在包里的private/hann.m返回的是对称窗还是周期窗如果和 MATLAB 内置版本不一致重叠区增益会偏离 1此时应统一按periodic生成。4.4 同时变调与变速Pitch2Duration 的映射参数大多数语音合成任务不是单纯变调或单纯变速而是两个同时改。Pitch2Duration.m里可以通过TimeStretch参数把时长变化叠加到标记映射中。常见做法如下% 目标音高乘 1.3时长缩短到 0.8 pitchScale 1.3; timeScale 0.8; newF0 f0sm .* pitchScale; synMarks Pitch2Duration(newF0, smarksFull, fs, ... TimeStretch, timeScale, Mapping, linear);TimeStretch小于 1 表示缩短大于 1 表示拉长。这个参数与pitchScale是独立控制的先按newF0算出每个分析标记对应的自然间距再按timeScale拉伸这个间距序列得到最终合成标记。当两者同时作用时matching的输出索引不再单调的概率变大所以合成前要检查anaIdx是否连续如果出现重复索引说明目标 F0 曲线上有局部极点应加一点平滑。5. 从 script_principal 到波形验证PSOLA.zip 的完整跑通与排错5.1 主脚本的执行顺序script_principal.m是这个包的串行入口。把Voice.wav放在当前目录下直接运行run(script_principal.m)如果报Undefined function hann先确认 MATLAB 路径下能搜到private/hann.m原包把辅助函数放在 private 子目录这个目录设计是不参与path更新的必须和主目录保持相对位置。主脚本的逻辑顺序基本是读取音频、调用PitchDetection、VoicedSegmentMarking、FindPeakCandidates、computeSMarks_*、Synthesis_weighted最后用sound(y, fs)播放。实际跑通时我建议把PitchDetection的参数先写死成 2.1 节的帧参数避免每次改。5.2 用 DisplayPitchAndWave 检查标记位置和音高DisplayPitchAndWave.m把原始波形、估计基频和 S-Marks 画在同一张图上。运行DisplayPitchAndWave(x, fs, smarksFull, f0sm, vuv);对照图看三件事标记是否落在每个周期最大的正峰附近相邻标记间隔是否在 F0 对应周期附近清音段是否也有均匀间隔。如果标记在波形上升沿而不是峰值处很可能是FindPeakCandidates的AmpThreshold太低让谐振峰抢了位置。5.3 常见现象与参数方向现象可能原因调整方向音高变成预期的一半自相关选了 2 倍周期峰提高中心削波阈值加大 MedFilt音高变成预期的两倍搜索范围上限过低或半周期峰把 F0 范围上限调高检查 minLag声音发沙、有金属声标记间隔 jitter 大增大 MaxJitter 纠错降低 SearchWindow清音段有明显颗粒清音标记过密增大 AddUnvoicedMarks 的 IntervalMs音量忽大忽小重叠相加未归一化确认 Synthesis_weighted 已执行 winSum 除法变调后时长不变Pitch2Duration 映射模式设置错误检查 Mapping 是 linear 还是 logarithmic调参时每次只改一个量。先把DisplayPitchAndWave的结果保存成图再改合成参数两张图对比比反复听声音更高效。拿到PSOLA.zip以后最先值得动手的不是合成函数本身而是把Voice.wav的波形和标记图画出来在图上验证算法假设是否成立。本文还有配套的精品资源点击获取
返回列表