1
\$\begingroup\$

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?

\$\endgroup\$
2
  • \$\begingroup\$ 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? \$\endgroup\$
    – harold
    Nov 13 '18 at 15:57
  • \$\begingroup\$ @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. \$\endgroup\$
    – markzzz
    Nov 13 '18 at 15:59
2
\$\begingroup\$

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.

\$\endgroup\$
1
  • \$\begingroup\$ I see, thanks. But Yes, frequency/pitch/gain change constantly (or control rated) \$\endgroup\$
    – markzzz
    Nov 13 '18 at 20:52
3
\$\begingroup\$

Prefer C++ headers

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.

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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.