生成带稳定10Hz颤音的440Hz正弦波:代码问题排查
问题排查与修复:颤音幅度随时间增大的音频生成问题
问题描述
尝试生成包含440Hz正弦波的.wav文件,该正弦波带有10Hz颤音,音高在430Hz至450Hz之间。但实际生成的音频中,颤音的音高变化幅度随时间逐渐增大,无法稳定保持在目标区间内。
原实现代码:
import math import wave import struct SAMPLE_RATE = 44100 NOTE_PITCH_HZ = 440.0 # 基准音高,Hz VIBRATO_HZ = 10.0 # 颤音频率,Hz VIBRATO_VARIANCE_HZ = 10.0 # 颤音相对于基准音高的变化范围,±Hz NOTE_LENGTH_SECS = 2.0 # 生成音频的时长,秒 NUM_SAMPLES = int(SAMPLE_RATE * NOTE_LENGTH_SECS) # 生成正弦波上的单个采样点 def _sine_sample(freq: float, sine_index: int): return math.sin(2.0 * math.pi * float(freq) * (float(sine_index) / SAMPLE_RATE)) samples = [] for i in range(NUM_SAMPLES): # 生成颤音的正弦值,映射到 -VIBRATO_VARIANCE_HZ 到 VIBRATO_VARIANCE_HZ 范围 vibrato_level = _sine_sample(VIBRATO_HZ, i) vibrato_change = vibrato_level * VIBRATO_VARIANCE_HZ # 根据颤音调整当前音高 note_pitch = NOTE_PITCH_HZ + vibrato_change sample = _sine_sample(note_pitch, i) * 32767.0 # 将振幅降低到80% samples.append(int(sample * 0.8)) # 创建单声道wav文件:2秒440Hz音调,带10Hz颤音,音高变化±10Hz(430-450Hz) with wave.open("vibrato.wav", "w") as wavfile: wavfile.setparams((1, 2, SAMPLE_RATE, NUM_SAMPLES, "NONE", "not compressed")) for sample in samples: wavfile.writeframes(struct.pack('h', sample))
问题原因
核心问题在于相位计算方式错误:
- 原代码中每次调用
_sine_sample时,都用当前采样索引i直接计算相位,即2π * freq * i/SAMPLE_RATE。 - 当颤音改变频率时,这种方式相当于每次都重新从0开始计算新频率的正弦波,导致相位不连续,相位误差随时间累积。最终表现为音高变化的幅度逐渐失控,本质是相位的“漂移”问题。
- 正确的逻辑应该是:频率是相位的变化率,需要维护一个累积相位变量,每次根据当前频率计算相位增量,逐步累加,再用累积相位计算正弦值。
修复后的代码
import math import wave import struct SAMPLE_RATE = 44100 NOTE_PITCH_HZ = 440.0 # 基准音高,Hz VIBRATO_HZ = 10.0 # 颤音频率,Hz VIBRATO_VARIANCE_HZ = 10.0 # 颤音相对于基准音高的变化范围,±Hz NOTE_LENGTH_SECS = 2.0 # 生成音频的时长,秒 NUM_SAMPLES = int(SAMPLE_RATE * NOTE_LENGTH_SECS) samples = [] # 初始化累积相位,用于主正弦波的连续计算 current_phase = 0.0 for i in range(NUM_SAMPLES): # 生成颤音的正弦值,映射到 -VIBRATO_VARIANCE_HZ 到 VIBRATO_VARIANCE_HZ 范围 vibrato_level = math.sin(2.0 * math.pi * VIBRATO_HZ * (i / SAMPLE_RATE)) vibrato_change = vibrato_level * VIBRATO_VARIANCE_HZ # 根据颤音调整当前音高 note_pitch = NOTE_PITCH_HZ + vibrato_change # 计算当前频率对应的相位增量 phase_increment = 2.0 * math.pi * note_pitch / SAMPLE_RATE # 累加相位 current_phase += phase_increment # 可选:对相位取模2π,防止数值过大影响计算精度 current_phase %= 2.0 * math.pi # 计算当前采样点的正弦值并调整振幅 sample = math.sin(current_phase) * 32767.0 * 0.8 samples.append(int(sample)) # 创建单声道wav文件 with wave.open("fixed_vibrato.wav", "w") as wavfile: wavfile.setparams((1, 2, SAMPLE_RATE, NUM_SAMPLES, "NONE", "not compressed")) # 一次性打包所有采样点,提升写入效率 wavfile.writeframes(struct.pack('h' * NUM_SAMPLES, *samples))
关键修改点
- 移除了原
_sine_sample函数,改为维护current_phase累积变量,保证相位连续。 - 每次循环计算当前频率对应的
phase_increment并累加到相位中,从根源避免相位漂移。 - 增加了相位取模操作,防止数值过大导致计算精度下降。
- 优化了wav文件写入逻辑,一次性打包所有采样点,提升效率。
内容的提问来源于stack exchange,提问作者Erik Nyquist
相关产品推荐
相关产品推荐

