Plugin Development C++

After a little escapade into exploring Projucer a few months back, I decided to delve a little deeper.

Last time I only really learned some basic C++ concepts (manipulating data with pointers, loops, etc), and how the process block works with the in/out buffer. This time, I take a baby step forward with implementing some simple processes: Tremolo and Delay.

 


Tremolo

Tremolo was chosen for its simplicity, but provided an opportunity to develop something that required processing on an inter-block level (which I knew I’d need later for developing delay).

The maths isn’t hard on this – let’s work back from the goal: To have an incoming signals’ amplitude multiplied by an LFO (namely a sine waveform). To do this, some concept of phase would need to be made, and added to by some amount (angleDelta) for each sample. A global variable will be needed to save to inside the process block to allow the phase to be rolled straight over to the next block (this would be needed in all cases other than if the sine wave period (and phase) was exactly that of the process block).

Excerpt from PluginProcessor.cpp

//Some Variables
double currentAngle = 0; //inter-block phase angle
double angleDelta; //phase change per sample
double lfoFreq = 4; //self-explanatory

// calc angleDelta
void updateAngleDelta(double sampleRate)
{
    const double cyclesPerSample = lfoFreq / sampleRate;
    angleDelta = cyclesPerSample * 2.0 * double_Pi;
}

// Init angleDelta on playback
void TremoloTestAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
{
    updateAngleDelta(sampleRate);
}

// Process the Audio Block
void TremoloTestAudioProcessor::processBlock (AudioSampleBuffer& buffer, MidiBuffer& midiMessages)
{
    ScopedNoDenormals noDenormals;
    const int totalNumInputChannels  = getTotalNumInputChannels();

    //for each channel
    for (int channel = 0; channel < totalNumInputChannels; ++channel)
    {
        float *channelData = buffer.getWritePointer(channel);

        // - - Tremolo - -
        for(int i=0; i<buffer.getNumSamples(); ++i)
            *(channelData+i) *= pow(sin(currentAngle+i*angleDelta), 2);
    }

    currentAngle += buffer.getNumSamples()*angleDelta;
}

You’ll note that after getting the phase and putting it into a sine to get an amplitude, I square the waveform. This prevents it from going from 1 to -1 and reversing the phase of the signal (might cause problems when mixing). Similar could have also been done with a linear equation, like y=(2x+1)/2. The linear equation halves the frequency unless you double the angleDelta, while the power equation puts the waveform out of phase by PI/2.

This could be embellished with more features like adding phase offsets per channel. A more advanced concept would be to try sync the LFO to the DAW host – this is on my list of things to try later.

Although this works in mono and stereo (and in theory other multi-channel formats), there’s also a bug where this plugin does not function as expected in dual-mono mode in the DAW host. This is still somewhat of a mystery to me – seems there might be some variables sharing memory going on.

There’s also a remote danger that the double variable, currentAngle, could become too large to precisely represent the phase. Could modulo it around 2*PI to solve, perhaps?


Delay

Delay was a little more tricky to make, as I had to develop some new skills in more-than-basic pointer controls for arrays.

As I’d need a global array to store the history of samples, you’d think I’d need to make a global array. However, this conflicts directly with the simple fact that I can’t set the size of the array in memory until I know how many channels of audio there are and the size of the delay (given; I could just make a massive buffer and cross my fingers, but that’s programming heresy).

How does one resolve this?

Turns out you make a global pointer variable, then, when you can calculate the size of the array, you can create one and set the pointer to the address of the new array.

Excerpt from PluginProcessor.cpp

float *delPtr;
int delay = 1000; //delay in samples

void DelayTestAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
{
     int const inCh = 2; //getTotalNumInputChannels();
     float *delBuf = new float[inCh*delay];
     float *delPtr = delBuf;
}

You’ve now got an address of a chunk of memory the right size. Note that this makes a single array that holds all of the channels’ data.

Once you’ve got an address of memory the right size, now the processing can begin.

I’m not sure if interlacing or concatenating channels is best for efficiency, but I’ve gone with concatenation (putting one channel’s data after another, in order).

Excerpt from PluginProcessor.cpp

float decay = 0.5;

void DelayTestAudioProcessor::processBlock (AudioSampleBuffer& buffer, MidiBuffer& midiMessages)
{
    ScopedNoDenormals noDenormals;
    const int totalNumInputChannels = getTotalNumInputChannels();

    int len = buffer.getNumSamples();
    // for each channel
    for (int channel = 0; channel < totalNumInputChannels; ++channel)
    {
        float *channelData = buffer.getWritePointer (channel);
        
        int chInd = channel*delay; //channel index for delBuf
        float *chPtr = delPtr+chInd; //channel pointer for delBuf

        /*
        CHAIN for DELAY:
        1. shuffle delBuf samples back by a bock
        2. copy channelData to end of delBuf
        3. add delBuf to channelData

        Swap 2 and 3 for feedback!
        */

        for(int i=0; i<delay-len; ++i)
            *(chPtr+i) = *(chPtr+len+i); //1

        for(int i=0; i<len; ++i) {
            *(chPtr+delay-len+i) = *(channelData+i); // 2
            *(channelData+i) += *(chPtr+i)*decay; //3
        }
    }
}

This doesn’t make me too happy – particularly step 1 (the shuffle) I can see might be unnecessary processing going on. This chain sets up a linear buffer, like an audio file, that you can play from beginning to end. A line.

But what if it was a circle? With the right maths there would be no theoretical start or end to shuffle the audio to. This saving would come with some extra maths, notably using modulo to wrap the array index. Would it be a worthwhile save? Almost definitely with bigger buffers, maybe not with small ones.

This also raises other questions: Do I have to switch to interleaving the samples to make it truly circular? Or do I make separate arrays for each channel?

If I were to do the latter, I think I’d have to pull an inception on the whole pointer to an array concept: Making a global pointer that points to an array of pointers that point to each channel array. I can’t see any particular benefit from this – maybe a little bit less calculations to generate the right index – which may well add up to a lot over time.

Also, how does one make simple, beautiful code to boolean toggle the order of step 2 and 3? Hmm… 🤔

 


Overall, an interesting days’ work. However, I’m conscious of the crackling and popping created by activating and deactivating the plugins. I must research this further – likely culminating in a “low-cost fade in/out for added/modified signals” and a “clear buffer on start/finish” automatic feature.

%d bloggers like this: