Creating Audio Plugins, Part 14

  • Tutorial
All posts in the series:
Part 1. Introduction and configuration
Part 2. Learning the code
Part 3. VST and AU
Part 4. Digital distortion
Part 5. Presets and GUI
Part 6. Signal synthesis
Part 7. Receiving MIDI messages
Part 8. Virtual keyboard
Part 9 Envelopes
Part 10. Refinement of the GUI
Part 11. Filter
Part 12. Low-frequency oscillator
Part 13. Redesign
Part 14. Polyphony 1
Part 15. Polyphony 2
Part 16. Antialiasing

Let's start creating a polyphonic synthesizer from the components that we have!

Last time we worked on the parameters and user interface, today we will begin work on the polyphonic audio processing underlying the plugin . In our case, we can play up to 64 notes at a time. This requires a profound change in the structure of the plug-in, but we can use the classes we have already written Oscillator, EnvelopeGenerator, MIDIReceiverand Filter.

In this post we will write a class Voice(voice) representing one sounding note. Then create a class VoiceManagerthat ensures that all notes are sounded and muffled on time.
In the next post, we will clean the code from unnecessary obsolete parts, add tone modulation and bring the controls to the interface in working condition. At first glance, the work is full. But firstly, we already have almost all the necessary components, and secondly, in the end we will have a real-polyphonic-subtractive-pancake-synthesizer!

What to where?

Let us think for a moment about which parts of the plugin architecture are global and which exist separately for each individual note. Imagine, here you are playing a few notes on the keys. Each time you press the key, a tone appears that fades and, possibly, the timbre of which changes by a filter along a certain envelope. When you press the second key, the first one still sounds, and the second tone appears with its envelopes of amplitude and filter. The second press does not affect the first tone, it sounds and changes by itself . So each voice is independent and has its own envelopes of amplitude and filter.
LFO is global and unique, it just works and does not restart when you press a key.
As for the filter, it’s clear that the cutoff frequency and resonance are global because all voices look at the same cutoff and resonance knobs in the GUI. But the cutoff frequency of the filter is modulated by the envelope, so that at each moment in time, the calculated cutoff frequency for each voice is different. Take a look at Filter::cutoff- it is called in getCalculatedCutoff. So for every voice you need your own filter.
Can we get by with two oscillators for all the voices? Everyone Voiceplays his own note, i.e. it has its own frequency, and therefore its own independent Oscillator.

In short, the structure is as follows:

  • There is one MIDIReceiverand one in the pluginVoiceManager
  • Have VoiceManagerone LFOand many votesVoice
  • There Voiceare two Oscillator, two envelope generators EnvelopeGenerators(for amplitude and filter) and oneFilter

Voice Class

As usual, create a new class, name it Voice. And, as usual, remember to add it to all Xcode targets and all VS projects. In Voice.h add:

#include "Oscillator.h"
#include "EnvelopeGenerator.h"
#include "Filter.h"

In the body of the class, start with the section private:

    Oscillator mOscillatorOne;
    Oscillator mOscillatorTwo;
    EnvelopeGenerator mVolumeEnvelope;
    EnvelopeGenerator mFilterEnvelope;
    Filter mFilter;

Nothing new here: each voice has two oscillators, a filter and two envelopes.
Each voice starts with a specific MIDI note and volume. Add there:

    int mNoteNumber;
    int mVelocity;

Each of the following variables sets the modulation value of the parameters:

    double mFilterEnvelopeAmount;
    double mOscillatorMix;
    double mFilterLFOAmount;
    double mOscillatorOnePitchAmount;
    double mOscillatorTwoPitchAmount;
    double mLFOValue;

All of them, except mLFOValue, are associated with the values ​​of the interface handles. In fact, these values ​​are the same for all voices, but we will not make them global and drop them into the plugin class. Each voice needs access to these parameters every sample, and the Voice class does not even know about the existence of a plug-in class (absent #include "SpaceBass.h"). Setting up such access would be a time-consuming task.
And there is one more parameter. Do you remember we added a flag isMutedto the class Oscillator? Move it to Voiceso that when the voice is silent, the values ​​of the oscillator, envelopes and filter are not calculated:

    bool isActive;

Now beforeprivate add public. Let's start with the constructor:

    : mNoteNumber(-1),
    isActive(false) {
        // Set myself free everytime my volume envelope has fully faded out of RELEASE stage:
        mVolumeEnvelope.finishedEnvelopeCycle.Connect(this, &Voice::setFree);

These lines initialize variables with reasonable values. VoiceNot active by default . Also, using signals and slots EnvelopeGenerator, we “release” the voice as soon as the amplitude envelope leaves the release stage.
Add setters to public:

 inline void setFilterEnvelopeAmount(double amount) { mFilterEnvelopeAmount = amount; }
    inline void setFilterLFOAmount(double amount) { mFilterLFOAmount = amount; }
    inline void setOscillatorOnePitchAmount(double amount) { mOscillatorOnePitchAmount = amount; }
    inline void setOscillatorTwoPitchAmount(double amount) { mOscillatorTwoPitchAmount = amount; }
    inline void setOscillatorMix(double mix) { mOscillatorMix = mix; }
    inline void setLFOValue(double value) { mLFOValue = value; }
    inline void setNoteNumber(int noteNumber) {
        mNoteNumber = noteNumber;
        double frequency = 440.0 * pow(2.0, (mNoteNumber - 69.0) / 12.0);

The only interesting point here is this setNoteNumber. It calculates the frequency for a given note using the formula we already know and passes it to both oscillators. After it add:

    double nextSample();
    void setFree();

As Oscillator::nextSampleit gives us a way out Oscillator, it Voice::nextSamplegives out the resulting value of the voice after the envelope of the amplitude and filter. Let's write the implementation in Voice.cpp :

double Voice::nextSample() {
    if (!isActive) return 0.0;
    double oscillatorOneOutput = mOscillatorOne.nextSample();
    double oscillatorTwoOutput = mOscillatorTwo.nextSample();
    double oscillatorSum = ((1 - mOscillatorMix) * oscillatorOneOutput) + (mOscillatorMix * oscillatorTwoOutput);
    double volumeEnvelopeValue = mVolumeEnvelope.nextSample();
    double filterEnvelopeValue = mFilterEnvelope.nextSample();
    mFilter.setCutoffMod(filterEnvelopeValue * mFilterEnvelopeAmount + mLFOValue * mFilterLFOAmount);
    return mFilter.process(oscillatorSum * volumeEnvelopeValue * mVelocity / 127.0);

The first line ensures that when a voice is inactive nothing is calculated and zero is returned. The next three lines are calculated nextSamplefor both oscillators and mixed them accordingly mOscillatorMix. When mOscillatorMixequal to zero, only audible oscillatorOneOutput. When 0.5both oscillators have equal amplitude.
Then the next sample of both envelopes is calculated. We apply filterEnvelopeValuethe filter cutoff frequency and take the LFO value into account. The overall cut modulation is the sum of the filter envelope and the LFO.
The tone modulation of both oscillators is simply the LFO output multiplied by the modulation value. We will write it in a minute.
The last line is interesting. First, the content of the brackets: we take the sum of two oscillators, apply the volume envelope and the note volume value. Then we pass the result through mFilter.process, as a result we get a filtered output, which we return.

The implementation is setFreeextremely simple:

void Voice::setFree() {
    isActive = false;

As already mentioned, the call to this function is carried out every time it mVolumeEnvelopecompletely fades out.


It's time to write a class for voice control. Create a class named VoiceManager. In the header, start with these lines:

#include "Voice.h"
class VoiceManager {

And continue privatewith the class members:

static const int NumberOfVoices = 64;
Voice voices[NumberOfVoices];
Oscillator mLFO;
Voice* findFreeVoice();

The constant NumberOfVoicesindicates the maximum number of voices simultaneously playing. The next line creates an array of voices. This structure uses a place for 64 votes, so it’s better to think about dynamic memory allocation . However, the plugin class new PLUG_CLASS_NAMEis already dynamically distributed (look for " " in Iplug_include_in_plug_src.h ), so that all members of the plugin class are also on the heap .

mLFOIs a global LFO for the plugin. It never restarts, it just oscillates independently. It can be argued that it should be inside the plugin class ( VoiceManagerno need to know about LFO). But this will add another layer of distinction between voices Voiceand LFOs, which means we will need more glue code .
findFreeVoiceThis is an auxiliary function for finding voices that are not currently sounding. Add its implementation to VoiceManager.cpp :

Voice* VoiceManager::findFreeVoice() {
    Voice* freeVoice = NULL;
    for (int i = 0; i < NumberOfVoices; i++) {
        if (!voices[i].isActive) {
            freeVoice = &(voices[i]);
    return freeVoice;

She simply iterates over all the voices and finds the first one silent. We return the pointer (instead of the link &), because in this case, unlike the link, you can return NULL. This will mean that all voices sound.

Now add the publicfollowing function headers:

void onNoteOn(int noteNumber, int velocity);
void onNoteOff(int noteNumber, int velocity);
double nextSample();

As the name implies, it onNoteOnis called when a MIDI Note On message is received. onNoteOff, Correspondingly, is called when the Note Off message. We will write the code of these functions in the .cpp class file:

void VoiceManager::onNoteOn(int noteNumber, int velocity) {
    Voice* voice = findFreeVoice();
    if (!voice) {
    voice->mVelocity = velocity;
    voice->isActive = true;

First we find a free voice with findFreeVoice. If nothing is found, we will not return anything. This means that when all voices sound, pressing another key will not have any result. Implementing the voice stealing approach will be one of the topics of the next post. If there is a free voice, we need to update it to its initial state ( resetwe will do it very soon). After that we set the correct values setNoteNumberand mVelocity. We mark the voice as active and transfer both envelopes to the attack stage.
If you start the assembly right now, an error pops up saying that we are trying to access privatemembers Voicefrom outside. In my opinion, the best solution in this situation would be to use the keyword friend. Add the appropriate line before publicin Voice.h :

friend class VoiceManager;

Thanks to this line Voicegives VoiceManageraccess to its privatemembers. I'm not a fan of the extensive use of this approach, but if you have a class Fooand a class FooManager, this is a good way to avoid writing a lot of setters.

onNoteOfflooks like that:

void VoiceManager::onNoteOff(int noteNumber, int velocity) {
    // Find the voice(s) with the given noteNumber:
    for (int i = 0; i < NumberOfVoices; i++) {
        Voice& voice = voices[i];
        if (voice.isActive && voice.mNoteNumber == noteNumber) {

We find all the voices with the number of the released note and translate their envelopes into the release stage. Why voices , not voices? Imagine that you have a very long stage of attenuation in an envelope of amplitude. You press the key and release it, and while the tail of the note still sounds, quickly press that key again. Naturally, you do not want to chop off the previous sounding note. That would be very ugly. It is necessary that the previous note is sounded, and that the new one starts to sound in parallel. Thus, you will need more than one voice per note. If you beat the keys very quickly, you will need a lot of votes.
So what happens if, for example, we have five active voices for Until the third octave and we release this key? CalledonNoteOffand transfers the envelopes of all five voices to the release stage. Four of them are already at this stage, so let's look at the first line EnvelopeGenerator::enterStage:

if (currentStage == newStage) return;

As you can see, nothing will happen for these four notes, there will be no snag here.

Let's now write a member function nextSamplefor VoiceManager. It should display the total value for all active votes:

double VoiceManager::nextSample() {
    double output = 0.0;
    double lfoValue = mLFO.nextSample();
    for (int i = 0; i < NumberOfVoices; i++) {
        Voice& voice = voices[i];
        output += voice.nextSample();
    return output;

We start with silence (0.0), iterate over all the voices, set the current LFO value and add the voice output to the total output. As we remember, if a voice is inactive, its function Voice::nextSamplewill not calculate anything and will end immediately.

Reusable components

Until now, we have created objects Oscillatorand Filterused them throughout the whole time the plugin works. But VoiceManager reuses free voices, so you need to figure out how to completely put your voice back to its original state. Let's start by adding a function to the publicheader Voice:

void reset();

We write the function body in .cpp:

void Voice::reset() {
    mNoteNumber = -1;
    mVelocity = 0;

As you can see, here the oscillators, envelopes and the filter are reset mNoteNumberand mVelocitythen reset. Let's write it!

In publicthe Oscillator.h section, add:

void reset() { mPhase = 0.0; }

This allows you to start the waveform first every time a voice starts to sound.

At the same time, while we are there, remove the flag isMutedfrom the section private. Remember to remove it also from the constructor initialization list and remove the member function setMuted. We are now monitoring the state of activity at the level Voice, so the oscillator no longer needs it. Remove this line from the function Oscillator::nextSample:

// remove this line:
if(isMuted) return value;

The function resetis a EnvelopeGeneratorlittle longer. In the publicheader section, EnvelopeGeneratorwrite the following:

void reset() {
    currentStage = ENVELOPE_STAGE_OFF;
    currentLevel = minimumLevel;
    multiplier = 1.0;
    currentSampleIndex = 0;
    nextStageSampleIndex = 0;

Here you just need to reset more values, everything is linear. It remains to add resetfor the class Filter(also in public):

void reset() {
    buf0 = buf1 = buf2 = buf3 = 0.0;

As you probably remember, these buffers contain the previous output filter samples. When we reuse voice, these buffers should be empty.

To summarize: every time it VoiceManageruses Voice, it calls a function resetto reset the voice to its initial state. This function, in turn, resets the voice oscillators, its envelope generators, and the filter.

static or not static?

The member variables for all votes are the same:

  • Oscillator: mOscillatorMode
  • Filter: cutoff, resonance,mode
  • EnvelopeGenerator: stageValue

At first I thought that such redundancy is evil, and all these things should be static members. Let's imagine that it mOscillatorModeis static. Then the LFO would have the same waveform as the other oscillators, but we don’t want this. Further, if the values ​​of stageValuethe envelope generator EnvelopeGeneratorwere static, the envelopes of the amplitudes and the filter would be the same.

This could be fixed by inheritance: by creating classes VolumeEnvelopeand FilterEnvelopethat inherit from the class EnvelopeGenerator. The parameter stageValuecould be static and VolumeEnvelopeandFilterEnvelopecould change it. This would clearly separate envelopes and all voices could have access to static members. But in this case we are not talking about large amounts of memory. All that we have to do with the structure that we created is to synchronize these variables between the envelopes of amplitudes and filters of all voices.

However, one thing can be static: sampleRate. It makes no sense that the synthesizer components operate at different sampling frequencies. Let's tweak this in Oscillator.h :

static double mSampleRate;

That means we should not initialize this variable through the initialization list. Remove mSampleRate(44100.0). In Oscillator.cpp after #includeadd:

double Oscillator::mSampleRate = 44100.0;

The sampling rate is now static and all oscillators use one of its values.
Let's do the same for EnvelopeGenerator. Make it sampleRatestatic, remove the constructor from the initialization list, and add in EnvelopeGenerator.cpp :

double EnvelopeGenerator::sampleRate = 44100.0;

In EnvelopeGenerator.h, make the setter static:

static void setSampleRate(double newSampleRate);

We have added a lot of new! Next time we will clean the excess and bring the GUI into working condition.

The code can be downloaded from here .
Original post .

Also popular now: