mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 16:47:31 -05:00
[WIP] The Two-Channel synthesizer is now fully-functional and the
channelizer sub-system is functionally complete. Updated the channelizer and synthesizer filter design methods to use a Kaiser windowed Nyquist filter. Updated the synthesizer filter design to use a new cutoff frequency of 105% of the channel bandwidth to resolve a slight dip in the synthesized channel gain at the channel union region. Updated the 2 channel output processor to use a low-pass filter that is designed for the runtime channel sample rate versus hard-coded 12.5 khz. Testing and redesign continues ...
This commit is contained in:
@@ -31,6 +31,7 @@ import org.jtransforms.fft.FloatFFT_1D;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
@@ -41,6 +42,7 @@ public class FilterFactory
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(FilterFactory.class);
|
||||
private static final double PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE = -6.020599842071533; //decibel(0.5, 0.0)
|
||||
private static final double MARGIN_OF_ERROR = 0.0003;
|
||||
|
||||
/**
|
||||
* Generates coefficients for a unity-gain, windowed low-pass filter
|
||||
@@ -462,7 +464,7 @@ public class FilterFactory
|
||||
{
|
||||
Set<Integer> factors = new TreeSet<Integer>();
|
||||
|
||||
/* Brute force */
|
||||
/* Brute force */
|
||||
for(int x = 1; x <= value; x++)
|
||||
{
|
||||
int remainder = (int)(value / x);
|
||||
@@ -526,7 +528,7 @@ public class FilterFactory
|
||||
int taps = getTapCount(outputSampleRate, passFrequency, passFrequency + 1500,
|
||||
attenuation);
|
||||
|
||||
/* Make tap count odd */
|
||||
/* Make tap count odd */
|
||||
if(taps % 2 == 0)
|
||||
{
|
||||
taps++;
|
||||
@@ -594,7 +596,7 @@ public class FilterFactory
|
||||
* Ported to java from gnuradio/filter/firdes.cc
|
||||
*
|
||||
* For 40db attenuation, calculate the number of symbols based on the following formula:
|
||||
* Symbols = -44 * alpha + 33
|
||||
* Symbols = -44 * alpha + 33
|
||||
*
|
||||
* Polyphase Channelizer notes:
|
||||
* -Set samples Per Symbol at 2 (or more) * number of channels
|
||||
@@ -716,6 +718,7 @@ public class FilterFactory
|
||||
|
||||
/**
|
||||
* Calculates the decibel magnitude of the real and imaginary complex sample
|
||||
*
|
||||
* @param real (x-axis) value
|
||||
* @param imag (y-axis) value
|
||||
* @return magnitude in decibels
|
||||
@@ -753,26 +756,6 @@ public class FilterFactory
|
||||
return taps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Polyphase M2 channelizer sync filter. Designed for an M*channel input and an M2 channel output.
|
||||
* @param channelBandwidth per channel
|
||||
* @param channels count
|
||||
* @param tapsPerChannel minumum. This may be increased to meet the band edge -6.02dB requirement
|
||||
* @param windowType to use
|
||||
* @param logResults to log the results of the design
|
||||
* @return filter
|
||||
* @throws FilterDesignException if the filter cannot be designed with a band edge of -6.02dB
|
||||
*/
|
||||
public static float[] getSincM2Channelizer(double channelBandwidth, int channels, int tapsPerChannel,
|
||||
Window.WindowType windowType, boolean logResults) throws FilterDesignException
|
||||
{
|
||||
float[] filter = getSincFilter(channelBandwidth * channels, channelBandwidth,
|
||||
channels, tapsPerChannel, windowType,logResults);
|
||||
|
||||
//Increase the overall gain of the filter to an objective of 1.0 times the number of channels
|
||||
return normalize(filter, 1.0f * (float)channels);
|
||||
}
|
||||
|
||||
/**
|
||||
* Polyphase M2 synthesizer sync filter. Designed for multiple M2 oversampled channel inputs and
|
||||
* an M*channel count output.
|
||||
@@ -780,19 +763,23 @@ public class FilterFactory
|
||||
* @param channelBandwidth per channel
|
||||
* @param channels count
|
||||
* @param tapsPerChannel minumum. This may be increased to meet the band edge -6.02dB requirement
|
||||
* @param windowType to use
|
||||
* @param logResults to log the results of the design
|
||||
* @return filter
|
||||
* @throws FilterDesignException if the filter cannot be designed with a band edge of -6.02dB
|
||||
* @throws FilterDesignException
|
||||
*/
|
||||
public static float[] getSincM2Synthesizer(double channelBandwidth, int channels, int tapsPerChannel,
|
||||
Window.WindowType windowType, boolean logResults) throws FilterDesignException
|
||||
public static float[] getSincM2Synthesizer(double channelBandwidth, int channels, int tapsPerChannel)
|
||||
throws FilterDesignException
|
||||
{
|
||||
float[] filter = getSincFilter(channelBandwidth * channels, channelBandwidth, channels,
|
||||
tapsPerChannel, windowType,logResults);
|
||||
int filterLength = (channels * tapsPerChannel) - 1;
|
||||
|
||||
//Decrease the overall gain of the filter to an objective of 1.0 divided by the number of channels
|
||||
return normalize(filter, 1.0f);
|
||||
//Design the prototype synthesizer with 105% of the channel bandwidth produced by the channelizer.
|
||||
float[] taps = FilterFactory.getKaiserSinc(filterLength, channelBandwidth * 1.05, 80.0);
|
||||
|
||||
//This is an odd length filter - increase the length by 1 by pre-padding a zero coefficient
|
||||
float[] extendedTaps = new float[taps.length + 1];
|
||||
System.arraycopy(taps, 0, extendedTaps, 1, taps.length);
|
||||
|
||||
//Adjust the overall gain of the filter to an objective of 1.0 divided by the number of channels
|
||||
return normalize(extendedTaps, 1.0f);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -820,18 +807,32 @@ public class FilterFactory
|
||||
*
|
||||
* @throws FilterDesignException if a filter cannot be designed with a band edge attenuation of -6.02 dB
|
||||
*/
|
||||
public static float[] getSincFilter(double sampleRate, double channelBandwidth, int channels,
|
||||
int tapsPerChannel, Window.WindowType windowType, boolean logResults) throws FilterDesignException
|
||||
|
||||
/**
|
||||
* Polyphase M2 channelizer sync filter. Designed for an M*channel input and an M2 channel output.
|
||||
*
|
||||
* @param channelBandwidth per channel
|
||||
* @param channels count
|
||||
* @param tapsPerChannel minimum. This may be increased to meet the band edge -6.02dB requirement
|
||||
* @param logResults to log the results of the design
|
||||
* @return filter
|
||||
* @throws FilterDesignException if the filter cannot be designed with a band edge of -6.02dB
|
||||
*/
|
||||
public static float[] getSincM2Channelizer(double channelBandwidth, int channels, int tapsPerChannel,
|
||||
boolean logResults) throws FilterDesignException
|
||||
{
|
||||
int currentTapsPerChannel = tapsPerChannel;
|
||||
int filterLength = (channels * currentTapsPerChannel) - 1;
|
||||
|
||||
double cutoffFrequency = channelBandwidth / sampleRate;
|
||||
double bandEdge = cutoffFrequency;
|
||||
double sampleRate = channelBandwidth * channels;
|
||||
double bandEdge = channelBandwidth / sampleRate;
|
||||
double cutoffFrequency = bandEdge / 2.0;
|
||||
double increment = cutoffFrequency * 0.1;
|
||||
|
||||
//Get an initial filter and band edge frequency response using the channel bandwidth as a cutoff
|
||||
float[] taps = FilterFactory.getSinc(cutoffFrequency, filterLength, windowType);
|
||||
float[] taps = null;
|
||||
|
||||
taps = FilterFactory.getKaiserSinc(filterLength, cutoffFrequency, 80.0);
|
||||
double response = FilterFactory.evaluate(taps, bandEdge);
|
||||
|
||||
//Set cutoff adjustment threshold - we'll test cutoff frequencies to around 1 hertz resolution
|
||||
@@ -841,13 +842,13 @@ public class FilterFactory
|
||||
while(increment > incrementThreshold)
|
||||
{
|
||||
//If the current cutoff meets the objective, test if a higher cutoff also meets the objective
|
||||
if(matchesObjective(response, PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE) &&
|
||||
if(matchesObjective(response, PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE, MARGIN_OF_ERROR) &&
|
||||
(cutoffFrequency + increment <= bandEdge))
|
||||
{
|
||||
float[] higherCutoffTaps = FilterFactory.getSinc(cutoffFrequency + increment, filterLength, windowType);
|
||||
float[] higherCutoffTaps = FilterFactory.getKaiserSinc(filterLength, cutoffFrequency + increment, 80.0);
|
||||
double higherCutoffResponse = FilterFactory.evaluate(higherCutoffTaps, bandEdge);
|
||||
|
||||
if(matchesObjective(higherCutoffResponse, PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE))
|
||||
if(matchesObjective(higherCutoffResponse, PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE, MARGIN_OF_ERROR))
|
||||
{
|
||||
cutoffFrequency += increment;
|
||||
taps = higherCutoffTaps;
|
||||
@@ -859,7 +860,7 @@ public class FilterFactory
|
||||
}
|
||||
}
|
||||
//If the current cutoff meets the objective, decrease the increment to test for smaller resolution cutoffs
|
||||
else if(matchesObjective(response, PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE))
|
||||
else if(matchesObjective(response, PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE, MARGIN_OF_ERROR))
|
||||
{
|
||||
increment /= 2.0;
|
||||
}
|
||||
@@ -882,7 +883,7 @@ public class FilterFactory
|
||||
if(currentTapsPerChannel > (tapsPerChannel + 10))
|
||||
{
|
||||
throw new FilterDesignException("Couldn't design filter with taps per channel count in the " +
|
||||
"range of " + tapsPerChannel + " - " + (tapsPerChannel + 10));
|
||||
"range of " + tapsPerChannel + " - " + (tapsPerChannel + 10) + " Sample Rate:" + sampleRate + " Channels:" + channels);
|
||||
}
|
||||
|
||||
filterLength = channels * currentTapsPerChannel - 1;
|
||||
@@ -890,13 +891,13 @@ public class FilterFactory
|
||||
increment = cutoffFrequency * 0.1;
|
||||
}
|
||||
|
||||
taps = FilterFactory.getSinc(cutoffFrequency, filterLength, windowType);
|
||||
taps = FilterFactory.getKaiserSinc(filterLength, cutoffFrequency, 80.0);
|
||||
response = FilterFactory.evaluate(taps, bandEdge);
|
||||
}
|
||||
}
|
||||
|
||||
//This will probably never happen, but ...
|
||||
if(!matchesObjective(response, PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE))
|
||||
if(!matchesObjective(response, PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE, MARGIN_OF_ERROR))
|
||||
{
|
||||
throw new FilterDesignException("Cannot design filter to specifications");
|
||||
}
|
||||
@@ -912,20 +913,22 @@ public class FilterFactory
|
||||
mLog.debug("Input Sample Rate: " + sampleRate);
|
||||
mLog.debug("Channel Bandwidth: " + channelBandwidth);
|
||||
mLog.debug("Channels: " + channels);
|
||||
mLog.debug("Window Type: " + windowType.name());
|
||||
mLog.debug("Window Type: " + Window.WindowType.KAISER.name());
|
||||
mLog.debug("Taps Per Channel - Requested:" + tapsPerChannel + " Actual:" + ((double)extendedTaps.length / (double)channels));
|
||||
mLog.debug("Filter Length: " + (extendedTaps.length));
|
||||
mLog.debug("Requested Cutoff Frequency: " + (sampleRate * bandEdge));
|
||||
mLog.debug("Actual Cutoff Frequency: " + (sampleRate * cutoffFrequency));
|
||||
mLog.debug("Attenuation at 0.0 OBJECTIVE: " + 0.0);
|
||||
mLog.debug("Attenuation at 1.0 OBJECTIVE: " + PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE);
|
||||
mLog.debug("Attenuation at 1.00 Channels: " + evaluate(taps, bandEdge * 1.00));
|
||||
mLog.debug("Attenuation at 1.25 Channels: " + evaluate(taps, bandEdge * 1.25));
|
||||
mLog.debug("Attenuation at 1.50 Channels: " + evaluate(taps, bandEdge * 1.50));
|
||||
mLog.debug("Attenuation at 1.75 Channels: " + evaluate(taps, bandEdge * 1.75));
|
||||
mLog.debug("Attenuation at 2.00 Channels: " + evaluate(taps, bandEdge * 2.00));
|
||||
mLog.debug("Attenuation at 1.00 Channels: " + evaluate(taps, bandEdge * 1.00) + "\tFrequency: " + (sampleRate * bandEdge * 1.0));
|
||||
mLog.debug("Attenuation at 1.25 Channels: " + evaluate(taps, bandEdge * 1.25) + "\tFrequency: " + (sampleRate * bandEdge * 1.25));
|
||||
mLog.debug("Attenuation at 1.50 Channels: " + evaluate(taps, bandEdge * 1.50) + "\tFrequency: " + (sampleRate * bandEdge * 1.5));
|
||||
mLog.debug("Attenuation at 1.75 Channels: " + evaluate(taps, bandEdge * 1.75) + "\tFrequency: " + (sampleRate * bandEdge * 1.75));
|
||||
mLog.debug("Attenuation at 2.00 Channels: " + evaluate(taps, bandEdge * 2.00) + "\tFrequency: " + (sampleRate * bandEdge * 2.0));
|
||||
}
|
||||
|
||||
return extendedTaps;
|
||||
//Adjust the overall gain of the filter to an objective of 1.0 times the number of channels
|
||||
return normalize(extendedTaps, 1.0f * (float)channels);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -952,7 +955,46 @@ public class FilterFactory
|
||||
double scalor = 2.0 * cutoff;
|
||||
double piScalor = Math.PI * scalor;
|
||||
|
||||
coefficients[half] = (float)(1.0 * scalor);
|
||||
coefficients[half] = (float)(1.0 * scalor * window[half]);
|
||||
|
||||
for(int x = 1; x <= half; x++)
|
||||
{
|
||||
double a = piScalor * x;
|
||||
double coefficient = scalor * Math.sin(a) / a;
|
||||
|
||||
coefficient *= window[half + x];
|
||||
coefficients[half + x] = (float)coefficient;
|
||||
coefficients[half - x] = (float)coefficient;
|
||||
}
|
||||
|
||||
return coefficients;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Nyquist filter using a Kaiser Window.
|
||||
*
|
||||
* @param cutoff frequency (0.0 <> 0.5)
|
||||
* @param length of the filter - must be odd-length
|
||||
* @param attenuation desired for adjacent channels
|
||||
* @return filter coefficients.
|
||||
* @throws FilterDesignException if the requested length is not odd
|
||||
*/
|
||||
public static float[] getKaiserSinc(int length, double cutoff, double attenuation) throws FilterDesignException
|
||||
{
|
||||
if(length % 2 == 0)
|
||||
{
|
||||
throw new FilterDesignException("Sinc filters must be odd-length");
|
||||
}
|
||||
|
||||
float[] coefficients = new float[length];
|
||||
int half = length / 2;
|
||||
|
||||
double[] window = Window.getKaiser(length, attenuation);
|
||||
|
||||
double scalor = 2.0 * cutoff;
|
||||
double piScalor = Math.PI * scalor;
|
||||
|
||||
coefficients[half] = (float)(1.0 * scalor * window[half]);
|
||||
|
||||
for(int x = 1; x <= half; x++)
|
||||
{
|
||||
@@ -969,12 +1011,29 @@ public class FilterFactory
|
||||
|
||||
/**
|
||||
* Compares two doubles for equals and avoids any rounding error that are present.
|
||||
*
|
||||
* @param a value to compare
|
||||
* @param objective value to compare
|
||||
* @return true if they are equivalent within the margin of error
|
||||
*/
|
||||
private static boolean matchesObjective(double a, double objective)
|
||||
private static boolean matchesObjective(double a, double objective, double marginOfError)
|
||||
{
|
||||
return Math.abs(a - objective) < 0.0000000001;
|
||||
return Math.abs(a - objective) <= marginOfError;
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
float[] taps = null;
|
||||
|
||||
try
|
||||
{
|
||||
taps = FilterFactory.getSincM2Channelizer(12500.0, 800, 9, true);
|
||||
}
|
||||
catch(FilterDesignException fde)
|
||||
{
|
||||
mLog.error("Error", fde);
|
||||
}
|
||||
|
||||
mLog.debug("Done: " + Arrays.toString(taps));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,39 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
|
||||
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
|
||||
* later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* You should have received a copy of the GNU General Public License along with this program.
|
||||
* If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
|
||||
/*
|
||||
* Kaiser Window design methods: getKaiser(), getKaiserBeta() and getBesselZerothOrder() with minor naming changes were
|
||||
* adopted from:
|
||||
*
|
||||
* https://github.com/rjeschke/neetutils-base/blob/master/src/main/java/com/github/rjeschke/neetutils/audio/FIRUtils.java
|
||||
*
|
||||
* Copyright (C) 2012 René Jeschke <rene_jeschke@yahoo.de>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.github.dsheirer.dsp.filter;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
@@ -62,6 +80,8 @@ public class Window
|
||||
return getHamming(length);
|
||||
case HANN:
|
||||
return getHann(length);
|
||||
case KAISER:
|
||||
throw new IllegalArgumentException("Kaiser Window cannot be created via this method. Use the getKaiser() method instead.");
|
||||
case NUTALL:
|
||||
return getNutall(length);
|
||||
case NONE:
|
||||
@@ -337,8 +357,13 @@ public class Window
|
||||
return coefficients;
|
||||
}
|
||||
|
||||
|
||||
private static double getKaiserBeta(double attenuation)
|
||||
/**
|
||||
* Calculates the beta (roll-off factor) for a Kaiser window based on the specified attenuation.
|
||||
*
|
||||
* @param attenuation in decibels
|
||||
* @return beta value in the range of 0.0 to 1.0
|
||||
*/
|
||||
public static double getKaiserBeta(double attenuation)
|
||||
{
|
||||
if(attenuation > 50.0)
|
||||
{
|
||||
@@ -354,6 +379,57 @@ public class Window
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Kaiser window.
|
||||
*
|
||||
* @param length of the window
|
||||
* @param attenuation desired
|
||||
* @return window
|
||||
*/
|
||||
public static double[] getKaiser(int length, double attenuation)
|
||||
{
|
||||
double[] coefficients = new double[length];
|
||||
|
||||
double beta = getKaiserBeta(attenuation);
|
||||
|
||||
double betaBesselZerothOrder = getBesselZerothOrder(beta);
|
||||
|
||||
double temp;
|
||||
|
||||
for(int x = 0; x < coefficients.length; x++)
|
||||
{
|
||||
temp = beta * Math.sqrt(1.0 - Math.pow(2.0 * x / (length - 1) - 1.0, 2));
|
||||
coefficients[x] = getBesselZerothOrder(temp) / betaBesselZerothOrder;
|
||||
}
|
||||
|
||||
return coefficients;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Zeroth order modified Bessel function.
|
||||
*
|
||||
* @param x Value.
|
||||
* @return Return value.
|
||||
*/
|
||||
public final static double getBesselZerothOrder(final double x)
|
||||
{
|
||||
double f = 1;
|
||||
final double x2 = x * x * 0.25;
|
||||
double xc = x2;
|
||||
double v = 1 + x2;
|
||||
for (int i = 2; i < 100; i++)
|
||||
{
|
||||
f *= i;
|
||||
xc *= x2;
|
||||
final double a = xc / (f * f);
|
||||
v += a;
|
||||
if (a < 1e-20) break;
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the window against an array of float-type samples
|
||||
*/
|
||||
@@ -421,6 +497,7 @@ public class Window
|
||||
FLAT_TOP("Flat Top"),
|
||||
HAMMING("Hamming"),
|
||||
HANN("Hann"),
|
||||
KAISER("Kaiser"),
|
||||
NUTALL("Nutall"),
|
||||
NONE("None");
|
||||
|
||||
@@ -436,4 +513,11 @@ public class Window
|
||||
return mLabel;
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
double[] kaiser = getKaiser(16, 80.0);
|
||||
|
||||
mLog.debug("Window:" + Arrays.toString(kaiser));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,12 +19,10 @@
|
||||
package io.github.dsheirer.dsp.filter.channelizer;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.Window.WindowType;
|
||||
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.buffer.ReusableChannelResultsBuffer;
|
||||
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
|
||||
import io.github.dsheirer.sample.buffer.ReusableComplexBufferQueue;
|
||||
import io.github.dsheirer.sample.real.IOverflowListener;
|
||||
import org.jtransforms.fft.FloatFFT_1D;
|
||||
import org.slf4j.Logger;
|
||||
@@ -34,6 +32,33 @@ import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Non-Maximally Decimated Polyphase Filter Bank (NMDPFB) channelizer that divides the input baseband complex sample
|
||||
* stream into equal bandwidth channels that are each oversampled by 2x for output.
|
||||
*
|
||||
* This polyphase channelizer is based off of the channelizer described by Fred Harris in Multirate Signal
|
||||
* Processing for Communications Systems, p230-233.
|
||||
*
|
||||
* Samples are loaded into this filter one block at a time (1/2 channel count) and a filtered output is calculated
|
||||
* to produce an overall 2x oversampled channel sample rate. Each sample block load is preceded by a serpentine
|
||||
* shift of the existing sample blocks. We use the java System.arrayCopy() method which is able to leverage
|
||||
* native processor intrinsics for efficiency.
|
||||
*
|
||||
* The prototype filter for the channelizer is rearranged to align with the structure of the sample buffer.
|
||||
*
|
||||
* Instead of using an array of channel filters as described in the Harris text, this filter and the sample buffer
|
||||
* are arranged as a contiguous array to maximize Java's ability to leverage native processor Single Instruction
|
||||
* Multiple Data (SIMD) intrinsics (since Java 8). The filter process is broken into four steps:
|
||||
*
|
||||
* -Multiply the inline array of samples and filter coefficients
|
||||
* -Accumulate the results for each sub-channel
|
||||
* -Rearrange the sub-channel results to correctly order the sub-channels
|
||||
* -Perform IFFT
|
||||
*
|
||||
* Note: design the prototype filter as a Nyquist windowed filter with a -6.02 db attenuation at the channel edge
|
||||
* frequency if you need Perfect Reconstruction where you'll later re-join two or more channels to form a wider
|
||||
* bandwidth channel or to isolate a signal that located between two channels.
|
||||
*/
|
||||
public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChannelizer
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ComplexPolyphaseChannelizerM2.class);
|
||||
@@ -55,25 +80,7 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann
|
||||
private int mTapsPerChannel;
|
||||
|
||||
/**
|
||||
* Non-Maximally Decimated Polyphase Filter Bank (NMDPFB) channelizer that divides the input frequency band into
|
||||
* equal bandwidth channels that are each oversampled by 2x for output.
|
||||
*
|
||||
* This polyphase channelizer is based off of the channelizer described by Fred Harris in Multirate Signal
|
||||
* Processing for Communications Systems, p230-233.
|
||||
*
|
||||
* Samples are loaded into this filter one block at a time (1/2 channel count) and a filtered output is calculated
|
||||
* to produce an overall 2x oversampled channel sample rate. Each sample block load is preceded by a serpentine
|
||||
* shift of the existing sample blocks. We use the java System.arrayCopy() method which is able to leverage
|
||||
* native processor intrinsics for efficiency.
|
||||
*
|
||||
* The prototype filter for the channelizer is rearranged to align with the structure of the sample buffer.
|
||||
*
|
||||
* Instead of using an array of channel filters as described in the Harris text, this filter and the sample buffer
|
||||
* are arranged as a contiguous array to maximize Java's ability to leverage native processor Single Instruction
|
||||
* Multiple Data (SIMD) intrinsics (since Java 8). The filter process is broken into three steps:
|
||||
* -Multiply the inline array of samples and filter coefficients
|
||||
* -Accumulate the results for each sub-channel
|
||||
* -Rearrange the sub-channel results to correctly order the sub-channels
|
||||
* Creates a NMDPFB channelizer instance.
|
||||
*
|
||||
* @param taps of a low-pass filter designed for the inbound sample rate with a cutoff frequency
|
||||
* equal to the channel bandwidth (sample rate / filters). If you need to synthesize (combine two or more
|
||||
@@ -98,25 +105,8 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann
|
||||
}
|
||||
|
||||
/**
|
||||
* Non-Maximally Decimated Polyphase Filter Bank (NMDPFB) channelizer that divides the input frequency band into
|
||||
* equal bandwidth channels that are each oversampled by 2x for output.
|
||||
*
|
||||
* This polyphase channelizer is based off of the channelizer described by Fred Harris in Multirate Signal
|
||||
* Processing for Communications Systems, p230-233.
|
||||
*
|
||||
* Samples are loaded into this filter one block at a time (1/2 channel count) and a filtered output is calculated
|
||||
* to produce an overall 2x oversampled channel sample rate. Each sample block load is preceded by a serpentine
|
||||
* shift of the existing sample blocks. We use the java System.arrayCopy() method which is able to leverage
|
||||
* native processor intrinsics for efficiency.
|
||||
*
|
||||
* The prototype filter for the channelizer is rearranged to align with the structure of the sample buffer.
|
||||
*
|
||||
* Instead of using an array of channel filters as described in the Harris text, this filter and the sample buffer
|
||||
* are arranged as a contiguous array to maximize Java's ability to leverage native processor Single Instruction
|
||||
* Multiple Data (SIMD) intrinsics (since Java 8). The filter process is broken into three steps:
|
||||
* -Multiply the inline array of samples and filter coefficients
|
||||
* -Accumulate the results for each sub-channel
|
||||
* -Rearrange the sub-channel results to correctly order the sub-channels
|
||||
* Creates a NMDPFB channelizer instance and designs a Perfect Reconstruction prototype filter appropriate for
|
||||
* the baseband sample rate and quantity of filter taps per polyphase sub-channel.
|
||||
*
|
||||
* @param sampleRate to be channelized.
|
||||
* @param tapsPerChannel to use when designing the filter
|
||||
@@ -128,7 +118,7 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann
|
||||
mTapsPerChannel = tapsPerChannel;
|
||||
|
||||
float[] filterTaps = FilterFactory.getSincM2Channelizer(getChannelSampleRate(), getChannelCount(),
|
||||
mTapsPerChannel, WindowType.BLACKMAN_HARRIS_7, true);
|
||||
mTapsPerChannel, true);
|
||||
|
||||
init(filterTaps);
|
||||
}
|
||||
@@ -181,7 +171,7 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann
|
||||
super.setSampleRate(sampleRate);
|
||||
|
||||
float[] filterTaps = FilterFactory.getSincM2Channelizer(getChannelSampleRate(), getChannelCount(),
|
||||
mTapsPerChannel, WindowType.BLACKMAN_HARRIS_7, true);
|
||||
mTapsPerChannel, true);
|
||||
|
||||
init(filterTaps);
|
||||
}
|
||||
@@ -243,7 +233,6 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann
|
||||
//Enqueue the channel results buffer for IFFT processing and distribution on a different thread
|
||||
mIFFTProcessor.receive(channelResultsBuffer);
|
||||
|
||||
// mLog.debug("Duration: " + (System.nanoTime() - start) + " Processing:" + processing);
|
||||
//Decrement the user count to let the originator know we're done with their buffer
|
||||
reusableComplexBuffer.decrementUserCount();
|
||||
}
|
||||
@@ -320,6 +309,7 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann
|
||||
int coefficientPointer = 0;
|
||||
int filterPointer = 0;
|
||||
|
||||
//Create a new filter that duplicates each tap to produce an interleaved I/Q filter
|
||||
while(coefficientPointer < coefficients.length)
|
||||
{
|
||||
filter[filterPointer++] = coefficients[coefficientPointer];
|
||||
@@ -398,55 +388,6 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann
|
||||
return elapsed;
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Buffers the channel results and dispatches the channel results buffer to the channel output processors when full.
|
||||
// * @param buffer containing filtered and IFFT'd channelResults with a corresponding timestamp
|
||||
// */
|
||||
// private void processChannelResults(ReusableChannelResultsBuffer buffer)
|
||||
// {
|
||||
// if(mChannelResultsBuffer == null)
|
||||
// {
|
||||
// mChannelResultsBuffer = new PolyphaseChannelResultsBuffer(buffer.getTimestamp(),
|
||||
// mChannelResultsBufferSize);
|
||||
// }
|
||||
//
|
||||
// try
|
||||
// {
|
||||
// mChannelResultsBuffer.add(buffer.getSamplesCopy());
|
||||
// }
|
||||
// catch(IllegalArgumentException iae)
|
||||
// {
|
||||
// //If the buffer is full (unlikely) or the channel results array length has changed (possible), flush the
|
||||
// //current buffer, create a new one, and store the current results
|
||||
// flushChannelResultsBuffer();
|
||||
//
|
||||
// mChannelResultsBuffer = new PolyphaseChannelResultsBuffer(buffer.getTimestamp(), mChannelResultsBufferSize);
|
||||
//
|
||||
// mChannelResultsBuffer.add(buffer.getSamplesCopy());
|
||||
// }
|
||||
//
|
||||
// if(mChannelResultsBuffer.isFull())
|
||||
// {
|
||||
// flushChannelResultsBuffer();
|
||||
// }
|
||||
//
|
||||
// buffer.decrementUserCount();
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Dispatches the non-empty channel results buffer to the channel output processors and nullifies the reference to
|
||||
// * the buffer so that a new buffer can be created upon receiving the next channel results.
|
||||
// */
|
||||
// private void flushChannelResultsBuffer()
|
||||
// {
|
||||
// if(mChannelResultsBuffer != null && !mChannelResultsBuffer.isEmpty())
|
||||
// {
|
||||
// dispatch(mChannelResultsBuffer);
|
||||
// }
|
||||
//
|
||||
// mChannelResultsBuffer = null;
|
||||
// }
|
||||
|
||||
/**
|
||||
* Initializes the channelizer filter structures.
|
||||
*
|
||||
@@ -454,12 +395,9 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann
|
||||
*/
|
||||
private void init(float[] coefficients)
|
||||
{
|
||||
//Setup the FFT
|
||||
mFFT = new FloatFFT_1D(getChannelCount());
|
||||
|
||||
int channelCount = getChannelCount();
|
||||
int bufferLength = getSubChannelCount() * mTapsPerChannel;
|
||||
|
||||
mSamplesPerBlock = getChannelCount(); //Same as subChannelCount / 2
|
||||
mTopBlockMap = getTopBlockMap(channelCount);
|
||||
mMiddleBlockMap = getMiddleBlockMap(channelCount);
|
||||
@@ -535,26 +473,13 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann
|
||||
int tapsPerChannel = 8;
|
||||
float[] taps = new float[channelCount * tapsPerChannel];
|
||||
|
||||
int sampleRate = 50000;
|
||||
|
||||
for(int x = 0; x < taps.length; x++)
|
||||
{
|
||||
taps[x] = x;
|
||||
}
|
||||
|
||||
ComplexPolyphaseChannelizerM2 channelizer = new ComplexPolyphaseChannelizerM2(taps, sampleRate, channelCount);
|
||||
float[] filter = getAlignedFilter(taps, channelCount, tapsPerChannel);
|
||||
|
||||
ReusableComplexBufferQueue queue = new ReusableComplexBufferQueue();
|
||||
|
||||
int sampleCount = channelCount * 2 * tapsPerChannel;
|
||||
ReusableComplexBuffer buffer = queue.getBuffer(sampleCount);
|
||||
buffer.incrementUserCount();
|
||||
|
||||
for(int x = 0; x < sampleCount; x++)
|
||||
{
|
||||
buffer.getSamples()[x] = x;
|
||||
}
|
||||
|
||||
channelizer.receive(buffer);
|
||||
mLog.debug(Arrays.toString(filter));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
package io.github.dsheirer.dsp.filter.channelizer;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.Window;
|
||||
import io.github.dsheirer.dsp.filter.channelizer.output.IPolyphaseChannelOutputProcessor;
|
||||
import io.github.dsheirer.dsp.filter.channelizer.output.OneChannelOutputProcessor;
|
||||
import io.github.dsheirer.dsp.filter.channelizer.output.TwoChannelOutputProcessor;
|
||||
@@ -45,13 +44,29 @@ import java.util.TreeSet;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
/**
|
||||
* Polyphase Channel Manager is a DDC channel manager and complex buffer queue/processor for a tuner. This class
|
||||
* provides DDC polyphase channel sources and wraps a polyphase channelizer processing sample buffers produced by
|
||||
* the tuner and distributing channelized sample buffers to each allocated DDC polyphase channel source. This
|
||||
* class is responsible for monitoring the tuner for changes in center frequency and/or sample rate and updating
|
||||
* active DDC polyphase channel sources accordingly. This class also monitors source event requests and
|
||||
* notifications received from active DDC polyphase channel sources to adjust sample streams as required.
|
||||
*
|
||||
* Channel bandwidth and channel count are determined by the sample rate of the baseband buffer stream provider. This
|
||||
* class is currently designed to provide channels each with a minimum usable bandwidth of 12.5 kHz and oversampled by
|
||||
* 2.0 to a minimum of 25.0 kHz channel sample rate. If the baseband stream provider sample rate is not evenly
|
||||
* divisible by 12.5 kHz channels for an even number of channels, the channel bandwidth will be increased.
|
||||
*
|
||||
* Note: add this channel manager as a source event listener to the complex buffer provider to ensure this manager
|
||||
* adapts to changes in source frequency and sample rate.
|
||||
*/
|
||||
public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelManager.class);
|
||||
private static final double MINIMUM_CHANNEL_BANDWIDTH = 12500.0;
|
||||
private static final double CHANNEL_OVERSAMPLING = 2.0;
|
||||
private static final int POLYPHASE_CHANNELIZER_TAPS_PER_CHANNEL = 8;
|
||||
private static final int POLYPHASE_SYNTHESIZER_TAPS_PER_CHANNEL = 20;
|
||||
private static final int POLYPHASE_CHANNELIZER_TAPS_PER_CHANNEL = 9;
|
||||
private static final int POLYPHASE_SYNTHESIZER_TAPS_PER_CHANNEL = 9;
|
||||
|
||||
private Broadcaster<SourceEvent> mSourceEventBroadcaster = new Broadcaster<>();
|
||||
private IReusableBufferProvider<ReusableComplexBuffer> mReusableBufferProvider;
|
||||
@@ -64,19 +79,12 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
private Map<Integer,float[]> mOutputProcessorFilters = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Polyphase Channel Manager is a DDC channel manager and complex buffer queue/processor for a tuner. This class
|
||||
* provides DDC polyphase channel sources and wraps a polyphase channelizer processing sample buffers produced by
|
||||
* the tuner and distributing channelized sample buffers to each allocated DDC polyphase channel source. This
|
||||
* class is responsible for monitoring the tuner for changes in center frequency and/or sample rate and updating
|
||||
* active DDC polyphase channel sources accordingly. This class also monitors source event requests and
|
||||
* notifications received from active DDC polyphase channel sources to adjust sample streams as required.
|
||||
* Creates a polyphase channel manager instance.
|
||||
*
|
||||
* Note: add this channel manager as a source event listener to the complex buffer provider to ensure this manager
|
||||
* adapts to changes in source frequency and sample rate.
|
||||
*
|
||||
* @param reusableBufferProvider that supports register/deregister of this channel manager
|
||||
* @param frequency of the provided complex buffer samples
|
||||
* @param sampleRate of the provided complex buffer samples
|
||||
* @param reusableBufferProvider (ie tuner) that supports register/deregister for reusable baseband sample buffer
|
||||
* streams
|
||||
* @param frequency of the baseband complex buffer sample stream (ie center frequency)
|
||||
* @param sampleRate of the baseband complex buffer sample stream
|
||||
*/
|
||||
public PolyphaseChannelManager(IReusableBufferProvider<ReusableComplexBuffer> reusableBufferProvider,
|
||||
long frequency, double sampleRate)
|
||||
@@ -106,6 +114,8 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
|
||||
/**
|
||||
* Creates a polyphase channel manager for the tuner controller
|
||||
*
|
||||
* @param tunerController for a tuner that provides a baseband complex buffer stream.
|
||||
*/
|
||||
public PolyphaseChannelManager(TunerController tunerController)
|
||||
{
|
||||
@@ -187,12 +197,13 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
|
||||
IPolyphaseChannelOutputProcessor outputProcessor = getOutputProcessor(polyphaseIndexes);
|
||||
|
||||
TunerChannel tunerChannel = new TunerChannel(100000000, 12500);
|
||||
|
||||
if(outputProcessor != null)
|
||||
{
|
||||
long centerFrequency = mChannelCalculator.getCenterFrequencyForIndexes(polyphaseIndexes);
|
||||
|
||||
TunerChannel tunerChannel = new TunerChannel(centerFrequency, 12500);
|
||||
|
||||
channelSource = new PolyphaseChannelSource(tunerChannel, outputProcessor, mChannelSourceEventListener,
|
||||
mChannelCalculator.getChannelSampleRate(), centerFrequency);
|
||||
}
|
||||
@@ -267,7 +278,6 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
*/
|
||||
private void stopChannelSource(PolyphaseChannelSource channelSource)
|
||||
{
|
||||
mLog.debug("Stopping channel source ... count:" + mChannelSources.size());
|
||||
synchronized(mBufferProcessor)
|
||||
{
|
||||
mChannelSources.remove(channelSource);
|
||||
@@ -292,7 +302,6 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
{
|
||||
//Do nothing
|
||||
}
|
||||
mLog.debug("Stopped channel source ... count:" + mChannelSources.size());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -517,7 +526,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
if(taps == null)
|
||||
{
|
||||
taps = FilterFactory.getSincM2Synthesizer(mChannelCalculator.getChannelBandwidth(), channels,
|
||||
POLYPHASE_SYNTHESIZER_TAPS_PER_CHANNEL, Window.WindowType.BLACKMAN_HARRIS_7, true);
|
||||
POLYPHASE_SYNTHESIZER_TAPS_PER_CHANNEL);
|
||||
|
||||
mOutputProcessorFilters.put(channels, taps);
|
||||
}
|
||||
@@ -533,7 +542,6 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
@Override
|
||||
public void receive(SourceEvent sourceEvent)
|
||||
{
|
||||
mLog.debug("****** Got a source event: " + sourceEvent.toString());
|
||||
switch(sourceEvent.getEvent())
|
||||
{
|
||||
case REQUEST_START_SAMPLE_STREAM:
|
||||
|
||||
@@ -21,27 +21,24 @@ import org.jtransforms.fft.FloatFFT_1D;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Implements a two-channel polyphase filer synthesizer. This class is intended to be used with an M2 polyphase
|
||||
* channelizer in order to recover a signal that overlaps the boundary between two channels. This synthesizer will
|
||||
* rejoin the two M2 neighboring channels, mix the signal of interest to baseband, and filter the the output stream to
|
||||
* rejoin the two M2 neighboring channels, mix the signal of interest to baseband, and filter the output stream to
|
||||
* half of the sample rate, producing an overall M2 output sample rate.
|
||||
*
|
||||
* The synthesizer concept is modeled on the structure described by Fred Harris et al, in 'Interleaving Different
|
||||
* Bandwidth Narrowband Channels in Perfect Reconstruction Cascade Polyphase Filter Banks for Efficient Flexible
|
||||
* Variable Bandwidth Filters in Wideband Digital Transceivers'.
|
||||
*
|
||||
* The data buffers and sub-filter kernels bear no resemblance to the structures defined in the paper. The data and
|
||||
* The data buffers and sub-filter kernels bear some resemblance to the structures defined in the paper. The data and
|
||||
* filter structures in this class are organized to take advantage of Java's ability to leverage SIMD processor
|
||||
* intrinsics for optimal efficiency in processing. The paper specifies splitting the filter kernel into N polyphase
|
||||
* partitions and creating N data buffers. This class organizes the data buffer as a contiguous sample
|
||||
* array and creates two copies of the filter kernel, one arranged for normal filtering and the second arranged for
|
||||
* phase-shifted filtering. Instead of calculating the dot-product for each sub-filter, we calculate the product of the
|
||||
* full data array against the filter and then accumulate each sub-filter. This allows java to use SIMD for the array
|
||||
* product and then normal processing for the accumulation. Since this is a two-channel processor and the results of
|
||||
* each filter accumulation are added, we use a single accumulator across both filters.
|
||||
* array and creates an I/Q interleaved filter kernel. Instead of calculating the dot-product for each sub-filter,
|
||||
* we calculate the product of the full data array against the filter and then accumulate each sub-filter. This allows
|
||||
* java to use SIMD for the array product and then normal processing for the accumulation. Since this is a two-channel
|
||||
* processor and the results of each filter accumulation are added, we use a single accumulator across both filters.
|
||||
*
|
||||
*/
|
||||
public class TwoChannelSynthesizerM2
|
||||
@@ -49,20 +46,19 @@ public class TwoChannelSynthesizerM2
|
||||
private final static Logger mLog = LoggerFactory.getLogger(TwoChannelSynthesizerM2.class);
|
||||
|
||||
private ReusableComplexBufferQueue mReusableComplexBufferQueue = new ReusableComplexBufferQueue("Two Channel Synthesizer M2");
|
||||
private float[] mDataBuffer;
|
||||
private float[] mNormalFilter;
|
||||
private float[] mPhaseShiftedFilter;
|
||||
private float[] mFilterProduct;
|
||||
private float[] mSerpentineDataBuffer;
|
||||
private float[] mIQInterleavedFilter;
|
||||
private float[] mFilterVectorProduct;
|
||||
private float[] mIFFTBuffer = new float[4];
|
||||
private float mIAccumulator;
|
||||
private float mQAccumulator;
|
||||
private FloatFFT_1D mFFT = new FloatFFT_1D(2);
|
||||
private boolean mFlag = true;
|
||||
private boolean mTopBlockFlag = true;
|
||||
|
||||
/**
|
||||
* Polyphase synthesizer for mixing two M2 oversampled channels into a composite channel using
|
||||
* perfect reconstruction filters (-6db at band edge). Output sample rate is the same as the
|
||||
* input sample rate of one of the channels.
|
||||
* Polyphase synthesizer for mixing two M2 oversampled channels into a composite channel using perfect
|
||||
* reconstruction filters (-6db at band edge). Output sample rate is the same as the input sample rate of one of
|
||||
* the channels.
|
||||
*/
|
||||
public TwoChannelSynthesizerM2(float[] filter)
|
||||
{
|
||||
@@ -70,24 +66,17 @@ public class TwoChannelSynthesizerM2
|
||||
}
|
||||
|
||||
/**
|
||||
* Partitions the filter into 2 polyphase partitions and uses only the first filter. The second
|
||||
* filter partition and all corresponding samples represent the oversampling excess that will be
|
||||
* thrown away.
|
||||
* Initializes the synthesizer filter and data buffers for operation.
|
||||
*
|
||||
* We create two instances of filter 1 to simplify buffer management where we have to swap the data
|
||||
* buffer at each sample to correct for phase shift introduced by buffer swapping when down sampling
|
||||
* from the M2 sample rates of each channel.
|
||||
*
|
||||
* @param filter to partition into 2 polyphase filters.
|
||||
* @param filter to use for polyphase synthesis of two channels.
|
||||
*/
|
||||
private void init(float[] filter)
|
||||
{
|
||||
int tapsPerChannel = (int)Math.ceil(filter.length / 2);
|
||||
|
||||
mNormalFilter = getNormalFilter(filter, tapsPerChannel);
|
||||
mPhaseShiftedFilter = getPhaseShiftedFilter(filter, tapsPerChannel);
|
||||
mDataBuffer = new float[mNormalFilter.length];
|
||||
mFilterProduct = new float[mNormalFilter.length];
|
||||
mIQInterleavedFilter = getInterleavedFilter(filter, tapsPerChannel);
|
||||
mSerpentineDataBuffer = new float[mIQInterleavedFilter.length];
|
||||
mFilterVectorProduct = new float[mIQInterleavedFilter.length];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -114,44 +103,54 @@ public class TwoChannelSynthesizerM2
|
||||
|
||||
for(int x = 0; x < channel1.length; x += 2)
|
||||
{
|
||||
//Load samples from each channel into buffer for IFFT
|
||||
mIFFTBuffer[0] = channel1[x]; //i
|
||||
mIFFTBuffer[1] = channel1[x + 1]; //q
|
||||
mIFFTBuffer[2] = channel2[x]; //i
|
||||
mIFFTBuffer[3] = channel2[x + 1]; //q
|
||||
|
||||
//Perform Inverse FFT (IFFT)
|
||||
mFFT.complexInverse(mIFFTBuffer, true);
|
||||
|
||||
System.arraycopy(mDataBuffer, 0, mDataBuffer, 4, mDataBuffer.length - 4);
|
||||
System.arraycopy(mIFFTBuffer, 0, mDataBuffer, 0, mIFFTBuffer.length);
|
||||
//Perform serpentine shift of data blocks in the data buffer - make room for 4 new samples
|
||||
System.arraycopy(mSerpentineDataBuffer, 0, mSerpentineDataBuffer, 4, mSerpentineDataBuffer.length - 4);
|
||||
|
||||
if(mFlag)
|
||||
//Top Block - load samples into data buffer in normal order
|
||||
if(mTopBlockFlag)
|
||||
{
|
||||
for(int y = 0; y < mDataBuffer.length; y++)
|
||||
{
|
||||
mFilterProduct[y] = mDataBuffer[y] * mNormalFilter[y];
|
||||
}
|
||||
System.arraycopy(mIFFTBuffer, 0, mSerpentineDataBuffer, 0, mIFFTBuffer.length);
|
||||
}
|
||||
//Bottom Block - swap samples via data loading to account for phase shift
|
||||
else
|
||||
{
|
||||
for(int y = 0; y < mDataBuffer.length; y++)
|
||||
{
|
||||
mFilterProduct[y] = mDataBuffer[y] * mPhaseShiftedFilter[y];
|
||||
}
|
||||
System.arraycopy(mIFFTBuffer, 0, mSerpentineDataBuffer, 2, 2);
|
||||
System.arraycopy(mIFFTBuffer, 2, mSerpentineDataBuffer, 0, 2);
|
||||
}
|
||||
|
||||
//Note: in order to use Java's ability to leverage SIMD intrinsics, we perform filtering in two steps
|
||||
//(multiply then accumulate) since our filter and data are structured with I and Q vectors interleaved.
|
||||
//This approach allows the Hotspot compiler to more easily recognize the scalor operations.
|
||||
|
||||
//Multiply data samples by the I/Q interleaved filter to form the vector product
|
||||
for(int y = 0; y < mSerpentineDataBuffer.length; y++)
|
||||
{
|
||||
mFilterVectorProduct[y] = mSerpentineDataBuffer[y] * mIQInterleavedFilter[y];
|
||||
}
|
||||
|
||||
//Accumulate output I/Q samples from vector product
|
||||
mIAccumulator = 0.0f;
|
||||
mQAccumulator = 0.0f;
|
||||
|
||||
for(int y = 0; y < mFilterProduct.length; y += 2)
|
||||
for(int y = 0; y < mFilterVectorProduct.length; y += 2)
|
||||
{
|
||||
mIAccumulator += mFilterProduct[y];
|
||||
mQAccumulator += mFilterProduct[y + 1];
|
||||
mIAccumulator += mFilterVectorProduct[y];
|
||||
mQAccumulator += mFilterVectorProduct[y + 1];
|
||||
}
|
||||
|
||||
output[x] = mIAccumulator;
|
||||
output[x + 1] = mQAccumulator;
|
||||
|
||||
mFlag = !mFlag;
|
||||
mTopBlockFlag = !mTopBlockFlag;
|
||||
}
|
||||
|
||||
channelBuffer1.decrementUserCount();
|
||||
@@ -162,120 +161,33 @@ public class TwoChannelSynthesizerM2
|
||||
return synthesizedComplexBuffer;
|
||||
}
|
||||
|
||||
private static float[] getNormalFilter(float[] coefficients, int tapsPerChannel)
|
||||
/**
|
||||
* Creates an interleaved I/Q filter where each coefficient from the filter argument is duplicated and the returned
|
||||
* filter is twice the length of the original filter.
|
||||
*
|
||||
* Note: the returned filter array is sized to: 2 * channel count * taps per channel, which may be slightly more
|
||||
* than twice the length of the original filter. Any Added filter array elements will contain zero values.
|
||||
*
|
||||
* @param coefficients to create an interleaved filter
|
||||
* @param tapsPerChannel count
|
||||
* @return
|
||||
*/
|
||||
private static float[] getInterleavedFilter(float[] coefficients, int tapsPerChannel)
|
||||
{
|
||||
float[] filter = new float[4 * tapsPerChannel];
|
||||
int channelCount = 2;
|
||||
|
||||
int idealLength = 2 * tapsPerChannel;
|
||||
float[] filter = new float[channelCount * tapsPerChannel * 2];
|
||||
|
||||
int offset = idealLength - 2;
|
||||
int coefficientPointer = 0;
|
||||
int filterPointer = 0;
|
||||
|
||||
int index;
|
||||
|
||||
for(int x = 0; x < idealLength; x += 2)
|
||||
//Create a new filter that duplicates each tap to produce an interleaved I/Q filter
|
||||
while(coefficientPointer < coefficients.length)
|
||||
{
|
||||
index = offset - x;
|
||||
|
||||
if(index < coefficients.length)
|
||||
{
|
||||
//Filter 1
|
||||
filter[2 * x + 0] = coefficients[index];
|
||||
filter[2 * x + 1] = coefficients[index];
|
||||
}
|
||||
|
||||
index = offset - x + 1;
|
||||
|
||||
if(index < coefficients.length)
|
||||
{
|
||||
//Filter 2
|
||||
filter[2 * x + 2] = coefficients[index];
|
||||
filter[2 * x + 3] = coefficients[index];
|
||||
}
|
||||
filter[filterPointer++] = coefficients[coefficientPointer];
|
||||
filter[filterPointer++] = coefficients[coefficientPointer++];
|
||||
}
|
||||
|
||||
return filter;
|
||||
}
|
||||
|
||||
private static float[] getPhaseShiftedFilter(float[] coefficients, int tapsPerChannel)
|
||||
{
|
||||
float[] filter = new float[4 * tapsPerChannel];
|
||||
|
||||
int idealLength = 2 * tapsPerChannel;
|
||||
|
||||
int offset = idealLength - 2;
|
||||
|
||||
int index;
|
||||
|
||||
for(int x = 0; x < idealLength; x += 2)
|
||||
{
|
||||
index = offset - x + 1;
|
||||
|
||||
if(index < coefficients.length)
|
||||
{
|
||||
//Filter 2
|
||||
filter[2 * x + 0] = coefficients[index];
|
||||
filter[2 * x + 1] = coefficients[index];
|
||||
}
|
||||
|
||||
index = offset - x;
|
||||
|
||||
if(index < coefficients.length)
|
||||
{
|
||||
//Filter 1
|
||||
filter[2 * x + 2] = coefficients[index];
|
||||
filter[2 * x + 3] = coefficients[index];
|
||||
}
|
||||
}
|
||||
|
||||
return filter;
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
mLog.debug("Starting");
|
||||
|
||||
int sampleCount = 30;
|
||||
|
||||
try
|
||||
{
|
||||
// float[] taps = FilterFactory.getSincM2Synthesizer(12500.0, 2, 20, Window.WindowType.BLACKMAN_HARRIS_7, true);
|
||||
|
||||
float[] taps = new float[15];
|
||||
for(int x = 0; x < taps.length; x++)
|
||||
{
|
||||
taps[x] = x;
|
||||
}
|
||||
|
||||
float[] normalFilter = getPhaseShiftedFilter(taps, 8);
|
||||
|
||||
mLog.debug("Filter: " + Arrays.toString(normalFilter));
|
||||
|
||||
// TwoChannelSynthesizerM2 synthesizer = new TwoChannelSynthesizerM2(taps);
|
||||
//
|
||||
// IOscillator oscillator = new LowPhaseNoiseOscillator(3000.0, 25000.0);
|
||||
//
|
||||
// float[] channel1 = oscillator.generateComplex(sampleCount);
|
||||
// float[] channel2 = new float[sampleCount * 2];
|
||||
//
|
||||
// float[] channel1 = new float[16];
|
||||
// float[] channel2 = new float[16];
|
||||
// for(int x = 1; x <= 16; x++)
|
||||
// {
|
||||
// channel1[x - 1] = x;
|
||||
// channel2[x - 1] = x + 16;
|
||||
// }
|
||||
//
|
||||
//
|
||||
// float[] synthesized = synthesizer.process(channel1, channel2);
|
||||
//
|
||||
// mLog.debug("1:" + Arrays.toString(channel1));
|
||||
// mLog.debug("X:" + Arrays.toString(synthesized));
|
||||
}
|
||||
catch(Exception fde)
|
||||
{
|
||||
mLog.error("Filter design error", fde);
|
||||
}
|
||||
|
||||
mLog.debug("Finished!!");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ package io.github.dsheirer.dsp.filter.channelizer.output;
|
||||
import io.github.dsheirer.dsp.filter.channelizer.TwoChannelSynthesizerM2;
|
||||
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
|
||||
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
|
||||
import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter;
|
||||
import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter2;
|
||||
import io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesigner;
|
||||
import io.github.dsheirer.dsp.mixer.FS4DownConverter;
|
||||
import io.github.dsheirer.sample.buffer.ReusableBufferAssembler;
|
||||
@@ -27,47 +27,18 @@ import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class TwoChannelOutputProcessor extends ChannelOutputProcessor
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(TwoChannelOutputProcessor.class);
|
||||
|
||||
|
||||
private static float[] mFilterCoefficients;
|
||||
|
||||
static
|
||||
{
|
||||
try
|
||||
{
|
||||
FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
|
||||
.sampleRate(25000)
|
||||
.gridDensity(16)
|
||||
.oddLength(true)
|
||||
.passBandCutoff(6000)
|
||||
.passBandAmplitude(1.0)
|
||||
.passBandRipple(0.01)
|
||||
.stopBandStart(6500)
|
||||
.stopBandAmplitude(0.0)
|
||||
.stopBandRipple(0.005) //Approximately 94 dB attenuation
|
||||
.build();
|
||||
|
||||
RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification);
|
||||
|
||||
if(designer.isValid())
|
||||
{
|
||||
mFilterCoefficients = designer.getImpulseResponse();
|
||||
}
|
||||
}
|
||||
catch(FilterDesignException fde)
|
||||
{
|
||||
mLog.error("Error designing low pass filter for two channel output processor", fde);
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<Integer,float[]> sLowPassFilterKernelMap = new HashMap<>();
|
||||
private TwoChannelSynthesizerM2 mSynthesizer;
|
||||
private FS4DownConverter mFS4DownConverter = new FS4DownConverter();
|
||||
private ComplexFIRFilter mLowPassFilter;
|
||||
private ComplexFIRFilter2 mLowPassFilter;
|
||||
|
||||
private int mChannelOffset1;
|
||||
private int mChannelOffset2;
|
||||
@@ -87,7 +58,23 @@ public class TwoChannelOutputProcessor extends ChannelOutputProcessor
|
||||
setPolyphaseChannelIndices(channelIndexes);
|
||||
setSynthesisFilter(filter);
|
||||
|
||||
mLowPassFilter = new ComplexFIRFilter(mFilterCoefficients, 1.0f);
|
||||
float[] lowPassFilter = sLowPassFilterKernelMap.get((int)sampleRate);
|
||||
|
||||
if(lowPassFilter == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
lowPassFilter = getLowPassFilter(sampleRate);
|
||||
sLowPassFilterKernelMap.put((int)sampleRate, lowPassFilter);
|
||||
}
|
||||
catch(FilterDesignException fde)
|
||||
{
|
||||
mLog.error("Couldn't design a low-pass filter for 2-channel synthesizer", fde);
|
||||
throw new IllegalStateException("Cannot design low-pass filter for 2 channel synthesizer", fde);
|
||||
}
|
||||
}
|
||||
|
||||
mLowPassFilter = new ComplexFIRFilter2(lowPassFilter, 1.0f);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -159,4 +146,39 @@ public class TwoChannelOutputProcessor extends ChannelOutputProcessor
|
||||
buffer.decrementUserCount();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a low-pass filter kernel for the specified sample rate.
|
||||
* @param sampleRate for the channel
|
||||
* @return filter
|
||||
* @throws FilterDesignException if the filter cannot be designed
|
||||
*/
|
||||
private static float[] getLowPassFilter(double sampleRate) throws FilterDesignException
|
||||
{
|
||||
double cutoff = sampleRate / 4.0;
|
||||
int passBandCutoff = (int)(cutoff * 0.92);
|
||||
int stopBandStart = (int)(cutoff * 1.08);
|
||||
|
||||
FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
|
||||
.sampleRate(sampleRate)
|
||||
.gridDensity(16)
|
||||
.oddLength(true)
|
||||
.passBandCutoff((int)passBandCutoff)
|
||||
.passBandAmplitude(1.0)
|
||||
.passBandRipple(0.01)
|
||||
.stopBandStart((int)stopBandStart)
|
||||
.stopBandAmplitude(0.0)
|
||||
.stopBandRipple(0.005) //Approximately 94 dB attenuation
|
||||
.build();
|
||||
|
||||
RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification);
|
||||
|
||||
if(designer.isValid())
|
||||
{
|
||||
return designer.getImpulseResponse();
|
||||
}
|
||||
|
||||
throw new FilterDesignException("Couldn't design low-pass filter for sample rate: " + sampleRate +
|
||||
" Pass End:" + passBandCutoff + " Stop Begin:" + stopBandStart);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package io.github.dsheirer.dsp.filter.design;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.Window;
|
||||
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
|
||||
import javafx.application.Application;
|
||||
import javafx.scene.Scene;
|
||||
@@ -76,7 +75,7 @@ public class FilterViewer extends Application
|
||||
|
||||
try
|
||||
{
|
||||
taps = FilterFactory.getSincM2Synthesizer(12500.0, 2, 19, Window.WindowType.BLACKMAN_HARRIS_7, true);
|
||||
taps = FilterFactory.getSincM2Synthesizer(12500.0, 2, 19);
|
||||
// taps = FilterFactory.getSincFilter(25000.0, 12500.0, 2, 19, Window.WindowType.BLACKMAN_HARRIS_7, true);
|
||||
}
|
||||
catch(FilterDesignException fde)
|
||||
|
||||
@@ -21,6 +21,7 @@ import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
|
||||
import io.github.dsheirer.sample.buffer.ReusableComplexBufferQueue;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
|
||||
@Deprecated //Use ComplexFIRFilter2 instead
|
||||
public class ComplexFIRFilter extends FIRFilter
|
||||
{
|
||||
private ReusableComplexBufferQueue mReusableComplexBufferQueue = new ReusableComplexBufferQueue("Complex FIR Filter");
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
|
||||
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
|
||||
* later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program.
|
||||
* If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.dsp.filter.fir.complex;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.fir.FIRFilter;
|
||||
import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter2;
|
||||
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
|
||||
import io.github.dsheirer.sample.buffer.ReusableComplexBufferQueue;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
|
||||
public class ComplexFIRFilter2 extends FIRFilter
|
||||
{
|
||||
private ReusableComplexBufferQueue mReusableComplexBufferQueue = new ReusableComplexBufferQueue("Complex FIR Filter");
|
||||
private RealFIRFilter2 mIFilter;
|
||||
private RealFIRFilter2 mQFilter;
|
||||
|
||||
/**
|
||||
* Complex FIR Filter for processing complex sample pairs that internally uses two RealFIRFilter
|
||||
* instances.
|
||||
*
|
||||
* @param coefficients - filter taps
|
||||
* @param gain - gain to apply to filtered outputs - use 1.0f for no gain
|
||||
*/
|
||||
public ComplexFIRFilter2(float[] coefficients, float gain)
|
||||
{
|
||||
mIFilter = new RealFIRFilter2(coefficients, gain);
|
||||
mQFilter = new RealFIRFilter2(coefficients, gain);
|
||||
}
|
||||
|
||||
/**
|
||||
* Complex FIR Filter for processing complex sample pairs that internally uses two RealFIRFilter
|
||||
* instances. This constructor uses a default gain of 1.0f.
|
||||
*
|
||||
* @param coefficients - filter taps
|
||||
*/
|
||||
public ComplexFIRFilter2(float[] coefficients)
|
||||
{
|
||||
this(coefficients, 1.0f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the inphase sample value.
|
||||
* @param sample to filter
|
||||
* @return filtered sample
|
||||
*/
|
||||
public float filterInphase(float sample)
|
||||
{
|
||||
return mIFilter.filter(sample);
|
||||
}
|
||||
|
||||
/**
|
||||
* Current filtered inphase sample value after invoking the filterInphase() method.
|
||||
*/
|
||||
public float currentInphaseValue()
|
||||
{
|
||||
return mIFilter.currentValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the quadrature sample value.
|
||||
* @param sample to filter
|
||||
* @return filtered sample
|
||||
*/
|
||||
public float filterQuadrature(float sample)
|
||||
{
|
||||
return mQFilter.filter(sample);
|
||||
}
|
||||
|
||||
/**
|
||||
* Current filtered quadrature sample value after invoking the filterInphase() method.
|
||||
*/
|
||||
public float currentQuadratureValue()
|
||||
{
|
||||
return mQFilter.currentValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the complex sample.
|
||||
* @param sample to filter
|
||||
* @return filtered sample
|
||||
*/
|
||||
public Complex filter(Complex sample)
|
||||
{
|
||||
float i = filterInphase(sample.inphase());
|
||||
float q = filterQuadrature(sample.quadrature());
|
||||
|
||||
return new Complex(i, q);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the complex samples from the reusable buffer and returns a new complex buffer with the filtered output
|
||||
*
|
||||
* Note: the original reusable buffer user count is decremented and a new reusable buffer is returned
|
||||
* with the user count already incremented to one.
|
||||
*
|
||||
* @param originalBuffer with complex samples to filter
|
||||
* @return new buffer containing filtered complex samples
|
||||
*/
|
||||
public ReusableComplexBuffer filter(ReusableComplexBuffer originalBuffer)
|
||||
{
|
||||
ReusableComplexBuffer filteredBuffer = mReusableComplexBufferQueue.getBuffer(originalBuffer.getSamples().length);
|
||||
filteredBuffer.setTimestamp(originalBuffer.getTimestamp());
|
||||
|
||||
float[] samples = originalBuffer.getSamples();
|
||||
float[] filteredSamples = filteredBuffer.getSamples();
|
||||
|
||||
for(int x = 0; x < originalBuffer.getSamples().length; x += 2)
|
||||
{
|
||||
filteredSamples[x] = filterInphase(samples[x]);
|
||||
filteredSamples[x + 1] = filterQuadrature(samples[x + 1]);
|
||||
}
|
||||
|
||||
originalBuffer.decrementUserCount();
|
||||
filteredBuffer.incrementUserCount();
|
||||
|
||||
return filteredBuffer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
mIFilter.dispose();
|
||||
mQFilter.dispose();
|
||||
mReusableComplexBufferQueue.dispose();
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package io.github.dsheirer.dsp.filter.fir.complex;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.complex.ComplexBuffer;
|
||||
|
||||
@Deprecated //Use ComplexFIRFilter2 instead
|
||||
public class ComplexFIRFilter_CB_CB extends ComplexFIRFilter implements Listener<ComplexBuffer>
|
||||
{
|
||||
private Listener<ComplexBuffer> mListener;
|
||||
|
||||
@@ -17,6 +17,7 @@ package io.github.dsheirer.dsp.filter.fir.real;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.fir.FIRFilter;
|
||||
|
||||
@Deprecated //Use RealFIRFilter2 instead
|
||||
public class RealFIRFilter extends FIRFilter
|
||||
{
|
||||
private float[] mBuffer;
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
|
||||
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
|
||||
* later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program.
|
||||
* If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.dsp.filter.fir.real;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.fir.FIRFilter;
|
||||
import io.github.dsheirer.sample.buffer.ReusableBuffer;
|
||||
import io.github.dsheirer.sample.buffer.ReusableBufferQueue;
|
||||
|
||||
/**
|
||||
* Finite Impulse Response (FIR) filter for filtering individual float samples or float sample arrays.
|
||||
*
|
||||
* Note: filtering operations in this class are structured to leverage SIMD processor intrinsics when
|
||||
* available to the Java runtime.
|
||||
*/
|
||||
public class RealFIRFilter2 extends FIRFilter
|
||||
{
|
||||
private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue();
|
||||
|
||||
private float[] mData;
|
||||
private float[] mCoefficients;
|
||||
private float mGain;
|
||||
private float mAccumulator;
|
||||
|
||||
/**
|
||||
* Float sample FIR filter base class.
|
||||
*
|
||||
* @param coefficients - filter coefficients in normal order.
|
||||
* @param gain value to apply to the filtered output. Use 1.0f for no gain
|
||||
*/
|
||||
public RealFIRFilter2(float[] coefficients, float gain)
|
||||
{
|
||||
mGain = gain;
|
||||
mCoefficients = coefficients;
|
||||
mData = new float[coefficients.length];
|
||||
}
|
||||
|
||||
/**
|
||||
* Float sample FIR filter base class that uses a default gain value of 1.0f.
|
||||
*
|
||||
* @param coefficients - filter coefficients in normal order.
|
||||
*/
|
||||
public RealFIRFilter2(float[] coefficients)
|
||||
{
|
||||
this(coefficients, 1.0f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes this filter to prepare for garbage collection.
|
||||
*/
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
mCoefficients = null;
|
||||
mData = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the sample argument. Loads the sample into the internal data buffer and performs
|
||||
* convolution between the sample buffer and the filter coefficients.
|
||||
*
|
||||
* @param sample to load
|
||||
* @return filtered value
|
||||
*/
|
||||
public float filter(float sample)
|
||||
{
|
||||
//Use array copy to leverage SIMD intrinsics
|
||||
System.arraycopy(mData, 0, mData, 1, mData.length - 1);
|
||||
mData[0] = sample;
|
||||
|
||||
mAccumulator = 0.0f;
|
||||
|
||||
//Use vector dot product to leverage SIMD intrinsics
|
||||
for(int x = 0; x < mCoefficients.length; x++)
|
||||
{
|
||||
mAccumulator += mData[x] * mCoefficients[x];
|
||||
}
|
||||
|
||||
/* Apply gain and return the filtered value */
|
||||
mAccumulator *= mGain;
|
||||
|
||||
return mAccumulator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Current filtered output value for the filter after the filter() method has been invoked.
|
||||
*/
|
||||
public float currentValue()
|
||||
{
|
||||
return mAccumulator;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Filters the samples contained in the unfilteredBuffer and returns a new reusable buffer with the
|
||||
* filtered samples.
|
||||
*
|
||||
* Note: user count on the returned (new) buffer is set to one and the user count is decremented on
|
||||
* the unfiltered buffer argument.
|
||||
*
|
||||
* @param unfilteredBuffer containing a sample array to be filtered
|
||||
* @return a new reusable buffer with the filtered samples.
|
||||
*/
|
||||
public ReusableBuffer filter(ReusableBuffer unfilteredBuffer)
|
||||
{
|
||||
float[] unfilteredSamples = unfilteredBuffer.getSamples();
|
||||
|
||||
ReusableBuffer filteredBuffer = mReusableBufferQueue.getBuffer(unfilteredSamples.length);
|
||||
float[] filteredSamples = filteredBuffer.getSamples();
|
||||
|
||||
for(int x = 0; x < unfilteredSamples.length; x++)
|
||||
{
|
||||
filteredSamples[x] = filter(unfilteredSamples[x]);
|
||||
}
|
||||
|
||||
filteredBuffer.incrementUserCount();
|
||||
unfilteredBuffer.decrementUserCount();
|
||||
|
||||
return filteredBuffer;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package io.github.dsheirer.dsp.filter.fir.real;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.real.RealBuffer;
|
||||
|
||||
@Deprecated //Use RealFIRFilter2 version instead
|
||||
public class RealFIRFilter_RB_RB extends RealFIRFilter
|
||||
implements Listener<RealBuffer>
|
||||
{
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
package io.github.dsheirer.dsp.filter.fir.real;
|
||||
|
||||
import io.github.dsheirer.sample.real.RealSampleListener;
|
||||
|
||||
public class RealFIRFilter_R_R extends RealFIRFilter implements RealSampleListener
|
||||
{
|
||||
private RealSampleListener mListener;
|
||||
|
||||
/**
|
||||
* Float FIR filter with streaming float provider and listener interfaces.
|
||||
*
|
||||
* @param coefficients
|
||||
* @param gain
|
||||
*/
|
||||
public RealFIRFilter_R_R( float[] coefficients, float gain )
|
||||
{
|
||||
super( coefficients, gain );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
super.dispose();
|
||||
|
||||
mListener = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( float sample )
|
||||
{
|
||||
if( mListener != null )
|
||||
{
|
||||
mListener.receive( filter( sample ) );
|
||||
}
|
||||
}
|
||||
|
||||
public float[] filter( float[] samples )
|
||||
{
|
||||
for( int x = 0; x < samples.length; x++ )
|
||||
{
|
||||
samples[ x ] = filter( samples[ x ] );
|
||||
}
|
||||
|
||||
return samples;
|
||||
}
|
||||
|
||||
public void setListener( RealSampleListener listener )
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
public void removeListener()
|
||||
{
|
||||
mListener = null;
|
||||
}
|
||||
}
|
||||
@@ -195,6 +195,8 @@ public class ChannelizerViewer extends JFrame
|
||||
|
||||
public class ChannelArrayPanel extends JPanel
|
||||
{
|
||||
private final Logger mLog = LoggerFactory.getLogger(ChannelArrayPanel.class);
|
||||
|
||||
public ChannelArrayPanel()
|
||||
{
|
||||
int bufferSize = CHANNEL_BANDWIDTH / CHANNEL_FFT_FRAME_RATE;
|
||||
@@ -261,6 +263,8 @@ public class ChannelizerViewer extends JFrame
|
||||
DiscreteChannelPanel channelPanel = new DiscreteChannelPanel(mSettingsManager, source, x);
|
||||
channelPanel.setDFTSize(mChannelPanelDFTSize);
|
||||
|
||||
mLog.debug("Testing Channel [" + x + "] is set to [" + source.getTunerChannel().getFrequency() + "]");
|
||||
|
||||
if(x % mChannelsPerRow == mChannelsPerRow - 1)
|
||||
{
|
||||
add(channelPanel, "grow,push,wrap 2px");
|
||||
@@ -310,7 +314,7 @@ public class ChannelizerViewer extends JFrame
|
||||
{
|
||||
setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill]"));
|
||||
mSpectrumPanel = new SpectrumPanel(settingsManager);
|
||||
mSpectrumPanel.setSampleSize(16);
|
||||
mSpectrumPanel.setSampleSize(28);
|
||||
add(mSpectrumPanel);
|
||||
|
||||
mDFTProcessor.addConverter(mComplexDecibelConverter);
|
||||
@@ -349,7 +353,7 @@ public class ChannelizerViewer extends JFrame
|
||||
{
|
||||
setLayout(new MigLayout("insets 0 0 0 0", "[center,grow,fill][]", "[grow,fill][]"));
|
||||
mSpectrumPanel = new SpectrumPanel(settingsManager);
|
||||
mSpectrumPanel.setSampleSize(16);
|
||||
mSpectrumPanel.setSampleSize(32);
|
||||
add(mSpectrumPanel, "span");
|
||||
|
||||
mDFTProcessor.addConverter(mComplexDecibelConverter);
|
||||
@@ -433,6 +437,8 @@ public class ChannelizerViewer extends JFrame
|
||||
|
||||
public class DiscreteChannelPanel extends JPanel implements Listener<ReusableComplexBuffer>, ISourceEventProcessor
|
||||
{
|
||||
private final Logger mLog = LoggerFactory.getLogger(DiscreteChannelPanel.class);
|
||||
|
||||
private TunerChannelSource mSource;
|
||||
private DFTProcessor mDFTProcessor = new DFTProcessor(SampleType.COMPLEX);
|
||||
private ComplexDecibelConverter mComplexDecibelConverter = new ComplexDecibelConverter();
|
||||
@@ -445,7 +451,7 @@ public class ChannelizerViewer extends JFrame
|
||||
mSource = source;
|
||||
setLayout(new MigLayout("insets 0 0 0 0", "[center,grow,fill][]", "[grow,fill][]"));
|
||||
mSpectrumPanel = new SpectrumPanel(settingsManager);
|
||||
mSpectrumPanel.setSampleSize(16);
|
||||
mSpectrumPanel.setSampleSize(32);
|
||||
add(mSpectrumPanel, "span");
|
||||
add(new JLabel("Index:" + index));
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
package io.github.dsheirer.gui.channelizer;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.Window;
|
||||
import io.github.dsheirer.dsp.filter.channelizer.TwoChannelSynthesizerM2;
|
||||
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
|
||||
import io.github.dsheirer.dsp.mixer.FS4DownConverter;
|
||||
@@ -282,8 +281,7 @@ public class SynthesizerViewer extends JFrame
|
||||
{
|
||||
try
|
||||
{
|
||||
float[] taps = FilterFactory.getSincM2Synthesizer(12500.0, 2, 12,
|
||||
Window.WindowType.BLACKMAN_HARRIS_7, true);
|
||||
float[] taps = FilterFactory.getSincM2Synthesizer(12500.0, 2, 12);
|
||||
mSynthesizer = new TwoChannelSynthesizerM2(taps);
|
||||
}
|
||||
catch(FilterDesignException fde)
|
||||
|
||||
@@ -19,7 +19,7 @@ import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
|
||||
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
|
||||
import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter;
|
||||
import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter2;
|
||||
import io.github.dsheirer.dsp.gain.ComplexFeedForwardGainControl;
|
||||
import io.github.dsheirer.dsp.psk.DQPSKDecisionDirectedDemodulator;
|
||||
import io.github.dsheirer.dsp.psk.InterpolatingSampleBuffer;
|
||||
@@ -42,7 +42,7 @@ public class P25DecoderC4FM extends P25Decoder
|
||||
protected P25MessageFramer mMessageFramer;
|
||||
private ComplexFeedForwardGainControl mAGC = new ComplexFeedForwardGainControl(32);
|
||||
private Map<Double,float[]> mBasebandFilters = new HashMap<>();
|
||||
private ComplexFIRFilter mBasebandFilter;
|
||||
private ComplexFIRFilter2 mBasebandFilter;
|
||||
|
||||
/**
|
||||
* P25 Phase 1 - standard C4FM modulation decoder. Uses Differential QPSK decoding with a Costas PLL and a
|
||||
@@ -60,7 +60,7 @@ public class P25DecoderC4FM extends P25Decoder
|
||||
{
|
||||
super.setSampleRate(sampleRate);
|
||||
|
||||
mBasebandFilter = new ComplexFIRFilter(getBasebandFilter(), 1.0f);
|
||||
mBasebandFilter = new ComplexFIRFilter2(getBasebandFilter());
|
||||
|
||||
mCostasLoop = new CostasLoop(getSampleRate(), getSymbolRate());
|
||||
mInterpolatingSampleBuffer = new InterpolatingSampleBuffer(getSamplesPerSymbol(), SAMPLE_COUNTER_GAIN);
|
||||
|
||||
@@ -18,7 +18,7 @@ package io.github.dsheirer.module.decode.p25;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.Window.WindowType;
|
||||
import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter;
|
||||
import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter2;
|
||||
import io.github.dsheirer.dsp.gain.ComplexFeedForwardGainControl;
|
||||
import io.github.dsheirer.dsp.psk.DQPSKGardnerDemodulator;
|
||||
import io.github.dsheirer.dsp.psk.InterpolatingSampleBuffer;
|
||||
@@ -37,7 +37,7 @@ public class P25DecoderLSM extends P25Decoder
|
||||
protected static final float SAMPLE_COUNTER_GAIN = 0.5f;
|
||||
|
||||
private Map<Double,float[]> mBasebandFilters = new HashMap<>();
|
||||
private ComplexFIRFilter mBasebandFilter;
|
||||
private ComplexFIRFilter2 mBasebandFilter;
|
||||
private ComplexFeedForwardGainControl mAGC = new ComplexFeedForwardGainControl(32);
|
||||
protected DQPSKGardnerDemodulator mQPSKDemodulator;
|
||||
protected P25MessageFramer mMessageFramer;
|
||||
@@ -64,7 +64,7 @@ public class P25DecoderLSM extends P25Decoder
|
||||
{
|
||||
super.setSampleRate(sampleRate);
|
||||
|
||||
mBasebandFilter = new ComplexFIRFilter(getBasebandFilter(), 1.0f);
|
||||
mBasebandFilter = new ComplexFIRFilter2(getBasebandFilter());
|
||||
|
||||
mCostasLoop = new CostasLoop(getSampleRate(), getSymbolRate());
|
||||
mInterpolatingSampleBuffer = new InterpolatingSampleBuffer(getSamplesPerSymbol(), SAMPLE_COUNTER_GAIN);
|
||||
|
||||
@@ -32,7 +32,7 @@ public class TestTunerController extends TunerController
|
||||
|
||||
public static final long MINIMUM_FREQUENCY = 1l;
|
||||
public static final long MAXIMUM_FREQUENCY = 1000000000l;
|
||||
public static final int SAMPLE_RATE = 10000000;
|
||||
public static final int SAMPLE_RATE = 50000;
|
||||
public static final int DC_NOISE_BANDWIDTH = 0;
|
||||
public static final double USABLE_BANDWIDTH_PERCENTAGE = 1.00;
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
public class DFTProcessor implements Listener<ReusableComplexBuffer>, ISourceEventProcessor, IDFTWidthChangeProcessor
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(DFTProcessor.class);
|
||||
private static final int BUFFER_QUEUE_MAX_SIZE = 12;
|
||||
private static final int BUFFER_QUEUE_MAX_SIZE = 20;
|
||||
private static final int BUFFER_QUEUE_OVERFLOW_RESET_THRESHOLD = 6;
|
||||
private static final String FRAME_RATE_PROPERTY = "spectral.display.frame.rate";
|
||||
|
||||
|
||||
@@ -33,8 +33,14 @@ import org.apache.commons.lang3.Validate;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import javax.swing.JPanel;
|
||||
import java.awt.Color;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.GradientPaint;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.geom.GeneralPath;
|
||||
import java.awt.geom.Line2D;
|
||||
import java.util.Arrays;
|
||||
@@ -295,7 +301,7 @@ public class SpectrumPanel extends JPanel implements DFTResultsListener, Setting
|
||||
*/
|
||||
public void setSampleSize(double sampleSize)
|
||||
{
|
||||
Validate.isTrue(2.0 <= sampleSize && sampleSize <= 32.0);
|
||||
Validate.isTrue(2.0 <= sampleSize && sampleSize <= 64.0);
|
||||
|
||||
mDBScale = (float)(20.0 * Math.log10(Math.pow(2.0, sampleSize - 1)));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user