# Audio processing with normalized [0.0, 1.0] domain

I've a code that process buffers of samples/audio data. Here's the code:

#include <iostream>
#include <math.h>
#include <cstring>
#include <algorithm>

#define PI 3.141592653589793238
#define TWOPI 6.283185307179586476

const int blockSize = 256;
const double mSampleRate = 44100.0;
const double mHostPitch = 2.0;
const double mRadiansPerSample = TWOPI / mSampleRate;

double mGainNormalizedValues[blockSize];
double mPitchNormalizedValues[blockSize];
double mOffsetNormalizedValues[blockSize];

class Oscillator
{
public:
double mPhase = 0.0;
double minPitch = -48.0;
double maxPitch = 48.0;
double rangePitch = maxPitch - minPitch;
double pitchPd = log(2.0) / 12.0;
double minOffset = -900.0;
double maxOffset = 900.0;
double rangeOffset = maxOffset - minOffset;

Oscillator() { }

void ProcessBuffer(double voiceFrequency, int blockSize, double *left, double *right) {
// precomputed data
double bp0 = voiceFrequency * mHostPitch;

// process block
for (int sampleIndex = 0; sampleIndex < blockSize; sampleIndex++) {
double output = (sin(mPhase)) * mGainNormalizedValues[sampleIndex];

*left++ += output;
*right++ += output;

// next phase
mPhase += std::clamp(mRadiansPerSample * (bp0 * WarpPitch(mPitchNormalizedValues[sampleIndex])) + WarpOffset(mOffsetNormalizedValues[sampleIndex]), 0.0, PI);
while (mPhase >= TWOPI) { mPhase -= TWOPI; }
}
}

inline double WarpPitch(double normalizedValue) { return exp((minPitch + normalizedValue * rangePitch) * pitchPd); }
inline double WarpOffset(double normalizedValue) { return minOffset + normalizedValue * rangeOffset; }
};

int main(int argc, const char *argv[]) {
int numBuffer = 1024;
int counterBuffer = 0;

Oscillator oscillator;

// I fill the buffer often
while (counterBuffer < numBuffer) {
// init buffer
double bufferLeft[blockSize];
double bufferRight[blockSize];
memset(&bufferLeft, 0, blockSize * sizeof(double));
memset(&bufferRight, 0, blockSize * sizeof(double));

// emulate params values for this buffer
for(int i = 0; i < blockSize; i++) {
mGainNormalizedValues[i] = i / (double)blockSize;
mPitchNormalizedValues[i] = i / (double)blockSize;
mOffsetNormalizedValues[i] = i / (double)blockSize;
}

// process osc buffer
oscillator.ProcessBuffer(130.81278, blockSize, &bufferLeft[0], &bufferRight[0]);

// do somethings with buffer

counterBuffer++;
}
}


Basically:

1. init a Oscillator object
2. for each buffer, fill some param arrays with values (gain, pitch, offset); gain remain normalized [0.0, 1.0], while pitch and offset range in -48/48 and -900/900
3. then I iterate the buffer, calculating the Oscillator's sine due to pitch and offset, and I apply a gain; later, I move the phase, incrementing it

The whole domain of operations are normalized [0.0, 1.0]. But when I need to manage pitch and offset, I need to switch domain and use different values (i.e. the Warp functions).

This required lots of computations and process. I'd like to avoid it, so I can improve the code and performances.

How would you do it? Can I keep into [0.0, 1.0]? Could I improve the performance?

• Does it make sense to add optimization that rely on the phase offset per step to be stable for a while, or is it always expected to fluctuate on a sample by sample basis? Nov 13 '18 at 15:57
• @harold: I could control rate it and save on cycles, but let consider the worst case; so yes, its always expected to fluctuate sample by sample. But feel free to give an example that can be stable for a while, it would still be interessant. Nov 13 '18 at 15:59

This is the "constant frequency" optimization mentioned in the comments, it is of course situational..

Sine and cosine are what happens to coordinates of a unit vector that is rotated around the origin, which means that a sequence like sin(start + k*rate) can be generated by starting a unit vector at the starting point [cos(start), sin(start)] and then successively multiplying it by the rotation matrix

[[cos(rate), -sin(rate)],
[sin(rate),  cos(rate)]]


To generate each of the values, with the result being the Y coordinate of the resulting vectors.

So for a stretch of audio in which the frequency is not itself warped (amplitude can be varied on top of this though and that ends up causing some frequency-spread of its own), only a pair of sine and cosine are needed, the rest happens with multiplication and addition. But of course this does not help if the frequency does change all the time.

• I see, thanks. But Yes, frequency/pitch/gain change constantly (or control rated) Nov 13 '18 at 20:52

Instead of <math.h>, it's better to include <cmath> and qualify names such as std::log.

# Prefer constants to macros

Re-write pi as a strongly-typed, scoped variable rather than a preprocessor macro. Same for 2*pi if you really feel the need.

# Manage line lengths

Some lines are very long. In many cases, they just need newlines adding (e.g. bodies of inline functions can have their own lines).

In the case of the std::clamp() call, it's probably worth using variables to give a name to the candidate value before clamping.

# Easier sizeof

Instead of recomputing the size of bufferLeft and bufferRight like this:

    memset(&bufferLeft, 0, blockSize * sizeof(double));
memset(&bufferRight, 0, blockSize * sizeof(double));


It's easier and clearer to just use the whole array size:

    memset(&bufferLeft, 0, sizeof bufferLeft);
memset(&bufferRight, 0, sizeof bufferRight);


Personally, I'd generally prefer std::fill to match types and ensure the intention is clear:

    std::fill(std::begin(bufferLeft), std::end(bufferLeft), 0.0);
std::fill(std::begin(bufferRight), std::end(bufferRight), 0.0);


Then I don't need to think about whether all-bits zero is the same as 0.0 or not.

• Thanks for all tips :) I'll adapt them! What about math/sine/normalized? :D That's the core of the question... Nov 13 '18 at 14:24