215 lines
9.5 KiB
Plaintext
215 lines
9.5 KiB
Plaintext
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace Exocortex.DSP
|
|
{
|
|
public enum PassFilterType
|
|
{
|
|
Bessel,
|
|
Butterworth,
|
|
Chebyshev,
|
|
CriticalDamping
|
|
}
|
|
public static class PassFilter
|
|
{
|
|
public static double[] HighPass(double[] values, double sampleRate, double centerFrequency, PassFilterType type, uint order)
|
|
{
|
|
return RunFilter(values, sampleRate, centerFrequency, type, order, false);
|
|
}
|
|
public static double[] LowPass(double[] values, double sampleRate, double centerFrequency, PassFilterType type, uint order)
|
|
{
|
|
return RunFilter(values, sampleRate, centerFrequency, type, order, true);
|
|
}
|
|
private static double[] RunFilter(double[] values, double sampleRate, double centerFrequency, PassFilterType type, uint order, bool lowPass)
|
|
{
|
|
Complex[] data = new Complex[values.Length];
|
|
for (int i = 0; i < values.Length; i++)
|
|
{
|
|
data[i] = (Complex)values[i];
|
|
}
|
|
Complex[] result;
|
|
switch (type)
|
|
{
|
|
case PassFilterType.Bessel:
|
|
result = BesselFilter(data, sampleRate, centerFrequency, order, lowPass);
|
|
break;
|
|
case PassFilterType.Chebyshev:
|
|
result = ChebyshevFilter(data, sampleRate, centerFrequency, order, 1D, 0.005, lowPass);
|
|
break;
|
|
case PassFilterType.Butterworth:
|
|
default:
|
|
result = ButterworthFilter(data, sampleRate, centerFrequency, order, 1D, lowPass);
|
|
break;
|
|
}
|
|
return result.Select(c => c.Re).ToArray();
|
|
}
|
|
private static Complex[] BesselFilter(Complex[] values, double sampleRate, double centerFrequency, uint order, bool lowPass)
|
|
{
|
|
var signal = (Complex[])values.Clone();
|
|
var N = signal.Length;
|
|
|
|
// Get the reverse Bessel polynomial-to-gain function denominator
|
|
Polynomial B = BesselDenominatorPolynomial((int)order);
|
|
var numerator = Math.Sqrt(B.GetCoefficient(0));
|
|
// Apply forward FFT
|
|
Fourier.FFT(signal, N, FourierDirection.Forward);
|
|
// Apply gain function
|
|
if (centerFrequency > 0)
|
|
{
|
|
var numBins = N / 2; // Half the length of the FFT by symmetry
|
|
var binWidth = sampleRate / N; // Hz
|
|
|
|
// Filter
|
|
System.Threading.Tasks.Parallel.For(1, N / 2, i =>
|
|
{
|
|
var binFreq = binWidth * i;
|
|
var gain = BesselGain(numerator, B, binFreq, centerFrequency, lowPass);
|
|
signal[i] *= gain;
|
|
signal[N - i] *= gain;
|
|
});
|
|
|
|
}
|
|
// Reverse filtered signal
|
|
Fourier.FFT(signal, N, FourierDirection.Backward);
|
|
signal = signal.Select(n => n / N).ToArray(); // scale result by length
|
|
|
|
return signal;
|
|
}
|
|
private static double BesselGain(double numerator, Polynomial B, double freq, double centerFreq, bool lowPass)
|
|
{
|
|
return numerator / Math.Sqrt(B.Evaluate(lowPass ? freq / centerFreq : centerFreq / freq)); // low-to-high pass is inverting f/fc
|
|
}
|
|
|
|
// uses logic from https://www.centerspace.net/butterworth-filter-csharp
|
|
private static Complex[] ButterworthFilter(Complex[] values, double sampleRate, double centerFrequency, uint order, double DCGain, bool lowPass)
|
|
{
|
|
var signal = (Complex[])values.Clone();
|
|
var N = signal.Length;
|
|
|
|
// Apply forward FFT
|
|
Fourier.FFT(signal, N, FourierDirection.Forward);
|
|
// Apply gain function
|
|
if (centerFrequency > 0)
|
|
{
|
|
var numBins = N / 2; // Half the length of the FFT by symmetry
|
|
var binWidth = sampleRate / N; // Hz
|
|
|
|
// Filter
|
|
System.Threading.Tasks.Parallel.For(1, N / 2, i =>
|
|
{
|
|
var binFreq = binWidth * i;
|
|
var gain = ButterworthGain(DCGain, binFreq, centerFrequency, order, lowPass);
|
|
signal[i] *= gain;
|
|
signal[N - i] *= gain;
|
|
});
|
|
|
|
}
|
|
// Reverse filtered signal
|
|
Fourier.FFT(signal, N, FourierDirection.Backward);
|
|
// scale result by length
|
|
signal = signal.Select(n => n / N).ToArray();
|
|
return signal;
|
|
}
|
|
private static double ButterworthGain(double DCGain, double freq, double centerFreq, uint order, bool lowPass)
|
|
{
|
|
return DCGain / Math.Sqrt(1 + Math.Pow(lowPass ? freq / centerFreq : centerFreq / freq, 2.0 * order)); // low-to-high pass is just inverting f/fc
|
|
}
|
|
|
|
// uses logic from https://www.centerspace.net/chebyshev-filter-csharp
|
|
private static Complex[] ChebyshevFilter(Complex[] values, double sampleRate, double centerFrequency, uint order, double DCGain, double ripple, bool lowPass)
|
|
{
|
|
var signal = (Complex[])values.Clone();
|
|
var N = signal.Length;
|
|
|
|
// Get the Chebyshev polynomial
|
|
Polynomial T = ChebyshevPolynomial((int)order);
|
|
// Apply forward FFT
|
|
Fourier.FFT(signal, N, FourierDirection.Forward);
|
|
// Remove DC offset
|
|
signal[0] = (Complex)0;
|
|
|
|
// Apply gain function
|
|
if (centerFrequency > 0)
|
|
{
|
|
var numBins = N / 2; // Half the length of the FFT by symmetry
|
|
var binWidth = sampleRate / N; // Hz
|
|
|
|
// Filter
|
|
System.Threading.Tasks.Parallel.For(1, N / 2, i =>
|
|
{
|
|
var binFreq = binWidth * i;
|
|
var gain = ChebyshevGain(DCGain, ripple, T, binFreq, centerFrequency, order, lowPass);
|
|
signal[i] *= gain;
|
|
signal[N - i] *= gain;
|
|
});
|
|
|
|
}
|
|
// Reverse filtered signal
|
|
Fourier.FFT(signal, N, FourierDirection.Backward);
|
|
// scale result by length
|
|
signal = signal.Select(n => n / N).ToArray();
|
|
|
|
return signal;
|
|
}
|
|
private static double ChebyshevGain(double DCGain, double ripple, Polynomial T, double freq, double centerFreq, uint order, bool lowPass)
|
|
{
|
|
return DCGain / Math.Sqrt(1 + ripple * Math.Pow(T.Evaluate(lowPass ? freq / centerFreq : centerFreq / freq), 2.0 * order));
|
|
}
|
|
|
|
private static Polynomial ChebyshevPolynomial(int order)
|
|
{
|
|
switch (order)
|
|
{
|
|
case 0:
|
|
return new SimplePolynomial(new double[] { 1 });
|
|
case 1:
|
|
return new SimplePolynomial(new double[] { 0, 1 });
|
|
case 2:
|
|
return new SimplePolynomial(new double[] { -1, 0, 2 });
|
|
case 3:
|
|
return new SimplePolynomial(new double[] { 0, -3, 0, 4 });
|
|
case 4:
|
|
return new SimplePolynomial(new double[] { 1, 0, -8, 0, 8 });
|
|
case 5:
|
|
return new SimplePolynomial(new double[] { 0, 5, 0, -20, 0, 16 });
|
|
case 6:
|
|
return new SimplePolynomial(new double[] { -1, 0, 1, 8, 0, -48, 0, 32 });
|
|
case 7:
|
|
return new SimplePolynomial(new double[] { 0, 7, 0, 56, 0, -112, 0, 64 });
|
|
case 8:
|
|
return new SimplePolynomial(new double[] { 1, 0, -32, 0, 160, 0, -256, 0, 128 });
|
|
default:
|
|
throw new NotImplementedException();
|
|
}
|
|
}
|
|
private static Polynomial BesselDenominatorPolynomial(int order)
|
|
{
|
|
// Polynomials in the square-rooted denominator of bessel gain functions
|
|
// Obtained via magnitude calc of H(iw) of each Bessel H(s): https://en.wikipedia.org/wiki/Bessel_filter#The_transfer_function
|
|
// at https://www.symbolab.com/solver/complex-numbers-magnitude-calculator/
|
|
switch (order)
|
|
{
|
|
case 2:
|
|
return new SimplePolynomial(new double[] { 9, 0, 3, 0, 1 });
|
|
case 3:
|
|
return new SimplePolynomial(new double[] { 225, 0, 45, 0, 6, 0, 1 });
|
|
case 4:
|
|
return new SimplePolynomial(new double[] { 11025, 0, 1575, 0, 135, 0, 10, 0, 1 });
|
|
case 5:
|
|
return new SimplePolynomial(new double[] { 893025, 0, 99225, 0, 6300, 0, 315, 0, 15, 0, 1 });
|
|
case 6:
|
|
return new SimplePolynomial(new double[] { 108056025, 0, 9823275, 0, 496125, 0, 18900, 0, 630, 0, 21, 0, 1 });
|
|
case 7:
|
|
return new SimplePolynomial(new double[] { 18261468225, 0, 1404728325, 0, 58939650, 0, 1819125, 0, 47250, 0, 1134, 0, 28, 0, 1 });
|
|
case 8:
|
|
return new SimplePolynomial(new double[] { 4108830350625, 0, 273922023375, 0, 9833098275, 0, 255405150, 0, 5457375, 0, 103950, 0, 1890, 0, 36, 0, 1 });
|
|
default:
|
|
throw new NotImplementedException();
|
|
}
|
|
}
|
|
}
|
|
}
|