A wavetable
Build one cycle in init, read it with interpolation, and turn a stored shape into a changing tone.
Anyone ready to build an oscillator with a waveform of their own.
A wavetable is a drawing of one cycle, stored as numbers. Instead of rebuilding a complicated waveform on every audio frame, an oscillator moves through that drawing and reads the value at its current position.
There are two separate jobs here: build the table once, then read it
repeatedly. Pole's init block gives the first job a home outside the audio
frame loop. That keeps the steady-state work small and makes the patch easier
to reason about.
Controls#
| Parameter | Default | Range | Meaning |
|---|---|---|---|
low | 55 Hz | 20–2,000 Hz | Lower endpoint of the pitch sweep. |
high | 880 Hz | 40–8,000 Hz | Upper endpoint of the pitch sweep. |
rate | 0.125 Hz | 0.01–8 Hz | Sweep cycles per second; the default takes eight seconds. |
level | 0.8 | 0–1 | Output amplitude multiplier. |
No audio input is needed. out carries the mono oscillator. The code sorts
the two frequency endpoints, so crossing the controls does not invert the sweep.
The complete patch#
processor Wavetable
{
param float low = 55.0f [20.0f, 2000.0f] smooth 20.0f;
param float high = 880.0f [40.0f, 8000.0f] smooth 20.0f;
param float rate = 0.125f [0.01f, 8.0f] smooth 20.0f;
param float level = 0.8f [0.0f, 1.0f] smooth 20.0f;
output stream float out;
float table[1024];
float phase = 0.0f;
float sweep = 0.0f;
init
{
for (wrap<1024> i)
{
let angle = 6.2831853f * float(i) / 1024.0f;
table[i] = (sin(angle) + 0.5f * sin(2.0f * angle)
+ 0.33f * sin(3.0f * angle)
+ 0.2f * sin(5.0f * angle)) * 0.48f;
}
}
void main()
{
loop
{
let sweepNext = sweep + rate / processor.frequency;
sweep = sweepNext - floor(sweepNext);
let triangle = sweep < 0.5f ? sweep * 2.0f : 2.0f - sweep * 2.0f;
let lower = min(low, high);
let upper = max(low, high);
let frequency = lower * pow(upper / lower, triangle);
let phaseNext = phase + frequency / processor.frequency;
phase = phaseNext - floor(phaseNext);
let position = phase * 1024.0f;
let index = int(position);
let fraction = position - float(index);
let a = table[index];
let b = table[index + 1];
out <- (a + (b - a) * fraction) * level;
advance();
}
}
}First, draw one cycle#
The bounded loop fills 1,024 entries during init. Each entry is a sum of four
sines: the fundamental plus its second, third and fifth harmonics. The weights
decide the tone. The final scale leaves room for those waves to add together.
A reset rebuilds the table. Changing the pitch controls does not: the table stores a shape, and pitch comes from how quickly the read position moves through it. To replace that shape with a loaded waveform, use an external buffer.
Then, move through the drawing#
sweep is a slow phase that becomes a triangle from 0 to 1 and back. The
frequency expression moves between lower and upper exponentially: equal
steps in the triangle correspond to equal pitch ratios. Halfway between
110 Hz and 440 Hz is 220 Hz in this sense, not the arithmetic midpoint 275 Hz.
The audio phase advances at that frequency. Multiplying it by 1,024 turns a
cycle position into a table position. Most positions land between entries,
so the patch reads the two neighbours and blends them using fraction.
At the last entry, index + 1 wraps to entry zero, the start of the next cycle.
Try it#
Remove all but the first sine in init, then run the patch again. You now have
a sine table with the same playback code. Restore the harmonics one at a time
to hear how the stored shape changes the sound.
Set low and high to the same value to stop the pitch sweep. This makes it
easier to compare changes to the waveform without the pitch changing too.
What interpolation does not solve#
Interpolation smooths movement between table entries. It does not remove harmonics above half the sample rate. The fifth harmonic of an 8 kHz note is 40 kHz, which is above the limit at 48 kHz and will alias. A production wavetable oscillator needs appropriately band-limited tables for its pitch range; a larger table alone does not solve that problem.
Next: an effects chain, where several processors become one graph.