我正在使用NAudio在C#中编写合成器。我试图让它在频率之间平滑滑动。但我有一种感觉,我不理解所涉及的数学。在切换到正确的下一个音高之前,它会以高音高音滑动。
从一个音高滑动到另一个音高的数学上正确的方法是什么?
以下是代码:
public override int Read(float [] buffer,int offset,int sampleCount) { int sampleRate = WaveFormat.SampleRate;
for (int n = 0; n < sampleCount; n++)
{
if (nextFrequencyQueue.Count > 0)
{
nextFrequency = nextFrequencyQueue.Dequeue();
}
if (nextFrequency > 0 && Frequency != nextFrequency)
{
if (Frequency == 0) //special case for first note
{
Frequency = nextFrequency;
}
else //slide up or down to next frequency
{
if (Frequency < nextFrequency)
{
Frequency = Clamp(Frequency + frequencyStep, nextFrequency, Frequency);
}
if (Frequency > nextFrequency)
{
Frequency = Clamp(Frequency - frequencyStep, Frequency, nextFrequency);
}
}
}
buffer[n + offset] = (float)(Amplitude * Math.Sin(2 * Math.PI * time * Frequency));
try
{
time += (double)1 / (double)sampleRate;
}
catch
{
time = 0;
}
}
return sampleCount;
}
答案 0 :(得分:2)
您正在使用绝对时间来确定波函数,因此当您稍微更改频率时,下一个样本就是您在新频率下开始运行时的样本。
我不知道已建立的最佳方法,但一个可能足够好的简单方法是计算相位(φ= t mod 1 / f old )并调整t以保持相位在新频率下(t =φ/ f new )。
更顺畅的方法是保留一阶导数。这更加困难,因为与波本身不同,一阶导数的幅度随频率而变化,这意味着保持相位是不够的。无论如何,这种增加的复杂性几乎肯定是过度的,因为你可以顺利地改变频率。
答案 1 :(得分:2)
一种方法是使用波表。你在一个数组中构造一个正弦波的完整周期,然后在你的Read函数中你可以简单地查找它。您读取的每个样本,都会按照所需输出频率计算出的数量。然后,当你想要滑动到一个新的频率时,你计算新的增量用于查找到表中,然后你不是直接在那里调整delta增量以在一段时间内移动到新值('滑翔' '或滑音时间)。
答案 2 :(得分:2)
Frequency = Clamp(Frequency + frequencyStep, nextFrequency, Frequency);
人耳不像那样工作,它非常非线性。自然是对数的。中间C的频率为261.626Hz。下一个注释C#与前一个注释相关,因子为Math.Pow(2,1 / 12.0)或约为1.0594631。所以C#是277.183 Hz,增量为15.557 Hz。
标尺上的下一个C的频率是523.252 Hz的两倍。之后的C#是554.366 Hz,增量为31.084 Hz。注意增量加倍。因此,代码段中的frequencyStep不应该是一个补充,它应该是乘法。
buffer[n + offset] = (float)(Amplitude * Math.Sin(2 * Math.PI * time * Frequency));
这也是一个问题。您计算的样本不能平滑地从一个频率转换到下一个频率。 “频率”发生变化时有一个步骤。您必须将偏移应用于“时间”,以便在采样时间“时间 - 1”生成完全相同的样本值,该值与先前使用之前的“频率”值计算的值相同。这些步骤产生高频伪像,其中有许多谐波对人耳来说是明显的。
此Wikipedia article中提供了背景信息。它有助于可视化您生成的波形,您可以轻松诊断步骤问题。我将复制Wiki图像: