All posts series:
Part 1. Introduction and setupPart 2. Learning CodePart 3. VST and AUPart 4. Digital DistortionPart 5. Presets and GUIPart 6. Signal synthesisPart 7. Receive MIDI MessagesPart 8. Virtual KeyboardPart 9. EnvelopesPart 10. Refinement GUIPart 11. FilterPart 12. Low-frequency oscillatorPart 13. RedesignPart 14. Polyphony 1Part 15. Polyphony 2Part 16. Antialiasing
To make our SpaceBass sound even better, you need to create an oscillator in which there is less aliasing. This is an optional improvement. Without it, the synthesizer will work as before, but with it the sound on the upper octaves will be much better.
Spectrum analysis
I would like to show you a very good free plugin:
Voxengo SPAN . It can be hung on a track, and it will show the spectrum of the signal passing through it. At this stage, it’s too early to write your own test procedures with
FFT , so that SPAN will be an indispensable tool for comparing the results of various algorithms for oscillators. Download and install.
Launch SpaceBass in REAPER and do the following:
')
- Turn the Mix knob all the way to the left so that only the first oscillator can be heard
- Choose a square wave for him (square wave)
- Set cutoff frequency to maximum, resonance to minimum
- LFO filter at minimum, handle the envelope around the middle
With such settings, the raw waveform of the first oscillator is audible. Now hang the SPAN on the same track after the synthesizer. Pinch the high note (I used the A-sixth octave) and look at the spectrogram in SPAN, it should look something like this:

It is very simple to read it: along the x axis is the frequency in hertz, frequencies from 66 Hz to 20 kHz are displayed in this plug-in. The
logarithmic scale, i.e. the distance between the octaves is always the same - between Before the first octave and Until the second as much as between Before the seventh and eighth. The frequency ratio of neighboring octaves is two to one. While signal harmonics are
multiplied (or divided) by different integers, the fundamental frequency. This means that the harmonics are distributed unevenly along the x axis.
The y-axis is the amplitude in decibels. Thus it is very easy to determine which frequency has what amplitude at the moment.
Depending on the settings, some points may differ on your and my spectrograms, but one thing is certain: something is wrong. We expected to see the spectrogram of the meander - a series of peaks, the amplitude of which decreases with increasing frequency, and
nothing between the peaks. And we certainly did not expect to see any spectral components below the fundamental frequency (on the left side of the graph). As you remember, such nonsense is
aliasing .
What to do with him? There are different approaches to the solution. The most accurate solution is to synthesize members
of the Fourier series to approximate the meander. In fact, it is the overlapping of sines with correctly selected amplitudes, starting from the fundamental frequency and higher, one sinus per harmonic. But the harmonic synthesis
will need to be stopped when the Nyquist frequency is reached . This approach will give an ideal
band-limited (eng. Bandlimited) waveform, all of whose spectral components are strictly between the fundamental frequency and the Nyquist frequency.
Naturally, there is one problem. This method could be suitable for the highest octaves, where the fundamental (fundamental) frequency is so high that few harmonics remain before the Nyquist frequency. But for the lower octaves there are a lot of harmonics, to put it mildly: at a frequency of 44.1 kHz, a square wave with a fundamental frequency of 100 Hz will have 219 harmonics to Nyquist, which means that in total,
sin()
must be calculated 220 times each sample. In the polyphonic model, this number is still multiplied by the number of notes played. On the one hand, the sinuses for each note need only be folded once for each note. But this is true if we do not have any modulation of the tone. Once it is there, the frequency can change each sample, so a lot of work needs to be done.
BLIPs and BLEPs
There are other approaches to the synthesis. Most notable:
The last two approaches are based on the fact that aliasing arises only because of sharp drops in the form of a wave. With those waveforms that we synthesize, the only problem is these sharp drops. Can we shake them like a piece of sandpaper? Simple rounding is equivalent to easy low-pass filtering, and this is not what we need. We need to filter nothing before the Nyquist frequency, and have nothing higher than it, as if it were just superimposed sinuses.
Synthesis of a meander from the sinuses looks like this:

The sinuses superimposed on each other are marked in blue, the resulting band-limited meander in red. As you can see, these are not just rounded corners. This waveform has characteristic vibrations, “ripples”.
If simplified, the BLEP methods first generate a waveform,
as we did before , and then apply this ripple. This eliminates (or strongly suppresses) aliasing.
If you clicked on the links above, you can already guess that the PolyBLEP method is the simplest. We use it!
Class PolyBLEPOscillator
PolyBLEPOscillator
is an Oscillator
, so we will
inherit publicly from the latter.
Create a new
PolyBLEPOscillator class in our project. If you have not read the previous articles, download the
finished project and start from now.
This is the class definition:
#include "Oscillator.h" class PolyBLEPOscillator: public Oscillator { public: PolyBLEPOscillator() : lastOutput(0.0) { updateIncrement(); }; double nextSample(); private: double poly_blep(double t); double lastOutput; };
We inherit publicity from
Oscillator
. To change the way of synthesis, we define a new member function
nextSample
. We also add a new
private
function
poly_blep
, which will generate oscillations on the meander ramps.
lastOutput
stores the last generated value (this is only needed for a triangular waveform).
Add the
poly_blep
implementation to the
PolyBLEPOscillator.cpp :
This may look a little sophisticated, but, in fact, the function almost always returns
0.0
, except when we are close to a differential. The first
if
for the case when we are at the beginning of the period, and the other
else if
for when we are almost at the very end. This is the behavior of the saw, because there is only one difference in it, between two periods.
Before implementing
nextSample
, you need to change something in the class of the oscillator. Make the
nextSample
function in
Oscillator.h virtual:
virtual double nextSample();
This means that we can change the behavior of the
nextSample
function in our subclass. Using
virtual
in code with critical execution times is not the best solution. You can use
templates (and avoid duplicating code), but I want to leave the explanation at a simple level and not be distracted from the topic of synthesis.
Change
private:
to
protected:
This way we will be able to access parameters such as
mPhase
from the member functions of
PolyBLEPOscillator
.
As I already said, we use our waveforms with aliasing from the
Oscillator
class and impose
poly_blep
on them. Currently,
nextSample
calculates the waveform and increments the phase. We need to separate these unrelated things.
Add the following
protected
member function:
double naiveWaveformForMode(OscillatorMode mode);
This function will calculate waveforms with aliasing.
Naive here means that the waveform is generated in a simple and incorrect way. Let's write it in
Oscillator.cpp (you can just copy it, because it is almost identical to
Oscillator::nextSample
)
double Oscillator::naiveWaveformForMode(OscillatorMode mode) { double value; switch (mode) { case OSCILLATOR_MODE_SINE: value = sin(mPhase); break; case OSCILLATOR_MODE_SAW: value = (2.0 * mPhase / twoPI) - 1.0; break; case OSCILLATOR_MODE_SQUARE: if (mPhase < mPI) { value = 1.0; } else { value = -1.0; } break; case OSCILLATOR_MODE_TRIANGLE: value = -1.0 + (2.0 * mPhase / twoPI); value = 2.0 * (fabs(value) - 0.5); break; default: break; } return value; }
The differences from
Oscillator::nextSample
as follows:
- The waveform is selected depending on the
mode
parameter transmitted from outside (instead of mOscillatorMode
) - Saw is now ascending, not descending
Since this function contains all the code from
Oscillator::nextSample
, replace the body of
nextSample
with this:
double Oscillator::nextSample() { double value = naiveWaveformForMode(mOscillatorMode); mPhase += mPhaseIncrement; while (mPhase >= twoPI) { mPhase -= twoPI; } return value; }
Here,
naiveWaveformForMode
is simply called to calculate the waveform and
mPhase
.
PolyBLEP generation
Let's go back to
PolyBLEPOscillator.cpp and write
nextSample
. Let's start like this:
double PolyBLEPOscillator::nextSample() { double value = 0.0; double t = mPhase / twoPI; if (mOscillatorMode == OSCILLATOR_MODE_SINE) { value = naiveWaveformForMode(OSCILLATOR_MODE_SINE); } else if (mOscillatorMode == OSCILLATOR_MODE_SAW) { value = naiveWaveformForMode(OSCILLATOR_MODE_SAW); value -= poly_blep(t); }
The variable
t
necessary for the
poly_blep
function to work. This is the current phase value divided by
twoPI
, so it is always between
0
and
1
. The first
if
separates the waveforms. Antialiasing is not needed for sine, since it has only one, the first harmonic itself is its fundamental frequency. For a saw, we first get a simple waveform from an oscillator, and then we impose a
pily_blep
on it - that's all!
Create a triangle like this: first, take the meander, and then
integrate it. Since we work with discrete values, integration simply means the summation of values. If you estimate, the meander begins with solid units, so their summation will give a linear increment. After the half-cycle, solid units are minus, their integration will give a linear decline. The triangle is just that: linear growth and linear decline.
Keeping this in mind, we will write the code immediately for both the meander and the triangle:
else { value = naiveWaveformForMode(OSCILLATOR_MODE_SQUARE); value += poly_blep(t); value -= poly_blep(fmod(t + 0.5, 1.0));
And again we start with a simple wave with aliasing. But this time we impose
two PolyBLEPs. One for the start of the period, the other is offset by
0.5
periods, since meander have two drops. The saw has only one differential.
What is missing is a triangle. Add to the end of the
else
block:
if (mOscillatorMode == OSCILLATOR_MODE_TRIANGLE) {
Earlier, I wrote that we will integrate the meander. This is not entirely accurate. If we simply integrate, this will lead to huge output values, that is, we will have a monstrous overload. Instead, you need to use a
quasi- integrator (leaky integrator). It summarizes the new value with the old one, but multiplied by the value slightly less than one. Thus, the values ​​do not go off-scale.
Let's finish the increment of the phase (everything is as before):
} mPhase += mPhaseIncrement; while (mPhase >= twoPI) { mPhase -= twoPI; } return value; }
It was just so easy to create a
PolyBLEPOscillator
!
Using the new oscillator
To use our brilliant new
PolyBLEPOscillator
you just need to swap a couple of lines in
Voice.h . Replace
#include "Oscillator.h"
with
#include "PolyBLEPOscillator.h"
.
In the
private
section, turn
mOscillatorOne
and
mOscillatorTwo
into objects of the
PolyBLEPOscillator
class:
PolyBLEPOscillator mOscillatorOne; PolyBLEPOscillator mOscillatorTwo;
That's all! Launch our plugin and let's look at the spectrum. As you can see, the aliasing effect is very noticeably eliminated. Screenshots before / after for comparison:
Saw:


Meander:


Triangle:


What about the LFO?
We still use the old
Oscillator
for the LFO. Should I switch to
PolyBLEPOscillator
? In fact, sharp boundaries are very desirable in the LFO, with the help of them you can get interesting effects. And aliasing doesn’t really bother us because the fundamental frequency is usually low, less than 30 Hz. Each next harmonic has an amplitude lower than the previous one, so frequencies above the Nyquist have a very small amplitude.
Total
We generated a meander and a saw without aliasing using aliasing waveforms, which we imposed on PolyBLEP. The triangle was generated using a quasi-integrator and a meander without aliasing. Now you can happily play our synth on the highest octaves and not be afraid that there will be disharmonious frequencies!
The code can be downloaded
from here .
Thanks for reading! :)
Original post .