Making Audio Plugins Part 16: Polyphony Part I

Let’s create a polyphonic synthesizer from the components we already have!

In the last post we have created our plugin’s parameters and the UI. Now we’ll build the underlying audio processing, but in a polyphonic way! That means that we’ll be able to play up to 64 notes at the same time! This will change our plugin’s structure fundamentally, but we’ll be able to reuse the Oscillator, EnvelopeGenerator, MIDIReceiver and Filter classes we already have.

In this post, we’ll create a Voice class that represents one playing note. We’ll then create a VoiceManager class that will make sure Voices are being started whenever a note is pressed.
In the following post, we’ll clean up code that’s not needed anymore, add pitch modulation, and make the GUI controls work. It sounds like quite a bit of work, but we’ve already got most of the components we need, and in the end we’ll have a polyphonic subtractive synthesizer!

What goes where?

Let’s think for a brief moment about which parts of our plugin are global, and which parts are separate for each played note. Imagine you’re playing a few notes on your keyboard. Whenever you hit a note, you hear a tone that decays and maybe changes its filter cutoff over time using a filter envelope. When you hit another note while the last note is still playing, you hear another tone with decay and changing filter cutoff, but the previous tone is not affected! It keeps going as if it is alone. So each voice is independent with its own volume & filter envelopes.
The LFO is global to our plugin. There’s just one that keeps running on and on, and it can’t be re-triggered at the start of a note.
So what about the filter? Obviously the filter’s cutoff and resonance are both global, because all voices listen to the same cutoff/resonance knob in our GUI. But the filter’s cutoff frequency is modulated by the filter envelope, so at any given time the calculated cutoff is different for each voice. If you have a brief look at the Filter::cutoff member function, you can see that it calls getCalculatedCutoff. So we need one Filter per Voice.
But can we create two global Oscillators and share them across all voices? Each Voice can play a different note, i.e. a different frequency. So these have to be independent as well.

In short, this will be our hierarchy:

  • Our plugin has one MIDIReceiver and one VoiceManager.
  • The VoiceManager has one LFO (which is an Oscillator) and many Voices.
  • A Voice has two Oscillators, two EnvelopeGenerators (for volume and filter) and one Filter.

The Voice class

In the SpaceBass Xcode project, create a new C++ class and name it Voice. As always, make sure you add it to all targets. Open Voice.h and include our components:

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

Inside the class, let’s begin with the private section:

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

That’s pretty straightforward, right? Each voice has two oscillators, a volume envelope, a filter envelope and a filter.
Each voice is triggered with a certain MIDI note number and a velocity. Add these two (to private):

    int mNoteNumber;
    int mVelocity;

Each of the following variables describes how much a parameter is modulated:

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

All of these properties except for mLFOValue represent the knob values in our GUI. Actually they are always the same for all voices, but we’re not putting them globally in our plugin class. The reason is that each voice needs access to them on every sample, and the Voice class doesn’t even know our plugin class (there’s no #include "SpaceBass.h"). So it would be quite tedious to give this kind of access.
There’s one more value. Do you remember how we added a isMuted property to the Oscillator class? We’ll move this to Voice level, so whenever a voice isn’t active, it’s not processing anything (this includes its envelopes and filter). Add this line:

    bool isActive;

Now let’s add the public section above the private section. We’ll start with the constructor:

public:
    Voice()
    : mNoteNumber(-1),
    mVelocity(0),
    mFilterEnvelopeAmount(0.0),
    mFilterLFOAmount(0.0),
    mOscillatorOnePitchAmount(0.0),
    mOscillatorTwoPitchAmount(0.0),
    mOscillatorMix(0.5),
    mLFOValue(0.0),
    isActive(false) {
        // Set myself free everytime my volume envelope has fully faded out of RELEASE stage:
        mVolumeEnvelope.finishedEnvelopeCycle.Connect(this, &Voice::setFree);
    };

This initializes the member variables to sensible defaults. Note that a Voice is not active by default. Also, by using the EnvelopeGenerator‘s Signal & Slot mechanism, we set a voice free whenever the volume envelope goes out of release stage.
Let’s add setters for the members. Add the following to the public section:

    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);
        mOscillatorOne.setFrequency(frequency);
        mOscillatorTwo.setFrequency(frequency);
    }

The only interesting part here is setNoteNumber. It calculates the frequency for the given note (it’s the same formula we used before) and passes it to both oscillators. Below that, add this:

    double nextSample();
    void setFree();

Just like Oscillator::nextSample gives us the output of an Oscillator, Voice::nextSample gives us the total output of a voice, after volume envelope and filtering. Let’s implement it (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 a voice doesn’t calculate anything while it’s not active. It just returns a muted output. The following three lines get the nextSample from both oscillators and sum them together according to mOscillatorMix. When mOscillatorMix is 0, only oscillatorOneOutput can be heard. When it’s 1, we only hear oscillatorTwoOutput. At 0.5, both oscillators are equally loud.
After that, we get the nextSample from both envelopes. We then apply the filterEnvelopeValue to the filter cutoff, but we also take the current LFO value into account. So the cutoff modulation is the sum of the filter envelope output and the LFO output.
The pitch modulation for both oscillators is just the LFO output multiplied by the pitch modulation amount. We’ll implement that in a bit.
The last line is quite interesting, let’s start inside the parentheses: We take the sum of our two oscillators, apply the volume envelope and velocity, and send the result through mFilter.process. This gets us the filtered output, which we return.

The implementation of setFree is pretty simple:

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

As you can see, this is just used to return a voice to inactive state. As described above, this will be called whenever a voice’s mVolumeEnvelope has fully faded out.

The VoiceManager

Let’s implement a class for managing voices! Create a new class named VoiceManager. In the header, start with this code:

#include "Voice.h"

class VoiceManager {
};

Add the following private members:

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

The constant NumberOfVoices indicates how many voices can play at the same time. In the next line, we create an array of Voices. This uses memory for 64 voices, so you could think about using dynamic memory for this. However, our plugin class is allocated dynamically (search for "new PLUG_CLASS_NAME" in IPlug_include_in_plug_src.h).
So all members of our plugin class go on the heap as well.

mLFO is the global LFO for our plugin. It never gets retriggered, it just runs freely. You could argue that this should be inside the plugin class (a VoiceManager doesn’t have to know about an LFO). But this introduces another layer of separation between the Voices and the LFO, which means that we would need more Glue Code.
findFreeVoice is a utility function that we can use to get a voice that’s not currently playing. Add the implementation to VoiceManager.cpp:

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

This just iterates over all voices and finds the first one that isn’t currently playing. We’re returning a pointer (instead of a & reference) here because as you can see, it’s possible to return NULL. This wouldn’t be allowed when we return a reference. In this case, NULL means that all voices are currently playing.

Now add the following public function prototypes:

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

As the name implies, onNoteOn will be called whenever our plugin receives a MIDI note on message. onNoteOff will be called on every note off message. Let’s implement these (in VoiceManager.cpp):

void VoiceManager::onNoteOn(int noteNumber, int velocity) {
    Voice* voice = findFreeVoice();
    if (!voice) {
        return;
    }
    voice->reset();
    voice->setNoteNumber(noteNumber);
    voice->mVelocity = velocity;
    voice->isActive = true;
    voice->mVolumeEnvelope.enterStage(EnvelopeGenerator::ENVELOPE_STAGE_ATTACK);
    voice->mFilterEnvelope.enterStage(EnvelopeGenerator::ENVELOPE_STAGE_ATTACK);
}

First, we’re finding a free voice using the findFreeVoice function we created above. If there’s no free voice, we just return. This means that when all voices are in use, pressing a note will do nothing. Implementing voice stealing will be the topic of a later post. If we get a free voice, we have to somehow reset it to a blank state (we’ll implement that in a moment). We then setNoteNumber and mVelocity to the right values. We mark the voice as active and make both envelopes go into attack stage.
When you try to build this, you’ll get an error for trying to access Voice‘s private members from outside. In my opinion, the best solution here is to use the friend keyword. Add the following line at the beginning of public in Voice.h:

friend class VoiceManager;

With this line, Voice gives VoiceManager access to all its private members. I’m no advocate of overusing this feature, but when you have a class Foo and a class FooManager, it’s a good way to avoid writing a lot of setters.

Let’s implement onNoteOff:

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) {
            voice.mVolumeEnvelope.enterStage(EnvelopeGenerator::ENVELOPE_STAGE_RELEASE);
            voice.mFilterEnvelope.enterStage(EnvelopeGenerator::ENVELOPE_STAGE_RELEASE);
        }
    }
}

We’re finding all voices (!) with the note number that was released, and put both of their envelopes in the release stage. Why the plural? Can there be more than one active voice for a given note number?
Imagine you have a long decay on the volume envelope. You press a key on the keyboard and release it. While it’s fading out, you quickly hit that key again. Of course you don’t want to cut the old note off, and you also want to hear the new note. So you need two voices. If you have a long release time and keep hammering one key quickly, you may have a lot of active voices for a given note number.
So what happens if you have 5 active voices for the middle C and you release the key? onNoteOff gets called and puts all 5 voices into release stage. 4 of them were already in that stage, so let’s have a look at the first line of EnvelopeGenerator::enterStage:

if (currentStage == newStage) return;

As you can see, it won’t have any effect on the 4 voices that already entered release stage. So it’s not a problem.

Now let’s implement the VoiceManager‘s nextSample member function. It should give the summed output of all active voices.

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

As you can see, we’re starting with silence (0.0). We then iterate over all voices, set the current LFO value, and add the voice’s output to the sum. Remember, if the voice isn’t active, Voice::nextSample will return immediately without calculating.

Reusable Components

Until now, we have created an Oscillator and Filter instance and have used it for the whole time our plugin is running. But our VoiceManager re-uses free voices. So we need a way to reset a voice completely to its initial state. Let’s start in Voice.h by adding a public member function:

void reset();

Put the implementation in Voice.cpp:

void Voice::reset() {
    mNoteNumber = -1;
    mVelocity = 0;
    mOscillatorOne.reset();
    mOscillatorTwo.reset();
    mVolumeEnvelope.reset();
    mFilterEnvelope.reset();
    mFilter.reset();
}

As you can see, we’re resetting mNoteNumber and mVelocity. We then reset the oscillators, envelopes and the filter. Let’s create these member functions! Starting in Oscillator.h, add the public member function:

void reset() { mPhase = 0.0; }

This lets the waveform start from the beginning everytime a voice starts to play.
While you’re there, remove the private isMuted property. Also remove it from the constructor’s initializer list and remove the public member function setMuted. We’re now handling the active/inactive state on the Voice level, so the Oscillator doesn’t need it anymore. In Oscillator.cpp, edit Oscillator::nextSample and remove this line:

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

The reset function for the EnvelopeGenerator is a little longer. Put this in EnvelopeGenerator‘s public section (in EnvelopeGenerator.h):

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

As you can see, we have more values to reset here, but it’s still pretty straightforward. Finally, add the function for the Filter class (again, in public):

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

As you can see from the filter algorithm, these values are the filter’s previous output samples. When we re-use a voice, we want these to be clean.

To summarize, whenever the VoiceManager reuses a Voice, it calls reset on it. This, in turn, calls reset on the voice’s Oscillators, the EnvelopeGenerators and the Filter.

static or not static?

These member variables are the same for all voices:

  • Oscillator: mOscillatorMode
  • Filter: cutoff, resonance, mode
  • EnvelopeGenerator: the stageValues

At first I thought that this kind of duplication is evil and these things should be static members. Let’s imagine that mOscillatorMode was static. That would mean that our LFO always has the same waveform as our regular oscillators, which is not what we want. Also, making the EnvelopeGenerator‘s stageValues static would mean that the volume and filter envelope are always the same.
This could be solved through inheritance: We could make a VolumeEnvelope and FilterEnvelope class, and have both inherit from EnvelopeGenerator. stageValue could be static, and both VolumeEnvelope and FilterEnvelope could override it. That would give a clean separation between the two, and all voices would be able to share the static members. But we’re not talking about a significant amount of memory here. The overhead (if any) is to keep these variables in sync across all voices’ volume/filter envelopes.

There’s one thing that should be static, though: The sampleRate. There’s no point in having different components run at different sample rates. In Oscillator.h, make the mSampleRate static:

static double mSampleRate;

This means that we shouldn’t initialize it through the initializer list anymore. Remove the mSampleRate(44100.0). Go into Oscillator.cpp and add this line right after the #include:

double Oscillator::mSampleRate = 44100.0;

The sample rate is now static, so all Oscillators use the same one.

Let’s do the same for the EnvelopeGenerator! Make the private member sampleRate static, remove it from the constructor’s initializer list and add the initialization to EnvelopeGenerator.cpp:

double EnvelopeGenerator::sampleRate = 44100.0;

Back in EnvelopeGenerator.h, make the setter static:

static void setSampleRate(double newSampleRate);

We’ve added a lot of new functionality! In the next post, we’ll clean up and make our plugin’s knobs work again. You can download the source files here.

If you found this useful, please feel free to
!
comments powered by | Disqus