Triangle hard sync with BLEP's opinions

DSP, Plugin and Host development discussion.
RELATED
PRODUCTS

Post

Sorry to bring up a BLEP topic again. I know there are already a lot of threads about it. But there is one special case that makes problems with triangles and i can't get it to work without additional aliasing at higher frequencies.

It's following step (the first triangle step goes down and the one after the sync goes up):

Code: Select all

*         *
 *       *
  *     *
   *   *
   *  *
   * *
   **
   *

or 

*         *
 *       *
  *     *
   *   *
    *  *
     * *
      **
       *
Has someone an idea how to make that work?

We have two issues here: there is something like an angle and there is also an additional normal step like we have it with a saw. Any help is welcome.

Post

You need a 2nd order impulse to deal with the change in delta.

A first order shape like a ramp has a constant delta and is reset (zero order) every cycle. This requires a first order impulse (blep).

In order to "correctly" anti-alias PCM without using zero-order-hold you need a zero order impulse (blit) and an integration step after the FIR impulses replace the samples. This could be considered a "-1" order impulse but it is better to recognize that a "band limited step" impulse is a first order impulse mixed with the inverse step such that it cancels out the step it is replacing.

From that point it should be obvious that we have N order steps beyond just first order. A triangle is an integrated pulse. Pulse harmonics fall off at 1/N^1 (first order) while triangle harmonics fall at 1/N^2 (second order). Therefore we need to anti-alias any change in both the first and second order derivatives of the waveform.

A spline (cubic = ^3) contains: you guessed it, third order derivatives as well. Spline shapes like cubic spline curves need to have their 3rd order derivatives filtered too. The harmonics of a third order waveform fall off at most by 1/N^3 (third order).

This can be applied to anti-alias a sine as well. It should be noted that for the usual third-order parabolic approximation the waveform can be perfectly filtered by only first, second and third order impulses. A real sine however is an infinite order abstract mathematical concept and could never exist in reality without infinitesimals and a true continuous domain. Generally even when dealing with a higher order sine approximation, a limit to third order impulse is considered "good enough" due to the rapid 1/N^3 harmonic falloff.

That said; although Xhip for example uses a third order parabolic sine it does not attempt to anti-alias the waveform beyond only the first order due to the very high expense of doing so. Accuracy is very difficult to achieve at such high orders (3rd or more) and so it is doubtful with the usual computational precision that much benefit if any could be derived from the application of higher order impulses. While the computations themselves are somewhat trivial (although not at all free) higher order impulses need significant lengths to provide good performance.

In Xhip for example the sine serves purely (pun intended) as a more pure source for cross modulation and so it is essential that it not be difficult to compute at very high frequencies to serve as an x-mod source supporting feedback. A filtered version of the waveform would no longer function correctly with feedback without a very costly high-precision computation. (The latency required would be impossible to deal with.)

At very high frequencies (1/2 nyquist) it is impossible to distinguish different waveforms whether sine, triangle, square or even ramp because the first harmonic no longer fits within the band. Due to this where the lack of sine anti-aliasing creates an issue for high frequency tones in Xhip it is recommended that either the triangle (for mixtures of mid and high frequency ranges) or ramp (for only high frequency ranges) be used instead.
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

Thanks for the quick reply. What i look for is a practical solution for the special step. I already use integrated BLEPs (BLAMPs) and normal BLEPs for the triangle and it works expect for the case above.

Is it possible to mix a BLAMP and BELP somehow to improve results?

I also consider to avoid the complex case above and always take the ramp with the same direction when making the hard sync.

Post

xhy3 wrote:Thanks for the quick reply. What i look for is a practical solution for the special step. I already use integrated BLEPs (BLAMPs) and normal BLEPs for the triangle and it works expect for the case above.

Is it possible to mix a BLAMP and BELP somehow to improve results?

I also consider to avoid the complex case above and always take the ramp with the same direction when making the hard sync.
From a quick look (sorry if I misunderstood your question), this is exactly what you need to do. Since you have both a discontinuity in the signal and in the derivative, you have to antialias both. Thus you need to use a BLEP to antialias the step and the BLAMP to antialias the derivative step. The amplitudes of BL-EP/-AMP are defined by the amplitudes of the respective steps.

Post

xhy3 wrote:Thanks for the quick reply. What i look for is a practical solution for the special step. I already use integrated BLEPs (BLAMPs) and normal BLEPs for the triangle and it works expect for the case above.

Is it possible to mix a BLAMP and BELP somehow to improve results?
No [assuming you mean mixing them in advance; you DO need to use both]. You always need to handle each (non-zero) derivates separately as they require different scaling. The blep is simply scaled to match the discontinuity. The "blamp" needs to be scaled to match the change in the slope. The change in slope is zero if we reset within the same segment and twice the slope of the segment (which depends on the frequency) if we reset from the other segment (the same as the bleps you insert into triangles as it transitions from one segment to the other).

Post

Thanks for the replies. I will then try to mix in the BLEP step and the BLAMP with the individual scaling. It sounds too good to be real :)

Post

Shame on me. I still struggle with the triangle implementation. I just don't get the right hard sync BLEP size when it jumps from ramp down to ramp up. I thought i could do something like this when the phase is in the second part of the triangle:

if (phase >= 0.5f) phaseTotal = 1.0f - phase;

But it does not work and is inaccurate. It would be great if someone can help me. I tried a lot of things.. but maybe i have to do it in a totally different way?

Post

Ultimately everything you need to know was in my first post to this thread. If you're unable to conceptualize and/or visualize what is going on it is no surprise you won't be able to get it to work correctly. You need to understand 100% of the problem before you can attempt to solve it, let alone write an algorithm to solve every possible variation of the problem.

If the triangle is 0 to 1 in 1/2 cycle it means the delta is 2 for the first half, then switches to -2. That sudden switch to -2 must be filtered.

You'll need to ensure your impulses have identical latency (often by delaying the first order to make up for higher order latencies) or you'll need to mix them into distinct buffers and apply delay lines to line them up correctly.

So at each "point" / "vector" in the waveform you need:

Code: Select all

// the event happened "age" samples ago
age = (phase - step.phase) / phase_delta;
delta[1] = step.y - last.y;
if (delta[1] != 0) {
amplitude = delta[1];
insert(amplitude, age, impulses.order[1]);
}
delta[2] = (step.y_delta - last.y_delta) * pow(phase_delta, 1);
if (delta[2] != 0) {
amplitude = delta[2];
insert(amplitude, age, impulses.order[2]);
}
Obviously you can do this in a loop:
for (order = 0; order < max_order; order++) { ... }

You'll also need to ensure you normalize your impulses correctly or apply re-normalization to the amplitude factor upon insertion.

The code I've posted here is to get you thinking about the problem: it won't work. The event you're interpolating is always at an arbitrary position. For example during a sync event you're filtering the difference between the source phase and target phase parameters. This is always the case because during a sample period you'll never move exactly one step in your table of vector points. (Although it can happen by chance: that corner-case will lead to a division by zero and needs to be handled.)
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:If the triangle is 0 to 1 in 1/2 cycle it means the delta is 2 for the first half, then switches to -2. That sudden switch to -2 must be filtered.

You'll need to ensure your impulses have identical latency (often by delaying the first order to make up for higher order latencies) or you'll need to mix them into distinct buffers and apply delay lines to line them up correctly.
Thanks for don't give up on me :) I have to read that again and again to understand it... this phase jump in the middle of the triangle is really annoying. My current test code:

Code: Select all

        phase += phaseIncrement;
...
// hard sync

        float phaseStartTotal = phaseStart + hardSyncFrac;
        bool directionChange = (phase >= 0.5f &&  phaseStartTotal < 0.5f) || (phaseStartTotal >= 0.5f &&  phase < 0.5f);
        
        if (phaseStartTotal >= 0.5f) phaseStartTotal = 0.5f - (phaseStartTotal - 0.5f);

        float phaseTotal = phase;
        if (phase >= 0.5f) phaseTotal = (0.5f) - (phase - 0.5f) + hardSyncFrac;
    
        if (directionChange)
            mixInTriangleSinc(blepPosition, -sign * phaseIncrement);
        
        mixInBlep(blepPosition, (phaseStartTotal - phaseTotal));
        
        phase = phaseStart + hardSyncFrac;
        if (phase >= 0.5f) sign = -1.0f;
        else sign = 1.0f;

...
// calc output
        float value = 0.0f;
        if (sign == 1.0f)
        {
            value = phase;
        }
        else
        {
            value = 0.5f - (phase - 0.5f);
        }

Post

Your code can't possibly work because it's making all sorts of assumptions. You'd be better off to create a general-purpose set_phase(fraction, target) function that you call from your main phase increment & sync loop.

For example:

Code: Select all

void phase_increment()
{
	phase += delta;
	while (phase >= next || sync) {
		const float overlap = phase - next;
		const float fraction = (phase > next ? overlap / delta : 0.0f);
		if (sync && syncfraction >= fraction) {
			set_phase(syncfraction, 0.0f);
			sync = false;
		} else {
			set_phase(fraction, next);
		}
	} 
}

Code: Select all

void set_phase(float fraction, float target)
{
	const float newphase = fraction * delta;
	const float sync_at = phase - newphase;
	const float oldlevel = get_old_level_at(sync_at * 0.99999f);
	const float olddelta = get_delta_at(sync_at * 0.99999f);
	const float newlevel = get_level_at(target);
	const float newdelta = get_delta_at(target);
	impulse.add(fraction, oldlevel - newlevel);
	delta.add(fraction, delta * (olddelta - newdelta));
	next = get_next_at(target);
	last = get_last_at(target);
	phase = target + newphase;
}
This is from my old code (used in Xhip in 2012.)

It works.
You do not have the required permissions to view the files attached to this post.
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

Note: "...but I don't need a wiggle-wiggle jibbajabbathehut wave, I just want pulse and triangle!"

That's called premature optimization.

Build a proper implementation first, then you can worry about trimming out parts that aren't needed and compare back to the proper implementation to ensure your optimizations will function correctly.

For example enhancing the code I posted in order to get through-zero operation is natural and only involves duplicating the code and shifting a few "one_less" functions around (*0.99999f).

Enhancing your code to get through-zero operation is not natural because your code is already optimized to do the bare minimum and can't be used to solve more advanced problems.
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

Nice waveforms :)

I see the point with my code. We assume i'm having solved all the sub sample problems and all steps work expect of the one special case where we jump from ramp up to ramp down or the opposite.

The only question i have is: What do i need to do to calculate the exact level of the blep for that kind of step (the blamp seems to work fine, because the level depends on the frequency)?
If the triangle is 0 to 1 in 1/2 cycle it means the delta is 2 for the first half, then switches to -2. That sudden switch to -2 must be filtered.

You'll need to ensure your impulses have identical latency (often by delaying the first order to make up for higher order latencies) or you'll need to mix them into distinct buffers and apply delay lines to line them up correctly.
What do you mean with filtered?

Is it the 0.99999f thing that is important or is this for the division of zero issue? I don't see any other special things in your code that help to solve my problem. What do these functions exactly?

Code: Select all

  const float oldlevel = get_old_level_at(sync_at * 0.99999f);
   const float olddelta = get_delta_at(sync_at * 0.99999f);
   const float newlevel = get_level_at(target);
   const float newdelta = get_delta_at(target);

   delta.add(fraction, delta * (olddelta - newdelta));

Post

0.99999f is used to get "x - 1 step of the mantissa". There are similar functions available in c++11 that I would use instead today if the multiplication didn't work correctly in some obscure corner case... but I'm not aware of one and a multiplication isn't expensive while it works just fine for the purpose.

This is required due to the way floating point comparisons work: when moving forward the old level will always be set back vs. the new level. Without the scaling sync_at may == target (a type of "corner case") which will rarely fail to insert the correct impulses. The alternative is to implement more functions based upon the sign of the delta and last/next which ensure the correct values are provided but this is far less efficient and more bug-prone that a multiplication.

First, calling them "impulses" is just plain stupid. We should start by referring to them as what they are: FIR kernels. I mentioned filtering the discontinuities in the wave form (any order) by which I meant replacing those discontinuities with the FIR kernels ("impulses").

What those functions do is totally up to you. They describe their purpose, not their implementation.
For example for a pulse: get_delta_at(x) { return 0; }
For a triangle: get_delta_at(x) { return x > 0.5 ? 1.0 : -1.0; }

For more complex waveforms you'll probably be looking up the delta value from a pre-computed table. There are lots of possible optimizations. One very easy solution is to just use function pointers, however the overhead of a function pointer might cost more than switch(waveform) statement once in-lining is considered.

pulse: get_level_at(x) { return x > width ? 1.0 : -1.0; }
ramp: get_level_at(x) { return x * 2.0 - 1.0; }
triangle: get_level_at(x) { return abs(x * 2.0 - 1.0) * 2.0 - 1.0; }

... obviously for more complex waveforms computing these equations is inefficient.
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

I see, thanks for the description. I now think i do the right thing. No magic :) Did you ever mix BLEP's and BLAM's for the special case above?

Can i really do that?

Code: Select all

        if (directionChange)
            mixInTriangleSinc(blepPosition, -sign * phaseIncrement); // the normal ones i'm using for ordinary triangle anti aliasing
        
        mixInBlep(blepPosition, phaseStartTotal - phaseTotal); // the calculated y distance (works if the phase stays in the same triangle part)
I'm starting to believe that it isn't that simple. I substract the step from the BLEP and the ramp from the BLAMP in the tables. Maybe this causes the issues when mixing it together?

Post

I use distinct buffers for different orders because I'm using minimum phase. This means each order has a unique delay.

(Removed technical rant about why not.)

So I can't tell you that it does work, but I can say that I have absolutely no reason to believe there is any technical reason that it wouldn't work. It works perfectly now, so simply rearranging the delay and mixing into a single buffer wouldn't change anything of any significance. There is just no benefit to doing so.

Or did you mean that not literally? Yes I use both types (and more, I've experimented with up to 4th order) in the waveforms I posted the graphs for. For example both 1st and 2nd order are used in the 2x pulse * ramp waveform because the delta switches between 0 and 1. Sync works perfectly in all these waveforms and is used when switching waveform. This is why you can see the initial 1st and 2nd order kernels at the beginning of some of the waves.

https://soundcloud.com/xhip/osc-b-demo

This is a clip of the oscillator including those 2nd order waveforms running with sync and PWM in a stereo unison. Several waveforms are demonstrated including pulse, ramp, triangle, 4pt cos, 8pt cos, z ramp, dual ramp, pulse2x chopped ramp and variable width ramp (adjustable slope ramp/triangle).
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 Reply

Return to “DSP and Plugin Development”