📜 ⬆️ ⬇️

Creating audio plug-ins, part 16

All posts series:
Part 1. Introduction and setup
Part 2. Learning Code
Part 3. VST and AU
Part 4. Digital Distortion
Part 5. Presets and GUI
Part 6. Signal synthesis
Part 7. Receive MIDI Messages
Part 8. Virtual Keyboard
Part 9. Envelopes
Part 10. Refinement GUI
Part 11. Filter
Part 12. Low-frequency oscillator
Part 13. Redesign
Part 14. Polyphony 1
Part 15. Polyphony 2
Part 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:
')


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 :

 // PolyBLEP by Tale // (slightly modified) // http://www.kvraudio.com/forum/viewtopic.php?t=375517 double PolyBLEPOscillator::poly_blep(double t) { double dt = mPhaseIncrement / twoPI; // 0 <= t < 1 if (t < dt) { t /= dt; return t+t - t*t - 1.0; } // -1 < t < 0 else if (t > 1.0 - dt) { t = (t - 1.0) / dt; return t*t + t+t + 1.0; } // 0 otherwise else return 0.0; } 


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:


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) { // Leaky integrator: y[n] = A * x[n] + (1 - A) * y[n-1] value = mPhaseIncrement * value + (1 - mPhaseIncrement) * lastOutput; lastOutput = value; } 


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:
image
image

Meander:
image
image

Triangle:
image
image

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 .

Source: https://habr.com/ru/post/232153/


All Articles