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...
}
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...
}
Code: Select all
fm = PowOfTwo(*fm1 * fm1Depth + *fm2 * fm2Depth);
phaseAccumulator += phaseIncrement * fm;
Code: Select all
// No FM, so just add the increment to the accumulator
phaseAccumulator += phaseIncrement;
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;
}
};
Code: Select all
struct SingleFm
{
float operator()(float phaseIncrement)
{
float output = phaseIncrement * PowOfTwo(*fm * fmDepth);
fm++;
return output;
}
const float *fm;
float fmDepth;
};
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;
};
Code: Select all
template<class FM>
void Process(FM fm, float *output, int offset, int count)
{
// Stuff...
// Inside the loop...
phaseAccumulator += fm(phaseIncrement);
}
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...
}
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.
