minBLEPS once and for all

DSP, Plugin and Host development discussion.
RELATED
PRODUCTS

Post

mystran wrote:
d_m_chambers_ wrote:
mystran wrote:
kryptonaut wrote: @mystran - what's wrong with the calculation of newPhase? It's the phaseincrement for a whole step multiplied by the post-crossing fraction of a step, which should be ok shouldn't it?
Does "exactCrossTime" go backwards from the end of the sample? If that's the case then I suppose you can do it like that as well.

Code: Select all

                
                //Calculate the exact time when mSyncPhase would have reached 1.0 (this is the value to use for exactCrossTime):
                double exactCrossTime = mSyncPhase/mSyncPhaseIncrement;

Code: Select all

            
            //Normal minBLEP:
            while(mPhase >= 1.0)
            {
                mPhase -= 1.0;
                double exactCrossTime = 1.0-((mPhaseIncrement-mPhase)/mPhaseIncrement);
edit: oh nvm, seems these are consistent but.. why use different math for the two cases?
Good point, yes, I'll use one or the other.

If you have the time and are interested I wonder if you might listen to the audio samples as I'm sure there is still loads of aliasing. Surely minBLEP should be able to do better that that? If so, any ideas why the existing code is not quite right? I guess I still need to worry about the things you mentioned earlier:
mystran wrote:(for higher order BLEPs, repeat the same with any non-zero derivatives and scale results by slaveFreq^N for the Nth derivative; this assumes your BLEP tables are sensibly scaled.. but it's hard to get the trivial step wrong at least)

......

There is an additional corner case where a slave has a transition (either from one segment to another, or normal reset) earlier during the same sample, so after (1) you should then go and check for any events (transitions or resets) for the slave with the crossing time as the upper limit of what to process and only once the next event is past this time can you continue with (2..4) .. but it just takes some restructuring of code really so I'd just get the normal cases working fine first.
Thanks again

Post

d_m_chambers_ wrote:

Code: Select all

//Master resets Slave minBLEP:
            if(mSyncPhase > 1.0)
            {
                
                //When mSyncPhase exceeds 1.0, subtract 1.0 from it:
                mSyncPhase -= 1.f;
                
                //Calculate the exact time when mSyncPhase would have reached 1.0 (this is the value to use for exactCrossTime):
                double exactCrossTime = mSyncPhase/mSyncPhaseIncrement;
                
                // calculate new value for Phase
                double newPhase=exactCrossTime * mPhaseIncrement;
                // calculate step in Phase
                double scale=mPhase-newPhase;
                // add in the minBLEP
                populateCircularBufferForSawtoothWave(exactCrossTime, scale);
                // update Phase
                mPhase=newPhase;

            }
So the problem is either here, or in the "scale" factor in populateCircularBuffer...

You can check which one is the case by making your scale 0.5 in the normal reset and then scaling your naive waveform by .5 before you add it to the buffer, which clearly should produce the same results just 6dB lower.. if it starts aliasing then populate is to blame, otherwise the above is to blame.

Post

d_m_chambers_ wrote:Surely minBLEP should be able to do better that that? If so, any ideas why the existing code is not quite right? I guess I still need to worry about the things you mentioned earlier:
mystran wrote:(for higher order BLEPs, repeat the same with any non-zero derivatives and scale results by slaveFreq^N for the Nth derivative; this assumes your BLEP tables are sensibly scaled.. but it's hard to get the trivial step wrong at least)
I'm just being a little pedantic here, the first wave-form where you need to start worrying about derivatives is the triangle wave (the slope changes between segments), for pulses and sawtooth you only need the basic step.
There is an additional corner case where a slave has a transition (either from one segment to another, or normal reset) earlier during the same sample, so after (1) you should then go and check for any events (transitions or resets) for the slave with the crossing time as the upper limit of what to process and only once the next event is past this time can you continue with (2..4) .. but it just takes some restructuring of code really so I'd just get the normal cases working fine first.
You do need to worry about this.. but make it work in the usual cases first, then it's easier to refactor it to handle the corner cases later. Your current code seems like it should work, there's some bug hiding somewhere and your priority should be finding that first. The above corner case only applies when the slave frequency is very slightly above the master frequency (or very slightly above one if it's harmonics).. it is mostly an issue because it causes some noise when sweeping the slave frequency, not something that causes excessive aliasing.

Post

mystran wrote:
You can check which one is the case by making your scale 0.5 in the normal reset and then scaling your naive waveform by .5 before you add it to the buffer, which clearly should produce the same results just 6dB lower.. if it starts aliasing then populate is to blame, otherwise the above is to blame.
That's a great idea.
mystran wrote: You do need to worry about this.. but make it work in the usual cases first, then it's easier to refactor it to handle the corner cases later. Your current code seems like it should work, there's some bug hiding somewhere and your priority should be finding that first. The above corner case only applies when the slave frequency is very slightly above the master frequency (or very slightly above one if it's harmonics).. it is mostly an issue because it causes some noise when sweeping the slave frequency, not something that causes excessive aliasing.
Yeah, I think I can hear that issue in various VSTs actually and it would be nice to get rid of it. Thanks very much for the tips and have a good weekend.

Post

mystran wrote: You can check which one is the case by making your scale 0.5 in the normal reset and then scaling your naive waveform by .5 before you add it to the buffer, which clearly should produce the same results just 6dB lower.. if it starts aliasing then populate is to blame, otherwise the above is to blame.
Sorry mystran, it's getting late here in GMT+3 time zone. I just wanted to confirm I understand you correctly. I just tried the following changes one at a time expecting either change to get the same clean non-HS sawtooth but 6dB lower:

Code: Select all

            //Normal minBLEP:
            while(mPhase >= 1.0)
            {
                mPhase -= 1.0;
                double exactCrossTime = 1.0-((mPhaseIncrement-mPhase)/mPhaseIncrement);
                populateCircularBufferForSawtoothWave(exactCrossTime, 0.5f); //UPDATED from 1.f to 0.5f
            }
            
Then in populate:

Code: Select all

inline void ZeusOsc::populateCircularBufferForSawtoothWave(float offset, float scale)
{
    const float *minBlep;
    MinBlepData *blepData= new MinBlepData();
    minBlep= blepData->getBlep();
    
    for(int i = 0; i < (cirBuffSize-1); i++)
    {
        double tempIndex = (offset*overS)+(i*overS);
        double tempFraction = tempIndex-floor(tempIndex);
        circularBuffer[(index+i)%cirBuffSize] += 0.5*(scale-LERP(tempFraction, minBlep[(int)floor(tempIndex)], minBlep[(int)ceil(tempIndex)]) * scale);  //UPDATED to 0.5* .......
    }
    
    delete blepData;
}
Best viewed by the Sourcetree screenshot:

Image

BUT, both of these changes gave aliasing and or weird spectrum so this implies by your logic that populate is not working for scale values not equal to 1.f? If so, then that massively cuts down the work I need to do to track the problem (hopefully). Meaning I need to fix populate right? :wink:

OR, was I supposed to make the two changes at once? - I'm pretty sure the answer is no here but wanted to check.

Post

d_m_chambers_ wrote: OR, was I supposed to make the two changes at once? - I'm pretty sure the answer is no here but wanted to check.
You were only supposed to pass 0.5 to the scale argument, but the other change you need to do (to keep it consistent) is scale the naive wave-form down by a factor of .5 (to match the BLEP, otherwise your BLEP only cancels half the step) where the line in question is the one that reads:

Code: Select all

circularBuffer[index] += (mPhase-mPhaseIncrement);
which should become

Code: Select all

circularBuffer[index] += .5*(mPhase-mPhaseIncrement);
and speaking of that line.. is there a good reason you are subtracting the phase increment here?

Either way, the scaling math in your populateCircularBufferForSawtoothWave(..) looks like it should be fine. It's just really hard to debug code that you can't run, the problem is probably some small detail somewhere, as this type of algorithm has to align everything exactly for the aliasing to cancel out properly. From experience, I want to share that the first 3 or so generations of my BLEP code back in the days were far from perfect, my aliasing levels kept going down with every new generation I wrote even without any changes to the actual BLEP data (and then even more by improving the quality of the BLEPs; it's been several times that I've thought "now I have it perfect" and then I find some random inaccuracy or oversight and fix it and whoops, we get another 30dB of SNR for free)!

So don't despair.. try writing the same thing in slight different way and see if something happens. As a tip, also always compare with a version where populateCircularBufferForSawtoothWave is commented out, you might occasionally find that you get less aliasing this way which can be very nice for finding out where to search for the problem.


Performance wise you should move the floor() and ceil() calls out of the inner loop (they are constants for the loop, although the compiler will probably figure that out for you, just that they are expensive if the CSE-optimisation fails for whatever reason) .. but really do something about the new/delete, those are potentially unbounded in terms of how long they can take (like, seriously, a single "new" or "delete" anywhere in audio code can result in drop-outs with low-latency buffers, in real-life situations, it's no joke).

Ps. I'm in Finland (Helsinki) so I think I'm the same time-zone as you. :)

Post

Thanks for the explanation. I tried your approach and still get a perfect alias free signal so that means the problem code is where the Master resets the Slave - nice to narrow it down. :D

I'll keep trying....

Post

In populateCircularBufferForSawtoothWave

Code: Select all

for(int i = 0; i < (cirBuffSize-1); i++)
should probably be:

Code: Select all

for(int i = 0; i < cirBuffSize; i++)
unless your BLEP data is overS samples shorter than your circular buffer.. but then again truncating by one sample only results in a slight worse filter, not huge amounts of aliasing..

Post Reply

Return to “DSP and Plugin Development”