Further thoughts on wave table oscillators

I see regular questions about wave table oscillators in various forums. While the process is straight forward, I sympathize that it’s not so simple to figure out what’s important if you really want to understand how it works. For instance, the choice of table size and interpolation—how do these affect the oscillator quality? I’ll give an overview here, which I hope helps in thinking about wave table oscillators.

Generating a signal

With a goal to generate a tone, we have an idea that instead of computing a tone for an arbitrary amount of time, we can save a lot of setup time and memory by computing just one cycle of the tone at initialization time, storing in a table, and playing it back repeatedly as needed.

This works perfectly and can play back any wave without aliasing. The problem is that the musical pitch will be related to the number of samples in the table and the sample rate. Even if we made a table for every possible pitch we intend to play, the tuning will be off for many intended notes, because we’re restricted to an integer number of samples.

Tuning the signal

We can solve the tuning problem, and avoid building a lot of tables, by resampling a single-cycle wave table, the equivalent of slowing down or speeding up the wave. The problem with slowing down the wave is that waveforms with infinite harmonics, such as sawtooth, will have a increasingly noticeable drop in high end the more we pitch it down. The easy solution is to start with a table that’s long enough to fit all the harmonics we can hear, for the lowest note we need, and only change pitch in the upward direction. To pitch up, we use an increment greater than 1 as we step through the table. To get at all possibles pitches, we step by a non-integer increment—2.0 pitches the tone up and octave, 1.5 pitches up a fifth. Of course, this means we need to interpolate between samples in our wave table. How we do this is a major consideration in a wave table oscillator.

Aliasing

The problem with pitching in the upward direction is aliasing, as harmonics are rasied above half the sample rate. We could implement a nice lowpass filter to remove the offending harmonics before the interpolation. But a cheaper solution is to pre-compute filtered tables, and choose the appropriate table as needed, depending on the target pitch. This is why my wave table oscillator uses a collection of tables. Read the wave table oscillator series for the details, I won’t cover this aspect further here. Just understand that we need to remove harmonics before resampling, for precisely the same reason we use a lowpass filter before the analog-to-digital converter when sampling. The multi-table approach simply lets us do the filter beforehand to make the oscillator more efficient.

Choosing table size

As noted previously, table size is determined primarily by a combination of the lowest pitch needed and how many harmonics we need. For instance, we’d like our sawtooth wave to have all harmonics up to as high as we can hear (let’s say 20 kHz). For a pitch of 20 Hz, that means 1001 harmonics (20, 40, 60, 80…20000). The sampling theorem tells us we need something over two samples for the highest frequency component, so clearly we need a table size of at least 2003. For convenience, let’s make that 2048, a power of two.

Further, if we want to save memory for our tables that serve higher frequencies, we can cut the table size in half for each octave we move up. A table serving 40 Hz could be 1024 samples, 80 Hz, 512 samples, and so on.

The choice of interpolation method

If we have a very good interpolator—ideally a “sinc” interpolator—our job is done. But such interpolators are computational expensive, and we might want to run many oscillators at the same time. To reduce overhead per sample, we can can implement a cheaper interpolator. Choices include no interpolation, such as just grabbing the nearest table value, linear interpolation, which estimates the sample with a straight line between the two adjacent samples when the index falls between them, and more complex interpolation that requires three or more table points. Cutting to the chase, we’d like to use linear, if we can. It produces significantly better results than no interpolation, but has only a small amount of additional computation per sample.

But by giving up on the idea of using an ideal interpolator, we introduce error. Linear interpolation works extremely well for heavily oversample signals. Recall that using a 512 sample sine table with linear interpolation, the results a good as we have any chance of hearing—error is -97 db relative to the signal. Sine waves are smooth, and as we have more samples per cycle, the curve between samples approaches a straight line, and linear interpolation approaches perfection.

The catch is that our table of 2048 for 20 Hz doesn’t necessarily hold a sine wave. For a sawtooth, it holds harmonic 1001 with just over two samples. Linear interpolation fails miserably for producing that harmonic. The errors cause significant distortion and aliasing with that harmonic. And a whole lot of harmonics below it, but improving as we move towards the lower harmonics where they will be awesome.

Our first thought might be that we need a table size 128 times as large, to get up above 512 for the highest harmonic. So, 262,144 samples, just for the lowest table?

No, we cheat

Here’s where psychoacoustics saves the day. Even though our distortion and noise floor figures for the higher harmonics look pretty grim, it’s extremely difficult to hear the problem over the sweetly rendered lower harmonics. And, fortunately, oscillators we like to listen to won’t likely have a weak fundamental and a strong extremely-high harmonic with nothing in between. Natural and pleasant sounding tones are heavily biased to the lower harmonics.

Also, if we choose to keep constant table sizes, then as we move up each octave, removing higher harmonics, the tables are progressively more oversampled as a result. Constant table sizes are convenient anyway, and don’t impact storage significantly. So, at the low end we’re saved by masking, and though we lose masking as we move up in frequency, at the same time the linear interpolation improves with increasingly oversampled tables. At the highest note we intend to play, near 20 kHz, error will be something like -140 dB relative to the signal.

This entry was posted in Digital Audio, Oscillators, Wavetable Oscillators. Bookmark the permalink.

12 Responses to Further thoughts on wave table oscillators

  1. Tony says:

    Hi Mr Redmon

    first thank you so much for all your work .
    I followed with lots of interest all the episodes of the wavetable oscillator and i would like to implement this in a fpga design.
    I have a question apart from this:
    What limits you see in a design where the wavetable can change continously in time?

    Regards

    • Nigel Redmon says:

      I suppose it would be mainly an issue of glitches as your playback and updating give discontinuities. People often crossfade between tables to minimize those.

  2. Tony says:

    Hi, thx for your reply..

    whatabout if i limit the wavetable update time to at least one playback cycle? that would be smooth, am i right? I know, it would be like “cheatin”, but.. what ears can hear the difference.. 😉 .
    I thought also of a median filter at the output that it gets triggered by a too high acceleration between one sample and the other.
    In my dream design there is no wavetables to interpolate with, as the wavetable is result of many mathematical processes.
    Again Best regards and thanks a lot for all your teaching on this platform

    • Nigel Redmon says:

      For quick changes, you mainly want to limit the amount of change, so you don’t have a click with a lot of energy. Don’t worry about cheating, it’s all about the end result, the sound. This would be a good topic for a discussion board like kvraudio, DSP and Plug-In Development forum. It’s a better discussion medium, and you’ll find plenty of people who’ve tackled or tough about these issues. Good luck!

      • Tony says:

        Thx a lot Mr Redmon … i need a lot of 😀

        I did a lot of research on this and retroengineered lots of designs, from ableton to serum and others..

        I hope i can bring the result in a close near future.

  3. robert bristow-johnson says:

    If memory is cheap (like if this is running native and you’re already using a gigabyte or more), then I would make the wavetables big (like 2K or 4K), all the same size (w.r.t. the fundamental pitch from MIDI), and a power of 2 in length (for simple modulo arithmetic), and interpolate between samples **and** between adjacent wavetables (in whatever dimension of movement) with linear interpolation. It makes the core wavetable synthesis code simple and efficient. Even though they’re all the same length, wavetables for higher notes will have fewer harmonics than wavetables for lower notes. But I would make them all the same size, unless this was a hardware model (where memory is not as cheap) and you want wavetables for higher octaves to be shorter than those for lower octaves.

    I might have sent you a C file showing how that sample-synthesis code works. I did see your code a couple years ago. You do more work with the addressing than I do, if I recall.

    • Nigel Redmon says:

      I normally use uniform-sized tables, with the exception of maybe I want the lowest table to be much larger than the others. Not important in typical music, but I like hear the ticks when sweeping a saw down sub-audio. If I cared a lot about that and stuck to synth waves, I might generate naive waveforms when sub-audio, but I’m not making synths. I usually use 4k, used to use 2k when RAM matter even slightly…And I agree on interpolating between tables (I’m going for less complexity and more understanding here, not interested in complete solutions and maintaining open source).

      • robert bristow-johnson says:

        well, i am for less complexity, too. that’s why (as long as memory is cheap) i wanna keep the same wavetable size, even for the top octaves in which the wavetable is grossly oversampled. but oversampling doesn’t hurt.

        Nigel, just for reference, i hope you don’t mind if i simply paste in here the core code for the wavetable oscillator that i wrote. it is not a complete solution, just the inner core code. it shows how to interpolate between wavetables in up to three dimensions. one of those dimensions will be the keyboard or MIDI note (so wavetables along that dimension will be losing harmonics as you go up in pitch).

        i’ll paste in the next comment. you can choose to do with it whatever you want. (i thought i might have sent this to you once, maybe 3 or 4 years ago, i don’t remember.)

        • robert bristow-johnson says:

          looks like a copy exists on dropbox. so i think i tried passing it to you or someone else before.

          https://www.dropbox.com/scl/fi/ztzftg8lyl1531r0dim9n/wavetable_oscillator.c?dl=0

          • Nigel Redmon says:

            Thanks, Robert! On a code note, C++ does make life easier, and C++ got a lot less needlesly awkward starting with C++11. I actually started to enjoy C++ coding after ’11.

          • robert bristow-johnson says:

            For some reason there is no “Reply” link under your comment below, Nigel.

            I am all for a clean object-oriented programming language. I’ve coded C code inside of C++ projects that other people have created (they were using JUCE framework). I am familiar with objects and member functions and I like the features of inheritance and operator overloading (it was always a shame that C did not have a “complex” type or even a “matrix” type both with operators like “+”, “-“, “*”, “/”, and for matrices “\” defined) .

            but C++ lost me with the ugly I/O operations and that “<<" operator. File I/O should be done with functions from the std library and not part of the language itself.

            I've always been able to productively code in C and, sometimes requiring help from a C++ jock, integrate the code into a C++ project.

            Anyway, I just wanted to share some fundamental wavetable code, the actual synthesis code, to sorta demonstrate how interpolation between wavetables along different dimensions can be done. When we were trying this out, usually one dimension was the MIDI pitch, another dimension was elapsed time after NoteOn, and if we needed a modwheel or pedal or key velocity then we had a third dimension of interpolation. We also tried out a method to detune some harmonics slightly using "cylindrical coordinates" (one of the dimensions was circular, not linear) that could cause some harmonics to detune more than others.

  4. robert bristow-johnson says:
    //
    //  This typedef in wavetable_oscillator.h
    //
    typedef struct
    	{
    	float* output_ptr;
    	int samples_per_block;
    
    	unsigned long phase;
    	long phaseIncrement;
    	long frequencyIncrement;
    
    	float scaler_fractionalBits;		// 2^(-num_fractionalBits)
    	unsigned int num_fractionalBits;
    	unsigned long mask_fractionalBits;	// 2^num_fractionalBits - 1
    	unsigned int mask_waveIndex;
    
    	float fadeDim1;
    	float fadeDim1Increment;
    	float fadeDim2;
    	float fadeDim2Increment;
    	float fadeDim3;
    	float fadeDim3Increment;
    
    	float* wave000;
    	float* wave001;
    	float* wave010;
    	float* wave011;
    	float* wave100;
    	float* wave101;
    	float* wave110;
    	float* wave111;
    	} wavetable_oscillator_data;
    //
    // #include "wavetable_oscillator.h"
    //
    
    
    void wavetable_0dimensional_oscillator(wavetable_oscillator_data* this_oscillator)
    	{
    	float* out = this_oscillator->output_ptr;
    	int num_samples_remaining = this_oscillator->samples_per_block;
    	
    	unsigned long phase = this_oscillator->phase;
    	long phaseIncrement = this_oscillator->phaseIncrement;
    	long frequencyIncrement = this_oscillator->frequencyIncrement;
    	
    	float scaler_fractionalBits = this_oscillator->scaler_fractionalBits;
    	unsigned int num_fractionalBits = this_oscillator->num_fractionalBits;
    	unsigned long mask_fractionalBits = this_oscillator->mask_fractionalBits;
    	unsigned int mask_waveIndex = this_oscillator->mask_waveIndex;
    		
    	float* wave000 = this_oscillator->wave000;
    	
    	while (num_samples_remaining-- > 0)
    		{
    		unsigned int waveIndex0 = (unsigned int)(phase>>num_fractionalBits) & mask_waveIndex;
    		unsigned int waveIndex1 = (waveIndex0 + 1) & mask_waveIndex;
    		float linearGain1 = scaler_fractionalBits * (float)(phase & mask_fractionalBits);
    		float linearGain0 = 1.0f - linearGain1;
    		
    		float _wave000 = wave000[waveIndex0]*linearGain0 + wave000[waveIndex1]*linearGain1;
    		
    		phase += phaseIncrement;
    		phaseIncrement += frequencyIncrement;
    		
    		*out++ = _wave000;
    		}
    	
    	this_oscillator->phase = phase;
    	this_oscillator->phaseIncrement = phaseIncrement;
    	}
    
    
    void wavetable_1dimensional_oscillator(wavetable_oscillator_data* this_oscillator)
    	{
    	float* out = this_oscillator->output_ptr;
    	int num_samples_remaining = this_oscillator->samples_per_block;
    	
    	unsigned long phase = this_oscillator->phase;
    	long phaseIncrement = this_oscillator->phaseIncrement;
    	long frequencyIncrement = this_oscillator->frequencyIncrement;
    	
    	float scaler_fractionalBits = this_oscillator->scaler_fractionalBits;
    	unsigned int num_fractionalBits = this_oscillator->num_fractionalBits;
    	unsigned long mask_fractionalBits = this_oscillator->mask_fractionalBits;
    	unsigned int mask_waveIndex = this_oscillator->mask_waveIndex;
    	
    	float fadeDim1 = this_oscillator->fadeDim1;
    	float fadeDim1Increment = this_oscillator->fadeDim1Increment;
    	
    	float* wave000 = this_oscillator->wave000;
    	float* wave001 = this_oscillator->wave001;
    	
    	while (num_samples_remaining-- > 0)
    		{
    		unsigned int waveIndex0 = (unsigned int)(phase>>num_fractionalBits) & mask_waveIndex;
    		unsigned int waveIndex1 = (waveIndex0 + 1) & mask_waveIndex;
    		float linearGain1 = scaler_fractionalBits * (float)(phase & mask_fractionalBits);
    		float linearGain0 = 1.0f - linearGain1;
    		
    		float _wave000 = wave000[waveIndex0]*linearGain0 + wave000[waveIndex1]*linearGain1;
    		float _wave001 = wave001[waveIndex0]*linearGain0 + wave001[waveIndex1]*linearGain1;
    		
    		_wave000 += (_wave001 - _wave000)*fadeDim1;
    		
    		fadeDim1 += fadeDim1Increment;
    		
    		phase += phaseIncrement;
    		phaseIncrement += frequencyIncrement;
    		
    		*out++ = _wave000;
    		}
    	
    	this_oscillator->fadeDim1 = fadeDim1;
    	
    	this_oscillator->phase = phase;
    	this_oscillator->phaseIncrement = phaseIncrement;
    	}
    
    
    void wavetable_2dimensional_oscillator(wavetable_oscillator_data* this_oscillator)
    	{
    	float* out = this_oscillator->output_ptr;
    	int num_samples_remaining = this_oscillator->samples_per_block;
    	
    	unsigned long phase = this_oscillator->phase;
    	long phaseIncrement = this_oscillator->phaseIncrement;
    	long frequencyIncrement = this_oscillator->frequencyIncrement;
    	
    	float scaler_fractionalBits = this_oscillator->scaler_fractionalBits;
    	unsigned int num_fractionalBits = this_oscillator->num_fractionalBits;
    	unsigned long mask_fractionalBits = this_oscillator->mask_fractionalBits;
    	unsigned int mask_waveIndex = this_oscillator->mask_waveIndex;
    	
    	float fadeDim1 = this_oscillator->fadeDim1;
    	float fadeDim1Increment = this_oscillator->fadeDim1Increment;
    	float fadeDim2 = this_oscillator->fadeDim2;
    	float fadeDim2Increment = this_oscillator->fadeDim2Increment;
    	
    	float* wave000 = this_oscillator->wave000;
    	float* wave001 = this_oscillator->wave001;
    	float* wave010 = this_oscillator->wave010;
    	float* wave011 = this_oscillator->wave011;
    	
    	while (num_samples_remaining-- > 0)
    		{
    		unsigned int waveIndex0 = (unsigned int)(phase>>num_fractionalBits) & mask_waveIndex;
    		unsigned int waveIndex1 = (waveIndex0 + 1) & mask_waveIndex;
    		float linearGain1 = scaler_fractionalBits * (float)(phase & mask_fractionalBits);
    		float linearGain0 = 1.0f - linearGain1;
    		
    		float _wave000 = wave000[waveIndex0]*linearGain0 + wave000[waveIndex1]*linearGain1;
    		float _wave001 = wave001[waveIndex0]*linearGain0 + wave001[waveIndex1]*linearGain1;
    		float _wave010 = wave010[waveIndex0]*linearGain0 + wave010[waveIndex1]*linearGain1;
    		float _wave011 = wave011[waveIndex0]*linearGain0 + wave011[waveIndex1]*linearGain1;
    		
    		_wave000 += (_wave010 - _wave000)*fadeDim2;
    		_wave001 += (_wave011 - _wave001)*fadeDim2;
    
    		_wave000 += (_wave001 - _wave000)*fadeDim1;
    		
    		fadeDim2 += fadeDim2Increment;
    		fadeDim1 += fadeDim1Increment;
    		
    		phase += phaseIncrement;
    		phaseIncrement += frequencyIncrement;
    		
    		*out++ = _wave000;
    		}
    	
    	this_oscillator->fadeDim1 = fadeDim1;
    	this_oscillator->fadeDim2 = fadeDim2;
    	
    	this_oscillator->phase = phase;
    	this_oscillator->phaseIncrement = phaseIncrement;
    	}
    
    
    void wavetable_3dimensional_oscillator(wavetable_oscillator_data* this_oscillator)
    	{
    	float* out = this_oscillator->output_ptr;
    	int num_samples_remaining = this_oscillator->samples_per_block;
    	
    	unsigned long phase = this_oscillator->phase;
    	long phaseIncrement = this_oscillator->phaseIncrement;
    	long frequencyIncrement = this_oscillator->frequencyIncrement;
    	
    	float scaler_fractionalBits = this_oscillator->scaler_fractionalBits;
    	unsigned int num_fractionalBits = this_oscillator->num_fractionalBits;
    	unsigned long mask_fractionalBits = this_oscillator->mask_fractionalBits;
    	unsigned int mask_waveIndex = this_oscillator->mask_waveIndex;
    	
    	float fadeDim1 = this_oscillator->fadeDim1;
    	float fadeDim1Increment = this_oscillator->fadeDim1Increment;
    	float fadeDim2 = this_oscillator->fadeDim2;
    	float fadeDim2Increment = this_oscillator->fadeDim2Increment;
    	float fadeDim3 = this_oscillator->fadeDim3;
    	float fadeDim3Increment = this_oscillator->fadeDim3Increment;
    	
    	float* wave000 = this_oscillator->wave000;
    	float* wave001 = this_oscillator->wave001;
    	float* wave010 = this_oscillator->wave010;
    	float* wave011 = this_oscillator->wave011;
    	float* wave100 = this_oscillator->wave100;
    	float* wave101 = this_oscillator->wave101;
    	float* wave110 = this_oscillator->wave110;
    	float* wave111 = this_oscillator->wave111;
    	
    	while (num_samples_remaining-- > 0)
    		{
    		unsigned int waveIndex0 = (unsigned int)(phase>>num_fractionalBits) & mask_waveIndex;
    		unsigned int waveIndex1 = (waveIndex0 + 1) & mask_waveIndex;
    		float linearGain1 = scaler_fractionalBits * (float)(phase & mask_fractionalBits);
    		float linearGain0 = 1.0f - linearGain1;
    		
    		float _wave000 = wave000[waveIndex0]*linearGain0 + wave000[waveIndex1]*linearGain1;
    		float _wave001 = wave001[waveIndex0]*linearGain0 + wave001[waveIndex1]*linearGain1;
    		float _wave010 = wave010[waveIndex0]*linearGain0 + wave010[waveIndex1]*linearGain1;
    		float _wave011 = wave011[waveIndex0]*linearGain0 + wave011[waveIndex1]*linearGain1;
    		float _wave100 = wave100[waveIndex0]*linearGain0 + wave100[waveIndex1]*linearGain1;
    		float _wave101 = wave101[waveIndex0]*linearGain0 + wave101[waveIndex1]*linearGain1;
    		float _wave110 = wave110[waveIndex0]*linearGain0 + wave110[waveIndex1]*linearGain1;
    		float _wave111 = wave111[waveIndex0]*linearGain0 + wave111[waveIndex1]*linearGain1;
    		
    		_wave000 += (_wave100 - _wave000)*fadeDim3;
    		_wave001 += (_wave101 - _wave001)*fadeDim3;
    		_wave010 += (_wave110 - _wave010)*fadeDim3;
    		_wave011 += (_wave111 - _wave011)*fadeDim3;
    
    		_wave000 += (_wave010 - _wave000)*fadeDim2;
    		_wave001 += (_wave011 - _wave001)*fadeDim2;
    
    		_wave000 += (_wave001 - _wave000)*fadeDim1;
    		
    		fadeDim3 += fadeDim3Increment;
    		fadeDim2 += fadeDim2Increment;
    		fadeDim1 += fadeDim1Increment;
    		
    		phase += phaseIncrement;
    		phaseIncrement += frequencyIncrement;
    		
    		*out++ = _wave000;
    		}
    	
    	this_oscillator->fadeDim1 = fadeDim1;
    	this_oscillator->fadeDim2 = fadeDim2;
    	this_oscillator->fadeDim3 = fadeDim3;
    	
    	this_oscillator->phase = phase;
    	this_oscillator->phaseIncrement = phaseIncrement;
    	}
    

Leave a Reply to robert bristow-johnson Cancel reply

Your email address will not be published. Required fields are marked *