news 2026/9/11 21:53:43

变声器Matlab代码实战:重采样、相位声码器与共振峰控制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
变声器Matlab代码实战:重采样、相位声码器与共振峰控制

简介:变声器Matlab代码包面向计算机、电子信息工程、数学等专业的大学生,主要服务课程设计、期末大作业和毕业设计中的音频变声课题,可帮助读者快速获得一套可运行、可修改的算法实现,规避自行编写时常见的参数混乱与调试困难。压缩包共4个文件,包含2个m脚本和2个mp3音频样例,整体体积仅5.82MB;m脚本分别承担核心变声处理与辅助控制逻辑,采用参数化编程,关键参数可方便更改,mp3样例则用于输入输出效果试听。代码注释明细、结构清晰,从音频读取、变声处理到结果播放保存的完整流程均有体现,并附带案例数据,直接运行即可看到效果。对初学信号处理与MATLAB编程的读者尤其友好,可作为理解变声器基本原理的实践入口。目前已有209人学习下载,适合作为课程实训或毕业设计的直接参考。

1. 变声器Matlab代码到底在改什么

解压一个叫“变声器matlab代码.zip”的文件包,最常见的状态是:README 里写着直接运行 main.m,运行完发现声音只是快了一点,或者高了一点,和“男声变女声”“变成机器人”的预期差得很远。问题通常不在代码抄没抄对,而在变声这件事本质上是在同时操纵三个量:基频、时长和音色。基频决定音高,时长决定语速,音色里的共振峰位置决定声音像谁。三者的组合才是“变声器”,单独调一个只是“变速器”或“变调器”。本文按一条离线语音变声的常见技术路线展开:先用 resample 理解音高位移,再用相位声码器把变调与变速解耦,最后用滤波和测试信号把效果调出层次。适合刚拿到代码包、正在找入口的学生,也适合想从效果反推算法的 DSP 工程师。

2. 从重采样入手:先跑通一条Matlab变声代码

要读懂代码包里的.m文件,先要理解数字音频里的音高放在哪里。一个频率为 f0 的周期信号,在采样率 fs 下每个周期占 fs/f0 个采样点。比如 fs=44100、f0=200Hz,那每个周期大约 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,采样率参数仍写 fs

rat(1 / pitchRatio, 1e-3)的作用是把1/pitchRatio转成两个小的整数,因为resample需要整数重采样因子,纯小数不能直接传进去。例如升 12 个半音时pitchRatio=21/pitchRatio正好是1/2,重采样后点数减半;按原 fs 播放时音高高一倍,时长也缩成一半。

2.2 半音数比倍数更符合听感

直接把pitchRatio写成 1.5 也能跑,但音乐和语音里更常用“半音”来表达变调幅度。人耳对频率的感知接近对数刻度,升 200Hz 到 400Hz 和升 1000Hz 到 2000Hz 听起来是同样的“八度跨度”,所以变声参数建议用半音数,而不是频率倍数。

semitonespitchRatio听感参考重采样后时长
122.00高八度,明显角色化0.5T
71.50明显变细,适合做少年感0.67T
31.19轻度年轻化0.84T
01.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,作为整个变声脚本的时间伸缩模块。参数上先固定nfft=2048hopA=256,后面再解释怎么调。

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造成的“预期相位增量”,剩下的就是真实频率偏离理想频率的部分。合成时把deltahopS/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 相位声码器的必调参数

nffthopAalpha是影响结果最明显的三个参数。默认参数在大多数 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:提升/衰减 dB,Q:滤波器宽度 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 提升 2dB,300Hz 衰减 2dB保留一定低频厚度,避免过薄
偏沉稳男声-7 到 -12200Hz 提升 2dB,1.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 时先怀疑削波,再怀疑相位声码器参数。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 21:52:49

计算机JAVA毕设实战-基于 SpringBoot 人脸识别技术的医疗挂号系统的设计与实现 基于 SpringBoot 的智能医疗预约挂号平台【完整源码+LW+部署说明+演示视频,全bao一条龙等】

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/9/11 21:52:35

串口协议设计六要素:从能通到稳通的工业级实践

1. 为什么串口对接总在凌晨三点崩&#xff1f;——从“能通”到“稳通”的本质差距你有没有过这种经历&#xff1a;语音模块接上MCU&#xff0c;串口助手一发指令&#xff0c;LED闪了&#xff0c;喇叭“滴”一声&#xff0c;心里一喜——通了&#xff01;结果第二天产线测试&am…

作者头像 李华
网站建设 2026/9/11 21:52:04

35岁嵌入式工程师的真实去向与转型路径

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 21:50:49

上手 KVM 第一步!两种部署方式实操,新手该选哪一个?

文章目录安装KVM-方式1-系统自带安装KVM-方式2-yum安装KVM-方式1-系统自带 点击创建新的虚拟机 选择自定义 下一步 选择稍后安装操作系统 客户机操作系统选择Linux&#xff0c;版本选择CentOS 8 64位 虚拟机名称命名KVM1 处理器数量选择合适的数量&#xff0c;选择下一步 选择…

作者头像 李华
网站建设 2026/9/11 21:50:44

全球储能资本开支首超水电

储能&#xff0c;终于从「新能源的附庸」走到了舞台中央。IEA 最新修正案给出的一组数据&#xff0c;是这个转变的注脚——2026 年全球电网侧电化学储能资本开支将达 1100 亿美元&#xff0c;首次超过水电&#xff0c;成为仅次于风电、光伏的第三大清洁电力投资领域。而中国这边…

作者头像 李华
网站建设 2026/9/11 21:48:25

基于深度学习的智能合约漏洞检测:从Solidity到TextCNN的完整实践

简介&#xff1a;面向计算机、人工智能、自动化等专业学生与从业者的深度学习区块链智能合约安全检测毕设资源包&#xff0c;解决从零实现合约漏洞分析与安全检测模型搭建的难题&#xff0c;适用于毕业设计、课程设计、期末大作业或区块链安全方向入门实践。项目经调试验证可稳…

作者头像 李华