Sound generation using wavetables

We know we can generate a simple sine wave using the formula sn = A × sin(2πfn/sr), where sn is the nth sample, f is the frequency and sr is the sampling rate. We can generalize this by using a wavetable to store one cycle of the desired sound. Let us imagine an array of samples that make up this single cycle, and let it be of length nt samples. Now the formula must change. Instead of generating a sample using the sin function, we use the array of samples (the wavetable) to give us the numbers. If we make this table of length sr/f samples, and make each entry sin(2πfn/sr), then the nth output sample is just the nth entry in the table. Since we have decoupled the shape of the wave from the formula we use to generate output samples, it is clear that the table can contain any values whatsoever; we can make our own arbitrary wave shapes. Furthermore, we can generate waves of different frequencies from the same table if we take the ratio of the table length, l, and sr/f as an increment to generate the next output sample.

This is clear if we do a simple exercise with ratios, as in the diagram below.

The upper graph shows the output sample number for one cycle of a periodic waveform we are using. no is the end of the cycle and equals sr/f for a given sampling rate and frequency. The lower graph shows the table we are using to store the waveform being generated. Its length is nt samples. If we set the ratio of noutput, a typical output sample, to ntable, a typical sample in the table, equal to the ratio of no to nt, then we have a relationship between table sample number and output sample number. i.e.

            noutput  /  ntable = no / nt

Since no = sr/f, we can rearrange this to calculate ntable:

            ntable = nt × f × noutput  / sr

The quantity nt × f / sr can be seen as an increment between table samples. If we double the frequency, for instance, we need to take every other sample from the table, i.e. have an increment of 2. In general, however, the increment will not be a whole number; it could be a fraction of a sample, or any number of samples. As a simple example, let nt = 10, sr = 10,000, f = 1,000. The output samples are then given by the formula to be the same as the table entries. However, if we double f to 2,000, then the nth output sample is the 2 × nth table entry. Since we take every other value, the frequency is clearly doubled. To triple the frequency we take every third entry, and so on. We can even generate frequencies that are not whole-number multiples by taking the nearest integer for the table index. For instance, for f = 1,500, the following table results:

 

noutput

f× nt×noutput / sr

Nearest integer

0

0

0

1

1.5

2

2

3

3

3

4.5

5

4

6

6

5

7.5

8

6

9

9

7

10.5

1 (11 wraps around to 1)

8

12

2

9

13.5

4

10

15

5

11

16.5

7

12

18

8

13

19.5

0

 

Notice how we need to ensure that we don't generate an index that overruns the end of the table, so we wrap around by subtracting the length of the table from the too large index. Notice also that the choice of which entries in the table are taken for each cycle depends on the arithmetic involved. The first cycle is entries 0,2,3,5,6,8,9 and the second cycle will be entries 1,2,4,5,7,8.

The recipe for outputting sample number noutput is to use the nth sample from the wavetable, an, where

n = round(f × nt × noutput / sr) mod nt

Accuracy and errors

How accurate is it to have a table that contains values we possibly never use? Clearly the bigger the table the better the approximation is. To handle low frequencies properly, where the increment tends to be smaller, it is useful to have more samples in the wavetable. A rough estimate shows that f × nt should be an order of magnitude bigger than sr. For a lowish frequency of 100 cps, and sr = 44,100, ntshould be about 4,410. It is common to have wavetables of 4096 samples, which gives a minimum increment of the order of 10.

A more serious problem is the fact that the increment given by f × nt  / sr is rarely a whole number, whereas we need a whole number to look up the table entry. We chose to round the increment, when multiplied by noutput, but this introduces errors which manifest themselves as noise in the output signal. This diagram shows that:

 

A better method is to use interpolation between the entries in the table according to the fractional part of the increment. In the table above, output sample 1 gives an index of 1.5 which we rounded to 2. We could, instead, take table entry 1 and table entry 2 and average them to produce the output sample. In general we can take the fractional part and add the same fraction of the difference between the adjacent entries and add this to the lower entry value. In a formula this is:

            Δn × (at+1 - at) + at

This linear interpolation means that the size of the wavetable can be smaller and still produce the same accuracy with the same noise level. This is illustrated by this diagram:

A different kind of error occurs with high frequencies. Since the increment will be large (> 1) the method skips table entries. The entries that are picked out are only an apporoximation of the table’s complete set of entries. This is illustrated below:

Of course this effect can be produced by a too-low sampling rate as well, but is often caused by a table length that is too small. The choice of table size, and sampling rate have to be carefully mode in order to represent the desired frequency range as well as possible.