Hard-Sync with MinBlep / How to manage edge cases

DSP, Plugin and Host development discussion.
RELATED
PRODUCTS

Post

Code: Select all

phase += delta;
while (phase >= next || sync) {
 overlap = phase - next;
 fraction = (phase > next ? overlap / delta : 0.0);
 // syncing, sync happened before next step
 if (sync && sync_fraction > fraction) {
  set_phase(sync_fraction, destination_phase);
 } else {
  set_phase(fraction, next < 1.0 ? next : 0.0);
 }
}
If you do things correctly there are no "edge cases".

Note that regardless of whether sync happens before the next step or after, you must always loop to handle both events or aliasing and error will result.

This is called "event handling with sub-sample precision."

If you want morphing conditions (variable pulse-width, morphing waveforms, ...) the code is a lot more complex in order to handle things as efficiently as possible. Still though you can generate any waveform with any set of conditions from one implementation. It isn't necessary to have multiple implementations one for each of pulse, ramp or other waveforms.

This is because ultimately any first-order changes in delta or other conditions (pulse-width modulation) are simply more of the same sort of events that you need to handle with sub-sample precision.
Free plug-ins for Windows, MacOS and Linux. Xhip Synthesizer v8.0 and Xhip Effects Bundle v6.7.
The coder's credo: We believe our work is neither clever nor difficult; it is done because we thought it would be easy.
Work less; get more done.

Post

Ok, it worked. I have chosen the ordering solution. It's the simplest one for a saw with one master oscillator. I had a mistake in the if statement, because of this it took a bit longer than planned. The one with the bigger phase is the first one :dog:

Now the same for the pulse :)

Thanks again!

Post

mystran wrote:
kryptonaut wrote: In the past I've used a tabulated BLEP derived from an IIR filter response - that way it's always causal (so you don't have to anticipate a step N samples ahead) and by adjusting the filter parameters you can experiment with different roll-offs, cutoff frequency, etc. for various effects and/or trading off BLEP length vs antialiasing.
The problem is that a minimum-phase BLEP is not zero-phase and trying to pretend so leads to all kinds of funky DC offset issues and other crap that BLEPs are specifically supposed to avoid. Since you can have arbitrary overlaps of any number of BLEPs anyway, your best strategy is to mix them into a buffer right-away when you find the relevant transition point, at which point moving the "naive" output around in the buffer to compensate for linear-phase BLEP latency is trivial.

edit: in short, if you think MinBLEPs make something easier in terms of implementation, then your implementation is almost certain broken in the first place!
Yes, the DC offset can be big. Up to 5 times the amplitude for saws at high frequencies when they overlap and hard sync, but it's easy to remove the offset. Just add a one pole high pass filter with the cutoff of the oscillator increment value for the current frequency.

How does FM work when you have the normal bleps? i can imagine that you can't change the frequency (phase increment) anymore for 1/2 the blep size once you have mixed the blep. Otherwise the blep misses the corner. Is there a solution for this issue?

Post

Impulses are being inserted. That is all you need to think about. Impulses do not have "frequency". They have an amplitude and an infinitesimal position in time, period.

The "frequency" of a set of impulses is merely an abstract determined by the distance between any two impulses. If this distance is always different it can't be said that the collection of impulses has a defined frequency. There are various ways to calculate averages and other statistical methods which have a wide variety of applications. None of them are related to frequency in Hertz.

The DC offset is constant with a linear relationship to the density of impulses. What variable refers directly to the impulse density? The "frequency". This is where the average comes in handy: over a period of time (one sample) the "average" density can be used to determine the exact offset with only very small error due to the window. If you're using impulses for an oscillator a good approximation to the density on a particular sample is equal to the sampled frequency on that same sample (delayed by latency of your impulses.)

So: dc free output = impulse buffer + naive waveform + frequency * c;

Computing the constant "DC of impulse at 100% density" is the only important part. (Each waveform has a different sum of impulses: a pulse is +1 -1 = 0, ramp is +1 = 1, a 75% duty ramp/triangle will have a sum of 1st and 2nd order impulses. You just need to sum the impulses on each discontinuity throughout the waveform. Waveform constant DC = impulse constant DC * waveform discontinuity sum for this impulse.)

That is all.

Xhip's oscillator:
  1. Uses minimum phase impulses
  2. Uses 0th, 1st, 2nd and 3rd order impulses (blit, blep, blamp, splamp, ++)
  3. Has DC offset at nyquist or any frequency of -112 dB
  4. Handles thru-zero "FM" (negative frequencies)
  5. Handles sync
  6. Can do morphing waveforms (from tables) and sub-sample precision "width" (morph) but this is disabled in Xhip itself
Try it if you want to see the lowest possible quality you can get from such an implementation.
Free plug-ins for Windows, MacOS and Linux. Xhip Synthesizer v8.0 and Xhip Effects Bundle v6.7.
The coder's credo: We believe our work is neither clever nor difficult; it is done because we thought it would be easy.
Work less; get more done.

Post

aciddose wrote:Impulses are being inserted. That is all you need to think about. Impulses do not have "frequency". They have an amplitude and an infinitesimal position in time, period.
I wonder how a good implementation for an integrated symmetrical blep would look like (without minBleps). You always have something like this:

output = impulse buffer + naive waveform

The impulse jump in the buffer needs to fit the jump in the naive waveform. Thats the point where i see some problems when the frequency modulates while you already have mixed the impulse into the buffer.
aciddose wrote: So: dc free output = impulse buffer + naive waveform + frequency * c;
Isn't it easier and more stable to just add a one pole high pass with the cutoff around the fundamental frequency? It does all the things with one subtraction, addition and multiplication.

Post

No, for the same reason you just talked about regarding integration.

The high-pass filter will always remove more than you want because it approximates the removal of DC over time. By computing the exact DC value per sample you can remove the exact value of the offset rather than approximating it. You can take impulse density into account exactly and get a better result (-112 dB vs. -60 dB with a naive high-pass filter.)

Test it yourself. The consequence of the error is in minor side-bands vs. DC offset and is ultimately a trade-off.

For frequency modulation it turns out that using the nth order (integrated) vs. low-pass filtered (which has a frequency coefficient) is actually a better approximation rather than worse. You have to remember that any integration is only approximate to begin with since we're attempting to produce a geometrical shape. Integrating impulses is only an approximation of that geometry and it is far more easy to exactly generate the geometry instead.

Can the difference be measured? Yes, it is trivial. It comes down to very minor (<1 dB) differences in the spectral result which decrease as the sample rate increases.

So we're only looking at approximating the same thing from different directions with numerous trade-offs and never a perfect result.
Free plug-ins for Windows, MacOS and Linux. Xhip Synthesizer v8.0 and Xhip Effects Bundle v6.7.
The coder's credo: We believe our work is neither clever nor difficult; it is done because we thought it would be easy.
Work less; get more done.

Post

aciddose wrote:No, for the same reason you just talked about regarding integration.

The high-pass filter will always remove more than you want because it approximates the removal of DC over time. By computing the exact DC value per sample you can remove the exact value of the offset rather than approximating it. You can take impulse density into account exactly and get a better result (-112 dB vs. -60 dB with a naive high-pass filter.)

Test it yourself. The consequence of the error is in minor side-bands vs. DC offset and is ultimately a trade-off.
Thanks for the detailed explanation. I didn't notice that the side bands also have the source in DC offsets. I will try it.
aciddose wrote: over a period of time (one sample) the "average" density can be used to determine the exact offset with only very small error due to the window.
I just wonder how you calculate the average density. Do you use a circular buffer with a defined size (number of samples) where the most values are zero and some of them are +1 or -1 (the impulses), window it and calculate the average or is there a more effizient way to do this?

Post

The buffer contains variable impulses (blit, blep, blamp, ...) at various amplitudes and positions. The density at any one position is approximately equal to the current sampled frequency (in 1/Hz) multiplied by the sum of impulses scaled by the DC of each impulse for a complete cycle.

If you use Xhip to test the frequency modulation (X-Mod) feature you should notice that if any DC leakage were allowed it would result in feed-through of the modulator frequency. You can try it with -36 st for the modulator and you'll find the level of feed-through is extremely low with this technique. (Actually you can use several octaves if you disable KBT and use detune mod sources, although it has an ultimately limited range.)

If you were integrating pulses with a lossy integrator (or otherwise) the feed-through is massive. It makes X-Mod impossible. The biggest issue is a variable amount of error. It is easy to over-look the fact that all digital filters are highly inaccurate because they are discrete (remember not LTI). If the approximation has a continuous error it means as the frequency is modulated the DC offset does not change.

This is what allows the minimal (-60 dB peak) modulator feed-through in Xhip. This is nearly inaudible and is reduced with increased oversampling.

http://xhip.net/temp/xmod.mp3
http://xhip.net/temp/xmod.7z (.wav)
Free plug-ins for Windows, MacOS and Linux. Xhip Synthesizer v8.0 and Xhip Effects Bundle v6.7.
The coder's credo: We believe our work is neither clever nor difficult; it is done because we thought it would be easy.
Work less; get more done.

Post

mystran wrote: The problem is that a minimum-phase BLEP is not zero-phase and trying to pretend so leads to all kinds of funky DC offset issues and other crap that BLEPs are specifically supposed to avoid. Since you can have arbitrary overlaps of any number of BLEPs anyway, your best strategy is to mix them into a buffer right-away when you find the relevant transition point, at which point moving the "naive" output around in the buffer to compensate for linear-phase BLEP latency is trivial.

edit: in short, if you think MinBLEPs make something easier in terms of implementation, then your implementation is almost certain broken in the first place!
I'm making slow progress :) i noticed the issues with minBleps and replaced it with a normal linear blep with a centered step.
At the moment i just add the blep at the end of a naive phase jump. It works well but i'm having a huge DC bias when i use it with saws, because of the bias the added blep produces.
What's a good way to to mix in the blep without the dc offset? Let the naive phase jump to be earlier half a blep size and mix it there?

Post

xhy3 wrote:
mystran wrote: The problem is that a minimum-phase BLEP is not zero-phase and trying to pretend so leads to all kinds of funky DC offset issues and other crap that BLEPs are specifically supposed to avoid. Since you can have arbitrary overlaps of any number of BLEPs anyway, your best strategy is to mix them into a buffer right-away when you find the relevant transition point, at which point moving the "naive" output around in the buffer to compensate for linear-phase BLEP latency is trivial.

edit: in short, if you think MinBLEPs make something easier in terms of implementation, then your implementation is almost certain broken in the first place!
I'm making slow progress :) i noticed the issues with minBleps and replaced it with a normal linear blep with a centered step.
At the moment i just add the blep at the end of a naive phase jump. It works well but i'm having a huge DC bias when i use it with saws, because of the bias the added blep produces.
What's a good way to to mix in the blep without the dc offset? Let the naive phase jump to be earlier half a blep size and mix it there?
Assuming you are processing N samples (this block) and your BLEP (well, one branch) is M samples long, you need an output buffer that's at least N+M samples long. For each BLEP you mix it into the output buffer starting from the current sample index. For the naive wave, you mix it with an extra M/2 samples offset (effectively delaying it by M/2 samples). At the end of the block, you take the first N samples as output for this block. Then you mix the last M samples at the beginning of the buffer for the next block (to get the overlap).

Post

mystran wrote:
xhy3 wrote:
mystran wrote: The problem is that a minimum-phase BLEP is not zero-phase and trying to pretend so leads to all kinds of funky DC offset issues and other crap that BLEPs are specifically supposed to avoid. Since you can have arbitrary overlaps of any number of BLEPs anyway, your best strategy is to mix them into a buffer right-away when you find the relevant transition point, at which point moving the "naive" output around in the buffer to compensate for linear-phase BLEP latency is trivial.

edit: in short, if you think MinBLEPs make something easier in terms of implementation, then your implementation is almost certain broken in the first place!
I'm making slow progress :) i noticed the issues with minBleps and replaced it with a normal linear blep with a centered step.
At the moment i just add the blep at the end of a naive phase jump. It works well but i'm having a huge DC bias when i use it with saws, because of the bias the added blep produces.
What's a good way to to mix in the blep without the dc offset? Let the naive phase jump to be earlier half a blep size and mix it there?
Assuming you are processing N samples (this block) and your BLEP (well, one branch) is M samples long, you need an output buffer that's at least N+M samples long. For each BLEP you mix it into the output buffer starting from the current sample index. For the naive wave, you mix it with an extra M/2 samples offset (effectively delaying it by M/2 samples). At the end of the block, you take the first N samples as output for this block. Then you mix the last M samples at the beginning of the buffer for the next block (to get the overlap).
Thanks for the fast response. I tried this, but when i mix the blep in the center of a naive step like this it does not fit. I think i did the same you described on a sample basis that looks like follows:

// saw -> delayM2() delays the sample by half a blep size
getNextBlep() + delayM2(phase - 0.5f);

How does your blep look like and how do you mix it together? Mine starts at 1 and jumps to 0 in the middle. This gives a unwanted step at the beginning when mixed in the middle of a naive phase jump

Post

I suggest that your blep buffer should have the naive step pre-subtracted from it, so that when you add it to your naively stepping waveform the blep ends up replacing the naive step. Then you just have to ensure that the sub-sample-accurate location of the step in your sawtooth waveform corresponds with the location of the step in the blep buffer.

So your (naive-step-subtracted) blep buffer will start and end at zero, with growing oscillations at the start, a zero-crossing in the centre (assuming a linear blep), and then diminishing oscillations at the end. The integral of this buffer should be zero, so that however you add it in it should not create any DC offset.

Post

kryptonaut wrote:I suggest that your blep buffer should have the naive step pre-subtracted from it, so that when you add it to your naively stepping waveform the blep ends up replacing the naive step. Then you just have to ensure that the sub-sample-accurate location of the step in your sawtooth waveform corresponds with the location of the step in the blep buffer.

So your (naive-step-subtracted) blep buffer will start and end at zero, with growing oscillations at the start, a zero-crossing in the centre (assuming a linear blep), and then diminishing oscillations at the end. The integral of this buffer should be zero, so that however you add it in it should not create any DC offset.
Thanks for that. I thought about this solution. I just was not sure if it also works when i scale the blep... I will try it. That was the missing part :)

Post

Thanks all together. A delayed naive waveform and mixing in a blep with a subtracted step worked perfectly :)

edit: @aciddose: i like the idea to calculate the the DC offset, but the overhead for something that is given by a normal blep is too big. Think switching to linear phase bleps is the best way to do this.

Post Reply

Return to “DSP and Plugin Development”