Making Audio Plugins Part 17: Polyphony Part II

In this post we’ll finish polyphony by cleaning up and making our knobs work again.

Spring Cleaning

Let’s begin with the MIDIReceiver. We’re now polyphonic, so there’s no need for the mLast variables anymore. In MIDIReceiver.h, remove mLastNoteNumber, mLastFrequency and mLastVelocity, including their initializations and getter functions, i.e. getLastNoteNumber, getLastFrequency and getLastVelocity. Also remove the noteNumberToFrequency member function. Just for confirmation, the class now looks like this:

class MIDIReceiver {
private:
    IMidiQueue mMidiQueue;
    static const int keyCount = 128;
    int mNumKeys; // how many keys are being played at the moment (via midi)
    bool mKeyStatus[keyCount]; // array of on/off for each key (index is note number)
    int mOffset;

public:
    MIDIReceiver() :
    mNumKeys(0),
    mOffset(0) {
        for (int i = 0; i < keyCount; i++) {
            mKeyStatus[i] = false;
        }
    };

    // Returns true if the key with a given index is currently pressed
    inline bool getKeyStatus(int keyIndex) const { return mKeyStatus[keyIndex]; }
    // Returns the number of keys currently pressed
    inline int getNumKeys() const { return mNumKeys; }
    void advance();
    void onMessageReceived(IMidiMsg* midiMessage);
    inline void Flush(int nFrames) { mMidiQueue.Flush(nFrames); mOffset = 0; }
    inline void Resize(int blockSize) { mMidiQueue.Resize(blockSize); }

    Signal2< int, int > noteOn;
    Signal2< int, int > noteOff;
};

In MIDIReceiver.cpp, modify the advance function:

void MIDIReceiver::advance() {
    while (!mMidiQueue.Empty()) {
        IMidiMsg* midiMessage = mMidiQueue.Peek();
        if (midiMessage->mOffset > mOffset) break;

        IMidiMsg::EStatusMsg status = midiMessage->StatusMsg();
        int noteNumber = midiMessage->NoteNumber();
        int velocity = midiMessage->Velocity();
        // There are only note on/off messages in the queue, see ::OnMessageReceived
        if (status == IMidiMsg::kNoteOn && velocity) {
            if(mKeyStatus[noteNumber] == false) {
                mKeyStatus[noteNumber] = true;
                mNumKeys += 1;
                noteOn(noteNumber, velocity);
            }
        } else {
            if(mKeyStatus[noteNumber] == true) {
                mKeyStatus[noteNumber] = false;
                mNumKeys -= 1;
                noteOff(noteNumber, velocity);
            }
        }
        mMidiQueue.Remove();
    }
    mOffset++;
}

Three things have changed here:

  1. The mLast members were removed
  2. noteOn is emitted when any note is pressed (doesn’t have to be different from mLastNoteNumber)
  3. noteOff is emitted when any note is released (doesn’t have to be equal to mLastNoteNumber)

Moving on to SpaceBass.h, remove the following things:

  • #includes for Oscillator.h, EnvelopeGenerator.h and Filter.h, and #include "VoiceManager.h" instead.
  • mOscillator, mEnvelopeGenerator, mFilter, mFilterEnvelopeGenerator and mLFO.
  • filterEnvelopeAmount and lfoFilterModAmount.
  • the member functions onNoteOn, onNoteOff, onBeganEnvelopeCycle, onFinishedEnvelopeCycle.

Feels much cleaner, right? The plugin class doesn’t interact with the components directly anymore, only with the VoiceManager.
Add the following to the private section:

VoiceManager voiceManager;

In SpaceBass.cpp, replace the constructor with this:

SpaceBass::SpaceBass(IPlugInstanceInfo instanceInfo) : IPLUG_CTOR(kNumParams, kNumPrograms, instanceInfo), lastVirtualKeyboardNoteNumber(virtualKeyboardMinimumNoteNumber - 1) {
    TRACE;

    CreateParams();
    CreateGraphics();
    CreatePresets();

    mMIDIReceiver.noteOn.Connect(&voiceManager, &VoiceManager::onNoteOn);
    mMIDIReceiver.noteOff.Connect(&voiceManager, &VoiceManager::onNoteOff);
}

The mMIDIReceiver now gets connected to the VoiceManager (instead of the plugin class). The EnvelopeGenerators are now handled by the VoiceManager and Voice classes, so we don’t Connect() them here anymore.
Our ProcessDoubleReplacing function shows a lot of compiler errors, because it tries to access things that we just deleted. In fact, the new implementation is very simple: It’s mainly a call to VoiceManager::nextSample:

void SpaceBass::ProcessDoubleReplacing(
    double** inputs,
    double** outputs,
    int nFrames)
{
    // Mutex is already locked for us.

    double *leftOutput = outputs[0];
    double *rightOutput = outputs[1];
    processVirtualKeyboard();
    for (int i = 0; i < nFrames; ++i) {
        mMIDIReceiver.advance();
        leftOutput[i] = rightOutput[i] = voiceManager.nextSample();
    }

    mMIDIReceiver.Flush(nFrames);
}

Note that all the code about the interaction between Oscillators, EnvelopeGenerators and the filter is gone.
Inside Reset(), we have no access to the components anymore. So we have to tell the VoiceManager to change the sampleRate for all components:

void SpaceBass::Reset()
{
    TRACE;
    IMutexLock lock(this);
    double sampleRate = GetSampleRate();
    voiceManager.setSampleRate(sampleRate);
}

We haven’t implemented this function yet. Add the following to VoiceManager‘s public section (in VoiceManager.h):

void setSampleRate(double sampleRate) {
    EnvelopeGenerator::setSampleRate(sampleRate);
    for (int i = 0; i < NumberOfVoices; i++) {
        Voice& voice = voices[i];
        voice.mOscillatorOne.setSampleRate(sampleRate);
        voice.mOscillatorTwo.setSampleRate(sampleRate);
    }
    mLFO.setSampleRate(sampleRate);
}

Basically this just calls setSampleRate on every voice and for each component. We could set Oscillator::mSampleRate statically, but we’d still have to call updateIncrement for each Oscillator on each Voice. I find it more clear to do it like this.

Our plugin is now polyphonic, with the exception that it doesn’t react to our knobs yet. But before we fix that, let’s add pitch modulation to our Oscillators.

Pitch Modulation

So far we’ve mainly done structural refactoring changes. We’ve moved code from one part to another and deleted code that’s not needed anymore. I’ve delayed pitch modulation until this point because it has nothing to do with polyphony and it would be confusing to just mix it in here and there.

In Voice.cpp, add this to Voice::nextSample (right above the return statement):

mOscillatorOne.setPitchMod(mLFOValue * mOscillatorOnePitchAmount);
mOscillatorTwo.setPitchMod(mLFOValue * mOscillatorTwoPitchAmount);

As you can see, the pitch modulation is determined by the LFO value multiplied with the knob value from the GUI.
In Oscillator.h, add a new private member:

double mPitchMod;

Initialize it in the constructor’s initializer list:

mPitchMod(0.0),

And add a public setter function:

void setPitchMod(double amount);

Let’s implement it (in Oscillator.cpp):

void Oscillator::setPitchMod(double amount) {
    mPitchMod = amount;
    updateIncrement();
}

Setting the pitch mod value influences the actual frequency that should be played. So we have to call updateIncrement. Inside updateIncrement, we somehow have to take mPitchMod into account. Let’s write a new updateIncrement step by step (replace the old one in Oscillator.cpp):

void Oscillator::updateIncrement() {
    double pitchModAsFrequency = pow(2.0, fabs(mPitchMod) * 14.0) - 1;
    if (mPitchMod < 0) {
        pitchModAsFrequency = -pitchModAsFrequency;
    }

Okay, what’s going on here? mPitchMod is between -1 and 1, but we want it in terms of frequency, something like “plus 491.3 Hz”. The pow term gives us that, but we have to bypass the fact that values are negative. We do that by calling fabs (absolute value), and the following if statement gets us the negativity back. Why the -1 at the end? If mPitchMod is 0, pow(2.0, 0) would give us 1, so we would have a pitch modulation of 1 Hz (which is wrong). To prevent this, we subtract -1.
Next, we make a calculatedFrequency out of the base mFrequency and the Hz value we just calculated:

    double calculatedFrequency = fmin(fmax(mFrequency + pitchModAsFrequency, 0), mSampleRate/2.0);

First, we’re adding the base frequency and the mod frequency together. The fmin call ensures that we’re not going higher than half the sample rate. We can’t go higher than this Nyquist frequency, or else we’ll get aliasing. Similarly, we can’t have a frequency below zero, so fmax ensures this won’t happen.
Let’s finish the function by setting the phase increment:

    mPhaseIncrement = calculatedFrequency * 2 * mPI / mSampleRate;
}

This is just like before, except that we’re of course using the calculatedFrequency value.

Making the GUI Knobs work

This is the last piece of the puzzle! After this section, our synthesizer will be finished!

Let’s begin with the LFO knobs. They don’t affect voices, so they need a different treatment than the other parameters.In SpaceBass.cpp, delete the existing OnParamChange implementation and add this instead:

void SpaceBass::OnParamChange(int paramIdx)
{
    IMutexLock lock(this);
    IParam* param = GetParam(paramIdx);
    if(paramIdx == mLFOWaveform) {
        voiceManager.setLFOMode(static_cast<Oscillator::OscillatorMode>(param->Int()));
    } else if(paramIdx == mLFOFrequency) {
        voiceManager.setLFOFrequency(param->Value());
    }
}

We’re getting the VoiceManager instance, check which parameter was modified, and call either setLFOMode or setLFOFrequency. We haven’t implemented these two yet, so let’s do that next. Open VoiceManager.h and add the two public member functions:

inline void setLFOMode(Oscillator::OscillatorMode mode) { mLFO.setMode(mode); };
inline void setLFOFrequency(double frequency) { mLFO.setFrequency(frequency); };

As you can see, these two simply forward to mLFO.

For all the other parameters, we’ll use C++ functional mechanisms. They are very nice and powerful, but rarely covered in C++ tutorials. I think you’ll agree that they’re really worth knowing! But what’s the problem we’re trying to solve?

When you turn a knob, OnParamChange is called with a parameter index and value. Let’s say mFilterCutoff with a value of 0.3. Now we have to tell the VoiceManager: “For each Voice, set the filter cutoff to 0.3. We would probably call a function setFilterCutoffForEachVoice that could look like this (just for demonstration):

VoiceManager::setFilterCutoffForEachVoice(double newCutoff) {
    for (int i = 0; i < NumberOfVoices; i++) {
        voice[i].mFilter.setCutoff(newCutoff);
    }
}

This doesn’t look too bad, but we’d need about ten of these functions for all the different parameters. Each of them would be a little different, but the for loop would be duplicated. If only there was a way of saying: “Here’s a change. Do this change to all voices”. Of course, there is a way. C++ allows you to take a function, pre-fill it with some input values, pass it around and call it on different things. This is similar to JavaScript’s Function.prototype.bind, but in C++ it’s also type-safe.
Let’s try this out! In VoiceManager.h, add this #include:

#include <tr1/functional>
// #include <functional> if that doesn't work

In the public section, add this line:

typedef std::tr1::function<void (Voice&)> VoiceChangerFunction;

This means: A VoiceChangerFunction is a function that takes a Voice& as first parameter and returns void. (In fact, it doesn’t have to be a function, as long as it’s something you can call with ().)
Below that, add this function:

inline void changeAllVoices(VoiceChangerFunction changer) {
    for (int i = 0; i < NumberOfVoices; i++) {
        changer(voices[i]);
    }
}

Note how it takes a VoiceChangerFunction, iterates through all voices, and applies the changer to each of them.
Below that, add the actual functions. They all look kind of similar: Each takes a Voice& and some other parameters, and modifies that one Voice.

// Functions to change a single voice:
static void setVolumeEnvelopeStageValue(Voice& voice, EnvelopeGenerator::EnvelopeStage stage, double value) {
    voice.mVolumeEnvelope.setStageValue(stage, value);
}
static void setFilterEnvelopeStageValue(Voice& voice, EnvelopeGenerator::EnvelopeStage stage, double value) {
    voice.mFilterEnvelope.setStageValue(stage, value);
}
static void setOscillatorMode(Voice& voice, int oscillatorNumber, Oscillator::OscillatorMode mode) {
    switch (oscillatorNumber) {
        case 1:
            voice.mOscillatorOne.setMode(mode);
            break;
        case 2:
            voice.mOscillatorTwo.setMode(mode);
            break;
    }
}
static void setOscillatorPitchMod(Voice& voice, int oscillatorNumber, double amount) {
    switch (oscillatorNumber) {
        case 1:
            voice.setOscillatorOnePitchAmount(amount);
            break;
        case 2:
            voice.setOscillatorTwoPitchAmount(amount);
            break;
    }
}
static void setOscillatorMix(Voice& voice, double value) {
    voice.setOscillatorMix(value);
}
static void setFilterCutoff(Voice& voice, double cutoff) {
    voice.mFilter.setCutoff(cutoff);
}
static void setFilterResonance(Voice& voice, double resonance) {
    voice.mFilter.setResonance(resonance);
}
static void setFilterMode(Voice& voice, Filter::FilterMode mode) {
    voice.mFilter.setFilterMode(mode);
}
static void setFilterEnvAmount(Voice& voice, double amount) {
    voice.setFilterEnvelopeAmount(amount);
}
static void setFilterLFOAmount(Voice& voice, double amount) {
    voice.setFilterLFOAmount(amount);
}

But we can’t pass these to changeAllVoices: They aren’t VoiceChangerFunctions because all of them take more arguments. We will pre-fill all arguments except for the first one (Voice&), which will turn them into VoiceChangerFunctions.
In SpaceBass.cpp, #include <tr1/functional> (or #include <functional>). Inside OnParamChange, add an else case at the end, so it looks like this:

    // ...
    } else {
        using std::tr1::placeholders::_1;
        using std::tr1::bind;
        VoiceManager::VoiceChangerFunction changer;
        switch(paramIdx) {
            // We'll add this part in a moment
        }
        voiceManager.changeAllVoices(changer);
    }
}

Here we just say that we don’t want to type std::tr1:: all the time. Now we can just type _1 and bind (more on these two in a moment). We also declare a VoiceChangerFunction. Then there’s a switch that makes changer different for every parameter. And finally, we call changeAllVoices, passing the changer we just created.

So how do we create such a changer?

  1. We take one of the functions defined earlier
  2. Using std::tr1::bind, we pre-fill all the arguments except for the first one (Voice&)
  3. This gives us a true VoiceChangerFunction.

This may sound a little complicated, so let’s just see it in practice. Add the following case inside the switch(paramIdx):

case mOsc1Waveform:
    changer = bind(&VoiceManager::setOscillatorMode,
                   _1,
                   1,
                   static_cast<Oscillator::OscillatorMode>(param->Int()));
    break;

The first parameter to bind is the function we want to pre-fill. In this case setOscillatorMode. All the following parameters are things that will be pre-filled. The _1 is the placeholder, and it means: I’m not pre-filling this parameter, I’ll pass it in when changer gets called. In our case this means that changer expects a Voice& as its first argument, which makes it a valid VoiceChangerFunction. After the placeholder, we pre-fill the function with the input that’s needed: Oscillator 1‘s waveform should be set to the parameter value. We have to cast the integer to a OscillatorMode enum.

Let’s create the changer for every other parameter. They all follow the same idea, except the number and type of the pre-fill parameters are different. If you have understood the principe, you don’t really have to type these in by hand:

case mOsc1PitchMod:
    changer = bind(&VoiceManager::setOscillatorPitchMod, _1, 1, param->Value());
    break;
case mOsc2Waveform:
    changer = bind(&VoiceManager::setOscillatorMode, _1, 2, static_cast<Oscillator::OscillatorMode>(param->Int()));
    break;
case mOsc2PitchMod:
    changer = bind(&VoiceManager::setOscillatorPitchMod, _1, 2, param->Value());
    break;
case mOscMix:
    changer = bind(&VoiceManager::setOscillatorMix, _1, param->Value());
    break;
    // Filter Section:
case mFilterMode:
    changer = bind(&VoiceManager::setFilterMode, _1, static_cast<Filter::FilterMode>(param->Int()));
    break;
case mFilterCutoff:
    changer = bind(&VoiceManager::setFilterCutoff, _1, param->Value());
    break;
case mFilterResonance:
    changer = bind(&VoiceManager::setFilterResonance, _1, param->Value());
    break;
case mFilterLfoAmount:
    changer = bind(&VoiceManager::setFilterLFOAmount, _1, param->Value());
    break;
case mFilterEnvAmount:
    changer = bind(&VoiceManager::setFilterEnvAmount, _1, param->Value());
    break;
    // Volume Envelope:
case mVolumeEnvAttack:
    changer = bind(&VoiceManager::setVolumeEnvelopeStageValue, _1, EnvelopeGenerator::ENVELOPE_STAGE_ATTACK, param->Value());
    break;
case mVolumeEnvDecay:
    changer = bind(&VoiceManager::setVolumeEnvelopeStageValue, _1, EnvelopeGenerator::ENVELOPE_STAGE_DECAY, param->Value());
    break;
case mVolumeEnvSustain:
    changer = bind(&VoiceManager::setVolumeEnvelopeStageValue, _1, EnvelopeGenerator::ENVELOPE_STAGE_SUSTAIN, param->Value());
    break;
case mVolumeEnvRelease:
    changer = bind(&VoiceManager::setVolumeEnvelopeStageValue, _1, EnvelopeGenerator::ENVELOPE_STAGE_RELEASE, param->Value());
    break;
    // Filter Envelope:
case mFilterEnvAttack:
    changer = bind(&VoiceManager::setFilterEnvelopeStageValue, _1, EnvelopeGenerator::ENVELOPE_STAGE_ATTACK, param->Value());
    break;
case mFilterEnvDecay:
    changer = bind(&VoiceManager::setFilterEnvelopeStageValue, _1, EnvelopeGenerator::ENVELOPE_STAGE_DECAY, param->Value());
    break;
case mFilterEnvSustain:
    changer = bind(&VoiceManager::setFilterEnvelopeStageValue, _1, EnvelopeGenerator::ENVELOPE_STAGE_SUSTAIN, param->Value());
    break;
case mFilterEnvRelease:
    changer = bind(&VoiceManager::setFilterEnvelopeStageValue, _1, EnvelopeGenerator::ENVELOPE_STAGE_RELEASE, param->Value());
    break;

Note that all of this is type-safe: You can’t, for example, make a changer to change the filter cutoff and pre-fill it with an enum value or with the wrong number of parameters.
If at some point you decide to handle your VoiceManager‘s voices as a linked list with dynamic memory allocation, you just have to change changeAllVoices. All other parts of the code can stay the same.

Finished!

Congratulations, you have built a polyphonic synthesizer! I know this was a lot of work, and I hope it wasn’t too difficult. If you have read this far and something still doesn’t work, please leave a comment and I’ll help you out. For reference, you can download the source code with all the images here. The next projects will be a smaller scale.

Click here to go to the next post, where we get rid of the Oscillator aliasing.

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