But I figured, if we use 2-sample PolyBLEPs then buffering becomes a trivial one sample delay thing (that we still do here, but no ring-buffers or anything), and the BLEP data is no longer an issue. The exact same concepts works, we just need less "dirty details" that make writing tutorials more painful. I'll probably give up and provide the "dirty details" in a later post anyway, but .. yeah ..

This should be beginner friendly, almost "your first oscillator" level.

However: this is still a "real" BLEP oscillator: you just replace the "one sample delay" that's held in two variables, with a proper ring-buffer, and you can use longer BLEPs without any additional trouble.. in fact it's a simplified and cleaned up version of code that uses polyphase FIR BLEPs.

For this example.. I support constant frequency, and constant mix between saw/pulse, but really these could come from buffer just as well. The PWM does come from a buffer, since I added a fix for more robust PWM, so it kinda made sense. The "broken" version is left as an #ifdef just so you can observe how it appears to works about 99% of time, but then occasionally with certain types of modulation there can be glitches.

Anyway, first we need the BLEPs, and here we use two-sample polynomial filter.

These appear to be what are most commonly suggested:

- Code: Select all
`// The first sample BLEP`

static inline float poly3blep0(float t)

{

// these are just sanity checks

// correct code doesn't need them

if(t < 0) return 0;

if(t > 1) return 1;

float t2 = t*t;

return t * t2 - 0.5f * t2 * t2;

}

// And second sample as wrapper, optimize if you want.

static inline float poly3blep1(float t)

{

return -poly3blep0(1-t);

}

How I derived it (if you actually care): Build C1 continuous impulse from two cubic "smooth steps" which on their own looks like p(t)=3*t^2-2*t^3 and map t : [0,1] -> [0,1] in a smooth way. Then integrate the poly to P(t)=t^3-0.5*t^4 which gives the "BLEP for the first sample" and then the second sample is 1-P(1-t) for symmetry, except we remove the trivial step, so just -P(1-t), which makes sense because.. well, it works out that way in math, it works and doesn't sound totally horrid.

Now we can go on with the basic oscillator, which needs a bit of state. So I wrapped it inside the minimum struct that makes it possible to test this thing, then copy-pasted it here.

I wrote the code for this (it's fresh), but I tested it and fixed the bugs.

It's only the COMMENTS that make it long, and those are rest of this tutorial.

I suggest copying to an editor that does syntax highlight.

- Code: Select all
`struct Oscillator {`

// these are internal state, not parameters

float phase;

float blepDelay;

float widthDelay;

int pulseStage;

// convenience constructor

Oscillator() { reset(); }

// the initialization code

void reset()

{

// set current phase to 0

phase = 0;

// blep delay "buffer" to zero

blepDelay = 0;

// previous pulseWidth to any valid value

widthDelay = 0.5f;

// only 2 stages here, set to first

pulseStage = 0;

}

// the freq should be normalized realFreq/samplerate

// the mix parameter is 0 for saw, 1 for pulse

// the pwm is audio-rate buffer, since that needs treatment

// rest are pretty trivial to turn into audio-rate sources

void render(float freq, float mix, float * pwm,

float * bufOut, int nsamples)

{

for(int i = 0; i < nsamples; ++i)

{

// the BLEP latency is 1 sample, so first

// take the delayed part from previous sample

float out = blepDelay;

// then reset the delay so we can build into it

blepDelay = 0;

// then proceed like a trivial oscillator

phase += freq;

// load and sanity-check the new PWM

// ugly things would happen outside range

float pulseWidth = pwm[i];

if(pulseWidth > 1) pulseWidth = 1;

if(pulseWidth < 0) pulseWidth = 0;

// Then replace the reset logic: loop until we

// can't find the next discontinuity, so during

// one sample we can process any number of them!

while(true)

{

// Now in order of the stages of the wave-form

// check for discontinuity during this sample.

// First is the "pulse-width" transition.

if(pulseStage == 0)

{

// if we didn't hit the pulse-width yet

// break out of the while(true) loop

if(phase < pulseWidth) break;

#ifdef NO_PWM_FIX

// otherwise solve transition: when during

// this sample did we hit the pw-border..

// t = (1-x) from: phase + (x-1)*freq = pw

float t = (phase - pulseWidth) / freq;

#else

// the above version is fine when pw is constant

// and it's what we use for the reset part, but

// for pw modulation, t could end up outside [0,1]

// and that will sound pretty ugly, so use lerp:

// phase + (x-1)*freq = (1-x)*pwOld + x*pw

// and again t = (1 - x), and hopefully..

float t = (phase - pulseWidth)

/ (widthDelay - pulseWidth + freq);

#endif

// so then scale by pulse mix

// and add the first sample to output

out += mix * poly3blep0(t);

// and second sample to delay

blepDelay += mix * poly3blep1(t);

// and then we proceed to next stage

pulseStage = 1;

}

// then whether or not we did transition, if stage

// is at this point 1, we process the final reset

if(pulseStage == 1)

{

// not ready to reset yet?

if(phase < 1) break;

// otherwise same as the pw, except threshold 1

float t = (phase - 1) / freq;

// and negative transition.. normally you would

// calculate step-sizes for all mixed waves, but

// here both saw and pulse go from 1 to 0.. so

// it's always the same transition size!

out -= poly3blep0(t);

blepDelay -= poly3blep1(t);

// and then we do a reset (just one!)

pulseStage = 0;

phase -= 1;

}

// and if we are here, then there are possibly

// more transitions to process, so keep going

}

// When the loop breaks (and it'll always break)

// we have collected all the various BLEPs into our

// output and delay buffer, so add the trivial wave

// into the buffer, so it's properly delayed

//

// note: using pulseStage instead of pw-comparison

// avoids inconsistencies from numerical inaccuracy

blepDelay += (1-mix)*phase

+ mix*(pulseStage ? 1.f : 0.f);

// and output is just what we collected, but

// let's make it range -1 to 1 instead

bufOut[i] += (2*out - 1);

// and store pulse-width delay for the lerp

widthDelay = pulseWidth;

}

}

}; // struct Oscillator

The reason it's structure this way, is that it makes it a lot easier to understand and alot easier to edit and extend. It's also the structure that actually works, when you want to do something fancier than basic saws and pulse.

And the exact same render-method, with comments removed.. so it's easier to see the structure:

- Code: Select all
`void render(float freq, float mix, float * pwm,`

float * bufOut, int nsamples)

{

for(int i = 0; i < nsamples; ++i)

{

float out = blepDelay;

blepDelay = 0;

phase += freq;

float pulseWidth = pwm[i];

if(pulseWidth > 1) pulseWidth = 1;

if(pulseWidth < 0) pulseWidth = 0;

while(true)

{

if(pulseStage == 0)

{

if(phase < pulseWidth) break;

#ifdef NO_PWM_FIX

float t = (phase - pulseWidth) / freq;

#else

float t = (phase - pulseWidth)

/ (widthDelay - pulseWidth + freq);

#endif

out += mix * poly3blep0(t);

blepDelay += mix * poly3blep1(t);

pulseStage = 1;

}

if(pulseStage == 1)

{

if(phase < 1) break;

float t = (phase - 1) / freq;

out -= poly3blep0(t);

blepDelay -= poly3blep1(t);

pulseStage = 0;

phase -= 1;

}

}

blepDelay += (1-mix)*phase

+ mix*(pulseStage ? 1.f : 0.f);

bufOut[i] += (2*out - 1);

widthDelay = pulseWidth;

}

}

Note that most of the time the loop does nothing: a few integer comparisons, one floating point comparison, and then break out. We only do extra work when there are discontinuities on the current sample, otherwise it's identical to naive!

If you really want to, you can use this with the same terms that I used for the filter that got more popular that it should... but really it's more of a clean starting point for something fancier (just like that filter was supposed to be).