Triangle hard sync with BLEP's opinions

DSP, Plugin and Host development discussion.
RELATED
PRODUCTS

Post

aciddose wrote: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).
Thanks for pointing that out and the audio demo. I come to the conclusion that my BLEP's and BLAMP's maybe don't have the same delay (maybe an error in the calculation) or maybe the BLAMP isn't accurate enough.
It looks like while the BLAMP does a perfect job with 32 times oversampling but the quality gets massive low with x128 oversampling. How did you generate the higher order bleps?

Post

I would personally keep the current segment of any piece-wise waveforms (eg. pulse, triangle..) in an explicit integer variable. This way all you need to check is whether the phase (at the end of the sample) is larger than the end-point of the current segment. If that's the case, you can solve the exact time backwards using the current frequency. If you're advancing the slave in preparation for a master reset, you can then check if that time is past the scheduled master reset time and break out of the (transition handling) loop if that's the case. This way it's also totally obvious if you need a slope corrector for triangle or not: just look at the segment-variable to know if we're in the first or the second segment.

Post

mystran wrote:...you can solve the exact time backwards using the current frequency.
Maybe i missed that. How would you do that. I use following:

Code: Select all

if (segment == 2) newPhase = 0.5f - (phase - 0.5f);
How can i bring in the frequency to get the right result?

Post

xhy3 wrote:
mystran wrote:...you can solve the exact time backwards using the current frequency.
Maybe i missed that. How would you do that. I use following:

Code: Select all

if (segment == 2) newPhase = 0.5f - (phase - 0.5f);
How can i bring in the frequency to get the right result?
If you have constant frequency over the sample (let's call it "deltaPhase") then we have:

newPhase = phase + deltaPhase

we can solve this back the other way too:

phase = newPhase - deltaPhase

Now if we wanted to solve for the fractional sample time t where phase = .5, we'd have:

phase + t*deltaPhase = .5

now substitute from above, and solve for t:

(newPhase - deltaPhase) + t*deltaPhase = .5
newPhase + (t-1)*deltaPhase = .5
(t-1)*deltaPhase = .5 - newPhase
(t-1) = (.5 - newPhase) / deltaPhase
t = 1 + (.5 - newPhase) / deltaPhase.

Once you know the exact time-point where the transition occurs, rest of it shouldn't be very complicated. :)

Post

mystran wrote:I would personally keep the current segment of any piece-wise waveforms (eg. pulse, triangle..) in an explicit integer variable. This way all you need to check is whether the phase (at the end of the sample) is larger than the end-point of the current segment.
That obviously doesn't work if you're using negative frequencies. Either you need to generate two tables, one forward and one reverse, or each step in the table needs both start and end values.

Ultimately no matter what you do the result contains redundant information (bug prone) and the computation is less efficient for basic 1-step or 2-step waveforms like ramp, pulse and triangle.

That's why I recommended that anyone should start by handling all these computations without additional premature optimizations like storing the last known step, waveform or other variables.

It's certainly a possible optimization in the case that it is only applied to waveforms with >2 steps and where negative frequencies aren't allowed. In other cases however there are more optimal solutions for those specific cases.
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:
xhy3 wrote:
mystran wrote:...you can solve the exact time backwards using the current frequency.
Maybe i missed that. How would you do that. I use following:

Code: Select all

if (segment == 2) newPhase = 0.5f - (phase - 0.5f);
How can i bring in the frequency to get the right result?
If you have constant frequency over the sample (let's call it "deltaPhase") then we have:

newPhase = phase + deltaPhase

we can solve this back the other way too:

phase = newPhase - deltaPhase

Now if we wanted to solve for the fractional sample time t where phase = .5, we'd have:

phase + t*deltaPhase = .5

now substitute from above, and solve for t:

(newPhase - deltaPhase) + t*deltaPhase = .5
newPhase + (t-1)*deltaPhase = .5
(t-1)*deltaPhase = .5 - newPhase
(t-1) = (.5 - newPhase) / deltaPhase
t = 1 + (.5 - newPhase) / deltaPhase.

Once you know the exact time-point where the transition occurs, rest of it shouldn't be very complicated. :)
I'm getting huge values when i use that formula. Do i use it when i insert the 0.5 blamp and remeber it for the hard sync?

Post

Why don't you just use the set_phase() implementation I gave you? It works correctly.

There is absolutely no reason not to use an inline version of set_phase() in your main loop. The compiler can optimize it as needed, especially if you use templates: template <typename wave> set_phase(...) { wave wf; wf.get_level_at(phase); ... }

The compiler will produce better optimized code than you ever could by hand and you'll be able to use the same implementation for any waveform you want.

That's assuming of course that you don't want to be able to dynamically modify the waveform and have any changes correctly anti-aliased as well. In which case the very minor overhead of a function call is well worth it when it is 1/00th as expensive as inserting a single FIR kernel.

Premature optimization is the root of all evil. You might think you know better "I don't need X or Y", but without having a full implementation how can you possibly know all the possible optimizations in order to choose the best available optimization? You can't.

The first step is to write working code.

The next step is to optimize it. That includes profiling and bench-marking to ensure your "optimization" is actually any advantage at all against the reference implementation.

How can you perform profiling and bench-marking without a reference implementation? How can you write unit tests without a reference implementation? You can't.

Code: Select all

float ramp_to_triangle(const float x)
{
	return std::abs(x * 2.0f - 1.0f) * 2.0f - 1.0f;
}

void blepgenerator::tests::triangle()
{
	const blepdata::impulse_t &impulse = blepdata::get_minblamp_impulse();
	ad::dsp::blep_generator<> b;
	wave_t wave(1, 32, rate, length, format_t::ieee_float);
	float *p = (float *)&wave.data[0];

	const float gain = std::sqrt(1.0f / 2.0f);
	const float delta = hz / float(rate);
	float phase = 0.49f;
	float next = 0.5f;

	for (unsigned int i = 0; i < wave.samples; i++) {
		phase += delta;

		if (phase >= next) {
			if (next >= 1.0f) {
				phase -= 1.0f;
				b.delta.add(phase / delta, -2.0f * delta, impulse);
				next = 0.5f;
			} else {
				b.delta.add((phase - next) / delta, 2.0f * delta, impulse);
				next = 1.0f;
			}
		}

		p[i] = gain * b(ramp_to_triangle(phase));
	}

	wave.store("./triangle.wav");
}
Simple tests like this are essential. You should also write unit tests for every component part.

16 samples per 8 zero crossings, minimum phase 2nd order:
http://xhip.net/temp/triangle_16bit.wav
Apparently most browsers won't play floating point RIFF wave format http://xhip.net/temp/triangle.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

aciddose wrote: ...
The compiler will produce better optimized code than you ever could by hand and you'll be able to use the same implementation for any waveform you want.

That's assuming of course that you don't want to be able to dynamically modify the waveform and have any changes correctly anti-aliased as well. In which case the very minor overhead of a function call is well worth it when it is 1/00th as expensive as inserting a single FIR kernel.

Premature optimization is the root of all evil. You might think you know better "I don't need X or Y", but without having a full implementation how can you possibly know all the possible optimizations in order to choose the best available optimization? You can't.

The first step is to write working code.

The next step is to optimize it. That includes profiling and bench-marking to ensure your "optimization" is actually any advantage at all against the reference implementation.

How can you perform profiling and bench-marking without a reference implementation? How can you write unit tests without a reference implementation? You can't.

...

Simple tests like this are essential. You should also write unit tests for every component part.

16 samples per 8 zero crossings, minimum phase 2nd order:
http://xhip.net/temp/triangle_16bit.wav
Apparently most browsers won't play floating point RIFF wave format http://xhip.net/temp/triangle.wav
I totally agree with you. My code is not optimized and i don't fear any methods. It's just pragmatic... i wanted a pulse, saw or triangle with hard sync and programmed it only for this cases. For me, the second step is a general solution for all cases.
I like the idea of unit tests for the oscillators. Maybe even compare the file to a reference file to be sure that it still works the way it should. Not sure about float results, but it should work for 16 bit output.

Post

Float isn't a problem, just use abs(x - y) < threshold.

Some very useful tests actually use some measurement of the spectrum compared against as well as with the expected harmonics ignored. You can then measure pass-band effects from the filter, aliasing amplitude, slope and other very useful properties.

If you get your oscillator working and can accurately measure these properties you can then compare those test results rather than comparing one signal directly against another.

Possibly with:
assert(stop_band <= expected_stop_band);
assert(pass_band_error <= expected_pass_band_error);

And so on.
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

Since for example we're entirely capable of generating perfect repeating waveforms at exactly the window width (width/n) because the FIR kernel has a finite length. It means we don't even need to window the signal in order to generate a fourier bode plot (spectrum + phase).
xcope0.png
This means there is no need to worry about side-bands or resolving capability, you essentially end up with exact amplitudes for each individual harmonic.

The stop-band level in this example is about -80 to -110 dB (depending on peak vs. average) and the pass-band error is significant at over 20 dB (peak is likely not the best measurement in general, but 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 Reply

Return to “DSP and Plugin Development”