有效生成采样波形而没有混叠伪影 解决方案可能需要的示例:

问题描述

对于我的一个项目,我正在处理采样声音生成,我需要创建各种频率的各种波形。波形为正弦时,一切正常,但当波形为矩形时,就麻烦了:听起来像是来自八十年代,随着频率的增加,音符听起来不对。在第 8 个八度上,每个音符听起来像是来自某个较低八度的随机音符。

无论我是否使用以下两种方法中的任何一种,不良效果都是一样的:

  • 生成矩形波形的纯数学方法sample = sign( secondsPerHalfWave - (timeSeconds % secondsPerWave) ) 其中 secondsPerWave = 1.0 / wavesPerSecondsecondsPerHalfWave = secondsPerWave / 2.0

  • 我的首选方法是使用线段描述波的一个周期并沿这些线进行插值。因此,矩形波形由一条从 x=0 到 x=0.5 在 y=1.0 处的水平线描述(无论采样率和频率如何),然后是另一条从 x=0.5 到 x=1.0 在 y= 处的水平线-1.0。

据我所知,文献认为这些波形生成方法“幼稚”,导致“混叠”,这是所有不良影响的原因。

当我查看生成的波形时,这实际上意味着每秒采样值不是每秒波值的精确倍数,因此每个波没有偶数个采样,这反过来意味着1.0级的样本数往往不等于-1.0级的样本数。

在这里找到了一个特定的解决方案:https://www.nayuki.io/page/band-limited-square-waves,它甚至包含 Java 中的源代码,它确实听起来很棒:所有不良影响都消失了,每个音符听起来都很纯净且频率正确。然而,这个解决方案完全不适合我,因为它在计算上非常昂贵。 (即使我用比 Java 内置函数快十倍的近似值替换了 sin()cos()。)此外,当我查看生成的波形时,它们看起来非常复杂,所以我想知道是否它们可以合法地称为矩形。

所以,我的问题是:

生成周期波形(例如不受混叠伪影影响的矩形波形)最计算效率方法是什么?

解决方案可能需要的示例:

  1. 在离散时间间隔生成正确样本值以描述声波的计算机音频问题在我看来与在离散整数 x 坐标处生成正确整数 y 坐标以绘制线条的计算机图形问题有些相关。 Bresenham 线生成算法非常有效(即使我们暂时忽略它正在处理整数数学这一事实)并且它通过累积某个误差项来工作,该误差项在正确的时间会导致Y坐标。是否可以使用类似的机制来计算样本值?

  2. 采样的工作方式被理解为在特定的、无限窄的时间点读取模拟信号的值。也许更好的方法是考虑读取最后一个样本和当前样本之间整个模拟信号切片的区域。这样,在矩形波形边缘之前采样 1.0 对采样值的贡献很小,而在边缘之后相当长的时间采样 -1.0 将贡献很大,因此自然会产生一个介于两个极值之间的点.这能解决问题吗?这样的算法存在吗?有人试过吗?

请注意,我在这里发布了这个问题而不是 dsp.stackexchange.com,因为我不想收到带有荒谬术语的答案,例如带限、谐波和低通滤波器、拉格朗日插值、直流补偿等.而且我不想要来自纯粹模拟世界或纯粹理论外太空的答案,并且没有机会使用数字计算机获得实际有效的实现。

我是程序员,不是音响工程师,在我的小程序员的世界里,事情很简单:我有一个样本数组,它们必须都在 -1.0 和 1.0 之间,并且会以一定的速率播放 (44100每秒采样数。)我可以使用算术运算和三角函数,我可以描述直线并使用简单的线性插值,我需要非常有效地生成样本,因为同时生成十几个波形并将它们混合在一起不得超过总 cpu间的 1%。

解决方法

我不确定,但您可能对混叠的性质有一些误解。我基于您将这个词放在引号中,并引用以下引用:

当我查看生成的 波形是每秒采样值不是精确倍数 每秒波数的值,因此每个波都没有偶数 样本数,这反过来意味着在 级别 1.0 通常不等于级别 -1.0 的样本数。

样本/秒和波数/秒根本不必是精确的倍数!可以回放奈奎斯特以下的所有音高。所以我不清楚你对此有何看法。

方波的特征声音来自奇次谐波的存在,例如,440 (A5) 的音符,方波声音可以通过组合 440、1320、2200、3080、3960 的正弦产生,等以 880 为增量进行。这就引出了一个问题,有多少奇次谐波?理论上,我们可以达到无穷大,以获得方波上尽可能尖锐的拐角。如果您只是在音频流中“绘制”它,则进程将远远超出奈奎斯特数。

但存在一个问题,即无法以数字方式准确再现高于奈奎斯特值的谐波。尝试这样做会导致混叠。因此,要获得系统能够产生的尽可能好的方波,必须避免理论上完美的方波中存在的高次谐波。

我认为最常见的解决方案是使用低通滤波算法。与仅计算正弦波(或进行 FM 合成,这是我的主要兴趣)相比,计算肯定更需要 CPU 密集型。我对DSP的数学也很薄弱,担心cpu费用,所以很长一段时间都避免这种方法。但它非常可行,值得多看一看,恕我直言。

另一种方法是使用加法合成,并根据需要包含尽可能多的正弦谐波以获得所需的音质。那么问题是,您添加的谐波越多,您进行的计算就越多。此外,必须跟踪最高谐波,因为它们限制了您可以演奏的最高音符。例如,如果使用 10 个谐波,500Hz 的音符将包含 10500Hz 的内容。这低于 44100 fps(即 22050 Hz)的奈奎斯特值。但是,在您的谐波含量超过限制并开始出现混叠之前,您只能使用 10 次谐波上升大约另一个八度音程(将所有内容加倍)。

您可能考虑的另一种解决方案是为您的方波创建一组查找表 (LUT),而不是即时计算多个正弦波。要创建表中的值,请迭代并添加正弦谐波中的值,这些值将安全地保持在您使用给定表的范围内的奈奎斯特下。我认为一个包含 1024 个值的表格来编码单个周期可能是一个很好的初步猜测,它会起作用。

例如,我正在猜测,但是八度音程 C4-C5 的表格可能使用 10 个谐波,C5-C6 的表格只有 5 个,C3-C4 的表格可能有 20 个。我不记得这是什么策略/技术被称为,但我记得它有一个名字,它是处理这种情况的公认方式。根据转场的声音和您想要的高端内容的数量,您可以使用更少或更多的 LUT。

可能还有其他方法需要考虑。关于别名的维基百科条目描述了一种被称为“带通”的技术,该技术似乎有意使用别名。我不知道那是关于什么的,也不知道它与您引用的文章有什么关系。

,

Soundpipe 库具有频率表的概念,它是一种数据结构,用于保存预先计算的波形,例如正弦。您可以使用所需的波形初始化频率表并通过 oscilator 播放它。甚至还有一个名为 oscmorph 的模块,它允许您在两个或多个波表之间变形。

这是一个如何生成正弦波的示例,取自 Soundpipe 的 documentation

int main() {
    UserData ud;
    sp_data *sp;
    sp_create(&sp);
    sp_ftbl_create(sp,&ud.ft,2048);
    sp_osc_create(&ud.osc);

    sp_gen_sine(sp,ud.ft);
    sp_osc_init(sp,ud.osc,ud.ft);
    ud.osc->freq = 500;
    sp->len = 44100 * 5;
    sp_process(sp,&ud,write_osc);

    sp_ftbl_destroy(&ud.ft);
    sp_osc_destroy(&ud.osc);
    sp_destroy(&sp);
    return 0;
}