mirror of https://github.com/f4exb/sdrangel.git
229 lines
11 KiB
C++
229 lines
11 KiB
C++
#include <math.h>
|
|
#include <complex>
|
|
#include "FreqSamplingCode.h"
|
|
#include "FIRFilterCode.h" // for the definition of TFIRPassTypes
|
|
#include "FFTCode.h"
|
|
#include "LowPassPrototypes.h"
|
|
|
|
namespace kitiirfir
|
|
{
|
|
|
|
/*
|
|
By Daniel Klostermann
|
|
Iowa Hills Software, LLC IowaHills.com
|
|
If you find a problem, please leave a note at:
|
|
http://www.iowahills.com/feedbackcomments.html
|
|
May 1, 2016
|
|
|
|
This code generates an FIR filter with the frequency over-sampling method. By this we
|
|
mean that we always sample the frequency domain at least 1024 times (a large power of
|
|
2 suitable for an FFT) even though we typically want a relatively small number of FIR
|
|
coefficients (< 50).
|
|
|
|
Most authors use N frequency samples to generate N taps. While valid, this tends to loose a
|
|
significant amount of frequency domain information if the tap count is small.
|
|
|
|
Using a large number of samples will generate a filter equivalent to those generated with
|
|
the equations for a classic Rectangular Windowed FIR filter. Those equations were derived
|
|
by using an infinite number of frequency samples, which is to say, performing an integral.
|
|
|
|
To see how well this works, use this code to sample a simple rectanglular low pass response
|
|
and compare the results to the coefficients generated by Windowed FIR code used the
|
|
BasicFIR() function in FIRFilterCode.cpp
|
|
|
|
See the example code in FilterKitMain for an example of using SampledFreqFIR().
|
|
This function is called after the HofSReal array is filled with the desired magnitude response.
|
|
The only trick to this method is getting the phase set correctly. There are two aspects to
|
|
setting the phase. Using the correct slope, which is the same for all filters, but does
|
|
depend on even or odd tap counts. And we must ensure that the phase value is correct at
|
|
Omega = 0.0 and Pi.
|
|
|
|
If the filter has a low pass response (magnitude != 0.0 at DC), then the phase at Omega=0 must
|
|
be zero. If the filter has a high pass response (magnitude != 0.0 at Pi), then the phase
|
|
must be zero of Omega = Pi.
|
|
|
|
A band pass filter, which has neither a low or high pass response, can have any phase value at
|
|
Omega = 0 and Pi. But a Notch filter, which has both a low and high pass response, must have
|
|
zero phase at both zero and Pi. The code below should make this more clear.
|
|
|
|
NumTaps Number of FIR taps
|
|
FirCoeff The output array for the FIR coefficients.
|
|
HofSReal The array containing the desired magnitude response.
|
|
HofSImag The imag part of the response (zero when this function is called).
|
|
OmegaC The center frequency. (Only needed for notch filters.)
|
|
PassType firLPF, firHPF, firBPF, firNOTCH Needed to set the phase properly. (defined in FIRFilterCode.h)
|
|
*/
|
|
|
|
void SampledFreqFIR(int NumTaps, double *FirCoeff, double *HofSReal,
|
|
double *HofSImag, double OmegaC, TFIRPassTypes PassType) {
|
|
int j, CenterJ, NumSamples, StartJ;
|
|
double dNumSamples, RadPerSample, Arg;
|
|
NumSamples = NUM_POS_FREQ_SAMPLES;
|
|
dNumSamples = (double) NumSamples;
|
|
|
|
// Limit test NumTaps
|
|
if (NumTaps > MAX_NUMTAPS)
|
|
NumTaps = MAX_NUMTAPS;
|
|
if (NumTaps > 2 * NUM_POS_FREQ_SAMPLES)
|
|
NumTaps = 2 * NUM_POS_FREQ_SAMPLES;
|
|
|
|
// Set the slope of the phase.
|
|
RadPerSample = -M_PI_2 * (2.0 * dNumSamples - 1.0) / dNumSamples; // Even tap count.
|
|
if (NumTaps % 2 == 1)
|
|
RadPerSample = -M_PI; // Odd tap count.
|
|
|
|
// Set the phase according to the type of response.
|
|
switch (PassType) {
|
|
case firLPF: // Low pass and band pass phase = 0 at DC
|
|
case firBPF:
|
|
for (j = 0; j < NumSamples; j++) {
|
|
Arg = RadPerSample * (double) j; // For band pass filters ONLY, an arbitrary amount of phase can be added to Arg. e.g. Add Pi/2 to generate a Hilbert filter, or +/- Pi/4 to 2 different filters to generate a pair of 45 degree Hilberts.
|
|
HofSImag[j] = HofSReal[j] * sin(Arg);
|
|
HofSReal[j] = HofSReal[j] * cos(Arg);
|
|
}
|
|
break;
|
|
|
|
case firHPF: // High pass phase = 0 at Pi
|
|
for (j = NumSamples; j >= 0; j--) {
|
|
Arg = RadPerSample * (double) (j - NumSamples);
|
|
HofSImag[j] = HofSReal[j] * sin(Arg);
|
|
HofSReal[j] = HofSReal[j] * cos(Arg);
|
|
}
|
|
break;
|
|
|
|
case firNOTCH: // Notch phase = 0 at DC and Pi
|
|
CenterJ = (int) (OmegaC * dNumSamples);
|
|
for (j = 0; j <= CenterJ; j++) {
|
|
Arg = RadPerSample * (double) j;
|
|
HofSImag[j] = HofSReal[j] * sin(Arg);
|
|
HofSReal[j] = HofSReal[j] * cos(Arg);
|
|
}
|
|
for (j = NumSamples; j >= CenterJ; j--) {
|
|
Arg = RadPerSample * (double) (j - NumSamples);
|
|
HofSImag[j] = HofSReal[j] * sin(Arg);
|
|
HofSReal[j] = HofSReal[j] * cos(Arg);
|
|
}
|
|
break;
|
|
case firALLPASS:
|
|
case firNOT_FIR:
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// Fill the negative frequency bins of HofS with the conjugate of HofS for the FFT.
|
|
for (j = 1; j < NumSamples; j++)
|
|
HofSReal[2 * NumSamples - j] = HofSReal[j];
|
|
for (j = 1; j < NumSamples; j++)
|
|
HofSImag[2 * NumSamples - j] = -HofSImag[j];
|
|
|
|
// The Fourier Transform requires the center freq bins to be 0 for LPF and BPF, 1 for HPF and Notch
|
|
if (PassType == firLPF || PassType == firBPF) {
|
|
HofSReal[NumSamples] = 0.0;
|
|
HofSImag[NumSamples] = 0.0;
|
|
} else {
|
|
HofSReal[NumSamples] = 1.0;
|
|
HofSImag[NumSamples] = 0.0;
|
|
}
|
|
|
|
// Do an inverse FFT on HofS to generate the impulse response. On return, HofSImag will be zero.
|
|
FFT(HofSReal, HofSImag, 2 * NumSamples, INVERSE);
|
|
|
|
// We just generated an impulse response that is 2*NumSamples long. Since we used linear phase
|
|
// the response will be symmetric about the center. In general, we only need a small number
|
|
// of these taps, so we use the taps from the center of HofSReal, starting at StartJ.
|
|
// We also need to scale the FFT's output by the size of the FFT.
|
|
StartJ = NumSamples - NumTaps / 2;
|
|
for (j = 0; j < NumTaps; j++)
|
|
FirCoeff[j] = HofSReal[StartJ + j] / (2.0 * dNumSamples);
|
|
|
|
}
|
|
//---------------------------------------------------------------------------
|
|
|
|
// This function shows how to sample an analog transfer function H(s) to generate an FIR filter.
|
|
// We didn't put much effort into this example because, in general, an analog prototype generates
|
|
// a rather poor FIR filter in the sense that it requires such a large number of taps to realize
|
|
// the response. For a discussion on this, see this article:
|
|
// http://iowahills.com/B2PolynomialFIRFilters.html
|
|
// In this example, we generate an FIR low pass from an Inverse Chebyshev low pass prototype.
|
|
// We sample the low pass prototype H(s) at frequencies determined by the bilinear transform.
|
|
void SampledFreqAnalog(int NumTaps, double *FirCoeff, double *HofSReal,
|
|
double *HofSImag, double OmegaC) {
|
|
int j, k, NumSamples;
|
|
double dNumSamples, Omega, Omega0;
|
|
std::complex<double> s, H;
|
|
TFIRPassTypes PassType = firLPF;
|
|
NumSamples = NUM_POS_FREQ_SAMPLES;
|
|
dNumSamples = (double) NumSamples;
|
|
|
|
// Limit test NumTaps
|
|
if (NumTaps > MAX_NUMTAPS)
|
|
NumTaps = MAX_NUMTAPS;
|
|
if (NumTaps > 2 * NUM_POS_FREQ_SAMPLES)
|
|
NumTaps = 2 * NUM_POS_FREQ_SAMPLES;
|
|
|
|
// Define the low pass filter prototype
|
|
TLowPassParams LPFProto; // defined in LowPassPrototypes.h
|
|
TSPlaneCoeff SCoeff; // defined in LowPassPrototypes.h
|
|
LPFProto.ProtoType = INVERSE_CHEBY; // BUTTERWORTH, CHEBYSHEV, GAUSSIAN, BESSEL, ADJUSTABLE, INVERSE_CHEBY, PAPOULIS, ELLIPTIC (defined in LowPassPrototypes.h)
|
|
LPFProto.NumPoles = 8; // 1 <= NumPoles <= 12, 15, 20 Depending on the filter.
|
|
LPFProto.Ripple = 0.25; // 0.0 <= Ripple <= 1.0 dB Chebyshev and Elliptic (less for high order Chebyshev).
|
|
LPFProto.StopBanddB = 60.0; // 20 <= StopBand <= 120 dB Inv Cheby and Elliptic
|
|
LPFProto.Gamma = 0.0; // -1.0 <= Gamma <= 1.0 Adjustable Gauss Controls the transition BW.
|
|
|
|
// Get the prototype filter's 2nd order s plane coefficients.
|
|
SCoeff = CalcLowPassProtoCoeff(LPFProto);
|
|
|
|
// Evaluate the prototype's H(s)
|
|
Omega0 = 1.0 / tan(OmegaC * M_PI_2); // This sets the corner frequency.
|
|
for (j = 0; j < NumSamples; j++) {
|
|
Omega = Omega0 * tan(M_PI_2 * (double) j / dNumSamples); // Frequencies per the bilinear transform.
|
|
s = std::complex<double>(0.0, Omega);
|
|
H = std::complex<double>(1.0, 0.0);
|
|
for (k = 0; k < SCoeff.NumSections; k++) {
|
|
H *= SCoeff.N2[k] * s * s + SCoeff.N1[k] * s + SCoeff.N0[k]; // The numerator
|
|
H /= SCoeff.D2[k] * s * s + SCoeff.D1[k] * s + SCoeff.D0[k]; // The denominator
|
|
H *= SCoeff.D0[k] / SCoeff.N0[k]; // The gain constants.
|
|
}
|
|
HofSReal[j] = H.real(); // We need to do this for the FFT, which uses real arrays.
|
|
HofSImag[j] = H.imag();
|
|
}
|
|
|
|
// Fill the negative frequency bins of HofS with the conjugate of HofS for the FFT.
|
|
for (j = 1; j < NumSamples; j++)
|
|
HofSReal[2 * NumSamples - j] = HofSReal[j];
|
|
for (j = 1; j < NumSamples; j++)
|
|
HofSImag[2 * NumSamples - j] = -HofSImag[j];
|
|
|
|
// The Fourier Transform requires the center freq bins to be 0 for LPF and BPF, 1 for HPF and Notch
|
|
if (PassType == firLPF || PassType == firBPF) {
|
|
HofSReal[NumSamples] = 0.0;
|
|
HofSImag[NumSamples] = 0.0;
|
|
} else {
|
|
HofSReal[NumSamples] = 1.0;
|
|
HofSImag[NumSamples] = 0.0;
|
|
}
|
|
|
|
// Do an inverse FFT on HofS to generate the impulse response. On return, HofSImag will be zero.
|
|
FFT(HofSReal, HofSImag, 2 * NumSamples, INVERSE);
|
|
|
|
// We just generated an impulse response that is 2*NumSamples long. We can use as many of these
|
|
// as we like, but if you look at HofSReal you will see that the tail of the response goes to
|
|
// zero rather quickly. The desired part of impulse response is at the beginning of HofSReal
|
|
// instead of the center because H(s) didn't have linear phase (more like minimum phase).
|
|
|
|
// Take the taps from the start of HofSReal and scale by the size of the FFT.
|
|
for (j = 0; j < NumTaps; j++)
|
|
FirCoeff[j] = HofSReal[j] / (2.0 * dNumSamples);
|
|
|
|
// Most FIR responses benefit from the application of a window to reduce the effects of
|
|
// truncating the impulse response. But a typical window, such as the Kaiser, can't be used
|
|
// because this impulse response isn't symmetric about the center tap.
|
|
// A Half Cosine window works quite nicely with these types of responses.
|
|
for (j = 0; j < NumTaps; j++)
|
|
FirCoeff[j] = FirCoeff[j] * cos(M_PI_2 * j / (double) NumTaps);
|
|
}
|
|
|
|
//---------------------------------------------------------------------------
|
|
|
|
} // namespace
|