Wavetable Generators

A general principle of computer programming is that we can trade memory for computational speed. If we store a waveform in a table, we can then index into the table to get the values rather than repeatedly calculating them. For periodic waveforms we only have to calculate one cycle of the waveform, an even greater savings. We can produce a continuous sound if we loop back to the beginning of the table when we reach the end. In other words, we increment the table index modulo the table length.

We can accomplish a change in frequency easily by incrementing the index by a value other than one. For example, if we increment by two, we would scan the table in half the time and produce a frequency twice the original frequency. Increments of other values would yield proportionate frequencies.

Given that the table contains one complete cycle of the waveform, the table length represents 2π radians. Thus, with a fixed table length, we calculate the increment value for the index by dividing the table length by the samples per period. This will give us an index value that will cause the code to skip through the table at different rates for different frequencies. We must allocate a table and fill it with a sin wave as before, but now we can keep the table in memory at all times. Generating the sound is reduced to calculating an index and retrieving the sample values from the table.

frqTL = tableLength / sampleRate;
indexIncr = frqTL * frequency;
index = 0;
for (n = 0; n < totalSamples; n++ {
sample[n] = wavetable[index];
index += indexIncr;
if (index >= tableLength)
index -= tableLength;
}

The wavetable does not have to be filled with a sine wave. We can sum sine waves together when we initialize the table, or even load the waveform from a file to playback a recorded or pre-computed waveform. We can also change between wavetables during sound generation to produce "morphing" of the waveform. Furthermore, the wavetable does not have to be limited to one cycle of the waveform. If we have a waveform that varies over some regular number of periods, we can still build a table for that waveform if we include multiple periods. However, when more than one period is included, the increment must be multiplied by the number of periods to produce the correct frequency.

Frequency modulation is easily accomplished by reading two values from the wave table, one for the carrier and one for the modulator, and then adding the modulator value to the carrier index.


Interpolation

Not all frequencies will result in an integer increment for the table index. Thus, the table index and increment must be able to represent fractional values, and the table index must be truncated or rounded to an integer during a table lookup. The result of conversion to an integer index means we are reading a value slightly different from the one we would calculate directly and are introducing a slight quantization error into the signal. The error in the sample value is equivalent to distortion in the signal and we should minimize or eliminate it if possible.

The true sample value lies somewhere between the value at the current index and the next value in the table. Thus, one option we can use to reduce the error is to interpolate between adjacent table entries. Ideally, we would want to interpolate based on the curve of the waveform. Although it is possible to do that using convolution, it adds additional computations, and a linear interpolation is typically used instead. To do this we take the fractional part of the index and multiply it by the difference between adjacent values and then add that to the sample value.

indexBase = floor(index);
indexFract = index - indexBase;
value1 = wavetable[indexBase];
value2 = wavetable[indexBase+1];
value = value1 + ((value2 - value1) * indexFract); 

Note that when indexBase is equal to tableLength-1 we will be reading beyond the end of the table. Thus, the table needs to have one additional value added beyond tableLength to avoid testing for an index at the end of the table. To accomplish this, the table memory must be allocated as size tableLength+1 and the last value in the table set to the value at index zero.

Using this type of interpolation adds some computation time, but is still much faster than repeatedly recalculating the waveform using the sin function. However, there is a possibility to get nearly the same results without interpolation. The amount of error in the sample value will depend in large part on the table length. For short tables, we have a small set of index values to select from and will be more likely to introduce an error than with a longer table.

The table index value has a fixed number of bits available to represent the integer and fractional parts of the index. With longer tables, the index varies over a larger range and thus more bits are contained in the integer portion of the index. For example, with a table of 256 values, only 8 bits of the index are used for the integer portion. For a table of 512 values, 9 bits are used, etc. With each doubling of the table length we gain one bit of precision in the index and are discarding one less bit of precision to obtain an integer index. This results in a smaller variation between the lookup value and the directly calculated value. In effect, lengthening the table is much like performing interpolation on a shorter table once rather than for each sample. Lengthening the table also means we have fewer bits in the fractional portion of the index and the quality of the interpolation diminishes. Thus, for shorter tables (4k or less) it is best to use interpolation, but for longer tables it is sufficient to simpy round-off the index. A table of 16K size generally works very well. Note that to make best use of the index range, table length should be a power of two.

Links:

Dan Mitchell's Personal Website