变声处理后的音频经常出现两种典型失真:一类是高频段带有明显的金属摩擦感,另一类是低频段出现断续的抖动或气泡声。很多项目在处理链中加入重采样以后,听感没有明显改善,于是容易把问题简单归因为采样率不够高。实际上,变声的自然度由采样率与相位声码器共同决定,尤其相位声码器内部的时间伸缩和相位传播方式,对最终音色的影响远大于采样率从44.1kHz提升到48kHz带来的变化。理清这两者的关系,才能针对性地降低不自然感。

采样率:决定带宽,但重采样质量更关键
采样率直接决定了系统能够表示的频率上限。根据奈奎斯特采样定理,44.1kHz采样率可以完整保留约20kHz以下的频率成分,48kHz则能覆盖到24kHz。对于人声来说,基频通常位于80Hz到300Hz,主要能量集中在4kHz以下,辅音和齿音则延伸到8kHz甚至更高。因此从带宽角度看,44.1kHz和48kHz都足以承载原始语音信息,单纯把采样率从44.1kHz换成48kHz不会让变声自然度发生质变。
问题往往出现在重采样环节。很多实时变声链路会混合使用44.1kHz的模型输入和48kHz的声卡输出,如果没有做高质量重采样,而是采用线性插值或简单抽取,就会产生镜像频率和高频衰减。非整数倍转换例如44.1kHz到48kHz,理想的实现是先升采样到两者最小公倍数,再降采样,并配合抗混叠滤波器;直接使用低阶插值会让高频听感发毛。工程上应优先统一声卡、模型和效果链的采样率,避免反复重采样。
以下代码展示基于多相滤波的高质量重采样,它比线性插值更干净:
import numpy as np
from scipy import signal
def high_quality_resample(x, src_rate, dst_rate):
gcd = np.gcd(src_rate, dst_rate)
up = dst_rate // gcd
down = src_rate // gcd
return signal.resample_poly(x, up, down)
x_48 = np.random.randn(48000)
x_44 = high_quality_resample(x_48, 48000, 44100)
这段代码的核心是计算升采样倍数和降采样倍数,并调用多相滤波重采样函数。相比在时域直接用线性插值,多相滤波可以抑制镜像,减少高频毛刺对变声结果的污染。
相位声码器:不自然的主要来源
相位声码器的工作流程可以概括为短时傅里叶变换、幅度相位处理、逆变换。它先把信号分成重叠的短帧,每一帧加窗后做FFT,得到频域的幅度谱和相位谱。要改变语速或音高,就需要在帧与帧之间重新安排时间步长,并根据原始相位差计算合成帧的新相位。这个过程中,任何相位累积误差都会被后续帧放大,最终表现为明显的音色劣化。
造成不自然的机制主要有两类。第一类是相位失相干:理想情况下同一频率bin在相邻帧之间存在稳定的相位增量,但如果有噪声、瞬态或窗函数选择不当,相位差会偏离理论值,合成时出现高频金属感和低频抖动。第二类是瞬态模糊:相位声码器假设帧内信号近似稳态,但语音中的爆破音、齿音和打击声是瞬态信号,它们的能量变化远快于一个帧长。对这些瞬态直接做相位传播,会把原本短促的冲击铺开成绵软的噪声,听感上就不够干净。
下面是一段简化版的相位传播实现,它通过归一化相位差来模拟相位声码器的核心计算:
import numpy as np
import librosa
n_fft = 2048
hop_length = 512
y = librosa.load('speech.wav', sr=48000)[0]
D = librosa.stft(y, n_fft=n_fft, hop_length=hop_length)
mag, phase = librosa.magphase(D)
omega_bin = 2 * np.pi * np.arange(n_fft // 2 + 1) / n_fft
omega_hop = omega_bin * hop_length
phase_accum = np.copy(phase[:, 0])
for t in range(1, phase.shape[1]):
delta = phase[:, t] - phase[:, t - 1] - omega_hop
delta = np.mod(delta + np.pi, 2 * np.pi) - np.pi
phase_accum = phase_accum + omega_hop + delta
phase[:, t] = phase_accum
D_new = mag * np.exp(1j * phase)
y_out = librosa.istft(D_new, hop_length=hop_length)
实际工程中的相位声码器会比这段代码复杂得多,例如加入峰值检测、相位锁定和瞬态检测,但核心思路一致:通过帧间相位差估计瞬时频率,再把相位累积到新时间轴。错误累积就是从这里开始的。
工程实践:从参数到算法的改善路径
窗长与FFT点数是影响听感最直接的参数。较大的窗长能提高频率分辨率,让低频谐波更清晰,但时间分辨率下降,瞬态容易模糊;较小的窗长能保留更多瞬态,却可能让低频基频的能量分散到相邻bin,反而产生粗糙感。语音变声通常选择1024或2048点FFT,在48kHz采样率下对应约21毫秒或42毫秒窗长,hop size一般取窗长的四分之一。这个配置能够在频率分辨率和瞬态保留之间取得折中。
如果高频金属感明显,可以尝试相位锁定。相位锁定会以每个频谱峰值为中心,把峰值附近若干bin的相位统一到峰值相位上,减少同一谐波内部多个bin的相位冲突。另一个有效手段是预加重:在处理前对信号做一阶高通,提升高频分量,处理完成后再用对称滤波器恢复。这能避免相位声码器在高频段放大噪声,也更容易控制齿音的锐度。
下面给出预加重和去加重的通用实现,适合放在STFT之前和ISTFT之后:
import numpy as np
def pre_emphasis(x, coeff=0.97):
out = np.copy(x)
out[1:] = x[1:] - coeff * x[:-1]
out[0] = x[0]
return out
def de_emphasis(y, coeff=0.97):
out = np.copy(y)
for i in range(1, len(y)):
out[i] = y[i] + coeff * out[i - 1]
return out
此外,如果变声后音色变得像另一个人的声道,而不是单纯音高变化,通常是因为共振峰被等比例移动。人声的自然度很大程度取决于共振峰位置,音高移动时最好保持共振峰包络不变。可以在频域估计频谱包络,将音高变换与包络调整分离;或者使用专门的共振峰保持算法,让基频移动但声道形状保留。
调试与听感验证:避免盲目调参
调试不自然问题时,建议先固定采样率链路,再调整STFT与相位策略。顺序上可以做三组对比:原始音频直接变调、加入高质量重采样后变调、在固定采样率下只调整相位声码器参数。这样可以快速判断失真来自重采样还是相位处理。频谱图是最直观的工具,高频区域如果出现密集的竖线或颗粒状亮点,通常说明相位累积出错;低频区域如果有横向断裂,可能是帧间不连续导致。
客观指标如PESQ或STOI在变声评价中只能作为参考,因为它们主要针对语音可懂度和整体失真,对金属感和自然度的敏感度有限。更可靠的方式是固定一批包含清音、浊音、爆破音和长元音的测试片段,做盲听打分。尤其要重点测试s、sh、t、p这类高频或瞬态丰富的音素,它们最容易暴露相位声码器的问题。
如果条件允许,可以在调试时加入瞬态检测和相位重置。当检测到能量在短时间发生剧烈变化时,不沿用上一帧的相位累积,而是直接使用当前帧的原始相位作为起点。这能显著改善爆破音和齿音的清晰度,代价是可能引入轻微的不连续,但配合短窗和交叉淡化,通常能获得更自然的听感。