MinBlepGenerator Woes?

Hi all! First time posting on the forums, and yes I have read the other topics about MinBLEPs. I’m working on my second VCV Rack module, an additive oscillator. All the features work well and the performance is solid, but I’m hitting a frustrating wall with band-limiting a couple different types of oscillator sync.

The module has two different types of sync:

• External hard sync
• Internal partial sync (non-fundamental partials can have their frequency spacing go >1x and will sync to the fundamental)

I’ll focus on the 2nd type for this question since it’s fairly simple to decompose into the simplest case (2 partials, the fundamental – at `freq` – and the one above, at `spacing * freq`). Conveniently, the issue still happens in this case. If `spacing` isn’t at an integer value, what I end up running into is that when the fundamental syncs the phase of the other partial there’s aliasing at high enough values of `freq` (I’m at 96kHz sample rate and limit `freq` to 10kHz and below and this doesn’t happen at integer values of `spacing` so it’s not the sines themselves). I’m adding a `MinBlepGenerator`’s `process` value to the output with what I think is the right sample offset/discontinuity value but I’d love for someone to tell me that I’m using it wrong. I’ve been basing this logic heavily on the VCV Rack Fundamental VCO module (see link for relevant code).

Summarizing my code substantially, here’s what’s going on:

``````// ... abridged ...

float phaseInc = freq * args.sampleTime;

// Step the fundamental phase and calculate if we need to sync partials
float fundAccum = this->phaseAccumulators[0];
float fundAccum += phaseInc;
bool fundSync = fundAccum >= 1.f; // sync partials if fundamental accumulator goes above 1
fundAccum -= std::floor(fundAccum); // wrap back to 0-1 range

// Step the next harmonic phase
float nextHarmonicAccum = this->phaseAccumulators[1];
nextHarmonicAccum += phaseInc * (1.f + spacing);
nextHarmonicAccum -= std::floor(nextHarmonicAccum);

// Get synced version of next harmonic phase
float nextHarmonicAccumSync = firstHarmonicAccum;
float minBlepP = 0.f;
if (fundSync) {
nextHarmonicAccumSync = fundAccum * (1.f + spacing);
nextHarmonicAccumSync -= std::floor(nextHarmonicAccumSync);

// The fundamental wrapped `fundPhaseWrapped` phase units ago and we
// incremented by `phaseInc` phase units since last sample so our discontinuity
// happened approximately `fundPhasewrapped / phaseInc` samples ago
minBlepP = -(fundPhaseWrapped / phaseInc);
}

// Calculate synced and unsynced outputs (omitted lower amplitudes for higher partials)
float outUnsynced = sin2pi(fundAccum) + sin2pi(nextHarmonicAccum);
float outSynced = sin2pi(fundAccum) + sin2pi(nextHarmonicAccumSync);

// Insert MinBLEP discontinuity if sync happened
if (fundSync) {
this->blep.insertDiscontinuity(minBlepP, outSynced - outUnsynced);
}

outputs[MAIN_OUTPUT].setVoltage(5.f * (outSynced + this->blep.process()));

// ... abridged ...
``````

Does anything stand out as obviously wrong (other than the style/comments on this very simplified code)? I really appreciate the help in advance.

min blep with sync is really difficult, I have found. why not copy Fundamental VCO?

I did my own internal sync in EV3, and even that almost killed me. Just copy Fundamental, it’s great.

Are you referring to this code for EV3? I just read through it! It feels like we’re doing pretty similar logic.

As for why I’m not just 100% copying Fundamental VCO, it wouldn’t perform well with the additive synthesis controls that exist in my module

yeah, that’s the EV3. Internal sync is ok (works fine), but real sync is more versatile. Don’t steal the whole code, just steal the way it does sync. I think it interpolates between input samples to figure out where the minBLEP should go.

But if yours is working fine, cool.

I’ve also got real sync, and unfortunately it suffers from the same problem if the master oscillator is at a high enough frequency

Well, the VCV one is pretty clean. Maybe you are doing it wrong?

Alternatively, maybe `MinBlepGenerator` fundamentally can’t solve the problem I’m experiencing since when I try two Fundamental VCOs and sync a 2kHz saw to a 4kHz square and sweep around there’s some pretty bad aliasing

I believe that. You got a picture to post? (that is the case I was attempting to show in the picture I posted).

It’s really odd actually. There are a few hot spots with really bad aliasing (like the first image, 4096.1Hz square wave syncing 5919Hz saw) and then other spots with more mild aliasing (2nd image). Granted I’ve been doing everything at 96kHz, I’m sure if I were at a higher sample rate things would be improved.

those don’t look so bad to me. I’d hafta think about what they “should” look like. but who really cares if at 4k you have aliasing down like 18 db. It’s better than most. But if you are going to go for better, go for it!

I’ve also found that using a minBLEP wave as the modulator sometimes is worse than expected. Not always because of aliasing, but sometimes because of the phase jitter that minBLEP intentionally adds.

That’s an interesting point about the phase jitter.

I temporarily modified my code so I could just listen to the output of `this->blep.process()` and it’s definitely doing something (I can even hear it doing what I imagine is “reverse aliasing” which should cancel out the parts of the wave that would alias), but when summed with my synced waveform it’s not reducing the perceived amount of aliasing.

I’m really hoping I’m just not using minBLEP as intended and that it’s an easy fix, since I’m really hoping to be able to release this module soon

7 Likes

Thinking about this more, is it possible that some of the aliasing could be coming from the BLEP impulses overlapping when the sync frequency is shorter than the amount of time the BLEP rings for?

Edit: tried alternating between 4 different BLEPs and it didn’t make a difference so that’s probably not it

Are you planning on implementing the fm/pm with some alias reduction?

I fade out each partial as it nears the Nyquist frequency so aliasing with fm/pm shouldn’t be too much of an issue. I remember reading somewhere that band-limited sine waves shouldn’t alias when being FM’d but maybe my memory of that is overgeneralized

Unfortunately FM can alias a lot. Here’s a typical text: Synthesis Chapter Four: Audio-rate FM Synthesis 5.

FM has harmonics up to infinity. Even the old DX-7 would reduce the modulation level at high frequencies to avoid super audible aliasing. Most FM VCOs in VCV use oversampling to reduce it - check out the options in the context menu of the Bogaudio FMOP.

You also have AM, which of course gives the sum of the two frequences, so it can alias a little. I’m not sure what your “drive” control does, but typically those apply some saturation… which will often alias if left to do so.

Yep I was hoping to implement ADAA for the drive. Thanks for the tip on FM

Also, I thought I’d made some kind of breakthrough by adding a discontinuity per partial when sync happens instead of for the summed overall waveform (my thinking being that there are some kind of small phase differences between the partials which could make a difference), but it didn’t end up helping at all. I’m feeling pretty defeated to be honest.

well, this stuff is hard. take a break and come back to it. I don’t understand exactly what the problem is that you are running into. If yours isn’t as clean as the others - copy them. If you want yours to be cleaner than the best out there then it may be very difficult (and difficult to hear the difference).

2 Likes

Thanks for the encouraging words! Some good news: the minblep is doing something. When comparing Fundamental VCO’s synced sine to Loom’s synced sine (dialing partial count down to 1 and matching levels), the anti-aliasing behavior looks identical.

Some bad news: Loom’s synced 64-harmonic saw has way more aliasing than Fundamental VCO’s synced saw. This suggests to me that there could be some kind of precision issue with the sync behavior at higher harmonics. I previously tried adding small discontinuities per partial instead of one discontinuity for the whole waveform but that didn’t work.

It’s nice to see that I at least didn’t mess up the basic MinBlep implementation, but hunting down this precision issue (if that’s what it is) might take a while.

One interesting thing I noticed while comparing to Additator is that with MinBLEP I get more low-frequency aliases whereas with no antialiasing Additator gets more high-frequency aliases. With <= 4 partials the anti-aliasing via MinBLEP successfully achieves less aliasing than with nothing, but above 4 partials it falls behind.

Solution:

It turns out that I was doing MinBlep right (it worked great on a square wave), but had glossed over the fact repeated in several papers on the topic (link1, link2, link3) that hard-synced sinusoids are not continuous in any of their derivatives. As a result, using BLEP to attempt to anti-alias sinusoids yields an approximation that only attenuates spurious frequencies at 6db/octave per derivative whose discontinuity is made bandlimited.

I opted to add an on-by-default option for 2x oversampling in my plugin, and am working on using a different scheme to anti-alias the summed sinusoids in my additive synthesis model based on this paper by Native Instruments. I’m also interested in trying to use a 2nd BLEP on the derivative of my summed sinusoids (summing `cos` instead) and integrating it to get another 6db/octave of attenuation.

Hopefully this resolution to the thread is useful for future readers who encounter aliasing issues with hard synced sinusoids. Cheers! I’ll post one last update when I release Loom.

1 Like