Templates for managing logic

DSP, Plugin and Host development discussion.
Post Reply New Topic
RELATED
PRODUCTS

Post

I've probably posted about something like this in the past, so forgive me if this is treading old ground.

Say you have an oscillator class in C++. For this example, the oscillator can have its frequency modulated by two sources, e.g. an LFO for vibrato and/or an envelope. The signature of the process method for this class might look something like this:

Code: Select all

void Oscillator::Process(const float *fm1,
                         const float *fm2,
                         float *output,
                         int offset,
                         int count)
{
    // Stuff...
}
The fm1 and fm2 pointers point to buffers filled with the output of the sources routed to modulate the frequency of the oscillator. The output pointer points to a buffer the oscillator will fill with its output. And the offset and count parameters give info on where in the output buffer to begin and how many samples to generate respectively.

It's possible that there are no sources routed to modulate the oscillator's frequency, or we could have only one source routed instead of two. We could indicate this by passing null pointers to the Process method or have setters for setting boolean values indicating whether the FM inputs are enabled.

Regardless, it would be nice if we could write several versions of our Process method that are optimized for each situation.

We could have a private method called ProcessNoFm that generates the oscillator's output when no sources are routing to its FM inputs. And another method called ProcessSingleFm in which only one FM source is used. Which method ultimately gets called is decided in the main Process method.

Something like...

Code: Select all

void Oscillator::Process(const float *fm1,
                         const float *fm2,
                         float *output,
                         int offset,
                         int count)
{
    if(fm1 != 0 && fm2 != 0)
    {
        ProcessBothFm(fm1, fm2, output, offset, count);
    }
    else if(fm1 != 0 && fm2 == 0)
    {
        ProcessSingleFm(fm1, fm1Depth, output, offset, count);
    }

    // And so on...
}
Basically the if and else/if statements form a decision table on which method gets called. The FM section of the ProcessBothFm method could look like this:

Code: Select all

fm = PowOfTwo(*fm1 * fm1Depth + *fm2 * fm2Depth);
phaseAccumulator += phaseIncrement * fm;
Whereas the ProcessNoFm's FM section would look like this:

Code: Select all

// No FM, so just add the increment to the accumulator
phaseAccumulator += phaseIncrement;
This could lead to quite a few methods, though, with quite a lot of code duplications. This is especially true if we add more modulation possibilities, e.g. an additional AM input.

I was thinking, though, maybe templates could help us out here...

First, encapsulate the FM sections into structs:

Code: Select all

struct NoFm
{
    float operator()(float phaseIncrement)
    {
        return phaseIncrement;
    }
};
Second, create one for only one FM source:

Code: Select all

struct SingleFm
{
    float operator()(float phaseIncrement)
    {
        float output = phaseIncrement * PowOfTwo(*fm * fmDepth);

        fm++;

        return output;
    }

    const float *fm;
    float fmDepth;
};
And another one for two FM sources:

Code: Select all

struct DoubleFm
{
    float operator()(float phaseIncrement)
    {
        float output = phaseIncrement * 
                       PowOfTwo(*fm1 * fm1Depth + *fm2 * fm2Depth);

        fm1++;
        fm2++;

        return output;
    }

    const float *fm1;
    float fm1Depth;
    const float *fm2;
    float fm2Depth;
};
Ok, finally we create a template method that represents the basic algorithm:

Code: Select all

template<class FM>
void Process(FM fm, float *output, int offset, int count)
{
    // Stuff...

    // Inside the loop...

    phaseAccumulator += fm(phaseIncrement);
}
Back in our Process method we decide which FM object gets passed to our template method:

Code: Select all

void Oscillator::Process(const float *fm1,
                         const float *fm2,
                         float *output,
                         int offset,
                         int count)
{
    if(fm1 != 0 && fm2 != 0)
    {
        DoubleFm fm = { fm1, fm1Depth, fm2, fm2Depth };

        Process(fm, output, offset, count);
    }
    else if(fm1 != 0 && fm2 == 0)
    {
        SingleFm fm = { fm1, fm1Depth };

        Process(fm, output, offset, count);
    }

    // And so on...
}
As our oscillator gets more complicated, we can look for other parts of the algorithm that can be abstracted out.

I think this is just more or less generic programming? And instead of explicitly writing our own variations of the Process method, we're letting templates do that for us?

Also, I'm assuming that since there's a lot of opportunities for inlining code here that a diligent compiler is going to optimize our code so that it's not going to cost more than a non-templated approach.

Post

hm.. i would suggest that the oscillator class could have sepparate methods for the different scenarios (processFull, processNoFM, ...)
but the choice - which of these will be used, to be made at compile time, or at init

that's if you want to have a "universal" oscillator class, that will be either modulatable, or non-modulatable, at all times

you could basicaly call the appropriate function when you design a synth

osc o1, o2;
o1.init(SampleRate);
o2.init(SampleRate);
...
// process loop
float tmp = o1.process(freq1);
*out = o2.processFM(freq2,tmp);
...
It doesn't matter how it sounds..
..as long as it has BASS and it's LOUD!

irc.libera.chat >>> #kvr

Post

antto wrote:hm.. i would suggest that the oscillator class could have sepparate methods for the different scenarios (processFull, processNoFM, ...)
but the choice - which of these will be used, to be made at compile time, or at init
Hmm, well, the problem with making a decision at compile time is that it can't be changed. What I'm aiming for is the ability to change which algorithm is used at run time. Say, for example, the GUI let's you choose which source to route to your oscillator's FM input, including the choice to have no modulation at all. It would be cool if we switch between code that has been optimized for each scenario at run time.

Maybe a traditional implementation of the Strategy design pattern would be more appropriate.

But I've been thinking, maybe a template approach to the strategy pattern would work... :)

Rather than having a template method, as I showed in my original post, we have a template class representing the general algorithm. We have this class derived from a base class that has a virtual method that is overriden by the template class. Then we instantiate the template class with whichever "functors" that are appropriate given the state of the containing class. And use a pointer to the base class to reference it. I'll explain in code:

Bass class:

Code: Select all

class ProcessAlgorithmBase
{
public:
    virtual ~ProcessAlgorithmBase()
    {
    }

    virtual void Process(const float *fm1,
                         const float *fm2,
                         float *output,
                         int offset,
                         int count) = 0;
};

Code: Select all

template<class FM>
class ProcessAlgorithm : public ProcessAlgorithmBase
{
public:
    void Process(const float *fm1,
                 const float *fm2,
                 float *output,
                 int offset,
                 int count)
    {
        // Set up loop...

        // Inside loop...

        phaseAccumulator += fm(phaseIncrement);
    }

private:
    FM fm;

    // Other state variables...
};
And the "functors" from my original post...

Code: Select all

struct NoFm 
{ 
    float operator()(float phaseIncrement) 
    { 
        return phaseIncrement; 
    } 
};

struct SingleFm...

struct DoubleFm...

etc.


Inside our oscillator class, we keep a pointer to the ProcessAlgorithmBase class

Code: Select all

class Oscillator
{
private:
    ProcessAlgorithmBase *processAlgorithm
};
Then we instantiate whichever version of the template class we need based on the current state of the Oscillator. Say, for example, our Oscillator class has two FM inputs. We can turn on/off these inputs with setter methods (this is different from my original example in which null pointers were used to indicate the state of the inputs):

Code: Select all

class Oscillator
{
public:
    void SetFm1Enabled(bool enabled);
   
    void SetFm2Enabled(bool enabled);

// stuff...
};
When the FM inputs are enabled/disabled, we check to see which version of the template class we need and create it:

Code: Select all

void Oscillator::SetFm1Enabled(bool enabled)
{
    this->fm1Enabled = enabled;

    if(fm1Enabled && fm2Enabled)
    {
        // Assumes the object has already been allocated
        delete processAlgorithm;

        processAlgorithm = new ProcessAlgorithm<DoubleFm>(/* stuff */);
    }
    else if(fm1Enabled && !fm2Enabled)
    {
        // ...
    }
    else if(...

    etc.
}
And in the Oscillator's Process method:

Code: Select all

void Oscillator::Process(const float *fm1,
                         const float *fm2,
                         float *output,
                         int offset,
                         int count)
{
    processAlgorithm->Process(fm1, fm2, output, offset, count);
}
We're basically just using the strategy design pattern only we've made a general purpose template strategy class that can be customized. This let's us use an optimized version that's appropriate for the given state of the containing class.

Post

ok, so then..
you can use pointers to functions..
..where you'll always pass the full number of parameters

the public function will be like process(float *freq, float *fm1, float *fm2, float *phase_mod, ...)
then that function could call the appropriate internal (private) function for the specific case, using the pointer

the class will have a pointer
the important thing is when to change this pointer
you probably have some GUI and/or midi-automation.. whenever the "connection" between the audiobuffers and the parameters is altered - you'll do a check, and set the pointer to the optimal (private) function..

erm, i'm kinda sleepy, not sure if this made sense..

oh, so when you don't want any FM, you'll first have to set the pointer to the function which has no FM, and you can simply pass null-pointers to the process() call

probably the only downside i know of, is with function pointers, each time it's called, some wierd stuff happens that takes time, but if that's once per a block of samples - it's kinda okay i guess
It doesn't matter how it sounds..
..as long as it has BASS and it's LOUD!

irc.libera.chat >>> #kvr

Post Reply

Return to “DSP and Plugin Development”