简介:一个基于MATLAB的变声器程序代码包,面向计算机、电子信息工程、数学等专业学生,适合课程设计、期末大作业或毕业设计中的音频信号处理实践。代码兼容MATLAB 2014、2019a、2021a,采用参数化编程,参数调整便捷,注释清晰,便于理解变声原理并自行修改音调、速度等效果。压缩包共含4个文件,包括2个M脚本与2个MP3示例音频:脚本提供算法实现与运行入口,音频用于输入样音测试,整体仅5.82MB。目前已有209人学习或下载。资源内附可直接运行的案例数据,结合原始音频与处理脚本可快速复现变声效果;通过阅读注释还能掌握核心信号处理流程,为后续扩展或二次开发提供基础。
1. 变声器matlab代码能干什么:先把音高、语速和音色分开
从网上下载的变声器matlab代码.zip,多半是课程设计或毕设:几个 .m 文件,读 wav、改采样率、sound 播放。跑通很容易,可把音高拉到两倍再播,得到的不是“另一个人”,而是语速飞快的花栗鼠。问题不在代码,在把变声理解成了单一操作。
变声要处理三个彼此独立的量:基频决定音高,时长决定语速,声道形状决定音色。男变女、机器音,本质是把三者拆开各自搬到目标位置。改音高用重采样或相位声码器,改语速用时间拉伸,改音色要搬移共振峰,实现路径和参数完全不同。网上绝大多数 matlab 教程讲到 FFT 就停了,变声器恰恰是把 FFT、LPC、滤波器设计串起来的一个完整闭环。
这套内容适合两类人:被“变声器源码”吸引、想从“能跑”升级到“能调”的 matlab 新手,以及做语音信号处理、把它当信号处理综合练习的从业者。下面按重采样、相位声码器、共振峰、实时与验证的顺序走。
2. 重采样改音高:变声器matlab代码里最基础的 30 行
2.1 读入音频并先看基频:变声前要有一把“尺子”
拿到任何 wav 第一步不是急着改,而是先摸清现状。常见变声器代码里直接用audioread读整个文件,但很多录音是双声道、带直流偏置的,直接处理会产生低频噗噗声。我一般会先压成单声道,再做一次基频统计,后面的 pitch_ratio 才有个基准值。
[x, fs] = audioread('voice.wav'); if size(x, 2) > 1 x = mean(x, 2); % 双声道压成单声道,变声器按单声道处理更稳 end x = x(:); % 强制转列向量 [f0, t0] = pitch(x, fs); % 基频分析,需要 Audio Toolbox f0 = f0(f0 > 0); % 丢掉静音帧,否则中位数会被大量 0 拉低 disp(['原始基频中位数: ', num2str(median(f0))]);pitch输出的单位是 Hz,它背后是自相关加窗分段估计,静音段返回 0,所以先过滤再去算中位数。用median而不是mean,是因为语音里有大量清音段(s、f 这些音没有稳定基频),均值会被极端值带偏。这段输出就是你的“尺子”:成年男声基频中位数通常在 100~150 Hz,女声在 180~260 Hz,儿童更高。后面改完再跑一遍这段,对比中位数变化,就知道音高到底动了多少。
2.2 重采样因子换算:pitch_ratio 与时长变化的关系
改音高最直接的做法是重采样。resample(x, p, q)的含义是输出采样数是输入的 p/q 倍,注意这个比例是反直觉的:想让音高变高,得减少采样数。目标音高是原来的 pitch_ratio 倍,输出采样数就要变成原来的 1/pitch_ratio,于是 p/q = 1/pitch_ratio。
pitch_ratio = 1.4; % 目标音高是原来的 1.4 倍 out = resample(x, 1000, round(1000 * pitch_ratio)); % 关键:播放采样率仍是原来的 fs,时长变为原来的 1/pitch_ratio sound(out, fs); audiowrite('pitch_up.wav', out / max(abs(out)), fs);用 1000 做中间整数,是为了把小数的 pitch_ratio 转成整数参数时保持精度,round(1000 * pitch_ratio)的误差在千分之一以内。注意resample内部带抗混叠滤波器,这比用interp1做线性插值干净得多,后者会在频谱上产生镜频,听感像“含着一口水说话”。播放和写文件时,采样率一律沿用原始 fs,这是整个变调逻辑成立的前提。
| pitch_ratio | 输出/输入采样数 | 听感方向 | 时长变化 |
|---|---|---|---|
| 0.7 | 1000/700 | 低沉、人声变“厚” | 拉长到 1.43 倍 |
| 1.0 | 1000/1000 | 原声 | 不变 |
| 1.4 | 1000/1400 | 偏高、语速明显变快 | 缩短到 0.71 倍 |
| 2.0 | 1000/2000 | 经典花栗鼠 | 只剩一半 |
2.3 变速副作用:单靠重采样做不出“变调不变速”
上面的表暴露了重采样的硬伤:音高和时长是绑定的。变声最常被要求的两种效果——变调但语速不变、语速变但音调不变——单靠resample都做不到。有人试图用resample改完后再用interp1把时长拉回去,结果两轮插值叠加,频谱混叠和相位错位一起出现,出来的声音像坏掉的收音机。
提示:任何只靠“重采样 + 补时长”的组合,本质上都没有解耦音高和语速。解耦要靠短时傅里叶变换域上的相位处理,也就是第三章的相位声码器。
另一个实际问题是性能。resample的 q 参数很大时内部滤波会显著变慢,对一首三分钟的歌全量处理很吃亏。调试阶段建议用audioread('voice.wav', [1 2*fs])只读前两秒,参数调好再处理整段。
3. 相位声码器:变声器matlab代码不丢语速的核心
3.1 STFT 帧、hop 与相位传播
相位声码器的基础是把语音切成重叠的短帧,每帧做 FFT,然后调整合成时的帧移。先定义三个量:帧长 N、分析帧移 H、合成帧移 Hout。时间拉伸系数 alpha 就是 Hout/H。相邻分析帧之间,同一个频点 k 的相位理论上应该增加2*pi*k*H/N,但由于窗函数截断和语音非平稳性,实际增量会有偏差。
相位声码器要做的,是把“分析相位增量”和“目标相位增量”之间的差包络到主值区间后补回去,再按 Hout 累加。写成伪代码就是:合成相位 = 上一帧合成相位 + Hout 对应的自然增量 + 包络后的相位残差。这一步看起来只是中学三角函数的计算,实际上它决定了合成语音有没有“金属味”。不做相位修正、直接对帧做 overlap-add,谐波之间会互相抵消,声音发虚、发颤。
帧参数的选择直接决定质量。N=1024、H=256 意味着 75% 重叠,频率分辨率约 43 Hz(@44.1 kHz),对 80~300 Hz 的语音基频来说够用。N 再大会把瞬态抹平,N 太小则低频谐波站不稳。
3.2 可直接跑的时间拉伸函数
这段函数是很多变声器源码里pvoc的简化版,不依赖istft,老版本 matlab 也能跑。新版 Signal Processing Toolbox 里已经有现成的istft,但在理解原理这件事上,自己写循环更划算:
function y = pv_stretch(x, alpha) % 相位声码器时间拉伸:alpha > 1 变慢,alpha < 1 变快 N = 1024; H = 256; % 帧长与分析帧移 win = hann(N, 'periodic'); x = [zeros(N,1); x(:); zeros(N,1)]; % 首尾补零,避免边缘帧能量塌陷 nf = floor((length(x) - N) / H); specA = zeros(N, nf); for m = 1:nf % 分析阶段:逐帧 FFT seg = x((m-1)*H+1 : (m-1)*H+N) .* win; specA(:,m) = fft(seg); end Hout = round(alpha * H); % 合成帧移 ylen = nf * Hout + N; y = zeros(ylen, 1); wsum = zeros(ylen, 1); half = N/2 + 1; phi = angle(specA(1:half, 1)); % 初始合成相位 omega = 2*pi*(0:half-1)' * H / N; % 相邻分析帧的自然相位增量 pos = 1; for m = 1:nf % 合成阶段:相位累积 + OLA Cur = specA(1:half, m); if m > 1 dphi = angle(Cur) - angle(specA(1:half, m-1)) - omega; dphi = mod(dphi + pi, 2*pi) - pi; % 卷绕到 (-pi, pi] phi = phi + omega * (Hout/H) + dphi; end newspec = abs(Cur) .* exp(1i*phi); fullspec = [newspec; conj(newspec(end-1:-1:2))]; % 补共轭对称 frame = ifft(fullspec); idx = pos : pos+N-1; y(idx) = y(idx) + real(frame) .* win; wsum(idx) = wsum(idx) + win.^2; % 归一化累积 pos = pos + Hout; end y = y ./ max(wsum, 1e-10); % 抑制除以零 y = y(N+1 : end-N); % 去掉补零 end参数上最值得琢磨的是dphi那两行。mod(dphi + pi, 2*pi) - pi把相位差限制在 ±π 之间,这一步跳过了,叠加时帧间相位就会互相打架。abs(Cur)保留原幅度谱,幅度谱在相邻帧之间变化小,这也是相位声码器能在音质上胜过直接 OLA 的根本原因。alpha 超过 2 后,单帧相位信息不足以支撑大拉伸,语音会明显变“糊”,这是方法本身的边界。
3.3 时间拉伸与重采样组合:任意音高且不改变时长
相位声码器只负责“变时长不动音高”,要“变调不动语速”,组合这两章的工具即可:先用真空带声码器拉伸 alpha 倍,再用resample把采样数压回原来的量级。拉伸把时长拉长,重采样把时长压回去的同时把音高抬上来:
alpha = 1.4; % 目标音高比例,也是时间拉伸因子 slow = pv_stretch(x, alpha); % 语速变慢,音高不变 y = resample(slow, 1000, round(1000 * alpha)); % 压缩回原时长,音高升高 1.4 倍 sound(y, fs);这里resample的因子是 1000/(1000×alpha),也就是 1/alpha,和第二章里的方向相反,因为pv_stretch已经先把时长变长了。两步合起来,输出的时长约等于输入,基频变为 alpha 倍。这套“先拉伸再压缩”的两步法,是课程设计和开源变声器代码里最常见的音高偏移实现,比直接在频域搬移谱线稳定得多。
| 目标效果 | 第一步 | 第二步 |
|---|---|---|
| 变调不变速 | pv_stretch(x, r) | resample(slow, 1000, round(1000*r)) |
| 变速不变调 | pv_stretch(x, r) | 直接输出 |
| 都变、带原始时长约束 | resample(x, 1000, round(1000/r)) | pv_stretch(·, r) |
第二行值得单独记:相位声码器本身就能做到变速不变调,这是它相对重采样最核心的卖点。第三行顺序反过来的做法适合想先改音色再校正时长的场景,但相位声码器对非语音信号(比如打击乐)效果很差,瞬态会被相位累积拖成混响尾巴,做音乐素材时要谨慎。
4. 共振峰搬移:让变声器matlab代码从“音高变了”到“人变了”
4.1 只改音高为什么像花栗鼠
把基频中位数从 120 Hz 抬到 200 Hz,听起来却不像女声,而是像动画片里的角色。原因是共振峰没动。频谱图上,谐波是一根根竖线,间距等于基频;共振峰是包络线上的鼓包,由声道形状决定。男人和女人的声道长度差大约 15%~20%,这个长度差直接映射到共振峰位置:F1 大概在 500 Hz 附近,F2 在 1500 Hz 附近,女人的普遍比男人的高 15%~20%。
重采样改音高时,竖线整体平移,但包络的鼓包还留在原地。结果就是谐波和共振峰的“相对关系”被破坏了,大脑收到的是“一个喉咙没变的生物在发高音”——这正是花栗鼠效果的来源。所以专业变声器里,音高和共振峰永远是分开调的两个旋钮,pitch_ratio 管“多高”,共振峰因子 gamma 管“像谁”。
4.2 用 LPC 抽出声道形状:lpc 与 freqz 看共振峰
线性预测编码(LPC)是拿声道建模最便宜的手段。它的假设是:当前语音采样可以由过去 p 个采样的线性组合预测出来,预测误差就是声门激励。预测误差信号叫残差,滤波器系数 a 的幅度响应就是声道包络。lpc一行就能把声源和声道解耦,这也是为什么多数变声器源码里都有 LPC 相关函数的根本原因。
xd = resample(x, 16000, fs); % 先统一降到 16k,LPC 估计更稳 xd = xd - mean(xd); % 去直流,防止低频能量污染包络 p = 18; % LPC 阶数:16k 采样常用 16~20 [a, g] = lpc(xd, p); % a 是声道逆滤波器系数,g 是增益 [H, F] = freqz(g, a, 512, 16000); figure; plot(F, 20*log10(abs(H))); % 谱包络,鼓包就是共振峰为什么要先降到 16 kHz?44.1 kHz 采样下语音能量集中在 8 kHz 以下,高频段几乎全是噪声和齿音,LPC 会把阶数浪费在拟合噪声上。降到 16 kHz 后,p 取 18 就能覆盖前四五个共振峰。阶数太低(小于 12),F1 和 F2 会糊成一个鼓包;太高(大于 30)会出现一堆假峰,搬移时把噪声也放大了。经验公式是 p ≈ fs/1000 + 4,16k 采样取 20 以内都合理。
4.3 频率轴缩放搬移共振峰:interp1 与最小相位重建
搬移共振峰的本质,是改变声道滤波器的频率响应,而不是改变残差的基频。常规做法是把包络的频轴整体做缩放:gamma 大于 1,包络上的鼓包整体往高频移,声音变“女生”;小于 1 则整体下压。频轴缩放后直接反变换会得到非因果滤波器,所以要先做最小相位化,把幅度谱转成对应的因果冲激响应,这一步是“能出声”和“能听”的分界线。
gamma = 1.2; % >1 共振峰上移(偏女声),<1 下移 K = 2048; Hw = freqz(g, a, K, 16000); % 取复数包络,后面要保留相位信息 f_axis = linspace(0, 8000, K).'; % 包络频轴整体缩放:原频率 f 的鼓包被搬到 f*gamma 处 f_warp = min(f_axis / gamma, 8000); mag_warp = interp1(f_axis, abs(Hw), f_warp, 'linear', abs(Hw(end))); % 最小相位重建:对对数谱做 ifft,再把非因果部分的系数翻倍 c = real(ifft(log(mag_warp + eps))); c(2:end) = 2 * c(2:end); % 最小相位化 h_new = real(ifft(exp(fft(c)))); h_new = h_new(1:256); % 截断成 256 点 FIR 滤波器 % 残差激励保留基频信息,用新声道滤波器重建 resid = filter(a, 1, xd); y_f = fftfilt(h_new, resid); y_f = y_f / max(abs(y_f)); audiowrite('formant_shift.wav', y_f, 16000);interp1的最后一个参数abs(Hw(end))是频轴超出 8 kHz 时的兜底值,不写的话会填充 NaN,整段输出变静音。最小相位化那句c(2:end) = 2*c(2:end)是倒谱域的常规操作,把倒谱系数翻倍等价于把零点和极点归到单位圆内。截断到 256 点是为了控制 FIR 长度,太长低频会共振发闷,太短(小于 64)包络细节丢失。
4.4 三参数组合:男变女、女变男的实际取值与翻车点
| 目标 | pitch_ratio | gamma | 翻车点 |
|---|---|---|---|
| 男→女 | 1.6~1.8 | 1.15~1.3 | 只调音高不调 gamma,出来是“小孩声” |
| 女→男 | 0.6~0.75 | 0.85~0.95 | gamma 不跟着降,像“女声硬压低” |
| 变童声 | 1.5 | 1.4~1.5 | gamma 过 1.5 后齿音变刺 |
组合顺序也影响结果。常见做法是先做第二章的重采样把音高抬上去,此时共振峰也被连带搬动了,再用 LPC 估一次包络、把 gamma 拉回目标位置。这相当于“先把整体抬起来,再把喉咙的形状单独修回去”,比反过来先搬共振峰再调音高更不容易引入金属感。gamma 超过 1.4 后,高频增益会明显抬升,齿音和气流声被放大,听起来像加了激励器的电子音——这是共振峰搬移的听感边界。
5. 把变声器matlab代码接到麦克风和文件:实时框架、参数与验证
5.1 从 wav 到麦克风:audioDeviceReader 的最小实时框架
离线处理只能做文件到文件,想对着麦克风实时变声,需要换一套 I/O 接口。Audio Toolbox 里audioDeviceReader负责从声卡拉音频块,audioDeviceWriter负责推给播放设备。下面这段是能跑的最小循环:
adr = audioDeviceReader('SampleRate', fs, 'SamplesPerFrame', 1024); adw = audioDeviceWriter('SampleRate', fs); disp('开始变声,Ctrl+C 退出'); while true in = adr(); % 取一帧 1024 点 out = realtime_shift(in, fs, 1.2); % 按帧做变调 adw(out); drawnow; end这里的坑在于,第三章的pv_stretch是整个文件级的处理,不能直接塞进 1024 点的帧里。实时场景需要维护一个长度约 3 到 4 帧的重叠缓冲,每次只处理中间部分再滑窗,这等于把相位声码器改成流式 OLA 版本。只做重采样的话延迟很低,但音高和时长还是绑定的。实测 1024 帧长在 44.1 kHz 下设备缓冲约 23 ms,处理耗时通常在 5 ms 以内,声音基本感觉不到延迟。
5.2 三个必调参数与听感对照表
| 参数 | 默认值 | 常用范围 | 调大后的听感 |
|---|---|---|---|
| pitch_ratio | 1.2 | 0.5~2.0 | 越高越尖,超过 1.8 容易破音、出现明显共振峰失真 |
| gamma | 1.1 | 0.8~1.4 | 越高越像女性/卡通,超过 1.4 齿音发刺 |
| N / H | 1024/256 | N 取 512~2048,H 取 N/4 | N 太大谐波干净但瞬态糊,太小基频抖动 |
调试顺序我一般是固定的:先动 pitch_ratio 找到“性别感”,再动 gamma 找到“人感”,最后动帧长解决“糊涂”或“发虚”。每次只改一个参数,用 5.3 的频谱图确认,而不是靠耳朵一遍遍试听——人耳对音色的记忆只有几秒,来回切换时很容易误判。
5.3 用频谱图和基频轨迹验证变声效果
调参之后别急着验收,先把验证脚本跑一遍。基频中位数能确认 pitch_ratio 是否真的生效,频谱图能同时看出谐波间距和共振峰包络的位置变化:
[f0x, ~] = pitch(x, fs); [f0y, ~] = pitch(y, fs); fprintf('原始基频中位: %.1f Hz\n', median(f0x(f0x>0))); fprintf('变声后基频中位: %.1f Hz\n', median(f0y(f0y>0))); figure('Position', [100 100 900 600]); subplot(2,1,1); spectrogram(x, hann(512), 384, 1024, fs, 'yaxis'); title('原始语音'); subplot(2,1,2); spectrogram(y, hann(512), 384, 1024, fs, 'yaxis'); title('变声后');读图时看两处:横向的亮线是谐波,相邻亮线间距应接近原始基频乘上 pitch_ratio;纵向的暗色鼓包包络是共振峰,鼓包位置应朝 gamma 方向整体平移。如果基频中位数达标但包络鼓包没动,就是只做了第二章没做第四章的典型症状。最后检查max(abs(y)),保持在 0.9 以下,超过 1.0 的削波会额外引入高频刺音。整套验证脚本固定下来后,每次调参都能量化对比,远比一边改 pitch_ratio 一边反复试听来得快。
本文还有配套的精品资源,点击获取