ar1st0crat / NWaves

.NET DSP library with a lot of audio processing functions
MIT License
466 stars 71 forks source link

FIR Bandpass Resampling becomes unstable after a long duration #59

Open ADH-LukeBollam opened 2 years ago

ADH-LukeBollam commented 2 years ago

First of all, thanks for the great library.

I'm having an issue with resampling using a bandpass FIR filter, here is my Resample configuration:

public static float[] ResampleSignalBandpass(float[] data, int oldSr, int newSr)
{
    var lowCutoff = 0.5 / newSr;   // high pass to remove baseline wander
    var highCutoff = 0.5;   // cutoff at nyquist
    var filterOrder = 11;

    var resampler = new Resampler();
    var lpFilter = new FirFilter(DesignFilter.FirWinBp(filterOrder, lowCutoff, highCutoff)); 
    var resampled = resampler.Resample(new NWaves.Signals.DiscreteSignal(oldSr, data), newSr, lpFilter);
    return resampled.Samples;
}

The original sample rate is 128 Hz The new sample rate is 192 Hz

After about 36 hours, I see a sudden change in the resample output: image

Then at 72 hours, I see another abrupt degradation in signal quality image

Is this a bug in the code, or something I can expect from resampling for a long duration?

ADH-LukeBollam commented 2 years ago

Just a small update, it turns out the signal quality degrades constantly over time. Even before 36 hours, the signal is starting to look more 'square' compared to at the beginning. Is this an error accumulating and eventually overflowing perhaps?

ADH-LukeBollam commented 2 years ago

The fix for me at the moment is to reset the filter after short sections of data with lpFilter.Reset(); but it does cause some aliasing at each boundary. Do you know if you'll have a chance to take a look at this @ar1st0crat ?

ar1st0crat commented 2 years ago

Hi! Thank you for the kind words about the library :-) Unfortunately, at the moment I don't have much time for reviewing issues regarding NWaves. Anyway, from what I see already, you're using high-pass filter in resampling operation. The purpose of FIR filter in resampling methods is antialising, so it must be a low-pass filter (with the cut-off frequency 0.5 / resample_factor). If you need to remove the baseline wander, then you should apply your highpass filter separately. Currently, you're applying the high-pass filter. (In NWaves cut-off frequency 0.5 is equivalent to 1.0 in sciPy/MATLAB). So, esentially you filter is HP, although you're constructing it as BP.

Hence, there are two options:

// 1) solution similar to yours:

// the filter will be essentially LP (although, technically it's BP):
var lowCutoff = 0.5 / newSr;   // high pass to remove baseline wander
var highCutoff = 0.5 / 3;   // 3 because greatest_common_divisor(128, 192) = 3
var filterOrder = 11;

var resampler = new Resampler();
var lpFilter = new FirFilter(DesignFilter.FirWinBp(filterOrder, lowCutoff, highCutoff)); 
var resampled = resampler.Resample(new NWaves.Signals.DiscreteSignal(oldSr, data), newSr, lpFilter);
return resampled.Samples;

// 2) slighly-less efficient, but more readable

var cutoff = 0.5 / newSr;   // high pass to remove baseline wander
var filterOrder = 11;

// filter to remove baseline wander, first:
var hpFilter = new FirFilter(DesignFilter.FirWinHp(filterOrder, cutoff)); 
var filtered = hpFilter.ApplyTo(new NWaves.Signals.DiscreteSignal(oldSr, data));

var resampler = new Resampler();

// resampling antialiasing LP filter will be constructed automatically inside Resample() function:
var resampled = resampler.Resample(filtered, newSr);

return resampled.Samples;

PS. Btw, is filter order=11 OK? Perhaps, it's too small. You can try bigger values. Like 101 for example. Filtering will be automatically be carried out via block convolution, so it's going to be quite fast.

ADH-LukeBollam commented 2 years ago

I just noticed, if you pass in a filter it doesn't actually get used at all which is probably why I was getting such bad aliasing https://github.com/ar1st0crat/NWaves/blob/726d8dd613fb3cd21e9a4429c6e4ceee6eca2db4/NWaves/Operations/Resampler.cs#L115

The if's need to be split up into something like:

if (g < 1)
{
    if (filter == null)
    {
        filter = new FirFilter(DesignFilter.FirWinLp(MinResamplingFilterOrder, g / 2));
    }

    input = filter.ApplyTo(signal).Samples;
}

otherwise the low pass filter you pass in won't be used.

ar1st0crat commented 2 years ago

No, AFAIR, filtering is not needed for band-limited upsampling, so my code is correct. I've just noticed - you're calling Resample() method. This method implements the so called band-limited resampling. It's quite slow and intended to be used for "complicated" resamplings (like 44.1kHz to 16 kHz, for example). In your case (128 -> 192) you have upsampling factor 3 and downsampling factor 2, so I would recommend calling ResampleUpDown():


// 1)
var lowCutoff = 0.5 / newSr;   // high pass to remove baseline wander
var highCutoff = 0.5 / 3;   // 3 because greatest_common_divisor(128, 192) = 3

var filterOrder = 99;
var filter = new FirFilter(DesignFilter.FirWinBp(filterOrder, lowCutoff, highCutoff)); 

var resampler = new Resampler();
var resampled = resampler.ResampleUpDown(new NWaves.Signals.DiscreteSignal(oldSr, data), 3, 2, filter);
return resampled.Samples;

// 2) this is the most correct version (but it will create intermediate HP-filtered signal):

var cutoff = 0.5 / newSr;   // high pass to remove baseline wander
var filterOrder = 11;

// filter to remove baseline wander, first:
var hpFilter = new FirFilter(DesignFilter.FirWinHp(filterOrder, cutoff)); 
var filtered = hpFilter.ApplyTo(new NWaves.Signals.DiscreteSignal(oldSr, data));

var resampler = new Resampler();

// resampling antialiasing LP filter will be constructed automatically inside ResampleUpDown() function:
var resampled = resampler.ResampleUpDown(filtered, 3, 2);

return resampled.Samples;
ADH-LukeBollam commented 2 years ago

This is just one example, my input data sample rate varies and often the resample ratio is not a nice integer, like 200=>192, 250=>192 etc.

The code for Resample() is still not working correctly if downsampling though right? If you pass in a filter yourself, it won't be used even in the case of downsampling, because it checks if the filter is null to do the filtering step.

ADH-LukeBollam commented 2 years ago

@ar1st0crat Just a reminder that theres a bug in resampler where if you pass in a filter to the Resample method, it doesn't get used.

https://github.com/ar1st0crat/NWaves/blob/0728ca68ffac85450814203f4876c7c9be2df7cf/NWaves/Operations/Resampler.cs#L115-L121

There is no else condition to use a provided filter if it is not null.

ar1st0crat commented 2 years ago

Yes, you're right. I remember about this issue and I keep it open. I wanted to revise the entire resampling part of NWaves, but unfortunately at the moment I have no time for it.

ADH-LukeBollam commented 1 year ago

I've figured out the source of the noise, its actually caused by poor floating point precision at large values here:

https://github.com/ar1st0crat/NWaves/blob/c1a6a4a8fdb70ce7cfd6b31ad59595e8461ff2e5/NWaves/Operations/Resampler.cs#L126

It's especially bad in my case because signal values are small, so the lack of precision is relatively quite large. I fixed it by changing the resample code to

// eliminate any floating point quantization errors for very large sample counts
decimal step = 1 / (decimal)g;

for (var n = 0; n < output.Length; n++)
{
    var x = n * step;

    for (var i = -order; i < order; i++)
    {
        var j = (int)Math.Floor(x) - i;

        if (j < 0 || j >= input.Length)
        {
            continue;
        }

        var t = (double)(x - j); // at this point we are back to a small scale, safe to return to double type