Anti-aliased square wave synthesis demo (BLIT)
Welcome to today's pointless experiment!
Ever want to synthesize some square waves but don't want to run at absurdly high sample rates to get rid of the pesky aliasing?
Frequency aliasing occurs in a simple NCO-type synthesizer because the edge of the waveform is rounded onto the next closest sample point when it really should be somewhere in-between.
Luckily, it is possible to generate such inbetween-samples edges by considering our square wave as the running sum of a pulse train of alternating polarity (ie. one pulse makes one edge). Synthesizing an inter-sample pulse in turn is rather easy, since that's just evaluating a sinc function on the samples surrounding the pulse (in theory, this would be an infinite amount of samples in both directions, but in practice we can cut it off after a few samples).
This technique is called Band-Limited Impulse Train.
The sampled sinc looks like this:
Notice how when the offset is an integer, the sample points line up with the zero-crossings of the sinc function and we get the good old lone spike.
Integrated it looks like this:
Now, the secret to doing this in real time is to precalculate these sinc impulse samples for a few fractional offsets and every time the oscillator generates an edge, pick the closest one to how far the edge lies in the "past" and add it into a buffer of same size. For each sample produced, this buffer is shifted over by one, shifting in zero at the tail and summing onto the head. The head of the buffer then becomes your output value, the running sum of all pulses. (This obviously incurs a delay of half the buffer size between oscillator and output)
Perhaps a poor explanation, but oh well.
Attached is an example implementation. Set
NO_BLIT to true to hear what a normal non-antialiased oscillator sounds like. Pretty big difference, right?
Things to further look into:
- How big do the samples really need to be? Currently they're 16 long, but could probably cut down to 12 with little loss.
- How many impulses are really needed? Currently I'm using 16, but more only really increase memory use. Effectively this is a multiplier of the sample rate, so doubling the sample rate should mean that only half the impulses are needed for an equivalent result.
- Since MULS only cares about the bottom 16 bits, two impulses could be interleaved to halve the table size