diff --git a/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java b/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java index 7c044567..989372fb 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java @@ -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 factors = new TreeSet(); - /* 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)); } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/Window.java b/src/main/java/io/github/dsheirer/dsp/filter/Window.java index ceb68afb..a97f1453 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/Window.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/Window.java @@ -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 + * You should have received a copy of the GNU General Public License along with this program. + * If not, see * ******************************************************************************/ + +/* + * 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 + * + * 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)); + } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java index d727d72c..e2bf21b0 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java @@ -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)); } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java index 4045673b..3a3a2750 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java @@ -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 mSourceEventBroadcaster = new Broadcaster<>(); private IReusableBufferProvider mReusableBufferProvider; @@ -64,19 +79,12 @@ public class PolyphaseChannelManager implements ISourceEventProcessor private Map 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 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: diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/TwoChannelSynthesizerM2.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/TwoChannelSynthesizerM2.java index 02a142cc..acc74239 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/TwoChannelSynthesizerM2.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/TwoChannelSynthesizerM2.java @@ -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!!"); - } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java index 42871dac..7852bd6d 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java @@ -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 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); + } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java index dc8aec31..c4298aef 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java @@ -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) diff --git a/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java b/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java index c4e3a909..a61f04f2 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java @@ -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"); diff --git a/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter2.java b/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter2.java new file mode 100644 index 00000000..30dd818e --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter2.java @@ -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 + * + ******************************************************************************/ +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(); + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter_CB_CB.java b/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter_CB_CB.java index 7e9ccc71..bb1e3c0a 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter_CB_CB.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter_CB_CB.java @@ -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 { private Listener mListener; diff --git a/src/main/java/io/github/dsheirer/dsp/filter/fir/real/RealFIRFilter.java b/src/main/java/io/github/dsheirer/dsp/filter/fir/real/RealFIRFilter.java index 1666de80..bc42fb6b 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/fir/real/RealFIRFilter.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/fir/real/RealFIRFilter.java @@ -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; diff --git a/src/main/java/io/github/dsheirer/dsp/filter/fir/real/RealFIRFilter2.java b/src/main/java/io/github/dsheirer/dsp/filter/fir/real/RealFIRFilter2.java new file mode 100644 index 00000000..7122ea76 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/filter/fir/real/RealFIRFilter2.java @@ -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 + * + ******************************************************************************/ +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; + } +} \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/dsp/filter/fir/real/RealFIRFilter_RB_RB.java b/src/main/java/io/github/dsheirer/dsp/filter/fir/real/RealFIRFilter_RB_RB.java index 22197bbe..5f9a7669 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/fir/real/RealFIRFilter_RB_RB.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/fir/real/RealFIRFilter_RB_RB.java @@ -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 { diff --git a/src/main/java/io/github/dsheirer/dsp/filter/fir/real/RealFIRFilter_R_R.java b/src/main/java/io/github/dsheirer/dsp/filter/fir/real/RealFIRFilter_R_R.java deleted file mode 100644 index f96feda4..00000000 --- a/src/main/java/io/github/dsheirer/dsp/filter/fir/real/RealFIRFilter_R_R.java +++ /dev/null @@ -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; - } -} diff --git a/src/main/java/io/github/dsheirer/gui/channelizer/ChannelizerViewer.java b/src/main/java/io/github/dsheirer/gui/channelizer/ChannelizerViewer.java index 0537bf77..8565848b 100644 --- a/src/main/java/io/github/dsheirer/gui/channelizer/ChannelizerViewer.java +++ b/src/main/java/io/github/dsheirer/gui/channelizer/ChannelizerViewer.java @@ -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, 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)); diff --git a/src/main/java/io/github/dsheirer/gui/channelizer/SynthesizerViewer.java b/src/main/java/io/github/dsheirer/gui/channelizer/SynthesizerViewer.java index 05997803..14e9aedf 100644 --- a/src/main/java/io/github/dsheirer/gui/channelizer/SynthesizerViewer.java +++ b/src/main/java/io/github/dsheirer/gui/channelizer/SynthesizerViewer.java @@ -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) diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FM.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FM.java index 1155ec58..14bc4d33 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FM.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FM.java @@ -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 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); diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSM.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSM.java index d9dea665..064652cf 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSM.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSM.java @@ -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 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); diff --git a/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java index 8d9a809c..37c6a1c0 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java @@ -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; diff --git a/src/main/java/io/github/dsheirer/spectrum/DFTProcessor.java b/src/main/java/io/github/dsheirer/spectrum/DFTProcessor.java index 4a281381..9c0f11c6 100644 --- a/src/main/java/io/github/dsheirer/spectrum/DFTProcessor.java +++ b/src/main/java/io/github/dsheirer/spectrum/DFTProcessor.java @@ -44,7 +44,7 @@ import java.util.concurrent.atomic.AtomicBoolean; public class DFTProcessor implements Listener, 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"; diff --git a/src/main/java/io/github/dsheirer/spectrum/SpectrumPanel.java b/src/main/java/io/github/dsheirer/spectrum/SpectrumPanel.java index 54738e6f..764e63ac 100644 --- a/src/main/java/io/github/dsheirer/spectrum/SpectrumPanel.java +++ b/src/main/java/io/github/dsheirer/spectrum/SpectrumPanel.java @@ -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))); }