Modulating (poly)BLEP hard-sync saw

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

Post

I have read that using (poly)BLEPs you can implement hard-sync, so I thought I would have a go at this. First I made this naive saw oscillator (in JSFX, but anyone not familiar with JS should also be able to understand it):

Code: Select all

desc:Naive saw
slider1:440<20,20000,1>Frequency (Hz)

@slider

dt = slider1 / srate;

@sample

y = 2*t - 1;

t += dt;
t -= floor(t);

spl0 = spl1 = 0.25 * y;
Then I added a simply 2-sample polyBLEP (like in mystran's excellent tutorial):

Code: Select all

desc:PolyBLEP saw
slider1:440<20,20000,1>Frequency (Hz)

@slider

dt = slider1 / srate;

@sample

y1 = y0;
y0 = 0;

t += dt;
t -= floor(t);

t < dt ? (
  x = t / dt;
  y1 -= 0.5*x*x;
  x = 1 - x;
  y0 += 0.5*x*x;
);

y0 += t;
y1 = 2*y1 - 1;

spl0 = spl1 = 0.25 * y1;
(I am sure this can be improved by using longer BLEPs and/or oversampling, but for simplicity's sake lets keep it this way for now.)

Next I made this naive hard-sync saw:

Code: Select all

desc:Naive hard-sync saw
slider1:440<20,20000,1>Master (Hz)
slider2:1000<20,20000,1>Slave (Hz)

@slider

dt0 = slider1 / srate;
dt1 = slider2 / srate;

@sample

y = 2*t1 - 1;

t0 += dt0;
t0 -= floor(t0);

t1 += dt1;
t1 -= floor(t1);
t0 < dt0 ? t1 = t0 / dt0 * dt1;

spl0 = spl1 = 0.25 * y;
And then I combined it with the polyBLEP saw:

Code: Select all

desc:PolyBLEP hard-sync saw
slider1:440<20,20000,1>Master (Hz)
slider2:1000<20,20000,1>Slave (Hz)

@slider

dt0 = slider1 / srate;
dt1 = slider2 / srate;

@sample

y1 = y0;
y0 = 0;

t0 += dt0;
t0 -= floor(t0);

t1 += dt1;
t1 -= floor(t1);
t0 < dt0 ? t1 = t0 / dt0 * dt1;

t = t0;
n = ceil(dt1 / dt0);
a = dt1 / dt0 - (n - 1);
loop(n,

  t < dt0 ? (
    x = t / dt0;
    y1 -= a * 0.5*x*x;
    x = 1 - x;
    y0 += a * 0.5*x*x;
  );

  t += 1 - dt0 / dt1;
  t -= floor(t);
  a = 1;
);

y0 += t1;
y1 = 2*y1 - 1;

spl0 = spl1 = 0.25 * y1;
This seems to work great! That is, until you modulate master or slave (or both), at which point spikes sometimes appear at the discontinuities. I guess this is because I add the BLEPs to a 2-sample ring buffer (as per mytran's tutorial), but the frequencies sometimes change after I have already added the BLEP, but I can't seem to wrap my head around this... Any ideas on how to fix this?

Post

Modulation of frequencies shouldn't really change anything (assuming the frequencies are treated constant for a complete sample) .. but basically for hard-sync you'd do something like:

1. step the master
2. check for master reset, solve the time
3. step the slave up to the reset point of the master (this is a fraction of the current sample)
4. check for slave reset, process as for a single oscillator
5. process the master reset and the synced slave reset (which uses the phase after step 4 to calculate the jump)
6. repeat from 2 if there are more master resets (well, doesn't happen if master is below Nyquist)
7. step the slave for the remaining part of the sample (from the last master reset time to the end of sample)
8. check for additional slave resets, process as for single oscillator (doesn't happen if slave is below Nyquist)

If you want, you can make some further assumptions about where the resets can happen if you bound the oscillator frequencies, but that's the general idea. My guess is that you get artifacts when you should process a slave reset just before the master sync-reset during the same sample, which is why it's necessary to do the whole "partial sample advance" for the slave before actually processing the sync-reset.

Alternative implementation is to step both by the full sample, but then when there is a master reset, you check whether there is a slave reset with time less than the master reset, and process such a reset first. I generally prefer the "partial sample" stepping approach though, as it's easier to handle multi-stage wave-forms where any "stage-transitions" in the slave need to be processed similarly whenever they happen before master reset.

Post

Thanks for the detailed explanation. :-) I was somehow approaching this from the master's phase point of view instead of the slave, which is why the polyBLEP hard-sync saw code I posted earlier is total rubbish. Anyway, after reading your post, and not really understanding/allowing it to sink in, I think I now have the basics right:

Code: Select all

desc:PolyBLEP hard-sync saw
slider1:440<20,20000,1>Master (Hz)
slider2:1000<20,20000,1>Slave (Hz)

@slider

dt0 = slider1 / srate;
dt1 = slider2 / srate;

@sample

y1 = y0;
y0 = 0;

t0 += dt0;
t0 -= floor(t0);

t1 += dt1;
t1 -= floor(t1);

t1 < dt1 ? (
  x = t1 / dt1;
  y1 -= 0.5*x*x;
  x = 1 - x;
  y0 += 0.5*x*x;
);

t0 < dt0 ? (
  t1 = t0 / dt0 * dt1;

  a = dt1 / dt0;
  a -= floor(a);
  x = t1 / dt1;
  y1 -= a * 0.5*x*x;
  x = 1 - x;
  y0 += a * 0.5*x*x;
);

y0 += t1;
y1 = 2*y1 - 1;

spl0 = spl1 = 0.25 * y1;
mystran wrote:Modulation of frequencies shouldn't really change anything (assuming the frequencies are treated constant for a complete sample)
Yes, you are absolutely right; with my new code modulation seems to work fine.
mystran wrote:My guess is that you get artifacts when you should process a slave reset just before the master sync-reset during the same sample, which is why it's necessary to do the whole "partial sample advance" for the slave before actually processing the sync-reset.
I think I am seeing this only now (with the new code), so lets see if I can fix this next.

Post

I can't seem to wrap my head around this:
mystran wrote: 3. step the slave up to the reset point of the master (this is a fraction of the current sample)
mystran wrote: 7. step the slave for the remaining part of the sample (from the last master reset time to the end of sample)
Could you elaborate a bit on this (perhaps post a minimal few lines of code)? ATM I reset the slave phase by doing slave_phase = master_phase / master_phase_increment * slave_phase_increment, is this what you mean is step 3? I guess not, because then I don't know what to do in step 7...

Post

Ok, so .. let's look at the single oscillator case first: the idea is to advance (or "step") the oscillator up the next transition/reset in time, insert a BLEP, then repeat until we reach the end of the current sampling period. However, it is more convenient to implement it by simply advancing the phase by the full sample, then looking at the current phase to see if the state is consistent: does the phase match the current known "stage" of the oscillator and is it within the allowable [0,1] range. If the state is inconsistent, we can solve backwards in time to figure out the time-instant where we should have inserted a BLEP and fix that by actually inserting one (and then repeat until the state becomes consistent again).

Now, if you understand the above (and if not, take some time to think about it, draw it on paper or whatever; it's critically important for the next part), when working with hard-sync as we work on fixing the consistency for the master during it's reset, we need the slave to be in a consistent state as well, so we can look at the slave-phase and actually get the right value.

So the method I suggested above is to push the master phase forward by a full sample as usual, but don't touch the slave phase yet. Instead, start processing any transitions/resets for the master. If you reach a reset, the slave at that point is still logically at the beginning of the current sampling period, so we need to move it forward in time up to the reset point (but not the end of sample). So if you have a master reset position say half-way into the current sampling interval, you would then bump the slave phase by half sample (and record the fact somewhere). Then you figure out if the slave is in consistent state, if not you process any "natural" transitions it might have (eg. you might have to reset the slave "naturally" first). Once the slave is consistent and has been advanced to the master reset point in time, you can process the reset (and the slave phase at this point is simply the current slave phase).

Once the master has been resolved back to the consistent state at the end of the whole sampling interval, the slave might still be at the beginning of the sample (no sync-reset this sample) or it might be at some mid-way position (we synced it somewhere during the sample). At this point, you need to process the slave for the remaining part of the sample (bump the phase by the remaining amount and then do all the consistency checking again). After all this, both oscillators are in a consistent state at the end of the sample, all the relevant BLEPs have been added to the buffers, and you can then sample the naive waveforms (and repeat for the next sample).

Now, there is an alternative implementation possible, where you simply bump the phase for both oscillators by a full sample, then during master reset you do a consistency check for slave-phase "up to master reset point" where you compare the time for any events against the master reset and only process those that are earlier (and then again you repeat it after processing the master, now allowing reset of the events). This is probably what you actually want to implement in practice (it saves you from having to track the "slave time" and implemented correctly, it produces the exact same results), but I'd imagine the version above is easier to understand.

Post

Thanks a lot for your even more detailed explanation. :)

What I somehow seem to have missed is that when advancing by a partial step, I also need to insert a BLEP for this partial step, and not the full step (duh!).

Anyway, I now think I have your algorithm up and running, except when the slave frequency is an integer multiple (or slightly larger, but not less) of the master frequency, so I still have to figure that out.

Code: Select all

desc:PolyBLEP hard-sync saw
slider1:440<20,20000,1>Master (Hz)
slider2:1000<20,20000,1>Slave (Hz)

@slider

dt0 = slider1 / srate;
dt1 = slider2 / srate;

@sample

function blep(x)
(
   0.5*x*x;
);

function iblep(x)
(
   x = 1 - x;
   -0.5*x*x;
);

y1 = y0;
y0 = 0;

// Push master phase forward by full sample.
t0 += dt0;
t0 -= floor(t0);

// Process transitions/reset for master.
t0 < dt0 ? (
  a = dt1 / dt0;
  a -= floor(a);
  x = t0 / dt0;
  y1 -= a * blep(x);
  y0 -= a * iblep(x);

  // Bump slave phase by partial sample.
  part = t0 / dt0;
  dt = (1 - part) * dt1;
  t1 += dt;
  t1 -= floor(t1);

  // Process "natural" transitions for slave.
  t1 < dt ? (
    x = t1 / dt;
    y1 -= blep(x);
    y0 -= iblep(x);
  );

  // Reset slave phase.
  t1 = 0;

) : part = 1;

// Bump slave phase by remaining amount.
dt = part * dt1;
t1 += dt;
t1 -= floor(t1);

// Pocess transitions/reset for slave.
t1 < dt ? (
  x = t1 / dt;
  y1 -= blep(x);
  y0 -= iblep(x);
);

y0 += t1;
y1 = 2*y1 - 1;

spl0 = spl1 = 0.25 * y1;

Post

Alright, I think I got it now:

Code: Select all

desc:PolyBLEP hard-sync saw
slider1:440<20,20000,1>Master (Hz)
slider2:1000<20,20000,1>Slave (Hz)

@slider

dt0 = (f0 = slider1) / srate;
dt1 = (f1 = slider2) / srate;

a = f1 / f0;
a -= floor(a);
a <= 0 ? a = 1;

// dc = (1 - floor(f1 / f0) * f0 / f1) * (a - 1);

@sample

function blep(x)
(
  0.5*x*x;
);

function iblep(x)
(
  x = 1 - x;
  -0.5*x*x;
);

y1 = y0;
y0 = 0;

// Push master phase forward by full sample.
t0 += dt0;
t0 -= floor(t0);

// Process transitions/reset for master.
t0 < dt0 ? (
  x = t0 / dt0;
  y1 -= a * blep(x);
  y0 -= a * iblep(x);

  // Bump slave phase by partial sample.
  part = x;
  dt = (1 - part) * dt1;
  t1 += dt;
  t1 -= floor(t1);

  // Bump slave phase by remaining amount.
  t1 < dt && a < 1 ? (
    t1 += part * dt1;
    t1 -= floor(t1);

    // Process transitions for slave.
    x = t1 / dt1;
    y1 -= blep(x);
    y0 -= iblep(x);
  );

  // Reset slave phase.
  t1 = part * dt1;
) : (

  // No sync-reset this sample, so bump slave phase by full sample.
  t1 += dt1;
  t1 -= floor(t1);

  // Process transitions for slave.
  t1 < dt1 ? (
    x = t1 / dt1;
    y1 -= blep(x);
    y0 -= iblep(x);
  );
);

// Sample naive waveform.
y0 += t1;

out = 2*y1 - 1;
// out -= dc;

spl0 = spl1 = 0.25 * out;
This could be further optimized, e.g. there is no need to bump the slave during reset if the slave frequency is an exact integer multiple of the master frequency (i.e. if a == 1). I have also added DC compensation, which is commented out but works just fine.

There is still some noise (and the occasional overshoot) when modulating the slave, especially at higher slave frequencies and with faster modulation, but I guess that is caused by more and more slave "divisions" coming in too fast.

EDIT: Removed redundant if from code.

Post

I was about to make something very similar, thank you. What I plan to do is to make an EPTR saw based on the EPTR pulse here:

http://www.yofiel.com/software/cycling- ... scillators

If it's helpful, and you also want phase modulation as well as phase reset, then please let me add the phase accumulator really should be in the range -1 to 1, not 0 to 1, during phase addition. It works with a 0~1 range, but when the modulator is a negative value and wraps back, say, to the latter part of the previous cycle, there is an incorrect phase inversion (or there isn't a phase inversion when there should be). i think you already started to appreciate this wrapping phemomenon from what you wrote above, so I hope this is helpful.

Post

ernestm wrote:f it's helpful, and you also want phase modulation as well as phase reset, then please let me add the phase accumulator really should be in the range -1 to 1, not 0 to 1, during phase addition. It works with a 0~1 range, but when the modulator is a negative value and wraps back, say, to the latter part of the previous cycle, there is an incorrect phase inversion (or there isn't a phase inversion when there should be). i think you already started to appreciate this wrapping phemomenon from what you wrote above, so I hope this is helpful.
Thank you for your suggestion, but with a phase range of 0..1 I currently have this:

Code: Select all

0.1 + 0.5 => 0.6
0.8 + 0.5 => 0.3
0.9 - 0.5 => 0.4
0.2 - 0.5 => 0.7
Is this wrong?

Note that in both JS and C/C++ floor() is not the same as int(). If I would use int() then a negative phase would indeed not wrap correctly.

Post

Well, it depends on what you are making with the phase accumulator. And I never thought this through, I just remember the criticism of something I shared in 2001 while I was still learning, and several experts said the criticism was right. So let's see, I'll try to think it through a little more. If the output signal's a saw swinging between -1 and 1, typically one multiplies by 2 and subtracts 1 to get the output range. Then when the phase accumulator is below .5, the output saw is negative, and when it's above .5, the saw is positive.

So now suppose the current phase accumulator value is below .5,and it's wrapping from 0 to 1. Then, adding a small negative FM phase value causes it to wrap backwards. But if the phase accumulator goes between -1 and 1, then adding a negative value means one is adding a negative to a negative, so the increment is in the forward direction, not the backwards direction. Is that how phase modulation should work? I'm not sure offhand, but the experts said yes, that's how phase modulation should work.

Post Reply

Return to “DSP and Plugin Development”