You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

生成带稳定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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 13:30:29