Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FIR Bandpass Resampling becomes unstable after a long duration #59

Open
ADH-LukeBollam opened this issue Mar 30, 2022 · 9 comments
Open

Comments

@ADH-LukeBollam
Copy link

ADH-LukeBollam commented Mar 30, 2022

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
Copy link
Author

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
Copy link
Author

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
Copy link
Owner

ar1st0crat commented Apr 6, 2022

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
Copy link
Author

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

if (g < 1 && filter is null)

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
Copy link
Owner

ar1st0crat commented Apr 7, 2022

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
Copy link
Author

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
Copy link
Author

ADH-LukeBollam commented Aug 31, 2022

@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.

if (g < 1 && filter is null)
{
filter = new FirFilter(DesignFilter.FirWinLp(MinResamplingFilterOrder, g / 2));
input = filter.ApplyTo(signal).Samples;
}

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

@ar1st0crat
Copy link
Owner

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
Copy link
Author

ADH-LukeBollam commented Sep 12, 2023

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

var x = n * step;

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants