[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:
Denny
2018-05-20 07:21:14 -04:00
parent 341e740e77
commit c13315f731
21 changed files with 698 additions and 459 deletions

View File

@@ -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));
}
}

View File

@@ -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));
}
}

View File

@@ -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));
}
}

View File

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

View File

@@ -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!!");
}
}

View File

@@ -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);
}
}

View File

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

View File

@@ -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");

View File

@@ -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();
}
}

View File

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

View File

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

View File

@@ -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;
}
}

View File

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

View File

@@ -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;
}
}

View File

@@ -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));

View File

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

View File

@@ -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);

View File

@@ -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);

View File

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

View File

@@ -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";

View File

@@ -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)));
}