我实现了三种变体,它们都有优点和缺点,但没有一种是完美的,就像我希望的那样。也许有人有更好的算法,并想在这里分享一下?
总的来说,我像jbarlow描述的那样做。我使用2^x的环形缓冲区长度,其中x足够大,例如12,这将意味着最大延迟长度为2^12=4096个样本,如果以48kHz渲染,则为约12Hz的最低基频率。
二次幂的原因是模数可以通过按位AND来完成,这比实际模数要便宜得多。
// init
int writepointer = 0;
// loop:
writepointer = (writepointer+1) & 0xFFF;
写指针保持简单,例如从0开始,并且每个输出样本始终增加1。
读指针从与写指针的增量开始,每次频率更改时都会重新计算。
float delta = samplingrate/frequency;
int readpointer = (writepointer-(int)delta)-1) & 0xFFF;
float frac = delta-(int)delta;
weight_a = frac;
weight_b = (1.0-frac);
readpointer = (readpointer + 1) & 0xFFF;
它也递增1,但通常位于两个整数位置之间。我们使用向下取整的位置存储整数读指针。这和下一个样本之间的权重是weight_a和_b。
变化#1:
忽略小数部分,按原样处理(整数)读指针。
优点:无副作用,完美延迟(由于延迟没有隐式低通效应,意味着对频率响应有完全控制,没有伪像)
缺点:基础频率大多数情况下稍微偏离,量化为整数位置。这对高音符号听起来非常走调,不能进行微妙的音高变化。
变化#2:
在读指针样本和下一个样本之间进行线性插值。
这意味着我实际上从环形缓冲区中读取了两个连续的样本,并将它们加权相加,分别加上weight_a和weight_b。
优点:完美的基础频率,没有伪像
缺点:线性插值引入了一个可能不需要的低通滤波器。更糟糕的是,低通变化取决于音高。如果小数部分接近0或1,则只有很少的低通滤波,而小数部分约为0.5则进行了重低通滤波。这使得乐器的某些音符比其他音符更明亮,并且它永远不能比这个低通允许的更明亮。(对于钢琴或古钢琴来说很糟糕)
变化#3:有点抖动。我总是从整数位置读取延迟,但跟踪我所做的错误,意味着有一个变量将小数部分相加。一旦超过1,我就从误差中减去1.0,并从第二个位置读取延迟。
优点:完美的基频,没有隐含的低通
缺点:引入可听见的伪像,使其听起来像低保真度的声音。(如最近邻下采样)。
结论:没有一个变化是令人满意的。要么你无法拥有正确的音高,中性的频率响应,要么你会引入伪像。
我在文献中读到,全通滤波器应该做得更好,但是延迟线不是已经是全通了吗?在实现上有什么区别?