From 8946d789ff97f4b2f94fb13ba78099360edf85c0 Mon Sep 17 00:00:00 2001 From: Denny Date: Tue, 4 Sep 2018 08:16:57 -0400 Subject: [PATCH] WIP - updated tuners to use CIC decimation tuner channel sources instead of polyphase channelizer. Channel sample rate reduced to 30 kHz - seems to work better with P25 decoders than previous 25 kHz. --- .../dsheirer/channel/state/ChannelState.java | 2 + .../github/dsheirer/dsp/filter/Filters.java | 27 +- .../channelizer/PolyphaseChannelManager.java | 31 -- .../channelizer/PolyphaseChannelSource.java | 2 +- .../filter/cic/ComplexPrimeCICDecimate2.java | 493 ++++++++++++++++++ .../dsp/filter/design/FilterViewer.java | 51 +- .../complex/ComplexHalfBandFilter2.java | 108 ++++ .../filter/halfband/real/HalfBandFilter2.java | 89 ++++ .../dsp/mixer/AbstractOscillator.java | 2 +- .../dsp/mixer/LowPhaseNoiseOscillator.java | 54 ++ .../gui/channelizer/ChannelizerViewer.java | 3 +- .../module/decode/p25/P25DecoderLSM.java | 5 + .../dsheirer/record/wave/WaveWriter.java | 2 +- .../ReusableComplexBufferAssembler.java | 23 + .../sample/decimator/ComplexDecimator.java | 113 ++-- .../github/dsheirer/source/tuner/Tuner.java | 6 +- .../tuner/channel/CICTunerChannelSource2.java | 239 +++++++++ .../tuner/channel/TunerChannelSource.java | 18 +- .../manager/CenterFrequencyCalculator.java | 150 ++++++ .../tuner/manager/ChannelSourceManager.java | 3 - .../HeterodyneChannelSourceManager.java | 220 ++++++++ .../manager/TunerChannelSourceManager.java | 7 - .../tuner/rtl/RTL2832TunerController.java | 2 +- .../tuner/test/TestTunerController.java | 1 + 24 files changed, 1528 insertions(+), 123 deletions(-) create mode 100644 src/main/java/io/github/dsheirer/dsp/filter/cic/ComplexPrimeCICDecimate2.java create mode 100644 src/main/java/io/github/dsheirer/dsp/filter/halfband/complex/ComplexHalfBandFilter2.java create mode 100644 src/main/java/io/github/dsheirer/dsp/filter/halfband/real/HalfBandFilter2.java create mode 100644 src/main/java/io/github/dsheirer/source/tuner/channel/CICTunerChannelSource2.java create mode 100644 src/main/java/io/github/dsheirer/source/tuner/manager/CenterFrequencyCalculator.java create mode 100644 src/main/java/io/github/dsheirer/source/tuner/manager/HeterodyneChannelSourceManager.java diff --git a/src/main/java/io/github/dsheirer/channel/state/ChannelState.java b/src/main/java/io/github/dsheirer/channel/state/ChannelState.java index 1d192bc6..a7896f01 100644 --- a/src/main/java/io/github/dsheirer/channel/state/ChannelState.java +++ b/src/main/java/io/github/dsheirer/channel/state/ChannelState.java @@ -148,6 +148,8 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder mTrafficChannelCallEvent = null; mSquelchLocked = false; + + setState(State.TEARDOWN); } public void dispose() diff --git a/src/main/java/io/github/dsheirer/dsp/filter/Filters.java b/src/main/java/io/github/dsheirer/dsp/filter/Filters.java index 1d036505..fe4cd96a 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/Filters.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/Filters.java @@ -1719,7 +1719,32 @@ public enum Filters 0.011873357051047719f, 0.0f, -0.007901319195893647f, 0.0f, 0.005055504379767936f, 0.0f, -0.003054430179754289f, 0.0f, 0.001695637278417295f, 0.0f, -0.000998606272947510f - } ); + }), + + /** + * FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() + * .sampleRate(100) + * .gridDensity(16) + * .oddLength(true) + * .passBandCutoff(21) + * .passBandAmplitude(1.0) + * .passBandRipple(0.01) + * .stopBandStart(29) + * .stopBandAmplitude(0.0) + * .stopBandRipple(0.01) //Approximately -85 dB suppression + * .build(); + */ + HALF_BAND_FILTER_27T(new float[] + { + 0.007495115f, 0.0f, -0.011045938f, 0.0f, + 0.019129561f, 0.0f, -0.031747308f, 0.0f, + 0.053220540f, 0.0f, -0.099569574f, 0.0f, + 0.316083550f, 0.5f, 0.316083550f, 0.0f, + -0.099569574f, 0.0f, 0.053220540f, 0.0f, + -0.031747308f, 0.0f, 0.019129561f, 0.0f, + -0.011045938f, 0.0f, 0.007495115f + + }); float[] mCoefficients; 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 eeadaadc..86ad51ab 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 @@ -34,7 +34,6 @@ import io.github.dsheirer.source.tuner.channel.TunerChannelSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -172,36 +171,6 @@ public class PolyphaseChannelManager implements ISourceEventProcessor return channelSource; } - public TunerChannelSource getTestChannel(int index) - { - PolyphaseChannelSource channelSource = null; - - try - { - List polyphaseIndexes = new ArrayList<>(); - polyphaseIndexes.add(index); - - IPolyphaseChannelOutputProcessor outputProcessor = getOutputProcessor(polyphaseIndexes); - - - if(outputProcessor != null) - { - long centerFrequency = mChannelCalculator.getCenterFrequencyForIndexes(polyphaseIndexes); - - TunerChannel tunerChannel = new TunerChannel(centerFrequency, 12500); - - channelSource = new PolyphaseChannelSource(tunerChannel, outputProcessor, mChannelSourceEventListener, - mChannelCalculator.getChannelSampleRate(), centerFrequency); - } - } - catch(IllegalArgumentException iae) - { - mLog.error("Couldn't get test channel with index:" + index, iae); - } - - return channelSource; - } - /** * Creates a processor to process the channelizer channel indexes into a composite output stream providing * channelized complex sample buffers to a registered source listener. diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java index f8689efb..7b33b9aa 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java @@ -201,7 +201,7 @@ public class PolyphaseChannelSource extends TunerChannelSource * @param frequency in hertz */ @Override - protected void setFrequency(long frequency) + public void setFrequency(long frequency) { mIndexCenterFrequency = frequency; diff --git a/src/main/java/io/github/dsheirer/dsp/filter/cic/ComplexPrimeCICDecimate2.java b/src/main/java/io/github/dsheirer/dsp/filter/cic/ComplexPrimeCICDecimate2.java new file mode 100644 index 00000000..cc9bbab6 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/filter/cic/ComplexPrimeCICDecimate2.java @@ -0,0 +1,493 @@ +/******************************************************************************* + * 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.cic; + +import io.github.dsheirer.dsp.filter.design.FilterDesignException; +import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification; +import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter2; +import io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesigner; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.ReusableComplexBufferAssembler; +import io.github.dsheirer.sample.complex.ComplexSampleListener; +import io.github.dsheirer.sample.decimator.ComplexDecimator; +import org.apache.commons.lang3.Validate; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Non-Recursive Prime-Factor CIC Filter. + * + * Implements the CIC filter described in Understanding Digital Signal Processing, 3e, Lyons, on page 769. This + * filter is comprised of multiple decimating stages each with a prime factor decimation rate. Multiple stages are + * cascaded to achieve the overall decimation rate. + * + * This filter currently supports a maximum decimation rate of 2801. Higher decimation rates can be added by + * adding additional prime factors to the PRIMES array. + */ +public class ComplexPrimeCICDecimate2 implements Listener +{ +// private final static Logger mLog = LoggerFactory.getLogger(ComplexPrimeCICDecimate2.class); + + /** + * PRIME numbers to use for decimation stage sizing. This list contains values that should + * support input sample rates up to 70 MHz with an output channel rate of at least 25 kHz. + */ + public static int[] PRIMES = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, + 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, + 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, + 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, + 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, + 467, 479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593, + 599, 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, + 709, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, 811, 821, 823, 827, + 829, 839, 853, 857, 859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, 941, 947, 953, + 967, 971, 977, 983, 991, 997, 997, 1009, 1013, 1019, 1021, 1031, 1033, 1039, 1049, 1051, + 1061, 1063, 1069, 1087, 1091, 1093, 1097, 1103, 1109, 1117, 1123, 1129, 1151, 1153, 1163, + 1171, 1181, 1187, 1193, 1201, 1213, 1217, 1223, 1229, 1231, 1237, 1249, 1259, 1277, 1279, + 1283, 1289, 1291, 1297, 1301, 1303, 1307, 1319, 1321, 1327, 1361, 1367, 1373, 1381, 1399, + 1409, 1423, 1427, 1429, 1433, 1439, 1447, 1451, 1453, 1459, 1471, 1481, 1483, 1487, 1489, + 1493, 1499, 1511, 1523, 1531, 1543, 1549, 1553, 1559, 1567, 1571, 1579, 1583, 1597, 1601, + 1607, 1609, 1613, 1619, 1621, 1627, 1637, 1657, 1663, 1667, 1669, 1693, 1697, 1699, 1709, + 1721, 1723, 1733, 1741, 1747, 1753, 1759, 1777, 1783, 1787, 1789, 1801, 1811, 1823, 1831, + 1847, 1861, 1867, 1871, 1873, 1877, 1879, 1889, 1901, 1907, 1913, 1931, 1933, 1949, 1951, + 1973, 1979, 1987, 1993, 1997, 1999, 2003, 2011, 2017, 2027, 2029, 2039, 2053, 2063, 2069, + 2081, 2083, 2087, 2089, 2099, 2111, 2113, 2129, 2131, 2137, 2141, 2143, 2153, 2161, 2179, + 2203, 2207, 2213, 2221, 2237, 2239, 2243, 2251, 2267, 2269, 2273, 2281, 2287, 2293, 2297, + 2309, 2311, 2333, 2339, 2341, 2347, 2351, 2357, 2371, 2377, 2381, 2383, 2389, 2393, 2399, + 2411, 2417, 2423, 2437, 2441, 2447, 2459, 2467, 2473, 2477, 2503, 2521, 2531, 2539, 2543, + 2549, 2551, 2557, 2579, 2591, 2593, 2609, 2617, 2621, 2633, 2647, 2657, 2659, 2663, 2671, + 2677, 2683, 2687, 2689, 2693, 2699, 2707, 2711, 2713, 2719, 2729, 2731, 2741, 2749, 2753, + 2767, 2777, 2789, 2791, 2797, 2801 + }; + + private static Map sLowPassFilters = new HashMap(); + private static Map> sPrimeFactors = new HashMap(); + + private List mDecimatingStages = new ArrayList(); + private DecimatingStage mFirstDecimatingStage; + private Output mOutput; + + /** + * Constructs a new decimation filter. + * + * An example setup for a 2.4 MHz input sample rate and 25 kHz output channel rate with a desired 12.5 kHz channel + * pass band might be: + * sampleRate = 2,400,000.0 + * decimation = 96 + * passFrequency = 6250 (1/4 of the channel rate) + * stopFrequency = 7000 + * + * Note: the transition bandwidth (stop - start) directly impacts the size of the low-pass cleanup filter, + * where smaller transition bandwidth creates a longer filter that requires more processing effort. + * + * @param sampleRate of the input sample stream + * @param decimation - overall decimation rate + * @param passFrequency for the final cleanup filter + * @param stopFrequency for the final cleanup filter + * @throws FilterDesignException if a final low-pass cleanup filter cannot be created for the output channel rate + * and specified pass/stop frequencies. + */ + public ComplexPrimeCICDecimate2(double sampleRate, int decimation, int passFrequency, int stopFrequency) + throws FilterDesignException + { + Validate.isTrue(decimation <= PRIMES[PRIMES.length - 1]); + + List stageSizes = getPrimeFactors(decimation); + + for(int x = 0; x < stageSizes.size(); x++) + { + DecimatingStage stage = new DecimatingStage(stageSizes.get(x), 1); + + mDecimatingStages.add(stage); + + if(x == 0) + { + /* Reference to first stage -- will receive all samples */ + mFirstDecimatingStage = stage; + } + else + { + /* Wire the current stage to the previous stage */ + mDecimatingStages.get(x - 1).setListener(stage); + } + } + + double channelRate = sampleRate / (double)decimation; + + mOutput = new Output(channelRate, passFrequency, stopFrequency); + + mDecimatingStages.get(mDecimatingStages.size() - 1).setListener(mOutput); + } + + public void dispose() + { + for(DecimatingStage stage : mDecimatingStages) + { + stage.dispose(); + } + + mDecimatingStages.clear(); + mDecimatingStages = null; + mFirstDecimatingStage = null; + + mOutput.dispose(); + mOutput = null; + } + + /** + * Adds a listener to receive the output of this CIC decimation filter + */ + public void setListener(Listener listener) + { + mOutput.setListener(listener); + } + + /** + * Removes listener from output of this CIC decimation filter + */ + public void removeListener() + { + mOutput.removeListener(); + } + + /** + * Calculates the prime factors of the decimation rate. If you wish to have decimation rates higher than the + * highest rate listed in the PRIMES array, then add additional prime factors to the PRIMES array. + * + * @param decimation - integral decimation rate + * @return - ordered list (smallest to largest) of prime factors + */ + public static List getPrimeFactors(int decimation) + { + if(decimation > PRIMES[PRIMES.length - 1]) + { + throw new IllegalArgumentException("Decimation rates higher than [" + PRIMES[PRIMES.length - 1] + + "] are not supported. Update the PRIMES array to add support for higher decimation rates"); + } + + if(sPrimeFactors.containsKey(decimation)) + { + return sPrimeFactors.get(decimation); + } + + List primeFactors = new ArrayList(); + + int pointer = 0; + + while(decimation > 0 && pointer < PRIMES.length) + { + int prime = PRIMES[pointer]; + + if(decimation % prime == 0) + { + primeFactors.add(prime); + decimation /= prime; + } + else + { + pointer++; + } + } + + sPrimeFactors.put(decimation, primeFactors); + + return primeFactors; + } + + /** + * Primary input method for receiving sample arrays composed as I,Q,I,Q, etc. + */ + @Override + public void receive(ReusableComplexBuffer buffer) + { + if(mFirstDecimatingStage != null) + { + float[] samples = buffer.getSamples(); + + for(int x = 0; x < samples.length; x += 2) + { + mFirstDecimatingStage.receive(samples[x], samples[x + 1]); + } + } + + buffer.decrementUserCount(); + } + + /** + * Decimating stage combines multiple CIC stages with a decimator. The + * number of stages is indicated by the order value and the size indicates + * the decimation rate of this stage. + */ + public class DecimatingStage implements ComplexSampleListener + { + private List mStages = new ArrayList(); + private Stage mFirstStage; + private ComplexDecimator mDecimator; + + public DecimatingStage(int size, int order) + { + for(int x = 0; x < order; x++) + { + Stage stage; + + if(size == 2) + { + stage = new TwoStage(); + } + else + { + stage = new Stage(size); + } + + mStages.add(stage); + + if(x == 0) + { + mFirstStage = stage; + } + else + { + mStages.get(x - 1).setListener(stage); + } + } + + mDecimator = new ComplexDecimator(size); + mStages.get(mStages.size() - 1).setListener(mDecimator); + } + + public void dispose() + { + for(Stage stage : mStages) + { + stage.dispose(); + } + + mStages.clear(); + mDecimator.dispose(); + mDecimator = null; + mFirstStage = null; + mStages = null; + } + + @Override + public void receive(float i, float q) + { + mFirstStage.receive(i, q); + } + + public void setListener(ComplexSampleListener listener) + { + mDecimator.setListener(listener); + } + } + + /** + * Single non-decimating CIC stage component. Uses a circular buffer and a running average internally to implement + * the stage so that stage size has essentially no impact on the computational requirements of the stage + */ + public class Stage implements ComplexSampleListener + { + protected ComplexSampleListener mListener; + + private float[] mISamples; + private float[] mQSamples; + + protected float mISum; + protected float mQSum; + + private int mSamplePointer = 0; + private int mSize; + + protected float mGain; + + protected Stage() + { + } + + public Stage(int size) + { + mSize = size - 1; + + mISamples = new float[mSize]; + mQSamples = new float[mSize]; + + float baseGain = 1.0f / (float)size; + mGain = baseGain * (1.0f / (1.0f - baseGain)); + } + + public void dispose() + { + mListener = null; + } + + public void receive(float i, float q) + { + /* Subtract the oldest sample and add in the newest sample */ + mISum = mISum - mISamples[mSamplePointer] + i; + mQSum = mQSum - mQSamples[mSamplePointer] + q; + + /* Overwrite the oldest sample with the newest */ + mISamples[mSamplePointer] = i; + mQSamples[mSamplePointer] = q; + + mSamplePointer++; + + if(mSamplePointer >= mSize) + { + mSamplePointer = 0; + } + + if(mListener != null) + { + mListener.receive((mISum * mGain), (mQSum * mGain)); + } + } + + public void setListener(ComplexSampleListener listener) + { + mListener = listener; + } + } + + /** + * Size 2 stage that eliminates the unnecessary circular buffer management for a two-stage. + */ + public class TwoStage extends Stage + { + public TwoStage() + { + mGain = 0.5f; + } + + public void receive(float i, float q) + { + if(mListener != null) + { + mListener.receive(((mISum + i) * mGain), ((mQSum + q) * mGain)); + } + + mISum = i; + mQSum = q; + } + } + + + /** + * Output adapter - applies gain correction and cleanup filter and broadcast + * to registered listener. + */ + public class Output implements ComplexSampleListener + { + /* Decimated output buffers will contain 1024 complex samples */ + private ReusableComplexBufferAssembler mBufferAssembler; + private ComplexFIRFilter2 mLowPassFilter; + private Listener mReusableComplexBufferListener; + + public Output(double outputSampleRate, int passFrequency, int stopFrequency) throws FilterDesignException + { + mBufferAssembler = new ReusableComplexBufferAssembler(2400, outputSampleRate); + + //This may throw an exception if we can't design a filter for the sample rate and pass/stop frequencies + float[] filterCoefficients = getLowPassFilter(outputSampleRate, passFrequency, stopFrequency); + mLowPassFilter = new ComplexFIRFilter2(filterCoefficients, 1.0f); + + mBufferAssembler.setListener(new Listener() + { + @Override + public void receive(ReusableComplexBuffer reusableComplexBuffer) + { + if(mReusableComplexBufferListener != null) + { + ReusableComplexBuffer filteredBuffer = mLowPassFilter.filter(reusableComplexBuffer); + mReusableComplexBufferListener.receive(filteredBuffer); +// mReusableComplexBufferListener.receive(reusableComplexBuffer); + } + else + { + reusableComplexBuffer.decrementUserCount(); + } + } + }); + } + + public void dispose() + { + mBufferAssembler.dispose(); + mLowPassFilter.dispose(); + } + + /** + * Interface for receiving CIC decimated output samples + */ + @Override + public void receive(float inphase, float quadrature) + { + mBufferAssembler.receive(inphase, quadrature); + } + + public void setListener(Listener listener) + { + mReusableComplexBufferListener = listener; + } + + public void removeListener() + { + mReusableComplexBufferListener = null; + } + + /** + * Creates a low-pass filter to use as the final cleanup filter for the decimated output stream + * + * @param sampleRate for the final output channel rate + * @param passFrequency for half of the desired channel rate + * @param stopFrequency for the attenuated band + * @return a newly designed filter or a previously designed (cached) filter + * @throws FilterDesignException + */ + private float[] getLowPassFilter(double sampleRate, int passFrequency, int stopFrequency) throws FilterDesignException + { + //Use existing filter if we've already designed one + if(sLowPassFilters.containsKey((int)sampleRate)) + { + return sLowPassFilters.get((int)sampleRate); + } + + FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() + .sampleRate(sampleRate) + .gridDensity(16) + .passBandCutoff(passFrequency) + .passBandAmplitude(1.0) + .passBandRipple(0.01) + .stopBandStart(stopFrequency) + .stopBandAmplitude(0.0) + .stopBandRipple(0.01) + .build(); + + RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification); + + //This will throw an exception if the filter cannot be designed + float[] taps = designer.getImpulseResponse(); + + sLowPassFilters.put((int)sampleRate, taps); + + return taps; + } + } +} 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 9cf838eb..c6bbb29f 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 @@ -8,6 +8,8 @@ import javafx.stage.Stage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Arrays; + public class FilterViewer extends Application { private final static Logger mLog = LoggerFactory.getLogger(FilterViewer.class); @@ -39,16 +41,27 @@ public class FilterViewer extends Application */ private float[] getFilter() { + FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() + .sampleRate(25000) + .gridDensity(16) + .passBandCutoff(6000) + .passBandAmplitude(1.0) + .passBandRipple(0.01) + .stopBandStart(6700) + .stopBandAmplitude(0.0) + .stopBandRipple(0.03) + .build(); + // FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() -// .sampleRate(8000) +// .sampleRate(100) // .gridDensity(16) // .oddLength(true) -// .passBandCutoff(300) +// .passBandCutoff(21) // .passBandAmplitude(1.0) // .passBandRipple(0.01) -// .stopBandStart(500) +// .stopBandStart(29) // .stopBandAmplitude(0.0) -// .stopBandRipple(0.03) //Approximately 60 dB attenuation +// .stopBandRipple(0.01) // .build(); // // FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder() @@ -61,15 +74,15 @@ public class FilterViewer extends Application // .passBandRipple(0.01) // .build(); - FIRFilterSpecification specification = FIRFilterSpecification.bandPassBuilder() - .sampleRate(8000) - .stopFrequency1(1000) - .passFrequencyBegin(1100) - .passFrequencyEnd(1900) - .stopFrequency2(2000) - .stopRipple(0.000001) - .passRipple(0.00001) - .build(); +// FIRFilterSpecification specification = FIRFilterSpecification.bandPassBuilder() +// .sampleRate(8000) +// .stopFrequency1(1000) +// .passFrequencyBegin(1100) +// .passFrequencyEnd(1900) +// .stopFrequency2(2000) +// .stopRipple(0.000001) +// .passRipple(0.00001) +// .build(); float[] taps = null; @@ -102,7 +115,19 @@ public class FilterViewer extends Application throw new IllegalStateException("Couldn't design filter"); } +// try +// { +// taps = FilterFactory.getSincM2Synthesizer(0.25, 2, 9); +// taps = FilterFactory.getSincM2Channelizer(12500.0, 2, 9, true); +// taps = FilterFactory.getSincM2Synthesizer( 25000.0, 12500.0, 2, 9); +// +// } +// catch(FilterDesignException e) +// { +// e.printStackTrace(); +// } + mLog.debug(Arrays.toString(taps)); return taps; } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/halfband/complex/ComplexHalfBandFilter2.java b/src/main/java/io/github/dsheirer/dsp/filter/halfband/complex/ComplexHalfBandFilter2.java new file mode 100644 index 00000000..6ab51f04 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/filter/halfband/complex/ComplexHalfBandFilter2.java @@ -0,0 +1,108 @@ +/******************************************************************************* + * 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.halfband.complex; + +import io.github.dsheirer.dsp.filter.Filters; +import io.github.dsheirer.dsp.filter.halfband.real.HalfBandFilter2; +import io.github.dsheirer.dsp.mixer.Oscillator; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.ReusableComplexBufferQueue; + +public class ComplexHalfBandFilter2 +{ + private ReusableComplexBufferQueue mBufferQueue = new ReusableComplexBufferQueue("ComplexHalfBandFilter2"); + + private HalfBandFilter2 mIFilter; + private HalfBandFilter2 mQFilter; + private float mResidualISample; + private float mResidualQSample; + private boolean mHasResidual; + private int mOutputBufferLength; + private float[] mSamples; + private int mSamplesPointer; + private float[] mFilteredSamples; + private int mFilteredSamplesPointer; + + public ComplexHalfBandFilter2(float[] coefficients, float gain) + { + mIFilter = new HalfBandFilter2(coefficients, gain); + mQFilter = new HalfBandFilter2(coefficients, gain); + } + + public ReusableComplexBuffer filter(ReusableComplexBuffer originalBuffer) + { + mSamples = originalBuffer.getSamples(); + mSamplesPointer = 0; + + mOutputBufferLength = (mHasResidual ? (mSamples.length + 2) : mSamples.length) / 4 * 2; + + ReusableComplexBuffer filteredBuffer = mBufferQueue.getBuffer(mOutputBufferLength); + mFilteredSamples = filteredBuffer.getSamples(); + mFilteredSamplesPointer = 0; + + if(mHasResidual) + { + mFilteredSamples[mFilteredSamplesPointer++] = mIFilter.filter(mResidualISample, mSamples[mSamplesPointer++]); + mFilteredSamples[mFilteredSamplesPointer++] = mQFilter.filter(mResidualQSample, mSamples[mSamplesPointer++]); + mHasResidual = false; + } + + while(mSamplesPointer + 3 < mSamples.length) + { + mFilteredSamples[mFilteredSamplesPointer++] = mIFilter.filter(mSamples[mSamplesPointer], mSamples[mSamplesPointer + 2]); + mFilteredSamples[mFilteredSamplesPointer++] = mIFilter.filter(mSamples[mSamplesPointer + 1], mSamples[mSamplesPointer + 3]); + mSamplesPointer += 4; + } + + if(mSamplesPointer < mSamples.length) + { + mHasResidual = true; + mResidualISample = mSamples[mSamplesPointer++]; + mResidualQSample = mSamples[mSamplesPointer]; + } + + originalBuffer.decrementUserCount(); + return filteredBuffer; + } + + public static void main(String[] args) + { + Oscillator oscillator = new Oscillator(1, 16); + ComplexHalfBandFilter2 filter = new ComplexHalfBandFilter2(Filters.HALF_BAND_FILTER_27T.getCoefficients(), 1.0f); + + ReusableComplexBufferQueue bufferQueue = new ReusableComplexBufferQueue("Test"); + + ReusableComplexBuffer complexBuffer = bufferQueue.getBuffer(8); + oscillator.generateComplex(complexBuffer); + ReusableComplexBuffer filteredBuffer = filter.filter(complexBuffer); + filteredBuffer.decrementUserCount(); + + ReusableComplexBuffer complexBuffer2 = bufferQueue.getBuffer(10); + oscillator.generateComplex(complexBuffer2); + ReusableComplexBuffer filteredBuffer2 = filter.filter(complexBuffer2); + filteredBuffer2.decrementUserCount(); + + ReusableComplexBuffer complexBuffer3 = bufferQueue.getBuffer(8); + oscillator.generateComplex(complexBuffer3); + ReusableComplexBuffer filteredBuffer3 = filter.filter(complexBuffer3); + filteredBuffer3.decrementUserCount(); + + + + + + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/filter/halfband/real/HalfBandFilter2.java b/src/main/java/io/github/dsheirer/dsp/filter/halfband/real/HalfBandFilter2.java new file mode 100644 index 00000000..4b4ccbde --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/filter/halfband/real/HalfBandFilter2.java @@ -0,0 +1,89 @@ +/******************************************************************************* + * 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.halfband.real; + +/** + * Implements a half-band filter that produces one filtered output for every two input samples. + * + * This filter is structured for Java 8+ compiler JIT optimization for SIMD instructions when + * supported by the host CPU. + */ +public class HalfBandFilter2 +{ + private static final float CENTER_COEFFICIENT = 0.5f; + private float[] mCoefficients; + private float[] mEvenSamples; + private float[] mOddSamples; + private int mCenterSampleIndex; + private float mAccumulator; + private int mPointer; + private float mGain; + + /** + * Creates a half band filter with inherent decimation by two. + * + * @param coefficients of the half-band filter that is odd-length where all odd index coefficients are + * zero valued except for the middle odd index coefficient which should be valued 0.5 + * @param gain value to apply to the output + */ + public HalfBandFilter2(float[] coefficients, float gain) + { + if(coefficients.length % 2 == 0) + { + throw new IllegalArgumentException("Half-band filter coefficients must be odd-length"); + } + + mGain = gain; + + int half = coefficients.length / 2 + 1; + + mCoefficients = new float[half]; + mEvenSamples = new float[half]; + + //Use only the even coefficients since the odd coefficients are all zero-valued + for(int x = 0; x < coefficients.length; x += 2) + { + mCoefficients[x / 2] = coefficients[x]; + } + + //Setup the odd samples array as a simple delay line, half the size of the even samples array + mOddSamples = new float[half / 2]; + mCenterSampleIndex = mOddSamples.length - 1; + } + + public float filter(float sample1, float sample2) + { + //Structured for SIMD array copy optimization when supported by host CPU + System.arraycopy(mEvenSamples, 0, mEvenSamples, 1, mEvenSamples.length - 1); + System.arraycopy(mOddSamples, 0, mOddSamples, 1, mOddSamples.length - 1); + + mEvenSamples[0] = sample1; + mOddSamples[0] = sample2; + + mAccumulator = 0.0f; + + //Structured for SIMD dot.product optimization when supported by host CPU + for(mPointer = 0; mPointer < mEvenSamples.length; mPointer++) + { + mAccumulator += (mEvenSamples[mPointer] * mCoefficients[mPointer]); + } + + mAccumulator += (mOddSamples[mCenterSampleIndex] * CENTER_COEFFICIENT); + mAccumulator *= mGain; + + return mAccumulator; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/mixer/AbstractOscillator.java b/src/main/java/io/github/dsheirer/dsp/mixer/AbstractOscillator.java index 144ff1a4..30594c41 100644 --- a/src/main/java/io/github/dsheirer/dsp/mixer/AbstractOscillator.java +++ b/src/main/java/io/github/dsheirer/dsp/mixer/AbstractOscillator.java @@ -127,8 +127,8 @@ public abstract class AbstractOscillator implements IOscillator for(int x = 0; x < sampleCount; x++) { - samples[x] = inphase(); rotate(); + samples[x] = quadrature(); } return samples; diff --git a/src/main/java/io/github/dsheirer/dsp/mixer/LowPhaseNoiseOscillator.java b/src/main/java/io/github/dsheirer/dsp/mixer/LowPhaseNoiseOscillator.java index 09c772d0..7ae6e9dc 100644 --- a/src/main/java/io/github/dsheirer/dsp/mixer/LowPhaseNoiseOscillator.java +++ b/src/main/java/io/github/dsheirer/dsp/mixer/LowPhaseNoiseOscillator.java @@ -18,6 +18,10 @@ package io.github.dsheirer.dsp.mixer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.concurrent.TimeUnit; + public class LowPhaseNoiseOscillator extends AbstractOscillator { private final static Logger mLog = LoggerFactory.getLogger(LowPhaseNoiseOscillator.class); @@ -82,4 +86,54 @@ public class LowPhaseNoiseOscillator extends AbstractOscillator //Update the gain value for the next rotation mGain = THREE_HALVES - ((mPreviousInphase * mPreviousInphase) + (mPreviousQuadrature * mPreviousQuadrature)); } + + public static long process(AbstractOscillator oscillator, int iterations, int sampleCount) + { + long start = System.nanoTime(); + + for(int x = 0; x < iterations; x++) + { + float[] samples = oscillator.generateComplex(sampleCount); + } + + long duration = System.nanoTime() - start; + + return duration; + } + + public static void main(String[] args) + { + mLog.debug("Starting ..."); + SimpleDateFormat sdf = new SimpleDateFormat("mm:ss.SSS"); + + AbstractOscillator lpno = new LowPhaseNoiseOscillator(100, 800); + AbstractOscillator regular = new Oscillator(100, 800); + + int iterations = 10000; + int sampleCount = 1000000; + + mLog.debug("Warm up - lpno"); + process(lpno, iterations, sampleCount); + process(lpno, iterations, sampleCount); + process(lpno, iterations, sampleCount); + + mLog.debug("Warm up - regular"); + process(regular, iterations, sampleCount); + process(regular, iterations, sampleCount); + process(regular, iterations, sampleCount); + + mLog.debug("Test - lpno"); + process(lpno, iterations, sampleCount); + process(lpno, iterations, sampleCount); + long duration = process(lpno, iterations, sampleCount); + mLog.debug("Duration Low Phase Noise: " + sdf.format(new Date(TimeUnit.NANOSECONDS.toMillis(duration)))); + + mLog.debug("Test - regular"); + process(regular, iterations, sampleCount); + process(regular, iterations, sampleCount); + duration = process(regular, iterations, sampleCount); + mLog.debug("Duration Regular: " + sdf.format(new Date(TimeUnit.NANOSECONDS.toMillis(duration)))); + +// mLog.debug(" Delta: " + sdf.format(new Date(TimeUnit.NANOSECONDS.toMillis(duration2 - duration)))); + } } 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 8565848b..8f9df9f9 100644 --- a/src/main/java/io/github/dsheirer/gui/channelizer/ChannelizerViewer.java +++ b/src/main/java/io/github/dsheirer/gui/channelizer/ChannelizerViewer.java @@ -259,7 +259,8 @@ public class ChannelizerViewer extends JFrame for(int x = 0; x < mChannelCount; x++) { - TunerChannelSource source = mTestTuner.getChannelSourceManager().getTestingSource(x); + TunerChannel tunerChannel = new TunerChannel(100000000, 12500); + TunerChannelSource source = mTestTuner.getChannelSourceManager().getSource(tunerChannel); DiscreteChannelPanel channelPanel = new DiscreteChannelPanel(mSettingsManager, source, x); channelPanel.setDFTSize(mChannelPanelDFTSize); 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 be99d126..7a2d3116 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 @@ -26,6 +26,8 @@ import io.github.dsheirer.dsp.psk.pll.AdaptivePLLGainMonitor; import io.github.dsheirer.dsp.psk.pll.CostasLoop; import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; import io.github.dsheirer.source.SourceEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.HashMap; import java.util.Map; @@ -34,6 +36,8 @@ public class P25DecoderLSM extends P25Decoder { protected static final float SAMPLE_COUNTER_GAIN = 0.3f; + private final static Logger mLog = LoggerFactory.getLogger(P25DecoderLSM.class); + private Map mBasebandFilters = new HashMap<>(); private ComplexFIRFilter2 mBasebandFilter; private ComplexFeedForwardGainControl mAGC = new ComplexFeedForwardGainControl(32); @@ -135,6 +139,7 @@ public class P25DecoderLSM extends P25Decoder break; case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE: //Reset the PLL if/when the tuner PPM changes so that we can re-lock + mLog.debug("******** Resetting costas loop for frequency correction change"); mCostasLoop.reset(); break; } diff --git a/src/main/java/io/github/dsheirer/record/wave/WaveWriter.java b/src/main/java/io/github/dsheirer/record/wave/WaveWriter.java index 9a7dd5f3..d6901da9 100644 --- a/src/main/java/io/github/dsheirer/record/wave/WaveWriter.java +++ b/src/main/java/io/github/dsheirer/record/wave/WaveWriter.java @@ -151,7 +151,7 @@ public class WaveWriter implements AutoCloseable int suffix = 1; - while(Files.exists(renamedPath)) + while(Files.exists(renamedPath) && suffix < 4) { String renamedWithSuffix = mFile.getFileName().toString(); renamedWithSuffix = renamedWithSuffix.replace(".tmp", "_" + suffix + ".wav"); diff --git a/src/main/java/io/github/dsheirer/sample/buffer/ReusableComplexBufferAssembler.java b/src/main/java/io/github/dsheirer/sample/buffer/ReusableComplexBufferAssembler.java index 8a06094a..fc4a8c99 100644 --- a/src/main/java/io/github/dsheirer/sample/buffer/ReusableComplexBufferAssembler.java +++ b/src/main/java/io/github/dsheirer/sample/buffer/ReusableComplexBufferAssembler.java @@ -63,6 +63,29 @@ public class ReusableComplexBufferAssembler mListener = null; } + /** + * Adds the samples to this assembler. As each buffer is assembled, it will be dispatched to the registered + * listener. + * + * @param inphase sample to assemble + * @param quadrature sample to assemble + */ + public void receive(float inphase, float quadrature) + { + if(mBuffer.remaining() >= 2) + { + mBuffer.put(inphase); + mBuffer.put(quadrature); + + mTimestampManager.increment(1); + + if(!mBuffer.hasRemaining()) + { + flush(); + } + } + } + /** * Adds the samples to this assembler. As each buffer is assembled, it will be dispatched to the registered * listener. diff --git a/src/main/java/io/github/dsheirer/sample/decimator/ComplexDecimator.java b/src/main/java/io/github/dsheirer/sample/decimator/ComplexDecimator.java index 180ed7c7..b9445ddf 100644 --- a/src/main/java/io/github/dsheirer/sample/decimator/ComplexDecimator.java +++ b/src/main/java/io/github/dsheirer/sample/decimator/ComplexDecimator.java @@ -1,58 +1,75 @@ +/******************************************************************************* + * 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.sample.decimator; import io.github.dsheirer.sample.complex.ComplexSampleListener; - +/** + * Decimator for complex samples + */ public class ComplexDecimator implements ComplexSampleListener { - private ComplexSampleListener mListener; - private int mCounter = 0; - private int mDecimationRate; + private ComplexSampleListener mListener; + private int mCounter = 0; + private int mDecimationRate; - /** - * Constructs a new Decimator object with the specified decimation rate. - */ - public ComplexDecimator( int rate ) - { - mDecimationRate = rate; - } - - public void dispose() - { - mListener = null; - } - - /** - * Sets a new decimation rate. Rate can be changed after this object - * is constructed. - */ - public synchronized void setDecimationRate( int rate ) - { - mDecimationRate = rate; - } - - /** - * Receives samples allowing only 1 of every (rate) sample to go on - * to the registered listener - */ - @Override - public void receive( float i, float q ) + /** + * Constructs a new Decimator object with the specified decimation rate. + */ + public ComplexDecimator(int rate) { - mCounter++; - - if( mCounter >= mDecimationRate ) - { - mListener.receive( i, q ); - - mCounter = 0; - } + mDecimationRate = rate; + } + + public void dispose() + { + mListener = null; + } + + /** + * Sets a new decimation rate. Rate can be changed after this object + * is constructed. + */ + public synchronized void setDecimationRate(int rate) + { + mDecimationRate = rate; + } + + /** + * Receives samples allowing only 1 of every (rate) sample to go on + * to the registered listener + */ + @Override + public void receive(float i, float q) + { + mCounter++; + + if(mCounter >= mDecimationRate) + { + mListener.receive(i, q); + + mCounter = 0; + } + } + + /** + * Sets the decimated output listener + */ + public void setListener(ComplexSampleListener listener) + { + mListener = listener; } - - /** - * Sets the decimated output listener - */ - public void setListener( ComplexSampleListener listener ) - { - mListener = listener; - } } diff --git a/src/main/java/io/github/dsheirer/source/tuner/Tuner.java b/src/main/java/io/github/dsheirer/source/tuner/Tuner.java index 857db597..67e8131e 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/Tuner.java +++ b/src/main/java/io/github/dsheirer/source/tuner/Tuner.java @@ -21,7 +21,7 @@ import io.github.dsheirer.source.ISourceEventProcessor; import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.source.tuner.TunerEvent.Event; import io.github.dsheirer.source.tuner.manager.ChannelSourceManager; -import io.github.dsheirer.source.tuner.manager.TunerChannelSourceManager; +import io.github.dsheirer.source.tuner.manager.HeterodyneChannelSourceManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -51,7 +51,9 @@ public abstract class Tuner implements ISourceEventProcessor //Register to receive frequency and sample rate change notifications mTunerController.addListener(this::process); - mChannelSourceManager = new TunerChannelSourceManager(mTunerController); +// mChannelSourceManager = new TunerChannelSourceManager(mTunerController); + mChannelSourceManager = new HeterodyneChannelSourceManager(mTunerController); + //Register to receive channel count change notifications mChannelSourceManager.addSourceEventListener(this::process); diff --git a/src/main/java/io/github/dsheirer/source/tuner/channel/CICTunerChannelSource2.java b/src/main/java/io/github/dsheirer/source/tuner/channel/CICTunerChannelSource2.java new file mode 100644 index 00000000..4bb77429 --- /dev/null +++ b/src/main/java/io/github/dsheirer/source/tuner/channel/CICTunerChannelSource2.java @@ -0,0 +1,239 @@ +/******************************************************************************* + * 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.source.tuner.channel; + +import io.github.dsheirer.dsp.filter.cic.ComplexPrimeCICDecimate2; +import io.github.dsheirer.dsp.filter.design.FilterDesignException; +import io.github.dsheirer.dsp.mixer.IOscillator; +import io.github.dsheirer.dsp.mixer.LowPhaseNoiseOscillator; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.buffer.OverflowableReusableBufferTransferQueue; +import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; +import io.github.dsheirer.sample.buffer.ReusableComplexBufferQueue; +import io.github.dsheirer.sample.complex.Complex; +import io.github.dsheirer.source.SourceEvent; + +import java.util.ArrayList; +import java.util.List; + +/** + * Complex sample source that provides a frequency-translated and decimated sample buffer stream. Uses a + * CIC decimation filter that requires the decimation rate to be an integer multiple. Sample buffer processing + * occurs on a scheduled runnable thread. + */ +public class CICTunerChannelSource2 extends TunerChannelSource implements Listener +{ +// private final static Logger mLog = LoggerFactory.getLogger(CICTunerChannelSource2.class); + + //Maximum number of filled buffers for the blocking queue + private static final int BUFFER_MAX_CAPACITY = 300; + + //Threshold for resetting buffer overflow condition + private static final int BUFFER_OVERFLOW_RESET_THRESHOLD = 100; + + //Pass band cutoff frequency for the complex channel - target = 12.5 kHz channel + private static final int CHANNEL_PASS_FREQUENCY = 6000; + + //Stop band frequency for the complex channel + private static final int CHANNEL_STOP_FREQUENCY = 6700; + + private OverflowableReusableBufferTransferQueue mBuffer; + private ReusableComplexBufferQueue mReusableComplexBufferQueue = new ReusableComplexBufferQueue("CICTunerChannelSource"); + private IOscillator mFrequencyCorrectionMixer; + private ComplexPrimeCICDecimate2 mDecimationFilter; + private Listener mListener; + private List mSampleBuffers = new ArrayList<>(); + private double mChannelSampleRate; + private long mChannelFrequencyCorrection = 0; + private long mTunerFrequency; + + /** + * Constructs a frequency translating and CIC decimating channel source. + * + * @param producerSourceEventListener to receive sample stream start/stop requests + * @param tunerChannel that details the desired channel frequency and bandwidth + * @param sampleRate of the incoming sample stream + * @param decimation to use in the CIC decimation filter. + * @throws FilterDesignException if a final cleanup filter cannot be designed using the remez filter + * designer and the filter parameters. + */ + public CICTunerChannelSource2(Listener producerSourceEventListener, TunerChannel tunerChannel, + double sampleRate, int decimation) throws FilterDesignException + { + super(producerSourceEventListener, tunerChannel); + + mDecimationFilter = new ComplexPrimeCICDecimate2(sampleRate, decimation, CHANNEL_PASS_FREQUENCY, CHANNEL_STOP_FREQUENCY); + + mBuffer = new OverflowableReusableBufferTransferQueue<>(BUFFER_MAX_CAPACITY, BUFFER_OVERFLOW_RESET_THRESHOLD); + //Setup the frequency mixer to the current source frequency + mChannelSampleRate = sampleRate / (double)decimation; + mTunerFrequency = tunerChannel.getFrequency(); + long frequencyOffset = mTunerFrequency - getTunerChannel().getFrequency(); + + mFrequencyCorrectionMixer = new LowPhaseNoiseOscillator(frequencyOffset, sampleRate); + } + + @Override + public void dispose() + { + // + } + + /** + * Primary interface for receiving incoming complex sample buffers to be frequency translated and decimated. + */ + @Override + public void receive(ReusableComplexBuffer buffer) + { + mBuffer.offer(buffer); + } + + /** + * Sets/updates the center frequency for the sample streaming being sent from the producer. + * + * @param frequency in hertz + */ + @Override + public void setFrequency(long frequency) + { + mTunerFrequency = frequency; + updateMixerFrequencyOffset(); + } + + /** + * Current frequency correction being applied to the channel sample stream + * + * @return correction in hertz + */ + public long getFrequencyCorrection() + { + return mChannelFrequencyCorrection; + } + + /** + * Changes the frequency correction value and broadcasts the change to the registered downstream listener. + * + * @param correction current frequency correction value. + */ + public void setFrequencyCorrection(long correction) + { + mChannelFrequencyCorrection = correction; + updateMixerFrequencyOffset(); + broadcastConsumerSourceEvent(SourceEvent.frequencyCorrectionChange(mChannelFrequencyCorrection)); + } + + /** + * Calculates the local mixer frequency offset from the tuned frequency, + * channel's requested frequency, and channel frequency correction. + */ + private void updateMixerFrequencyOffset() + { + long offset = mTunerFrequency - getTunerChannel().getFrequency() - mChannelFrequencyCorrection; + mFrequencyCorrectionMixer.setFrequency(offset); + } + + /** + * Sets the sample rate of the incoming sample stream from the producer + * + * @param sampleRate in hertz + */ + @Override + protected void setSampleRate(double sampleRate) + { + //Not implemented. Sample rate changes are not permitted once sample stream starts + } + + /** + * Sets the frequency correction for the outbound sample stream to the consumer + * + * @param correction in hertz + */ + @Override + protected void setChannelFrequencyCorrection(long correction) + { + mChannelFrequencyCorrection = correction; + broadcastConsumerSourceEvent(SourceEvent.channelFrequencyCorrectionChange(correction)); + updateMixerFrequencyOffset(); + } + + /** + * Frequency correction value currently being applied to the outbound sample stream + */ + @Override + public long getChannelFrequencyCorrection() + { + return mChannelFrequencyCorrection; + } + + /** + * Sets the listener to receive the complex buffer sample output from this channel + * + * @param complexBufferListener to receive complex buffers + */ + @Override + public void setListener(Listener complexBufferListener) + { + mDecimationFilter.setListener(complexBufferListener); + } + + @Override + public void removeListener(Listener listener) + { + mDecimationFilter.removeListener(); + } + + + @Override + public double getSampleRate() + { + return mChannelSampleRate; + } + + /** + * Primary processing method that is invoked on a recurring basis to process any queued complex buffers. + * + * Mixes the target frequency to baseband and then passes the buffer to the CIC decimation filter + */ + protected void processSamples() + { + mBuffer.drainTo(mSampleBuffers); + + for(ReusableComplexBuffer complexBuffer : mSampleBuffers) + { + float[] samples = complexBuffer.getSamples(); + + ReusableComplexBuffer translatedComplexBuffer = mReusableComplexBufferQueue.getBuffer(samples.length); + float[] translatedSamples = translatedComplexBuffer.getSamples(); + + /* Perform frequency translation */ + for(int x = 0; x < samples.length; x += 2) + { + mFrequencyCorrectionMixer.rotate(); + + translatedSamples[x] = Complex.multiplyInphase(samples[x], samples[x + 1], + mFrequencyCorrectionMixer.inphase(), mFrequencyCorrectionMixer.quadrature()); + + translatedSamples[x + 1] = Complex.multiplyQuadrature(samples[x], samples[x + 1], + mFrequencyCorrectionMixer.inphase(), mFrequencyCorrectionMixer.quadrature()); + } + + mDecimationFilter.receive(translatedComplexBuffer); + complexBuffer.decrementUserCount(); + } + + mSampleBuffers.clear(); + } +} diff --git a/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java b/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java index 8d9f9482..c86ddf51 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java +++ b/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java @@ -66,7 +66,7 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc * Sets the center frequency for the sample streaming being sent from the producer. * @param frequency in hertz */ - protected abstract void setFrequency(long frequency); + public abstract void setFrequency(long frequency); /** * Sets the sample rate of the incoming sample stream from the producer @@ -118,12 +118,7 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc { //Broadcast current frequency and sample rate so consumer can configure correctly broadcastConsumerSourceEvent(SourceEvent.frequencyChange(getFrequency())); - - if(mProducerSourceEventListener != null) - { - mProducerSourceEventListener.receive(SourceEvent.startSampleStreamRequest(this)); - } - + broadcastProducerSourceEvent(SourceEvent.startSampleStreamRequest(this)); mScheduledIntervalProcessor.start(); } @@ -133,12 +128,8 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc */ public void stop() { - if(mProducerSourceEventListener != null) - { - mProducerSourceEventListener.receive(SourceEvent.stopSampleStreamRequest(this)); - mProducerSourceEventListener.receive(SourceEvent.sourceDisposeRequest(this)); - } - + broadcastProducerSourceEvent(SourceEvent.stopSampleStreamRequest(this)); + broadcastProducerSourceEvent(SourceEvent.sourceDisposeRequest(this)); mScheduledIntervalProcessor.stop(); } @@ -171,6 +162,7 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc { switch(sourceEvent.getEvent()) { + case NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE: case NOTIFICATION_FREQUENCY_CHANGE: case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_LOCKED: case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_UNLOCKED: diff --git a/src/main/java/io/github/dsheirer/source/tuner/manager/CenterFrequencyCalculator.java b/src/main/java/io/github/dsheirer/source/tuner/manager/CenterFrequencyCalculator.java new file mode 100644 index 00000000..c61b8ba2 --- /dev/null +++ b/src/main/java/io/github/dsheirer/source/tuner/manager/CenterFrequencyCalculator.java @@ -0,0 +1,150 @@ +/******************************************************************************* + * 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.source.tuner.manager; + +import io.github.dsheirer.source.tuner.TunerController; +import io.github.dsheirer.source.tuner.channel.TunerChannel; + +import java.util.SortedSet; +import java.util.TreeSet; + +/** + * Calculates tuner center frequency for a set of tuner channels. + */ +public class CenterFrequencyCalculator +{ + public static final long INVALID_FREQUENCY = -1; + + /** + * Determines the optimal center frequency for a given tuner and set of tuner channels. + * + * @param tunerController that is providing tuner channels + * @param channels to calculate + * @return optimal center frequency or INVALID_FREQUENCY if a center frequency cannot be calculated. + */ + public static long getCenterFrequency(TunerController tunerController, SortedSet channels) + { + long candidateFrequency = INVALID_FREQUENCY; + + boolean isValidCandidateFrequency = true; + + //If there is only 1 channel set the center frequency so that the channel is positioned to the right of center + if(channels.size() == 1) + { + candidateFrequency = channels.first().getMinFrequency() - tunerController.getMiddleUnusableHalfBandwidth() + 1; + } + else + { + long minChannelFrequency = channels.first().getMinFrequency(); + long maxChannelFrequency = channels.last().getMaxFrequency(); + + //Start by placing the highest frequency channel at the high end of the spectrum + candidateFrequency = maxChannelFrequency - (tunerController.getUsableBandwidth() / 2); + + //Iterate the channels and make sure that none of them overlap the center DC spike buffer, if one exists + if(tunerController.getMiddleUnusableHalfBandwidth() > 0) + { + boolean processingRequired = true; + + while(isValidCandidateFrequency && processingRequired) + { + processingRequired = false; + + long minAvoid = candidateFrequency - tunerController.getMiddleUnusableHalfBandwidth(); + long maxAvoid = candidateFrequency + tunerController.getMiddleUnusableHalfBandwidth(); + + //If any of the center channel(s) overlap the central DC spike avoid area, we'll iteratively + //increase the tuned frequency causing the set of channels to move left in the tuned bandwidth until + //we either find a good center tune frequency, or we walk the lowest frequency channel out of the + //minimum tuned range, in which case we'll throw an exception indicating we don't have a solution. + for(TunerChannel channel : channels) + { + if(channel.overlaps(minAvoid, maxAvoid)) + { + //Calculate a tuned frequency adjustment that places this overlapping channel just to the + //left of the central DC spike avoid zone + long adjustment = channel.getMaxFrequency() - minAvoid + 1; + + //If the candidate frequency doesn't push the lowest channel out of bounds, make adjustment + if(candidateFrequency + adjustment - (tunerController.getUsableBandwidth() / 2) <= minChannelFrequency) + { + candidateFrequency += adjustment; + processingRequired = true; + } + //Otherwise, punt and indicate that we can't find a center frequency or add the channel + else + { + isValidCandidateFrequency = false; + } + + //break out of the for/each loop, so that we can start over again with all of the channels + break; + } + } + } + } + } + + if(isValidCandidateFrequency) + { + return candidateFrequency; + } + + return INVALID_FREQUENCY; + } + + /** + * Indicates if channel along with all of the other currently sourced + * channels can fit within the tunable bandwidth. + * + */ + /** + * + * Indicates if channel along with all of the other currently sourced + * channels can fit within the tunable bandwidth. + + * @param channel to test + * @param tunerController that will provide the channel + * @param channels is a current set of channels being sourced by the tuner controller + * @return + */ + public static boolean canTune(TunerChannel channel, TunerController tunerController, SortedSet channels) + { + //Make sure we're within the tunable frequency range of this tuner + if(tunerController.getMinFrequency() < channel.getMinFrequency() && tunerController.getMaxFrequency() > channel.getMaxFrequency()) + { + //If this is the first lock, then we're good + if(channels.isEmpty()) + { + return true; + } + else + { + SortedSet allChannels = new TreeSet<>(channels); + allChannels.add(channel); + + //If the bandwidth of the channel set is less than or equal to the tuner's usable bandwidth, then + //check to see if we can find a valid center frequency + if(allChannels.last().getMaxFrequency() - allChannels.first().getMinFrequency() <= tunerController.getUsableBandwidth()) + { + return getCenterFrequency(tunerController, allChannels) != INVALID_FREQUENCY; + } + } + } + + return false; + } +} diff --git a/src/main/java/io/github/dsheirer/source/tuner/manager/ChannelSourceManager.java b/src/main/java/io/github/dsheirer/source/tuner/manager/ChannelSourceManager.java index 1b113313..3a3550df 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/manager/ChannelSourceManager.java +++ b/src/main/java/io/github/dsheirer/source/tuner/manager/ChannelSourceManager.java @@ -54,9 +54,6 @@ public abstract class ChannelSourceManager implements ISourceEventProcessor */ public abstract TunerChannelSource getSource(TunerChannel tunerChannel); - //TODO: remove this testing method when finished with polyphase channel manager - public abstract TunerChannelSource getTestingSource(int index); - /** * Adds a listener to receive source events */ diff --git a/src/main/java/io/github/dsheirer/source/tuner/manager/HeterodyneChannelSourceManager.java b/src/main/java/io/github/dsheirer/source/tuner/manager/HeterodyneChannelSourceManager.java new file mode 100644 index 00000000..3e6e8545 --- /dev/null +++ b/src/main/java/io/github/dsheirer/source/tuner/manager/HeterodyneChannelSourceManager.java @@ -0,0 +1,220 @@ +/******************************************************************************* + * 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.source.tuner.manager; + +import io.github.dsheirer.dsp.filter.design.FilterDesignException; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.source.SourceEvent; +import io.github.dsheirer.source.SourceException; +import io.github.dsheirer.source.tuner.TunerController; +import io.github.dsheirer.source.tuner.channel.CICTunerChannelSource2; +import io.github.dsheirer.source.tuner.channel.TunerChannel; +import io.github.dsheirer.source.tuner.channel.TunerChannelSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Channel provider for heterodyne and decimate method of channel provisioning. + */ +public class HeterodyneChannelSourceManager extends ChannelSourceManager +{ + private final static Logger mLog = LoggerFactory.getLogger(HeterodyneChannelSourceManager.class); + + private final static int OBJECTIVE_CHANNEL_SAMPLE_RATE = 30000; + + private List mChannelSources = new CopyOnWriteArrayList<>(); + private SortedSet mTunerChannels = new TreeSet<>(); + private TunerController mTunerController; + private ChannelSourceEventProcessor mChannelSourceEventProcessor = new ChannelSourceEventProcessor(); + + public HeterodyneChannelSourceManager(TunerController tunerController) + { + mTunerController = tunerController; + mTunerController.addListener(this); + } + + @Override + public SortedSet getTunerChannels() + { + return mTunerChannels; + } + + @Override + public int getTunerChannelCount() + { + return mTunerChannels.size(); + } + + @Override + public TunerChannelSource getSource(TunerChannel tunerChannel) + { + if(CenterFrequencyCalculator.canTune(tunerChannel, mTunerController, mTunerChannels)) + { + int decimation = mTunerController.getBandwidth() / OBJECTIVE_CHANNEL_SAMPLE_RATE; + + try + { + //Attempt to create the channel source first, in case we get a filter design exception + CICTunerChannelSource2 tunerChannelSource = new CICTunerChannelSource2(mChannelSourceEventProcessor, + tunerChannel, mTunerController.getSampleRate(), decimation); + + //Add to the list of channel sources so that it will receive the tuner frequency change + mChannelSources.add(tunerChannelSource); + + //Set the current tuner frequency + tunerChannelSource.setFrequency(mTunerController.getFrequency()); + + //Add to the channel list and update the tuner center frequency as needed + mTunerChannels.add(tunerChannel); + updateTunerFrequency(); + broadcast(SourceEvent.channelCountChange(getTunerChannelCount())); + + return tunerChannelSource; + } + catch(FilterDesignException fde) + { + mLog.error("Error creating CIC tuner channel source - couldn't design cleanup filter", fde); + } + } + + return null; + } + + /** + * Calculates a new center frequency and updates the tuner center frequency + */ + private void updateTunerFrequency() + { + long centerFrequency = CenterFrequencyCalculator.getCenterFrequency(mTunerController, getTunerChannels()); + + if(centerFrequency == CenterFrequencyCalculator.INVALID_FREQUENCY) + { + mLog.error("Couldn't calculate center frequency for tuner and tuner channels"); + return; + } + + if(centerFrequency != mTunerController.getFrequency()) + { + try + { + mTunerController.setFrequency(centerFrequency); + } + catch(SourceException se) + { + mLog.error("Couldn't update tuner center frequency to " + centerFrequency, se); + } + } + } + + @Override + public void process(SourceEvent tunerSourceEvent) throws SourceException + { + switch(tunerSourceEvent.getEvent()) + { + case NOTIFICATION_FREQUENCY_CHANGE: + //Tuner center frequency has changed - update channels + updateTunerFrequency(tunerSourceEvent.getValue().longValue()); + break; + case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE: + //The tuner is self-correcting for PPM error - relay to channels + broadcastToChannels(tunerSourceEvent); + break; + case NOTIFICATION_SAMPLE_RATE_CHANGE: + //no-op + break; + default: + mLog.info("Unrecognized Source Event received from tuner: " + tunerSourceEvent); + } + } + + /** + * Broadcasts the source event to any channel + */ + private void broadcastToChannels(SourceEvent sourceEvent) + { + for(CICTunerChannelSource2 channelSource: mChannelSources) + { + try + { + channelSource.process(sourceEvent); + } + catch(Exception e) + { + mLog.error("Error broadcasting source event to channel: " + sourceEvent); + } + } + } + + /** + * Updates all channel sources to use the new tuner center frequency + * + * @param tunerFrequency in hertz + */ + private void updateTunerFrequency(long tunerFrequency) + { + for(CICTunerChannelSource2 channelSource : mChannelSources) + { + channelSource.setFrequency(tunerFrequency); + } + } + + /** + * Processes channel source events + */ + public class ChannelSourceEventProcessor implements Listener + { + @Override + public void receive(SourceEvent sourceEvent) + { + switch(sourceEvent.getEvent()) + { + case REQUEST_START_SAMPLE_STREAM: + if(sourceEvent.getSource() instanceof CICTunerChannelSource2) + { + mTunerController.addBufferListener((CICTunerChannelSource2)sourceEvent.getSource()); + } + break; + case REQUEST_STOP_SAMPLE_STREAM: + if(sourceEvent.getSource() instanceof CICTunerChannelSource2) + { + mTunerController.removeBufferListener((CICTunerChannelSource2)sourceEvent.getSource()); + } + break; + case REQUEST_SOURCE_DISPOSE: + if(sourceEvent.getSource() instanceof CICTunerChannelSource2) + { + CICTunerChannelSource2 channelSource = (CICTunerChannelSource2)sourceEvent.getSource(); + mChannelSources.remove(channelSource); + mTunerChannels.remove(channelSource.getTunerChannel()); + channelSource.dispose(); + broadcast(SourceEvent.channelCountChange(getTunerChannelCount())); + } + break; + case NOTIFICATION_MEASURED_FREQUENCY_ERROR_SYNC_LOCKED: + //Rebroadcast so that the tuner source can process this event + broadcast(sourceEvent); + break; + default: + mLog.info("Unrecognized Source Event received from channel: " + sourceEvent); + } + } + } +} diff --git a/src/main/java/io/github/dsheirer/source/tuner/manager/TunerChannelSourceManager.java b/src/main/java/io/github/dsheirer/source/tuner/manager/TunerChannelSourceManager.java index 9455f161..496f119f 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/manager/TunerChannelSourceManager.java +++ b/src/main/java/io/github/dsheirer/source/tuner/manager/TunerChannelSourceManager.java @@ -49,13 +49,6 @@ public class TunerChannelSourceManager extends ChannelSourceManager mTunerController.addListener(mPolyphaseChannelManager); } - @Override - public TunerChannelSource getTestingSource(int index) - { - //TODO: remove this debug method - return mPolyphaseChannelManager.getTestChannel(index); - } - /** * Indicates if the channel min/max frequencies are within the tunable frequency range of the tuner controller * diff --git a/src/main/java/io/github/dsheirer/source/tuner/rtl/RTL2832TunerController.java b/src/main/java/io/github/dsheirer/source/tuner/rtl/RTL2832TunerController.java index 9b4fb200..9ba5b8d2 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/rtl/RTL2832TunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/rtl/RTL2832TunerController.java @@ -1241,7 +1241,7 @@ public abstract class RTL2832TunerController extends USBTunerController RATE_1_200MHZ(0x0600, 1200000, "1.200 MHz"), RATE_1_440MHZ(0x0500, 1440000, "1.440 MHz"), RATE_1_600MHZ(0x0480, 1600000, "1.600 MHz"), - RATE_1_800MHZ(0x0480, 1800000, "1.800 MHz"), + RATE_1_800MHZ(0x0400, 1800000, "1.800 MHz"), RATE_1_920MHZ(0x03C0, 1920000, "1.920 MHz"), RATE_2_048MHZ(0x0384, 2048000, "2.048 MHz"), RATE_2_304MHZ(0x0320, 2304000, "2.304 MHz"), 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 c2c8540f..51e00065 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 @@ -144,5 +144,6 @@ public class TestTunerController extends TunerController public void setSampleRate(int sampleRate) throws SourceException { mSampleGenerator.setSampleRate(sampleRate); + mFrequencyController.setSampleRate(sampleRate); } }