Warning: This thing can be quite loud. Turn down your speakers or headphones before use!
In this round of experimentation with wavetable synthesis I began exploring the possibilities of pitch bending and modulation. At first this seemed to be a difficult task because of the general nature of wavetables, wherein waveforms are written to memory as lists of numbers. Those numbers are later read from memory and used to generate audible sound. This saves processing time, as the numbers do not have to be calculated repeatedly.
So, how can frequency modulation be achieved on a set of static numbers? First we need to understand the problem in it’s entirety. Suppose we have a list of 44,100 samples that represent one sine cycle or a 1Hz tone. Clearly this is out of the range of human hearing but if we were to read this data out of memory and use it to fill an audio buffer it might look something like this:
var pointer:int = 0;
var samples:List; // -- a list of audio data
for( var i:int = 0; i < BUFFER_SIZE; i++ )
{
buffer.writeData( samples[pointer] )
if( pointer > samples.length )
{
pointer = 0;
} else {
pointer++;
}
}
From the previous example, consider what would happen if we incremented the pointer by two in instead of one… We would read every other sample, shortening the wave by a factor of .5 and effectively increasing the frequency of the waveform to 2Hz. Now imagine if we increased the pointer by 100. We would take every hundredth sample and a tone of 441Hz would be audible as 44100/100 = 441.
Typically we are interested in using frequency values to determine increment values. So, the question is: “For a specific frequency, what value do we increment the pointer by?”. I’ve heard this increment value referred to as the phase accumulator and the pointer is typically called the phase, so that is how I will reference them from this point forward. Fortunately, there is a well known equation that can determine the phaseAccumulator based on the. In the example below we assume that we are working with a much shorter wavetable, one with only 8192 samples in it.
var freq:Number = 440; var wavelet:List; // -- list of audio data, has a length of 8192 samples const SAMPLE_RATE = 44100; var phaseAccumulator:Number = ( freq / SAMPLE_RATE ) * wavelet.length;
The above example results in a phaseAccumulator of 81.7342407. If you recall from the first example we are adding the phaseAccumulator to the phase variable on every iteration and using the phase as a pointer in the wavelet. Now, we have a slight problem. We can not use a floating point number as a pointer. Instead we have to round the pointer before addressing a sample in the wavelet. Our example now looks something like this.
var freq:Number = 440;
var wavelet:List; // -- list of audio data, has a length of 8192 samples
const SAMPLE_RATE = 44100;
var phaseAccumulator:Number = ( freq / SAMPLE_RATE ) * wavelet.length;
for( var i:int = 0; i < BUFFER_SIZE; i++ )
{
buffer.writeData( samples[ Math.round( phase ) ] ) // -- only use the round value of phase, do not actually round it
if( phase + phaseAccumulator > samples.length )
{
phase = 0;
} else {
phase += phaseAccumulator;
}
}
So now we have a way to calculate all frequencies from a static list of audio data. Pretty nice… this also opens up the possibility of introducing modulation and pitch-bending. To achieve the portamento effect in the example above, I use something of an old tweening trick. A second frequency variable is added, which represents a “target frequency”. Then using a simple easing equation to modulate the frequency variable we can calculate a new phaseAccumulator value. The big difference between this approach and the above example is that we now move two equations into the loop and continually calculate new values for frequency and phaseAccumulator.
for( var i:int = 0; i < BUFFER_SIZE; i++ )
{
buffer.writeData( samples[ Math.round( phase ) ] ) // -- only use the round value of phase, do not actually round it
freq += ( targetFreq - _freq ) * portTime; // -- small number between .009 and .0001, lower the number / longer the effect
phaseAccumulator = ( freq / SAMPLE_RATE ) * length;
if( phase + phaseAccumulator > samples.length )
{
phase = 0;
} else {
phase += phaseAccumulator;
}
}
By easing the frequency variable we are effectively modulating the phaseAccumulator which speeds up or slows down the advancement of the phase variable across the wavelet. This adds a nice portamento or pitch bending affect. Additionally we can introduce a more regular LFO type effect by further modulating the phaseAccumulator at a regular rate. In the source code you will see that another wave table is being used as an input value for the modulation effect.
One thing to mention is that by rounding the phase value, aliasing occurs. This causes tiny artifacts to be attached to the tones and is most noticeable in pure sine tones. It can be avoided through various methods, each of which would make great topics for future articles.
Pingback: Tweets that mention Wavetable Synthesis Round III | makemachine -- Topsy.com
Nice clear well-written article, Jeremy.
I’m so glad you mentioned aliasing at the end, too, as – I know it sounds snobbish – my ears are getting tired of naive oscillator/wavetable implementations!
I hope you’ll write an article on resolving aliasing as a lot of the papers out there are too academic for the normal non-professional coders who want to get into this stuff. It’ll save me having to put finger to keyboard, too.
Hi Anthony,
Yes, aliasing seems like a complex problem. I read a couple of days ago that aliasing is caused by frequencies outside of the Nyquist theorem being folded back into audio signal. To prevent this you can implement band pass filtering to eliminate those frequencies. This is a very simplified view of it, perhaps a good starting point though. Thanks for the suggestion for a new article. It could be a great topic for Wavetable Synthesis Round IV.
~Jeremy
Yeah, that’s pretty much the basics of it, although bandpass filtering ain’t quite the cure because it’s too late by then: you can band-limit wavetables before hand, oversample and then brick-limit filter, do some crazy math to implement band-limited realtime oscillator calculations if you need oscillator sync, etc.
It can get quite deep, but then so do most audio programming topics!
If you’re interested, I’ll rustle up a bunch of links and papers and stuff.
Oh, and if you pass through London on your European travels and fancy a drink & chat about synth programming topics give me a poke. It’s kinda what I live for.
Hey Anthony,
Thanks for the tips! Yeah, band limiting, that could make for a good article. At some point it seems like the performance boost that wavetables provide would be lost with all of the additional processing to reduce aliasing.
Wish I could visit London on this trip! Perhaps this summer. If so, a beer and synth talk sounds great
.
~Jeremy
What you can do is have a “stack” of wavetables – one per octave or fifth or similar – pre-band-limited to ensure they contain nothing above Nyquist for the frequency range for which they’re going to be used.
Standard phase accumulator implementation like you have then chooses the appropriate wavetable according to the playback pitch. Think of it like the audio version of texture mip-maps, which are also used precisely to avoid lookup aliasing!
Of course, things get a wee bit fiddly when doing pitch-bends that cross from wavetable index to another but that’s essentially just a phase-locked cross-fade: a few more adds and multiplies.
Simple and a very small over-head (compared to all the other exponential stuff a synth needs) for a far smoother, richer sound and we all want that, right? Well, except for when we don’t but that’s what non-linear temporal distortion algorithms are for and that’s a whole ‘nother topic.
Oh, finally found the link I uncovered a while ago to a site which allows spectrographic comparison of different software’s resampling aliasing.
Pretty pictures!
http://lowbroweye.com/post/1726341202/pretty-sound-patterns
Hi Jeremy—very cool site! just happened upon it.
“One thing to mention is that by rounding the phase value, aliasing occurs.”
Some tips:
* In this case, truncation is identical to rounding, error-wise—round just adds a single -sample phase shift. (I only mention this because I see so many times in wavetable discussions that rounding is an improvement over truncation.) Linear interpolation, of course, is the next stepup.
* Instead of reseting your phase accumulator to zero, subtract off the integer part (that can mean subtracting 1.0 if you take care to constrain your phase increment, or just truncate to integer and subtract that off). The discontinuity from resetting the phase to zero is a sizable source of error in your oscillator, and can be heard easily if you compare the sine at various frequencies.
* It’s useful to consider the time-domain view of aliasing. The problem is the lack of resolution in placing abrupt changes—a raw square wave, for instance, or hard sync. If the wave period isn’t an integer multiple of the sample period, the period will modulate between the two closest samples at a rate that’s dependent on the wave frequency—aliasing in the time domain.
Nigel,
Thanks for all of the pointers! This is relatively new territory for me so I really appreciate it. Btw, your site looks great and I look forward to further reading.
~J