
简介变声器Matlab代码包面向计算机、电子信息工程、数学等专业的大学生主要服务课程设计、期末大作业和毕业设计中的音频变声课题可帮助读者快速获得一套可运行、可修改的算法实现规避自行编写时常见的参数混乱与调试困难。压缩包共4个文件包含2个m脚本和2个mp3音频样例整体体积仅5.82MBm脚本分别承担核心变声处理与辅助控制逻辑采用参数化编程关键参数可方便更改mp3样例则用于输入输出效果试听。代码注释明细、结构清晰从音频读取、变声处理到结果播放保存的完整流程均有体现并附带案例数据直接运行即可看到效果。对初学信号处理与MATLAB编程的读者尤其友好可作为理解变声器基本原理的实践入口。目前已有209人学习下载适合作为课程实训或毕业设计的直接参考。1. 变声器Matlab代码到底在改什么解压一个叫“变声器matlab代码.zip”的文件包最常见的状态是README 里写着直接运行 main.m运行完发现声音只是快了一点或者高了一点和“男声变女声”“变成机器人”的预期差得很远。问题通常不在代码抄没抄对而在变声这件事本质上是在同时操纵三个量基频、时长和音色。基频决定音高时长决定语速音色里的共振峰位置决定声音像谁。三者的组合才是“变声器”单独调一个只是“变速器”或“变调器”。本文按一条离线语音变声的常见技术路线展开先用 resample 理解音高位移再用相位声码器把变调与变速解耦最后用滤波和测试信号把效果调出层次。适合刚拿到代码包、正在找入口的学生也适合想从效果反推算法的 DSP 工程师。2. 从重采样入手先跑通一条Matlab变声代码要读懂代码包里的.m文件先要理解数字音频里的音高放在哪里。一个频率为 f0 的周期信号在采样率 fs 下每个周期占 fs/f0 个采样点。比如 fs44100、f0200Hz那每个周期大约 220 个点。若把这些点之间的时间关系压缩让每个周期只占 150 个点再按原来的 fs 播放周期在时间轴上变短听到的音高就会上升。这就是重采样做变声的基本逻辑。2.1 重采样为什么能改变音高但也会改变语速直接在 Matlab 里做这件事不需要训练任何模型也不需要 Audio Toolbox。核心就两行用重采样把信号变到“虚拟采样率”然后用原始采样率播放。这里的量要分清resample(x, p, q)会把输入采样率变成原来的p/q倍而不是把采样率字段写进 WAV 文件。我们要做的是让输出信号在按原 fs 播放时音高变成目标倍数所以实际上要构造一个“虚拟采样率”输入采样率fs目标虚拟采样率fs / pitchRatio然后用resample把数据采样率从 fs 变成 fs/pitchRatio。播放时仍然传 fs时间轴被压缩音高就升上去了。[x, fs] audioread(speech.wav); % 读入 wav if size(x, 2) 1 x mean(x, 2); % 双声道先合并成单声道 end x x / max(abs(x)); % 归一化避免后面出现削波 semitones 7; % 升 7 个半音约 1.5 倍 pitchRatio 2^(semitones / 12); % 半音数与频率倍数的换算 [num, den] rat(1 / pitchRatio, 1e-3); % 把小数转成近似整数分数 y resample(x, num, den); % 输出信号采样率约为 fs/pitchRatio player audioplayer(y, fs); % 播放器仍按原始 fs 播放 playblocking(player); audiowrite(pitch_up.wav, y, fs); % 写回 wav采样率参数仍写 fsrat(1 / pitchRatio, 1e-3)的作用是把1/pitchRatio转成两个小的整数因为resample需要整数重采样因子纯小数不能直接传进去。例如升 12 个半音时pitchRatio21/pitchRatio正好是1/2重采样后点数减半按原 fs 播放时音高高一倍时长也缩成一半。2.2 半音数比倍数更符合听感直接把pitchRatio写成 1.5 也能跑但音乐和语音里更常用“半音”来表达变调幅度。人耳对频率的感知接近对数刻度升 200Hz 到 400Hz 和升 1000Hz 到 2000Hz 听起来是同样的“八度跨度”所以变声参数建议用半音数而不是频率倍数。semitonespitchRatio听感参考重采样后时长122.00高八度明显角色化0.5T71.50明显变细适合做少年感0.67T31.19轻度年轻化0.84T01.00原声T-70.67明显变低沉1.5T-120.50低八度2T注意这里每一行都附带“时长变化”。这是因为重采样只做了一件事改变每个周期占的采样点数并没有能力把“音高”和“语速”分开。如果你只想让音高变、语速不变下一章的相位声码器才是正路。2.3 为什么直接重采样听起来像小黄人重采样之所以被很多入门代码采用是因为它简单、稳定、不依赖额外工具箱。但它的代价是不仅仅基频移动了整个频谱都被拉伸共振峰也跟着移动。共振峰是声道对声音的滤波作用形成的它决定了我们觉得一个声音是成年男性、小孩还是卡通角色。重采样把基频和共振峰一起移动所以升调多了以后声音会显得“幼态”也就是俗称的小黄人效果。如果做直播音效小黄人效果反而是优势。但如果目标是声音转换希望“把 A 的声音变成 B 的音色但仍然是自然人声”就必须绕开纯重采样或者用滤波做共振峰补偿。下一步先解决音高和语速的解耦问题。3. 相位声码器实现变声器的变速与变调解耦相位声码器是很多变声器代码的核心。它工作在频域把语音分成互相重叠的短帧做 FFT 后保留每一帧的幅度谱和相位谱。合成时改变帧与帧之间的间距就能在不改变音高的情况下改变时长反过来先用重采样改变音高再用相位声码器把时长拉回原样就得到“变调但不变速”的效果。3.1 为什么用相位声码器而不是逐点插值时域里的简单时间伸缩比如线性插值会让波形周期变长或变短结果是音高也跟着变达不到独立控制。PSOLA 思路也很常见但需要先可靠地检测每个基音周期标记对噪声、清音段和低信噪比录音比较敏感。相位声码器不需要显式检测基频只要 STFT 参数选得合理对一般语音都能工作所以更适合作为代码包里的第一版实现。它的核心是相位累加。假设分析帧间隔是hopA某一频率分量在相邻帧之间实际前进的相位和理想正弦前进的相位之差可以用来估计“瞬时频率”。合成帧间隔改成hopS后用这个瞬时频率乘以hopS就能让每个频率分量在时间拉伸后仍然保持原来的音高。3.2 在Matlab里实现一个可复用的相位声码器函数下面这段代码可以直接保存成pvTimeStretch.m作为整个变声脚本的时间伸缩模块。参数上先固定nfft2048、hopA256后面再解释怎么调。function y pvTimeStretch(x, alpha) % 相位声码器时间伸缩 % alpha 1 表示放慢 1 表示加快音高不变 nfft 2048; hopA 256; hopS round(hopA * alpha); % 合成帧间隔 win 0.5 - 0.5 * cos(2 * pi * (0:nfft-1) / nfft); x x(:); N length(x); outLen round(alpha * N); % 目标输出长度 frameCount ceil(N / hopA); % 覆盖到最后一个采样点 allocLen (frameCount - 1) * hopS nfft; y zeros(allocLen, 1); wsum zeros(allocLen, 1); k (0:nfft/2).; binPhase 2 * pi * hopA * k / nfft; % 理想正弦的分析帧相位增量 prevAnaPhase zeros(nfft/2 1, 1); prevSynPhase zeros(nfft/2 1, 1); for m 0:frameCount-1 idxA m * hopA 1 : min(m * hopA nfft, N); seg zeros(nfft, 1); seg(1:length(idxA)) x(idxA); seg seg .* win; X fft(seg); X X(1:nfft/2 1); mag abs(X); phi angle(X); if m 0 delta mod(phi - prevAnaPhase - binPhase pi, 2*pi) - pi; else delta zeros(size(phi)); end synPhase prevSynPhase binPhase * (hopS / hopA) ... delta * (hopS / hopA); Y mag .* exp(1i * synPhase); frame real(ifft([Y; conj(Y(end-1:-1:2))], nfft)); idxS m * hopS 1 : m * hopS nfft; y(idxS) y(idxS) frame .* win; wsum(idxS) wsum(idxS) win .^ 2; prevAnaPhase phi; prevSynPhase synPhase; end y y ./ max(wsum, 1e-6); y y(1:min(outLen, length(y))); % 裁剪到目标长度 end代码里最关键的是delta的计算。它把当前帧相位和上一帧相位之差减去该频点由帧移hopA造成的“预期相位增量”剩下的就是真实频率偏离理想频率的部分。合成时把delta按hopS/hopA缩放得到新的相位增量。这保证了时间轴被拉伸或压缩时各频率分量不会发生音高漂移。3.3 组合调用重采样加相位声码器实现变调不变速有了时间伸缩模块变声器主函数就短很多了。先把音高通过重采样改变此时语速也变了再用相位声码器把时长恢复。注意第二个参数必须是pitchRatio因为重采样后时长变成了原来的1/pitchRatio正好需要拉伸pitchRatio倍。function y voiceChanger(x, fs, semitones) % 变调不变速的核心函数 % semitones 为正数时升调负数时降调 pitchRatio 2^(semitones / 12); [num, den] rat(1 / pitchRatio, 1e-3); xShift resample(x, num, den); % 第一步改变音高 y pvTimeStretch(xShift, pitchRatio); % 第二步恢复语速 y y / (max(abs(y)) eps); end这个组合里resample负责把基频移动到目标位置pvTimeStretch负责把时长拉回。如果只需要慢放或快放可以跳过resample直接调用pvTimeStretch(x, speed)这时音高不变语速改变。3.4 相位声码器的必调参数nfft、hopA、alpha是影响结果最明显的三个参数。默认参数在大多数 44.1kHz 语音上能工作但遇到低沉男声或剧烈变调时需要调整。参数默认值调小/调大的影响nfft2048调大频率分辨率更细低频更稳调小瞬态保留更好但低频可能模糊hopA256调小时间分辨率更高相位估计更细计算量变大调大声音可能发闷alpha由变调比决定低于 0.5 或高于 2 时相位声码器容易产生金属声如果处理的是男低音建议把nfft加到 4096否则 80Hz 附近的基频可能落在两个频点之间导致最后的合成音出现“电流声”。反过来如果是电话录音或已经经过压缩的语音nfft保持 2048 就够了。4. 共振峰与滤波参数把变声器代码调出不同音色上一章完成了“变调不变速”但如果你实际听一下声音仍然带着明显的电子感。原因还是共振峰。重采样把频谱整体拉伸共振峰跟着移动所以升调后声道的“长度”也变了听起来像一个变小的人。要得到更自然的变声需要处理共振峰或者至少用滤波去重新塑形音色。4.1 共振峰才是声音像不像的关键语音学里有个概念叫声道长度成年男性、女性、小孩的声道长度不同共振峰位置也不同。重采样变调会把整个频谱拉伸相当于让一个成年男性的声道变成小孩的声道。如果这是刻意要的卡通效果没问题但如果你要让“沉稳大叔”变成“温柔御姐”必须是基频升高、声道长度相对保留而不是简单把频谱整体上移。精确做法是对频谱包络做频移估计共振峰位置把包络往低频或高频搬再和新的基频参数合成。常见做法是用 LPC 或倒谱做包络估计然后对系数做频率弯折。大多数代码包里不会完整实现这一步而是用一组参数 EQ 来补听感。我们这里给一个可直接套用的峰值 EQ。function y peakingEQ(x, fs, Fc, gaindB, Q) % 峰值EQ滤波器 % Fc中心频率gaindB提升/衰减 dBQ滤波器宽度 A 10^(gaindB / 40); w0 2 * pi * Fc / fs; alpha sin(w0) / (2 * Q); b0 1 alpha * A; b1 -2 * cos(w0); b2 1 - alpha * A; a0 1 alpha / A; a1 -2 * cos(w0); a2 1 - alpha / A; b [b0, b1, b2] / a0; a [a0, a1, a2] / a0; y filter(b, a, x); end这个滤波器的逻辑是在Fc附近形成一个可控制的峰或谷gaindB为正时提升为负时衰减Q控制峰的宽窄。放在变声链的最后一级等于在相位声码器合成完后给语音包一层“新的声道”。4.2 把变声链串起来升调加共振峰补偿假设目标是“偏明亮的少年音”可以升 9 个半音然后在中高频加一点亮度在低频压掉一点浑浊感[x, fs] audioread(input.wav); if size(x, 2) 1 x mean(x, 2); end x x / max(abs(x)); y voiceChanger(x, fs, 9); % 先变调不变速 y peakingEQ(y, fs, 1400, 3, 1.2); % 提亮中高频 y peakingEQ(y, fs, 280, -2, 1.0); % 减少低频浑浊 y y / (max(abs(y)) eps); audiowrite(bright_voice.wav, y, fs);1400Hz是元音共振峰常见的集中区域提升 2 到 4dB 就能明显感觉到声音“亮”起来但超过 6dB 容易变成刺耳的金属声。280Hz附近的低频对男声来说是胸腔共鸣区适度衰减会让声音更轻快。4.3 参数速查表不同目标角色不需要每次都从头试下面这组起点参数可以直接套。角色目标semitones滤波建议说明偏卡通/萝莉121.2kHz 提升 3dB重采样已经带高了共振峰不要再加太多高频偏少年感7 到 91.4kHz 提升 2dB300Hz 衰减 2dB保留一定低频厚度避免过薄偏沉稳男声-7 到 -12200Hz 提升 2dB1.2kHz 衰减 2dB注意不要低于 80Hz否则会出现箱体共鸣电话音0300Hz 到 3kHz 带通用bandpass或两级滤波实现滤波只是快速补救。如果要做严格的声音转换应该在基频重映射之后再对频谱包络做独立的共振峰搬移。代码包里如果同时有f0检测和共振峰估计函数建议优先走那条路。EQ 这种方案胜在稳定任何一段语音都能跑不容易爆音。5. 用扫频信号和语谱图验收变声器Matlab代码效果参数调完之后耳朵会骗人频率计不会。变声器最容易犯的错是听感觉得“好像变细了”实际基频并没有到目标半音数只是共振峰被 EQ 推高了。因此验收时先拿单频正弦波测变调精度再看语谱图确认基频和共振峰确实发生了预期移动。5.1 用正弦波验证变调比例先生成一个 220Hz 的纯音套用变声代码然后看输出主频是不是落在理论值附近。220Hz 升 7 个半音的理论值是fs 44100; t (0:fs-1) / fs; x 0.3 * sin(2 * pi * 220 * t); y voiceChanger(x, fs, 7); [pxx, f] periodogram(y, hann(length(y)), [], fs); [~, idx] max(pxx); expected 220 * 2^(7/12); fprintf(expected %.2f Hz, detected %.2f Hz\n, expected, f(idx));输出如果不是 311Hz 附近而是 300Hz 或者 330Hz问题几乎都出在rat(1/pitchRatio, 1e-3)的近似误差上。把容差改到1e-5可以更准但重采样因子会变大计算变慢。对语音来说几个音分的偏差可以接受正弦波测试只是用来确认方向没反。5.2 用语谱图看基频和共振峰真正的人声不是单频语谱图比波形更直观。横向亮条是共振峰纵向亮纹是基频周期。重采样变调会让横向亮条和纵向亮纹一起上移相位声码器只改变纵向亮纹的间距横向共振峰位置基本不变。figure; subplot(2, 1, 1); spectrogram(x, hann(512), 256, 2048, fs, yaxis); title(original); subplot(2, 1, 2); spectrogram(y, hann(512), 256, 2048, fs, yaxis); title(after voiceChanger);对比两幅图时先看低频第一条横向亮条。如果它已经让整段亮条整体抬高说明共振峰被重采样带走了。接下来再用peakingEQ把特定频段拉回来每改一次参数就重新看一次语谱图比反复播放试听更容易定位问题。5.3 批量处理一段语音目录最后一个实用技巧是把变声脚本包装成批处理循环。变声器代码包里通常有很多测试 wav手工一个个跑既不安全也不可复现。建议输出到独立目录不覆盖原始文件mkdir(out); files dir(fullfile(voice, *.wav)); for i 1:numel(files) [x, fs] audioread(fullfile(files(i).folder, files(i).name)); y voiceChanger(x, fs, 7); % 如果角色效果需要可以在这一步追加 peakingEQ y peakingEQ(y, fs, 1400, 2, 1.2); audiowrite(fullfile(out, files(i).name), y, fs); end批量处理时最好把semitones和滤波参数都提到脚本头部作为变量这样调试完一组参数后整个语料库能一次性重出。遇到某个 wav 特别爆音时单独降一下该文件的max(abs(y))归一化门限就够了不需要改主算法。voiceChanger函数里已经做了归一化所以批量场景下最该关注的是 EQ 增益超过 6dB 时先怀疑削波再怀疑相位声码器参数。本文还有配套的精品资源点击获取