3

好的,我已经在 C 中实现了 karplus strong 算法。这是一个简单的算法来模拟弹拨的弦音。您从长度为 n 的环形缓冲区开始(n = 您想要的采样频率/频率),将其通过一个简单的两点平均滤波器 y[n] = (x[n] + x[n-1])/2,输出它,然后将其反馈回延迟线。冲洗并重复。这会随着时间的推移消除噪音,从而产生自然的拨弦声。

但我注意到,对于整数延迟线长度,可以将几个高音调匹配到相同的延迟长度。此外,整数延迟长度不允许平滑变化的音高(如颤音或滑音)我已经阅读了几篇关于 karplus 算法扩展的论文,他们都谈到使用插值延迟线进行分数延迟或全通滤波器

http://quod.lib.umich.edu/cgi/p/pod/dod-idx?c=icmc;idno=bbp2372.1997.068
http://www.jaffe.com/Jaffe-Smith-Extensions-CMJ-1983 .pdf
http://www.music.mcgill.ca/~gary/courses/projects/618_2009/NickDonaldson/index.html

我之前已经实现了插值延迟线,但仅在波形缓冲区不变的波表上实现。我只是以不同的速度逐步完成延迟。但让我感到困惑的是,当谈到 KS 算法时,论文似乎在谈论实际改变延迟长度,而不仅仅是我逐步通过它的速率。ks 算法使事情变得复杂,因为我应该不断地将值反馈到延迟线中。

那么我将如何实施呢?我是否将内插值反馈回来或什么?我是否完全摆脱了两点平均低通滤波器?

全通滤波器将如何工作?我应该用全通滤波器替换 2 点平均滤波器吗?我如何使用线性插值法或全通滤波器法在远处的音高之间滑行?

4

2 回答 2

2

数字信号处理算法经常被表示为框图,这是有充分理由的——这是思考它们的绝佳方式。在对它们进行编码时,将每个块视为具有固定输入和输出的单独单元。我认为你的一些问题来自于试图过早地结合系统的各种元素。

这是 Karplus Strong 的框图。

维基百科 Karplus 强框图

对于延迟块,您需要实现分数延迟线。这将包括它自己的低通滤波器,但这是延迟线如何实现的细节。Karplus Strong 效果还需要低通滤波器。这些过滤器的特性会有所不同。不要试图结合。顺便说一句,您选择的平均低通滤波器的频率响应很差,会引入“梳状滤波器”效应。您可能想要设计更复杂的 FIR 或 IIR 滤波器。

那么我将如何实施呢?我是否将内插值反馈回来或什么?我是否完全摆脱了两点平均低通滤波器?

您确实将内插、求和的样本反馈回延迟线,就像框图所示。在某些情况下,这可能会开始增加系统的净增益,如果您担心的话,您可能需要“标准化”延迟的输出,以免它失控。

有许多有效的策略可以实现分数延迟线,包括您提到的插值和全通滤波。这个想法是您将要维护readwrite索引到延迟线。延迟线的长度不是内存缓冲区的总长度,而是以延迟线的总长度为模后的索引之间的差值。使延迟线尽可能大,不要担心调整它的大小。

我发现将读写视为永远不会回绕或过期的自由运行计数器最方便,因为那时

current_delay_length = (write - read) % total_delay_length
current_read_sample = delay_line[read % total_delay_length]

哪里%是模数。如果写入和读取计数器是浮点值或设置为定点,则它们也可以包含小数长度。无论如何,这使得修改延迟线的长度变得容易。确保强制执行最小延迟非常重要(写入 > 读取)。

信不信由你,您将通过更改您通过它的速率来更改延迟线长度,就像固定长度的缓冲区一样。通常,您会稍微调整读取索引。它不应该落后于写指针超过缓冲区长度或超过它,否则你会遇到故障。但是您可以在写指针之后随意移动读指针。改变调制会得到不同的效果。

我强调诸如滑音之类的效果来自于延迟线的读写索引是如何被操纵的,而不是它是如何实现的。您将从全通滤波器或线性插值延迟线中获得类似的声音。例如,更好的分数延迟线将减少混叠噪声并支持读取指针的更快速变化。

于 2011-07-27T19:44:51.867 回答
1

我实现了三种变体,各有优缺点,但没有一个是我希望的那样完美。也许有人有更好的算法并想在这里分享?

一般来说,我会像 jbarlow 描述的那样做。我使用 2^x 的环形缓冲区长度,其中 x“足够大”,例如 12,这意味着最大延迟长度为 2^12=4096 个样本,如果以 48kHz 渲染,这是 ~12Hz 作为最低基频. 2 的幂的原因是模可以通过按位与来完成,这比实际的模便宜得多。

// init
int writepointer = 0;

// loop:
writepointer = (writepointer+1) & 0xFFF;

写指针保持简单,例如从 0 开始,并且对于每个输出样本始终递增 1。

读指针以相对于写指针的增量开始,每次频率发生变化时都会重新计算。

// init
float delta = samplingrate/frequency;
int readpointer = (writepointer-(int)delta)-1) & 0xFFF;
float frac = delta-(int)delta;
weight_a = frac;
weight_b = (1.0-frac);

// loop:
readpointer = (readpointer + 1) & 0xFFF;

它也增加 1,但通常或多或少位于两个整数位置之间。我们使用向下舍入的位置存储在整数读取指针中。这个和下一个样本之间的权重是 weight_a 和 _b。

变体 #1:忽略小数部分并按原样执行(整数)读取指针。

优点:无副作用,完美延迟(由于延迟没有隐含的低通,意味着完全控制频率响应,没有伪影)

缺点:基本频率大多略微偏离,量化到整数位置。这对于高音音符来说听起来很失谐,并且无法进行细微的音高变化。

变化 #2:读取指针样本和下一个样本之间的线性插值。意味着我实际上从环形缓冲区中读取了两个连续的样本并将它们相加,分别由 weight_a 和 weight_b 加权。

优点:完美的基频,无伪影

缺点:线性插值引入了可能不需要的低通滤波器。更糟糕的是,低通因音高而异。如果小数部分接近 0 或 1,则仅进行少量低通滤波,而小数部分在 0.5 左右进行重低通滤波。这使得乐器的一些音符比其他音符更亮,而且它永远不会比这个低通允许的更亮。(不适合钢吉他或大键琴)

变化#3:一种抖动。我总是从整数位置读取延迟,但跟踪我所做的错误,这意味着有一个变量可以将小数部分相加。一旦超过 1,我从误差中减去 1.0,并从第二个位置读取延迟。

优点:完美的基频,没有隐含的低通

缺点:引入了可听的人工制品,使其听起来低保真。(例如与最近邻居进行下采样)。

结论:没有一个变化是令人满意的。要么你不能有正确的音高,一个中性的频率响应,要么你引入了人工制品。

我在文献中读到全通滤波器应该做得更好,但延迟线不是已经全通了吗?实施会有什么不同?

于 2012-10-21T19:18:24.003 回答