资讯动态

数字变声器怎么实现?从基音检测、共振峰搬移到MATLAB完整信号链解析

发布时间:2026/10/15 15:26:54 来源:尧图企业网站定制
简介这是一份基于MATLAB的数字变声器与滤波器实现资源面向信号处理初学者、通信或电子类课程设计学生以及需要快速上手MATLAB音频处理的研究者。资源围绕音频变声与滤波两大核心任务覆盖从音频读取、傅里叶变换、频谱分析、基频调整到滤波器设计、信号重构与效果评估的完整流程适合用于三级项目或实践入门。压缩包共11个文件其中包含10个.m脚本和1个wav示例音频脚本承担变声、滤波、频谱压缩、基频检测等功能wav文件用于测试调试整个资源包约20KB轻量易用。已有2772人学习浏览实用性得到验证。通过阅读和运行代码可以掌握audioread/audiowrite、fft/ifft、fir1、filter等关键函数的应用理解男女声调变换、低通高通带通带阻滤波器的设计与效果评估为后续更复杂的音频处理开发积累扎实基础。1. 数字变声器的第一课先忘记“滤波”先记住“音高”我第一次做变声器的时候第一反应是“用滤波器把声音改一改”。这是最大的误解。数字变声器的核心不是改变音色而是改变音高映射——让“啊”在输出时变成“哦”或“依”。滤波器只是处理链上的配角它负责把你已经改过的信号修干净。一个只有滤波器的“变声器”最多能做出闷罐音和电话音做不出小黄人和恶魔音。把这件事拆开数字变声器在 MATLAB 里做本质上是一条信号链录音或读 wav → 基音检测 → 移调用重采样或相位声码器→ 共振峰搬移 → 滤波整形 → 播放或写回文件。滤波器在这个链条里承担两个工作一个是处理前的带通去噪和去工频另一个是处理后的去爆音、去毛刺。这套方案能解决什么问题它能做到“音高变、音色也变”而不是简单粗暴地快放慢放。本文适合三种人正在做课程设计的学生、想给语音交互做趣味音效的开发者、以及第一次把理论课上的 z 变换和自相关函数拿来落地的工程师。读完你可以从零在 MATLAB 里搭出一个能跑的实时非实时变声链路并且知道每个参数动下去会发生什么。2. 数字变声器的理论基础基音、共振峰和那段被误解的“重采样”2.1 为什么慢放不是变声变调与变速的解耦很多人第一次尝试变声是把 wav 文件整体重采样。这确实是改变音高的手段但代价是语速也跟着变了。放慢变成唐老鸭加快变成花栗鼠这是变速不是变声。数字变声器的目标是保持语速不变、只改变音高或者音高和语速分别独立控制。这里的关键是把“时间轴”和“频率轴”解耦。常见做法有两种时域做法是“基音同步重叠叠加”PSOLA频域做法是“相位声码器”Phase Vocoder。PSOLA 在 MATLAB 里写起来不复杂但对基音周期的检测精度很敏感相位声码器通过短时傅里叶变换修改帧的峰值频率再合成质量更稳但参数多。我一般会建议初学者走 PSOLA 思路因为它的每个步骤都看得见摸得着先分析出基音周期再按周期把信号切成小段最后把小段按目标周期重排。在这个过程中你不需要处理复数频谱也不会有相位积累的玄学问题。实现 PSOLA 第一步是准确的基音周期检测这是整个变声器的地基。% 自相关法基音检测帧长30ms帧移10ms frameLen round(fs * 0.03); frameShift round(fs * 0.01); frames buffer(x, frameLen, frameLen - frameShift, nodelay); minLag round(fs / 400); % 400Hz 上限 maxLag round(fs / 60); % 60Hz 下限 for i 1:size(frames, 2) f frames(:,i) - mean(frames(:,i)); % 去直流 [r, lags] xcorr(f, maxLag, normalized); validRange (lags minLag) (lags maxLag); [~, idx] max(r(validRange)); pitchPeriod(i) lags(validRange); end这段代码的核心是xcorr之后只搜索 60Hz 到 400Hz 的滞后区间。这个区间对应正常人声的基频范围男性偏低女性偏高。要特别说明的是normalized参数让自相关值归一化到 [-1,1]否则低能量帧的相关值会虚高。还要注意buffer函数的nodelay参数如果不加它MATLAB 默认会在每帧尾部补零这会让基音检测在帧边界出现偏移对后续 PSOLA 的“按周期切段”非常致命。2.2 共振峰为什么变调之后声音像“纸片人”用 PSOLA 把基频提高一倍之后你得到的声音往往不是“同一个人说了句高音”而是“一个变薄的声音在说话”。原因是共振峰也跟着变了。人的声道形状决定了共振峰位置男性声道长、共振峰低女性声道短、共振峰高。当你直接重排基音周期时共振峰也会按同样比例移动结果就是声道长度被“改了”。解决思路是把共振峰拉回原来的位置或者按目标性别移动到新的位置。最简单可控的做法是用线性预测编码LPC求包络把频谱包络分离出来包络就是共振峰结构残差就是声源激励。变声时只修改激励的基频再把包络按目标共振峰位置重映射回去。这个“源-滤波器分离”的思路是几乎所有影视级变声器的底层模型。% 用 LPC 分离声道和激励阶数取 fs/1000 2 p round(fs / 1000) 2; lpcCoeff lpc(frame, p); residual filter(lpcCoeff, 1, frame); % 激励信号 vocalTract filter(1, lpcCoeff, residual); % 尽量还原声道这段代码最重要的参数是 LPC 阶数p。采样率 16k 时取 18采样率 44.1k 时取 46。阶数太低共振峰分辨率不够声音发闷阶数太高会把激励里的周期细节也建模进去导致分离不干净。判断分离是否干净有一个土办法把vocalTract播放出来它听起来应该是“带闷感的元音”而不是“有明确音高的乐音”如果还能听出音高说明阶数太高把残差里的周期成分也吃进去了。2.3 MATLAB 里主流处理链从哪里读入到哪里写回在写任何算法之前先养成用统一格式归档的习惯。语音处理领域里最常见的坑是采样率和位深不统一。MATLAB 从 2018 版本开始audioread默认返回 double 类型、范围在 [-1, 1] 之间但老代码里的wavread不是这样。如果你混用新旧接口归一化范围错了后面所有滤波器都会表现异常。[x, fs] audioread(voice.wav); % 返回 double范围约 [-1, 1] if size(x, 2) 1 x mean(x, 2); % 双声道取均值合成单声道 end x x - mean(x); % 去除直流偏置 x x / max(abs(x)); % 归一化到 [-1, 1]避免后续溢出读取之后我都建议先做一步“安全处理”转单声道、去直流、归一化。这一步看起来简单但能挡掉非常多后续问题。mean(x, 2)对双声道取平均是常见的下混方式它会损失立体声声像但对变声处理不是问题。去直流很重要因为很多录音设备有直流偏置它会直接让滤波器的输出产生一个不可听但真实存在的偏移量后续做帧重叠时会产生周期性爆音。2.4 引入 PSOLA 实战思路变调不变速的最小闭环基音检测完成后PSOLA 的核心操作是“同步切段”和“按比率重排”。假设检测到的基音周期序列是T0(i)目标变调比率是alphaalpha 2 表示提高八度那么合成时段的间隔应该是T0 / alpha。alpha 1.5; % 变调比率1 变高1 变低 synthPos 1; outLen round(length(x) / alpha); % 输出长度按比率变化 y zeros(outLen, 1); segIdx 1; while synthPos outLen center round(((segIdx - 1) * frameShift frameLen/2) / alpha); win hamming(frameLen); seg frame .* win; startOut synthPos - round(frameLen/2); if startOut 1 y(1 : synthPos round(frameLen/2)) ... y(1 : synthPos round(frameLen/2)) seg(1 : end); else y(startOut : startOut frameLen - 1) ... y(startOut : startOut frameLen - 1) seg; end synthPos synthPos round(frameLen / alpha); segIdx segIdx 1; end这个循环里最关键的是alpha放在哪里center和synthPos都要除以它。如果只在合成端除、分析端不除时间轴会错位声音听起来像“卡住了”。重叠相加时用hamming窗是为了让相邻段的边界平滑过渡避免每个切点都产生一个“咔哒”声。需要特别注意的是最后两行对边界的处理很多翻车现场都是因为这一段在数组边缘溢出。3. 滤波器的角色先把声音做干净再做魔改3.1 滤波器选型FIR 和 IIR 放在变声链的哪些位置滤波器在变声链里有三个位置输入前端、共振峰整形、输出后端。输入前端做带通切掉 60Hz 以下的工频干扰和 8kHz 以上对语音无意义的高频这个位置可以用 IIR 的 Butterworth阶数低、延迟小。共振峰整形是把特定频段抬高或压低模拟“喉音重”“鼻音重”的效果这个位置用 FIR 更稳因为 FIR 是线性相位不会在特定频率处产生相位畸变对语音清晰度影响最小。输出后端去爆音也就是所谓“消磁声”这个位置可以用 IIR 的高通来切掉低频隆隆声。% 输入前端400Hz - 4000Hz 带通4阶 Butterworth [b, a] butter(4, [400 4000] / (fs/2), bandpass); xFiltered filter(b, a, x); % 输出后端高通截止频率 80Hz去除呼吸低频 [bHp, aHp] butter(2, 80 / (fs/2), high); yFinal filter(bHp, aHp, yOut);这里出现的 400-4000Hz 是语音能量最集中的频段但要注意切到 4000Hz 会把某些人声的高频齿音也切掉声音会显得闷。所以前端带通我一般只做一个 60Hz 到 7kHz 的宽带带通重点是把 60Hz 以下的东西切干净高频端不用太激进。butter是 IIR 里最常用的滤波器通带内最平坦相位非线性但人耳对这个频段的相位不敏感用在前后端完全没有问题。3.2 单频点变调的经典滤波方案一串双二阶滤波器在真正做共振峰搬移之前有一种更传统的“廉价变声法”用多个峰值滤波器Peak Filter同时调整几个固定频点的增益来模拟共振峰移动。这是早期硬件变声器最喜欢用的方案因为双二阶滤波器在定点 DSP 上极好实现。具体做法是把 500Hz、1000Hz、2000Hz、4000Hz 四个频点各放一个峰值滤波器分别设置增益变调时按规则联动。% 双二阶峰值滤波系数计算fs16000中心频率 fcQ值增益 gainDb fc 1000; Q 4; gainDb 6; w0 2 * pi * fc / fs; alpha sin(w0) / (2 * Q); A 10^(gainDb / 40); b [(1 alpha * A) (-2 * cos(w0)) (1 - alpha * A)]; a [(1 alpha / A) (-2 * cos(w0)) (1 - alpha / A)]; [filtered, ~] filter(b, a, xFrame);这条公式来自 RBJ Audio EQ Cookbook是所有数字滤波器的入门配方。Q值是带宽控制参数Q 越大共振峰越尖越像金属声Q 越小共振峰越宽越像广播声。这套方案适合做“即时生效、可实时调节”的趣味道具但缺点是不建模真实声道变调感很“电子”。3.3 时变滤波器为什么 LPC 包络搬移听起来更真实如果你追求“人变声后还是人”的效果LPC 包络搬移是最终答案。核心思想是把 LPC 求出的系数当作一个全极点滤波器要移动共振峰就是把这个滤波器的极点位置按比率缩放然后重新构造滤波系数。% 极点缩放每个极点半径不变角度按 alpha 缩放 r abs(roots(lpcCoeff)); theta angle(roots(lpcCoeff)); thetaNew theta * alphaFactor; polesNew r .* exp(1j * thetaNew); % 用新极点重新构造多项式系数 bNew poly(polesNew); aNew bNew; [out, ~] filter(1, aNew, residual);这里有个非常隐蔽的坑poly出来的系数是单精度敏感的阶数一高数值误差会累积。所以 LPC 阶数在控制器里要设成可变的一般 10 阶到 22 阶之间不要无脑拉高。极点缩放后如果某个极点的角度超过 Nyquist 对应弧度它会产生镜像频率听起来就是明显的金属失真。处理办法是对thetaNew做min(abs(thetaNew), pi - abs(thetaNew))钳制。3.4 滤波参数的工程化采样率变了所有数字都重来滤波器最坑的地方在于所有设计公式里都带fs/2。同一段代码在 16kHz 下跑得好好的拿到 44.1kHz 下直接用截止频率会偏移近 2.7 倍。比如butter(2, 80 / (fs/2))在 16k 下是 80Hz 高通在 44.1k 下实际变成约 220Hz 高通低频人声会被切掉一大块。这个问题的血泪教训是滤波器系数不要写死必须在代码里用变量fs现场计算并且把fs从读取 wav 的地方一路传下来。另一个常见问题是滤波器的时变行为如果共振峰滤波器每帧都换系数比如按照基音周期实时移动帧与帧之间的系数突变会产生“啵啵啵”的噪声。解决办法是系数插值也就是把目标系数和上一帧系数做线性加权过渡而不是直接切换。这个技巧在变声器里非常关键它直接决定输出是“平滑的变声”还是“一串噼里啪啦的爆音”。4. 把变声器和滤波器接成一套完整流程从 wav 到 wav4.1 整体处理链分帧、变调、共振峰搬移、滤波重组的串联结构现在把所有模块串起来。完整链路是读取音频 → 前端带通滤波 → 分帧 → 基音检测 → PSOLA 变调 → LPC 分离激励与声道 → 共振峰搬移 → 后端高通滤波 → 写回 wav。% 主流程伪代码结构 [x, fs] audioread(input.wav); [x, fs] preProcess(x, fs); % 去直流 归一化 前端滤波 pitch detectPitch(x, fs); % 自相关基音检测 [xPitch, fsNew] pitchShiftPSOLA(x, fs, pitch, 1.5); % 变调不变速 y lpcFormantShift(xPitch, fsNew, 0.85); % 共振峰搬移 y postFilter(y, fsNew); % 后端去爆音高通 audiowrite(output.wav, y, fsNew);要注意的是PSOLA 变调输出的采样率其实没有变但时间长度变了。比如 alpha 1.5 时输出时长变为原来的 2/3语速“感知上”不变是因为音高提高了实际物理时长变短了。如果需要时长严格一致可以在最后做一次resample拉回原来的长度。4.2 分帧重叠参数怎么配帧长、帧移、窗型的三组推荐值变声器这种非线性处理对分帧参数比滤波器敏感得多。推荐三组经验值普通语音 16k 采样率帧长 30ms、帧移 10ms、Hamming 窗音乐或歌曲类素材 44.1k 采样率帧长 46ms、帧移 15ms、Hann 窗低码率电话音 8k 采样率帧长 25ms、帧移 8ms、矩形窗加 50% 重叠。帧长决定了基音检测的最低可测频率。30ms 帧长能可靠检测到 33Hz 以上的基频对男声80-200Hz足够了。帧移决定了变调时时间分辨率的细腻程度帧移越大处理越粗糙会产生“颗粒感”。窗口类型也有讲究Hamming 窗主瓣宽频域泄露少但时域上旁瓣衰减略慢Hann 窗旁瓣衰减更快更适合需要干净时域边界的 PSOLA。% 分帧参数实践 frameLen round(fs * 0.03); % 16k - 480点 frameShift round(fs * 0.01); % 16k - 160点 win hamming(frameLen, periodic); % 周期窗更适合分帧重叠periodic这个参数值得专门提一下它生成的窗最后一个点是 0这样窗与窗之间互补后相加的平坦度更好。用非周期性窗做重叠相加会在每个帧边界留下细微的“折痕”连续听就是背景里一层淡淡的电噪声。4.3 实时处理与离线处理的差异两种模式的代码组织方式MATLAB 做实时变声不太顺手但可以做“半实时”用audiorecorder录一段处理完马上播放。更专业的做法是用dsp.AudioFileReader和dsp.AudioFileWriter做流式处理。这两种方式的代码组织差异在于离线方式可以一次性把整个信号装进内存所有帧共享全局统计量流式方式必须逐帧独立处理滤波器状态要保存。% 流式处理框架 reader dsp.AudioFileReader(input.wav, SamplesPerFrame, 512); writer dsp.AudioFileWriter(output.wav, SampleRate, fs); while ~isDone(reader) frameIn reader(); % 每帧独立做变调 滤波 frameProc singleFrameProcess(frameIn, fs); writer(frameProc); end release(reader); release(writer);流式处理有个大前提滤波器状态在帧与帧之间自然延续但变调器的状态必须由你显式保存。比如 PSOLA 的segIdx、synthPos、帧缓冲区的重叠区都必须定义为persistent变量或者封装成对象属性。我见过很多人在流式里直接调用离线版的函数结果每帧都是从零开始输出全是“噗噗噗”的断续声。4.4 从 16kHz 到 44.1kHz采样率转换的三种做法语音素材常常是 16kHz 或 8kHz而目标输出可能需要 44.1kHz 才能在某些播放器里正常工作。MATLAB 提供resample函数做采样率转换但它对语音有轻微的影响重采样会让频带边缘产生振铃。更稳的做法是先升采样再做低通滤波最后抽取。MATLAB 的resample内部实际上就是这个流程所以通常不需要你手动写。% 采样率转换p/q 倍必须保证互质 p 44100; q 16000; [g, yNew] resample(y, p, q);resample返回第二个参数yNew是滤波器延迟补偿很多人在用的时候把这个返回值丢掉这会让输出多出几十个采样延迟串到下一帧就变成“尾部拖音”。一定要用[g, yNew]的写法取回补偿后的信号。另一个坑是resample后信号整体幅值会被缩放需要做一次g / max(abs(g))归一化。5. 避坑手册这 5 个问题能毁掉 80% 的变声器项目5.1 变调后出现“哗啦哗啦”的水声现象输出信号像泡在水里每帧连接处有碎玻璃一样的杂音。原因帧与帧的相位不连续或者窗函数用错了类型导致重叠相加时出现周期性“台阶”。解决第一确认窗函数是否用了periodic选项第二检查输出帧的长度是否与输入帧一致PSOLA 里输入输出帧长必须一致不一致就需要考虑补零后的权重补偿第三帧移必须与窗长匹配用 50% 重叠帧长的一半作帧移时窗函数满足 COLA常数重叠相加特性这样能保证相加后幅度稳定。5.2 共振峰搬移后声音变成“婴幼儿”现象音高没变但声音像小孩子在说话或者像“天线宝宝”。原因共振峰搬移因子太大了。共振峰频率整体移高声道变短音色就变成小孩声。这是变声器里最经典的“纸片人”问题。解决把共振峰缩放因子控制在 0.8 到 1.2 之间。想要“女声变男声”不是把共振峰无限压低而是降低约 20% 同时配合基频降低这才能保持自然。搬移量超过 30% 时一定要配合 LPC 包络的平滑处理避免包络形状断裂。5.3 输出的每个字末尾都有“嗡”一声现象每个音节结束的位置尾音发闷像含着一口痰说话。原因后端高通滤波器的截止频率设得太低了。很多滤波器的默认高通只切 40Hz但那针对全频带音乐语音里需要切到 80-120Hz 才能真正去除低频尾部余音。解决把后端高通的截止频率调到 100Hz 左右。如果你发现切了 100Hz 之后声音变得有点干那可以考虑用两个并联的高通一个 80Hz 慢滚降负责去除“嗡”一个 150Hz 快滚降负责“去肉”并联输出。5.4 同一段代码换一个 wav 文件就翻车现象自己录的语音处理正常网上随便找的一段语音一处理就爆音或者音调乱飞。原因不同录音的采样率、声道数、电平差异很大。48kHz 的素材被当作 16kHz 处理基音检测范围整个偏了。解决在audioread之后立刻打印三条信息fs、通道数、数据中位数绝对值。然后做一套强制统一流程全部转单声道、重采样到内部统一采样率推荐 16kHz 或 32kHz、归一化到峰值为 0.9。只要处理前后采样率保持一致后面所有滤波器系数都基于同一个fs计算就稳了。5.5 滤波器系数一更新就“啵”地爆一声现象共振峰参数做自动化调节时每次参数变化都会出现一声爆破音。原因IIR 滤波器的状态变量在系数突变瞬间会产生瞬态响应这个瞬态就是“啵”声。FIR 滤波器因为是有限脉冲响应不会出现这个问题但相位会有一点变化。解决用系数插值。每次参数更新时不要直接换成新系数而是用 10 到 30 个采样点做线性过渡从旧系数渐变到新系数。实现方式是分别计算旧、新两套系数的输出再让两路输出按权重相加权重从 1 线性变到 0。6. 变声器的下一步从“能变”到“像样”的调音习惯到了这个阶段变声链已经能跑了。但“能跑”和“听着自然”之间差着几个习惯。第一个习惯是每次调参数都要做 A/B 对比原始语音、只变调、变调加共振峰搬移、加上过渡处理后的成品四个版本轮播对比。你会发现多数“不对”的感觉出现在变调和共振峰搬移之间而不是滤波环节。第二个习惯是学会看频谱图而不是只靠耳朵。打开spectrogram一张图看出基频轨迹是否连续、共振峰横带是否平滑、高频是否出现难看的竖条纹那是爆音。音频调优不靠玄学靠眼睛看频谱加耳朵听两相印证效率翻倍。第三个习惯是给所有参数加“脚本化拨盘”调试接口。把变调比率、共振峰因子、后端高通截止频率三个参数做成函数入口参数写一个批量处理的脚本一次性生成 3x3x327 个版本然后循环播放筛选。比在命令行里一个参数一个参数敲快得多。最后分享一个教训大多数变声器的“假”感不是来自算法不够高级而是来自基音检测不够稳。自相关法在安静环境下表现尚可但一遇到背景噪声检测出的基频轨迹就会抖动PSOLA 跟着抖输出就飘。如果你要拿去用在实时场景把前端加一个快速噪声门电平低于阈值直接静音这比任何高级滤波都管用。你从这套链路里最终拿走的不只是 MATLAB 函数而是对“信号怎么在模块间流转”的判断力。滤波器系数、帧长、重叠窗、共振峰因子这些参数每一个都是可解释的改它们之前你应该能说出“为什么要改”而不是“试试看”。希望这篇笔记帮到你少走一段弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑