如何生成具有一致“颤音”的正弦波?

huangapple go评论75阅读模式
英文:

How can I generate a sine wave with consistent "vibrato"

问题

我正在尝试创建一个包含440Hz正弦波音调和10Hz颤音的.wav文件,颤音会使音高在430Hz和450Hz之间变化。我的方法肯定有问题,因为当我听生成的.wav文件时,它听起来像是颤音的振幅(例如颤音波形的峰值和谷值逐渐增加),而不是保持在430-450Hz之间。我的方法有什么问题?以下是一些最小的Python代码,用于说明这个问题:

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      # 要生成的.wav文件的长度,以秒为单位

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(在430Hz和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))

希望这可以帮助你找到问题所在。

英文:

I am trying to create a .wav file which contains a 440Hz sine wave tone, with 10Hz vibrato that varies the pitch between 430Hz and 450Hz. Something must be wrong with my approach, because when I listen to the generated .wav file, it sounds like the "amplitude" of the vibrato (e.g. the highest/lowest pitch reached by the peaks and troughs of the waveform of the vibrato) just progressively increases over time, instead of staying between 430-450Hz. What is wrong with my approach here? Here is some minimal python code which illustrates the issue:

import math
import wave
import struct

SAMPLE_RATE = 44100

NOTE_PITCH_HZ = 440.0        # Note pitch, Hz
VIBRATO_HZ = 10.0             # Vibrato frequency, Hz
VIBRATO_VARIANCE_HZ = 10.0    # Vibrato +/- variance from note pitch, Hz

NOTE_LENGTH_SECS = 2.0      # Length of .wav file to generate, in seconds

NUM_SAMPLES = int(SAMPLE_RATE * NOTE_LENGTH_SECS)

# Generates a single point on a sine wave
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):
    # Generate sine point for vibrato, map to range -VIBRATO_VARIANCE_HZ:VIBRATO_VARIANCE_HZ
    vibrato_level = _sine_sample(VIBRATO_HZ, i)
    vibrato_change = vibrato_level * VIBRATO_VARIANCE_HZ

    # Mofidy note pitch based on vibrato state
    note_pitch = NOTE_PITCH_HZ + vibrato_change
    sample = _sine_sample(note_pitch, i) * 32767.0

    # Turn amplitude down to 80%
    samples.append(int(sample * 0.8))

# Create mono .wav file with a 2 second 440Hz tone, with 10Hz vibrato that varies the
# pitch by +/- 10Hz (between 430Hz and 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))

答案1

得分: 1

以下是您要翻译的内容:

"A more straight forward approach that will accomplish what you want is to use a phasor (linear ramp that goes from 0 to 1 then shoots back down to 0) to look up the sin of that value. Then, you can control the amount the phasor increments (the frequency of vibrato)."

"这是代码。我降低了采样率以便更容易查看:"

"导入数学"
"导入matplotlib.pyplot as plt"

"采样率 = 10000"

"音符音高赫兹 = 100.0 # 音符音高,赫兹"
"颤音赫兹 = 20.0 # 颤音频率,赫兹"
"颤音方差赫兹 = 20.0 # 颤音音高的正负方差,赫兹"

"音符长度秒数 = 2.0 # 要生成的.wav文件的长度,秒"

"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))"

"phasor_state = 0"
"phasored_samples = []"
"samples = []"
"unmodulated_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"
" samples.append(_sine_sample(note_pitch, i)+5)"
" unmodulated_samples.append(_sine_sample(NOTE_PITCH_HZ, i))"
" phasored_samples.append(math.sin(2math.piphasor_state)+10)"
" phasor_inc = note_pitch/SAMPLE_RATE"
" phasor_state += phasor_inc"
" if phasor_state>=1:"
" phasor_state -=1"
"plt.plot(unmodulated_samples, label='未调制')"
"plt.plot(samples, label='不起作用')"
"plt.plot(phasored_samples, label='使用相位器')"
"plt.legend()"
"plt.show()"

"放大输出可以显示这些方法之间的差异:"

"请记住,这仍然不太正确。小提琴手或歌手会在更或多或少的线性轨迹上上下振动,而不是正弦轨迹。要更加“正确”(如果这是您的目标),应该计算相位增量的变化为三角波,而不是正弦波。"

英文:

A more straight forward approach that will accomplish what you want is to use a phasor (linear ramp that goes from 0 to 1 then shoots back down to 0) to look up the sin of that value. Then, you can control the amount the phasor increments (the frequency of vibrato).

Here is the code. I lowered the sampling rate to make it easier to look at:

import math
import matplotlib.pyplot as plt

SAMPLE_RATE = 10000

NOTE_PITCH_HZ = 100.0        # Note pitch, Hz
VIBRATO_HZ = 20.0             # Vibrato frequency, Hz
VIBRATO_VARIANCE_HZ = 20.0    # Vibrato +/- variance from note pitch, Hz

NOTE_LENGTH_SECS = 2.0      # Length of .wav file to generate, in seconds

NUM_SAMPLES = int(SAMPLE_RATE * NOTE_LENGTH_SECS)

# Generates a single point on a sine wave
def _sine_sample(freq: float, sine_index: int):
    return math.sin(2.0 * math.pi * float(freq) * (float(sine_index) / SAMPLE_RATE))

phasor_state = 0
phasored_samples = []
samples = []
unmodulated_samples = []
for i in range(NUM_SAMPLES):

    # Generate sine point for vibrato, map to range -VIBRATO_VARIANCE_HZ:VIBRATO_VARIANCE_HZ
    vibrato_level = _sine_sample(VIBRATO_HZ, i)
    vibrato_change = vibrato_level * VIBRATO_VARIANCE_HZ

    # Mofidy note pitch based on vibrato state
    note_pitch = NOTE_PITCH_HZ + vibrato_change
    samples.append(_sine_sample(note_pitch, i)+5)
    unmodulated_samples.append(_sine_sample(NOTE_PITCH_HZ, i))
    phasored_samples.append(math.sin(2*math.pi*phasor_state)+10)
    phasor_inc = note_pitch/SAMPLE_RATE
    phasor_state += phasor_inc
    if phasor_state>=1:
        phasor_state -=1
plt.plot(unmodulated_samples, label='unmodulated')
plt.plot(samples, label='not working')
plt.plot(phasored_samples, label='using phasor')
plt.legend()
plt.show()

A zoom in on the output shows you the difference between these approaches:
如何生成具有一致“颤音”的正弦波?

Keep in mind though, that this still isn't quite right. A violinist or vocalist will vibrate up and down in a more or less linear trajectory, not a sinusoidal one. To be more 'correct' (if that is what you are going for, that is) would be to compute the change in phase increment as a triangle wave, not a sinusoidal one.

答案2

得分: 1

你的错误是没有进行积分。

你不能简单地将瞬时频率与经过的时间相乘,也不能只取频率的正弦值,而没有时间成分。当频率变化时,简单地乘以会忽略振荡的历史(它已经旋转了多少)。积分会处理"行程"(旋转)。

这是一个使用 numpy 的脚本。我使用了较小的值,以便绘图看起来可读。

方法:

  • 生成每个样本时间的数组,通常很有用。
  • 生成每个样本频率的数组,这将会摇摆不定。
  • 在时间上积分,以得到相位
  • 缩放以获得弧度(它曾经是周期)
  • 在其上放一个 sin() 以获得幅度

一些参数:

NOTE_LENGTH_SECS = 0.2      # 要生成的.wav文件的长度,以秒为单位
SAMPLE_RATE = 10000

NOTE_PITCH_HZ = 100.0        # 音符音高,Hz
VIBRATO_HZ = 20.0             # 颤音频率,Hz
VIBRATO_VARIANCE_HZ = 20.0    # 与音符音高的变化范围,Hz

生成振荡:

t = np.arange(0, NOTE_LENGTH_SECS, 1 / SAMPLE_RATE)

freq = NOTE_PITCH_HZ + VIBRATO_VARIANCE_HZ * np.sin(2 * np.pi * VIBRATO_HZ * t)

# 积分。`∫ f dt` 其中 dt = 1/fs。
phase = np.cumsum(freq) / SAMPLE_RATE

# 将相位从周期转换为弧度
phase *= 2 * np.pi

# 生成波形
signal = np.sin(phase)

绘图:

plt.figure(figsize=(12, 4))
plt.plot(t, signal * 20, label='Signal') # 为了可见性而放大
plt.plot(t, freq, label='Frequency')
plt.xlabel('时间(秒)')
plt.ylabel('振幅')
plt.show()

如何生成具有一致“颤音”的正弦波?

健全性检查:

  • 我要求100 Hz 的频率,带有一些摆动,0.2 秒,所以应该是 20 个周期。看起来是这样的。

  • 摆动以 20 Hz 震荡。在 0.2 秒内,这就是 四次 摆动。看起来是这样的。

  • 摆动范围在 120 到 80 Hz 之间,所以峰值之间的间距应该是类似 3:2 的。看起来是这样的。

英文:

Your mistake was not integrating.

You can't just take instantaneous frequency and multiply it by elapsed time, nor can you just take the sine of a frequency without any time component in it. When frequency changes, simply multiplying would ignore the history of the oscillation (how much it has spun already). Integrating takes care of the "distance traveled" (spun).

Here's a script using numpy. I used smaller values so the plotting looks readable.

Approach:

  • generate per-sample array of time. generally useful.
  • generate per-sample array of frequency. that's going to wobble.
  • integrate the frequency over time, to get phase.
  • scale to get radians (it used to be cycles)
  • slap a sin() on it to get amplitude

Some parameters:

NOTE_LENGTH_SECS = 0.2      # Length of .wav file to generate, in seconds
SAMPLE_RATE = 10000

NOTE_PITCH_HZ = 100.0        # Note pitch, Hz
VIBRATO_HZ = 20.0             # Vibrato frequency, Hz
VIBRATO_VARIANCE_HZ = 20.0    # Vibrato +/- variance from note pitch, Hz

Generating the oscillation:

t = np.arange(0, NOTE_LENGTH_SECS, 1 / SAMPLE_RATE)

freq = NOTE_PITCH_HZ + VIBRATO_VARIANCE_HZ * np.sin(2 * np.pi * VIBRATO_HZ * t)

# integrate. `∫ f dt` where dt = 1/fs.
phase = np.cumsum(freq) / SAMPLE_RATE

# convert phase from cycles to radians
phase *= 2 * np.pi

# generate waveform
signal = np.sin(phase)

Plotting:

plt.figure(figsize=(12, 4))
plt.plot(t, signal * 20, label='Signal') # amplification for visibility
plt.plot(t, freq, label='Frequency')
plt.xlabel('Time (s)')
plt.ylabel('Amplitude')
plt.show()

如何生成具有一致“颤音”的正弦波?

Sanity check:

  • I asked for 100 Hz with some wobble, 0.2 seconds, so that should be 20 cycles. Looks like it.

  • The wobble wobbles at 20 Hz. Over 0.2s, that is four wobbles. Looks like it.

  • Wobble spans 120 to 80 Hz, so the space between peaks should be something like 3:2. Looks like it.

huangapple
  • 本文由 发表于 2023年6月5日 10:33:41
  • 转载请务必保留本文链接:https://go.coder-hub.com/76403216.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定