16  第十六章 波表合成

青山霁后云犹在,画出东南四五峰。

16.1 波表

波表(Wavetable)技术诞生于1978年,由德国工程师沃尔夫冈·帕姆(Wolfgang Palm)发明创造,并在PPG Wave合成器上实现了商业运用。

要了解波表技术,首先得知道什么是波表,波表是一个按顺序排列的存储了波形数据的集合,其中每个波形数据通常是一个单周期波形(Single Cycle Waveform,SCW),例如一个单周期的三角波或方波,一个波表通常又会包含32、64或更多数量的SCW,这些SCW一起构成了一个波表序列(Wavetable Bank):

图 16.1: 一个波表序列

16.2 单周期波形

SCW可以定义为重复振动波形的最小单元,每个SCW在波表里被存储为单个周期内波形的幅度大小。

这区别于对声音进行采样,香农定理指出,为了避免信息丢失(混叠现象),采样频率(定义为每秒钟需要采样的次数)至少要为原始信息最高频率的2倍。

图 16.2: 混叠现象

因此音频信号的常见采样率为\(44100\,\text{Hz}\)也就不奇怪了,对于更专业的场景,采样率可以达到\(96000\,\text{Hz}\)或更高。

回到SCW,波表里存储的是波形的幅度数据,这些数据被称为样本点,样本点描述了波形的形状,而和采样率没有关系。一个SCW的样本点总数通常为2 的 \(N\) 次幂,例如256、1024或2048个样本点。

以256个样本点的单周期锯齿波为例:

图 16.3: 256样本单周期锯齿波

如果正好在1秒的时间里输出这256个样本并不断重复,那最终输出锯齿波频率正好就是\(1\,\text{Hz}\)(然而这样的输出方式并不好,下一小节将说明正确的输出方式)。

16.3 特定频率输出

回顾一下DAC,它接收一串数字输入并得到一个模拟电压输出,Arduino UNO R4的DAC是12位的,提供\(0 \sim 3.3\,\text{V}\)的模拟输出。

为了得到特定频率的输出,首先用定时器设定好DAC采样频率\(f_s\) (也是DAC的中断频率),假设\(f_s\) 为\(48000\,\text{Hz}\),因此每\(\frac{1}{48000}\)秒会更新一次DAC。

若SCW的样本点总数是\(N\)(为了便于计算,不同于 图 16.3,接下来样本点排序默认从数字0开始),对于目标输出频率\(f\),DAC第\(n\)次更新会输出第\(P[n]\)个样本点的值,其中\(P[n] = P[n-1] + \Delta P\),\(\Delta P\)计算方式为:

\[ \Delta P = \frac{f \cdot N}{f_s} \tag{16.1}\]

当\(P[n]\) > \(N\) - 1时,更新\(P[n]\)的值为\(P[n]\) - \(N\) + 1。

例如一个SCW样本点总数\(N\)等于256,\(f_s\) 等于48000,想要输出频率为\(440\,\text{Hz}\),则可以计算出\(\Delta P = \frac{440 \times 256}{48000} \approx 2.34\)。初始时刻,设定 \(P[0] = 0\),DAC输出第0个样本点的幅值,在下一个DAC采样时刻,DAC应该输出第2.34(\(P[1] = 0 + 2.34 = 2.34\))个样本点的幅值。

对于非整数样本点,其幅值大小可以通过线性插值求得:首先对\(P[1]\)向下取整(⌊\(P[1]\)⌋)得到整数2,然后以第2个样本点的幅值(假设第\(i\)个样本点的幅值记为A[\(i\)])为基数加上(\(P[1]\) - ⌊\(P[1]\)⌋) * (A[3] - A[2])。

以此类推再下一个DAC采样时刻输出第4.68个样本点的幅值。另外还要注意的是插值计算出的结果可能也是个小数,需要四舍五入一次以便于输入到DAC。

图 16.4: 线性插值

上述改变频率的方法被称为直接数字合成法(Direct Digital Synthesis,DDS),让我们思考一个问题,为什么不直接改变DAC的采样频率以尽可能完整地输出所有的样本点?这既能改变频率,又能让输出图形减少失真。我认为一个很重要的原因是硬件实现上的困难,DDS中DAC的采样频率保持在某个固定大小,但如果改变DAC采样频率而保持所有样本点都输出呢?想要获得\(10000\,\text{Hz}\)的输出频率,对于256个样本点的SCW来说,需要\(2560000\,\text{Hz}\)(\(2.56\,\text{MHz}\))的DAC采样频率。随着输出频率升高,这个数字会变得越来越大,而且大多数波表合成器的样本点远不止256个,这样随意大范围地改变采样频率可能会造成系统不稳定。

16.4 波表扫描

波表扫描(Wavetable Scanning)是波表合成器的核心,波表合成器的音色之所以能够进行动态、平滑的转换,全依赖于波表扫描。

上文提到过一个波表包含了很多个SCW片段,把波表扫描想象成一根“指挥棒”,指挥家手里那根银色棒棒的魔力在于:只有它才能指示出正确的节奏。

波表扫描不用于改变频率,只用于选择播放哪个SCW片段。对于包含了 \(M\) 个 SCW的波表,波表扫描的参数(定义为\(P_s\) )是一个从 0 到 \(M - 1\)(也是排序从0开始)的实数,可以自由地游走在\(0 \sim M - 1\)之内,从而选择不同的SCW。

例如某个时刻\(P_s\) 等于 13,这表明合成器将输出第13个SCW的波形,而音色平滑变化的关键在于,\(P_s\) 是一个实数,所以会有小数部分。\(P_s = 13\) 和 \(P_s = 13.7\)输出的音色是不一样的,后者输出的波形会介于第13个SCW和第14个SCW之间,并且更偏向于第14个SCW,这样就避免了\(P_s\) 从 13 到 14转移过程中声音的突然改变。实现方法很简单,就是线性插值:

图 16.5: 波表扫描的线性插值

假设 \(P_s\) 位于 \(K\) 和 \(K + 1\) 之间,先对 \(P_s\) 向下取整 \(\lfloor P_s \rfloor\) 得到 \(K\),然后用 \(P_s - \lfloor P_s \rfloor\) 计算小数部分,这样新的波表 \([\text{SCW}_{P_s}]\) 为:

\[ [\text{SCW}_{P_s}] = [\text{SCW}_K] + (P_s - \lfloor P_s \rfloor) \cdot ([\text{SCW}_{K+1}] - [\text{SCW}_K]) \tag{16.2}\]

其中\([\text{SCW}_K]\)表示对第 \(K\) 个 SCW的所有样本点分别操作。