diff --git a/.idea/dictionaries/denny.xml b/.idea/dictionaries/denny.xml index 30d94b58..a9674196 100644 --- a/.idea/dictionaries/denny.xml +++ b/.idea/dictionaries/denny.xml @@ -3,6 +3,7 @@ bahroo broadcastify + dinf esnid fleetsync heterdyned diff --git a/.idea/libraries/imports.xml b/.idea/libraries/imports.xml index e97bf8ec..b90eef82 100644 --- a/.idea/libraries/imports.xml +++ b/.idea/libraries/imports.xml @@ -6,6 +6,7 @@ + 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 440400f9..a0447635 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java @@ -51,7 +51,7 @@ public class FilterFactory * @param window - to apply against the coefficients * @return */ - public static float[] getSinc(int sampleRate, long frequency, int length, Window.WindowType window) + public static float[] getSinc(double sampleRate, long frequency, int length, Window.WindowType window) { int evenLength = length % 2 == 0 ? length : length + 1; @@ -137,7 +137,7 @@ public class FilterFactory * @param length * @return */ - public static float[] getUnityResponseArray(int sampleRate, long frequency, int length) + public static float[] getUnityResponseArray(double sampleRate, long frequency, int length) { float[] unityArray = new float[length * 2]; @@ -201,7 +201,7 @@ public class FilterFactory * @param windowType - window to apply against the generated coefficients * @return */ - public static float[] getLowPass(int sampleRate, long cutoff, int filterLength, Window.WindowType windowType) + public static float[] getLowPass(double sampleRate, long cutoff, int filterLength, Window.WindowType windowType) { return getSinc(sampleRate, cutoff, filterLength, windowType); } @@ -220,7 +220,7 @@ public class FilterFactory * - passFrequency < stopFrequency * - stopFrequency <= sampleRate/2 */ - public static float[] getLowPass(int sampleRate, int passFrequency, int stopFrequency, int attenuation, + public static float[] getLowPass(double sampleRate, int passFrequency, int stopFrequency, int attenuation, Window.WindowType windowType, boolean forceOddLength) { if(stopFrequency < passFrequency || stopFrequency > (sampleRate / 2)) @@ -323,9 +323,9 @@ public class FilterFactory * @param attenuation in dB * @return */ - public static int getTapCount(int sampleRate, long pass, long stop, int attenuation) + public static int getTapCount(double sampleRate, long pass, long stop, int attenuation) { - double frequency = ((double)stop - (double)pass) / (double)sampleRate; + double frequency = ((double)stop - (double)pass) / sampleRate; return (int)(Math.round((double)attenuation / (22.0d * frequency))); } 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 5fd66a2e..ee5ec599 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 @@ -292,6 +292,10 @@ public class PolyphaseChannelManager implements ISourceEventProcessor //Defer channelizer configuration changes to be handled on the buffer processor thread mBufferSourceEventMonitor.receive(sourceEvent); break; + case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_LOCKED: + case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_UNLOCKED: + //no-op + break; default: mLog.info("Unrecognized source event: " + sourceEvent.toString()); break; 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 b7e26b4a..22354565 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,8 +1,7 @@ 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 io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesigner; import javafx.application.Application; import javafx.scene.Scene; import javafx.stage.Stage; @@ -41,14 +40,15 @@ public class FilterViewer extends Application private float[] getFilter() { FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() - .sampleRate(48000) + .sampleRate(25000) .gridDensity(16) - .passBandCutoff(2500) + .oddLength(true) + .passBandCutoff(6000) .passBandAmplitude(1.0) .passBandRipple(0.01) - .stopBandStart(4000) + .stopBandStart(6500) .stopBandAmplitude(0.0) - .stopBandRipple(0.008) + .stopBandRipple(0.028) //Approximately 60 dB attenuation .build(); // FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder() @@ -73,29 +73,29 @@ public class FilterViewer extends Application float[] taps = null; - try - { - taps = FilterFactory.getSincM2Channelizer(12500.0, 2, 19, Window.WindowType.BLACKMAN_HARRIS_7, true); - } - catch(FilterDesignException fde) - { - mLog.error("Error"); - } - // try // { -// RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification); -// -// if(designer.isValid()) -// { -// taps = designer.getImpulseResponse(); -// } +// taps = FilterFactory.getSincM2Channelizer(12500.0, 2, 19, Window.WindowType.BLACKMAN_HARRIS_7, true); // } // catch(FilterDesignException fde) // { -// mLog.error("Filter design error", fde); +// mLog.error("Error"); // } + try + { + RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification); + + if(designer.isValid()) + { + taps = designer.getImpulseResponse(); + } + } + catch(FilterDesignException fde) + { + mLog.error("Filter design error", fde); + } + return taps; } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/fir/FIRFilterSpecification.java b/src/main/java/io/github/dsheirer/dsp/filter/fir/FIRFilterSpecification.java index d26483dd..6fe1294e 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/fir/FIRFilterSpecification.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/fir/FIRFilterSpecification.java @@ -295,8 +295,9 @@ public class FIRFilterSpecification */ public static class LowPassBuilder { - private int mSampleRate; + private double mSampleRate; private int mOrder; + private Boolean mOddLength; private int mGridDensity = 16; private int mPassBandEndFrequency; private int mStopBandStartFrequency; @@ -309,7 +310,7 @@ public class FIRFilterSpecification { } - public LowPassBuilder sampleRate(int sampleRateHz) + public LowPassBuilder sampleRate(double sampleRateHz) { mSampleRate = sampleRateHz; return this; @@ -325,6 +326,16 @@ public class FIRFilterSpecification return this; } + /** + * Indicates if the filters should be an odd-length (even order) filter. This will override any requested + * filter order if it is set, otherwise the filter length will be determined from the filter order. + */ + public LowPassBuilder oddLength(boolean oddLength) + { + mOddLength = oddLength; + return this; + } + public LowPassBuilder gridDensity(int density) { mGridDensity = density; @@ -371,13 +382,32 @@ public class FIRFilterSpecification { if(mOrder < 6) { - mOrder = estimateFilterOrder(mSampleRate, mPassBandEndFrequency, - mStopBandStartFrequency, mPassBandRipple, mStopBandRipple); + mOrder = estimateFilterOrder(mSampleRate, mPassBandEndFrequency, mStopBandStartFrequency, + mPassBandRipple, mStopBandRipple); } - FIRLinearPhaseFilterType type = (mOrder % 2 == 0 - ? FIRLinearPhaseFilterType.TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL - : FIRLinearPhaseFilterType.TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL); + FIRLinearPhaseFilterType type = null; + + if(mOddLength != null) + { + if(mOddLength) + { + type = FIRLinearPhaseFilterType.TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL; + //Force order to even + mOrder += mOrder % 2; + } + else + { + type = FIRLinearPhaseFilterType.TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL; + //Force order to odd + mOrder += (mOrder % 2 == 0 ? 1 : 0); + } + } + else + { + type = (mOrder % 2 == 0 ? FIRLinearPhaseFilterType.TYPE_1_ODD_LENGTH_EVEN_ORDER_SYMMETRICAL + : FIRLinearPhaseFilterType.TYPE_2_EVEN_LENGTH_ODD_ORDER_SYMMETRICAL); + } FIRFilterSpecification spec = new FIRFilterSpecification(type, mOrder, mGridDensity); @@ -386,7 +416,7 @@ public class FIRFilterSpecification spec.addFrequencyBand(passBand); FrequencyBand stopBand = new FrequencyBand(mSampleRate, mStopBandStartFrequency, - mSampleRate / 2, mStopBandAmplitude, mStopBandRipple); + (int)(mSampleRate / 2), mStopBandAmplitude, mStopBandRipple); spec.addFrequencyBand(stopBand); return spec; @@ -398,7 +428,7 @@ public class FIRFilterSpecification */ public static class HighPassBuilder { - private int mSampleRate; + private double mSampleRate; private int mOrder = -1; private int mGridDensity = 16; private int mPassBandStartFrequency; @@ -412,7 +442,7 @@ public class FIRFilterSpecification { } - public HighPassBuilder sampleRate(int sampleRateHz) + public HighPassBuilder sampleRate(double sampleRateHz) { mSampleRate = sampleRateHz; return this; @@ -474,8 +504,8 @@ public class FIRFilterSpecification { if(mOrder < 6) { - mOrder = estimateFilterOrder(mSampleRate, mStopBandEndFrequency, - mPassBandStartFrequency, mPassBandRipple, mStopBandRipple); + mOrder = estimateFilterOrder(mSampleRate, mStopBandEndFrequency, mPassBandStartFrequency, + mPassBandRipple, mStopBandRipple); } // High pass is a Type 1 filter -- ensure order is even @@ -493,7 +523,7 @@ public class FIRFilterSpecification spec.addFrequencyBand(stopBand); FrequencyBand passBand = new FrequencyBand(mSampleRate, mPassBandStartFrequency, - mSampleRate / 2, mPassBandAmplitude, mPassBandRipple); + (int)(mSampleRate / 2), mPassBandAmplitude, mPassBandRipple); spec.addFrequencyBand(passBand); return spec; @@ -507,7 +537,7 @@ public class FIRFilterSpecification { private int mOrder; private int mGridDensity = 16; - private int mSampleRate; + private double mSampleRate; private int mStopFrequency1; private int mPassFrequencyBegin; private int mPassFrequencyEnd; @@ -544,7 +574,7 @@ public class FIRFilterSpecification /** * Sets the filter sample rate in hertz */ - public BandPassBuilder sampleRate(int sampleRate) + public BandPassBuilder sampleRate(double sampleRate) { mSampleRate = sampleRate; return this; @@ -644,15 +674,13 @@ public class FIRFilterSpecification if(mPassFrequencyEnd >= mStopFrequency2) { throw new IllegalArgumentException("Pass band end frequency [" + mPassFrequencyEnd - + "] must be less than stop band 2 beginning frequency [" + mStopFrequency2 - + "]"); + + "] must be less than stop band 2 beginning frequency [" + mStopFrequency2 + "]"); } if(mStopFrequency2 >= (mSampleRate / 2)) { - throw new IllegalArgumentException("Stop band 2 beginning frequency [" - + mStopFrequency2 + "] must be less than half of the sample rate [" - + (mSampleRate / 2) + "]"); + throw new IllegalArgumentException("Stop band 2 beginning frequency [" + mStopFrequency2 + + "] must be less than half of the sample rate [" + (mSampleRate / 2) + "]"); } } @@ -693,7 +721,7 @@ public class FIRFilterSpecification public static class ChannelizerBuilder { private int mGridDensity = 16; - private int mSampleRate; + private double mSampleRate; private int mChannelCount; private int mChannelBandwidth; private int mTapsPerChannel; @@ -708,7 +736,7 @@ public class FIRFilterSpecification /** * Sets the filter sample rate in hertz */ - public ChannelizerBuilder sampleRate(int sampleRate) + public ChannelizerBuilder sampleRate(double sampleRate) { mSampleRate = sampleRate; return this; @@ -805,7 +833,7 @@ public class FIRFilterSpecification FIRFilterSpecification spec = new FIRFilterSpecification(type, order, mGridDensity); - double bandEdge = (double)mChannelBandwidth / (double)mSampleRate; + double bandEdge = (double)mChannelBandwidth / mSampleRate; double passEnd = bandEdge * (1.0 - mAlpha); double stopStart = bandEdge * (1.0 + mAlpha); @@ -877,15 +905,13 @@ public class FIRFilterSpecification * @param stopBandRipple stop band ripple in dB * @return estimated filter length */ - public static int estimateFilterOrder(int sampleRate, int frequency1, int frequency2, + public static int estimateFilterOrder(double sampleRate, int frequency1, int frequency2, double passBandRipple, double stopBandRipple) { - double df = Math.abs(frequency2 - frequency1) / (double)sampleRate; + double df = Math.abs(frequency2 - frequency1) / sampleRate; - double ddp = Math - .log10(stopBandRipple <= passBandRipple ? passBandRipple : stopBandRipple); - double dds = Math - .log10(stopBandRipple <= passBandRipple ? stopBandRipple : passBandRipple); + double ddp = Math.log10(stopBandRipple <= passBandRipple ? passBandRipple : stopBandRipple); + double dds = Math.log10(stopBandRipple <= passBandRipple ? stopBandRipple : passBandRipple); double a1 = 5.309e-3; double a2 = 7.114e-2; @@ -917,12 +943,12 @@ public class FIRFilterSpecification * @param passBandRippleDb * @param stopBandRippleDb */ - public static int estimateBandPassOrder(int sampleRate, int passBandStart, int passBandEnd, + public static int estimateBandPassOrder(double sampleRate, int passBandStart, int passBandEnd, double passBandRippleDb, double stopBandRippleDb) { - double df = (double)Math.abs(passBandEnd - passBandStart) / (double)sampleRate; - double ddp = (double)Math.log10(passBandRippleDb); - double dds = (double)Math.log10(stopBandRippleDb); + double df = (double)Math.abs(passBandEnd - passBandStart) / sampleRate; + double ddp = Math.log10(passBandRippleDb); + double dds = Math.log10(stopBandRippleDb); double a1 = 0.01201; double a2 = 0.09664; @@ -987,13 +1013,13 @@ public class FIRFilterSpecification * @param amplitude of the frequency band relative to unity (0.0 - 1.0) * @param ripple of the frequency band specified as db ripple */ - public FrequencyBand(int sampleRate, int start, int end, double amplitude, double ripple) + public FrequencyBand(double sampleRate, int start, int end, double amplitude, double ripple) { - this((double)start / (double)sampleRate, (double)end / (double)sampleRate, + this((double)start / sampleRate, (double)end / sampleRate, amplitude, ripple); } - public FrequencyBand(int sampleRate, int start, int end, double amplitude, double ripple, double weight) + public FrequencyBand(double sampleRate, int start, int end, double amplitude, double ripple, double weight) { this(sampleRate, start, end, amplitude, ripple); mWeight = weight; 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 33cd4862..7e9ccc71 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,52 +3,65 @@ package io.github.dsheirer.dsp.filter.fir.complex; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.complex.ComplexBuffer; -public class ComplexFIRFilter_CB_CB extends ComplexFIRFilter - implements Listener +public class ComplexFIRFilter_CB_CB extends ComplexFIRFilter implements Listener { - private Listener mListener; - - /** - * Complex FIR Filter for processing complex buffers. Wraps two real FIR - * filters for processing each of the inphase and quadrature samples. - * - * @param coefficients - odd length symmetric filter taps - * @param gain - gain value to apply to each of the filtered samples - */ - public ComplexFIRFilter_CB_CB( float[] coefficients, float gain ) - { - super( coefficients, gain ); - } - - public void dispose() - { - mListener = null; - } + private Listener mListener; - @Override - public void receive( ComplexBuffer buffer ) - { - if( mListener != null ) - { - float[] samples = buffer.getSamples(); - - for( int x = 0; x < samples.length; x += 2 ) - { - samples[ x ] = filterInphase( samples[ x ] ); - samples[ x + 1 ] = filterQuadrature( samples[ x + 1 ] ); - } - - mListener.receive( buffer ); - } - } - - public void setListener( Listener listener ) - { - mListener = listener; - } - - public void removeListener() - { - mListener = null; - } + /** + * Complex FIR Filter for processing complex buffers. Wraps two real FIR + * filters for processing each of the inphase and quadrature samples. + * + * @param coefficients - odd length symmetric filter taps + * @param gain - gain value to apply to each of the filtered samples + */ + public ComplexFIRFilter_CB_CB(float[] coefficients, float gain) + { + super(coefficients, gain); + } + + public void dispose() + { + mListener = null; + } + + @Override + public void receive(ComplexBuffer buffer) + { + if(mListener != null) + { + float[] samples = buffer.getSamples(); + + for(int x = 0; x < samples.length; x += 2) + { + samples[x] = filterInphase(samples[x]); + samples[x + 1] = filterQuadrature(samples[x + 1]); + } + + mListener.receive(buffer); + } + } + + /** + * Filters the samples and send the results to the registered listener + */ + public void receive(float[] samples) + { + if(mListener != null) + { + float[] filtered = filter(samples); + + //TODO: this should be a reusable buffer here. + mListener.receive(new ComplexBuffer(filtered)); + } + } + + public void setListener(Listener listener) + { + mListener = listener; + } + + public void removeListener() + { + mListener = null; + } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/resample/AudioResampler.java b/src/main/java/io/github/dsheirer/dsp/filter/resample/AudioResampler.java new file mode 100644 index 00000000..c67401b9 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/filter/resample/AudioResampler.java @@ -0,0 +1,70 @@ +/******************************************************************************* + * 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.resample; + +import com.laszlosystems.libresample4j.Resampler; +import io.github.dsheirer.module.Module; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.source.ISourceEventListener; +import io.github.dsheirer.source.SourceEvent; + +public class AudioResampler extends Module implements ISourceEventListener +{ + private Resampler mResampler; + + public AudioResampler(double inputRate, double outputRate) + { + double resampleRate = outputRate / inputRate; + + mResampler = new Resampler(true, resampleRate, resampleRate); + } + + public float[] resample(float[] samples) + { + return null; + } + + @Override + public void reset() + { + + } + + @Override + public void start() + { + + } + + @Override + public void stop() + { + + } + + @Override + public void dispose() + { + + } + + @Override + public Listener getSourceEventListener() + { + //TODO: we should get the channel sample rate and setup the resampler to output 8 kHz audio + return null; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java index 5092c317..dac8df21 100644 --- a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java @@ -101,6 +101,9 @@ public class DecoderFactory { private final static Logger mLog = LoggerFactory.getLogger(DecoderFactory.class); + private static final int NBFM_PASS_FREQUENCY = 6000; + private static final int NBFM_STOP_FREQUENCY = 6500; + //Low-pass filter with ~60 dB attenuation and 89 taps private static final FIRFilterSpecification MPT1327_FILTER_SPECIFICATION = FIRFilterSpecification.lowPassBuilder() .sampleRate( 48000 ).gridDensity( 16 ).passBandCutoff( 3200 ).passBandAmplitude( 1.0 ).passBandRipple( 0.02 ) @@ -174,9 +177,6 @@ public class DecoderFactory /* Baseband low-pass filter pass and stop frequencies */ DecodeConfiguration decodeConfig = channel.getDecodeConfiguration(); - int iqPass = decodeConfig.getDecoderType().getChannelBandwidth() / 2; - int iqStop = iqPass + 1250; - switch(decodeConfig.getDecoderType()) { case AM: @@ -189,7 +189,7 @@ public class DecoderFactory case NBFM: modules.add(new NBFMDecoder(decodeConfig)); modules.add(new AlwaysUnsquelchedDecoderState(DecoderType.NBFM, channel.getName())); - modules.add(new FMDemodulatorModule(iqPass, iqStop)); + modules.add(new FMDemodulatorModule(NBFM_PASS_FREQUENCY, NBFM_STOP_FREQUENCY)); modules.add(new DemodulatedAudioFilterModule(4000, 6000)); modules.add(new AudioModule(metadata)); break; @@ -197,14 +197,14 @@ public class DecoderFactory MessageDirection direction = ((DecodeConfigLTRStandard) decodeConfig).getMessageDirection(); modules.add(new LTRStandardDecoder(aliasList, direction)); modules.add(new LTRStandardDecoderState(aliasList)); - modules.add(new FMDemodulatorModule(iqPass, iqStop)); + modules.add(new FMDemodulatorModule(NBFM_PASS_FREQUENCY, NBFM_STOP_FREQUENCY)); modules.add(new DemodulatedAudioFilterModule(4000, 6000)); modules.add(new AudioModule(metadata)); break; case LTR_NET: modules.add(new LTRNetDecoder((DecodeConfigLTRNet) decodeConfig, aliasList)); modules.add(new LTRNetDecoderState(aliasList)); - modules.add(new FMDemodulatorModule(iqPass, iqStop)); + modules.add(new FMDemodulatorModule(NBFM_PASS_FREQUENCY, NBFM_STOP_FREQUENCY)); modules.add(new DemodulatedAudioFilterModule(4000, 6000)); modules.add(new AudioModule(metadata)); break; @@ -227,14 +227,14 @@ public class DecoderFactory (aliasList != null ? aliasList.getName() : null), mptConfig.getTrafficChannelPoolSize())); } - modules.add(new FMDemodulatorModule(iqPass, iqStop)); + modules.add(new FMDemodulatorModule(NBFM_PASS_FREQUENCY, NBFM_STOP_FREQUENCY)); modules.add(new DemodulatedAudioFilterModule(P25_C4FM_DEMOD_FILTER, 1.0f)); modules.add(new AudioModule(metadata)); break; case PASSPORT: modules.add(new PassportDecoder(decodeConfig, aliasList)); modules.add(new PassportDecoderState(aliasList)); - modules.add(new FMDemodulatorModule(iqPass, iqStop)); + modules.add(new FMDemodulatorModule(NBFM_PASS_FREQUENCY, NBFM_STOP_FREQUENCY)); modules.add(new DemodulatedAudioFilterModule(4000, 6000)); modules.add(new AudioModule(metadata)); break; @@ -247,7 +247,7 @@ public class DecoderFactory switch(modulation) { case C4FM: - modules.add(new FMDemodulatorModule(P25_C4FM_IQ_FILTER)); +// modules.add(new FMDemodulatorModule(P25_C4FM_IQ_FILTER)); modules.add(new DemodulatedAudioFilterModule(P25_C4FM_DEMOD_FILTER, 1.0f)); modules.add(new P25_C4FMDecoder(aliasList, decodeConfig.getAFCMaximumCorrection())); modules.add(new P25DecoderState(aliasList, channelType, Modulation.C4FM, diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25_LSMDecoder.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25_LSMDecoder.java index 518eb07f..f5bf2108 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25_LSMDecoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25_LSMDecoder.java @@ -235,7 +235,7 @@ public class P25_LSMDecoder extends P25Decoder implements IReusableComplexBuffer @Override public void receive(ReusableComplexBuffer reusableComplexBuffer) { - float[] samples = reusableComplexBuffer.getCopyOfSamples(); + float[] samples = reusableComplexBuffer.getSamplesCopy(); //TODO: redesign the filter chain so that we can simply pass a float array ... diff --git a/src/main/java/io/github/dsheirer/module/demodulate/am/AMDemodulatorModule.java b/src/main/java/io/github/dsheirer/module/demodulate/am/AMDemodulatorModule.java index 8677b89a..e55b51cb 100644 --- a/src/main/java/io/github/dsheirer/module/demodulate/am/AMDemodulatorModule.java +++ b/src/main/java/io/github/dsheirer/module/demodulate/am/AMDemodulatorModule.java @@ -113,7 +113,7 @@ public class AMDemodulatorModule extends Module implements IReusableComplexBuffe @Override public void receive(ReusableComplexBuffer reusableComplexBuffer) { - float[] samples = reusableComplexBuffer.getCopyOfSamples(); + float[] samples = reusableComplexBuffer.getSamplesCopy(); //TODO: redesign the filter chain so that we can simply pass a float array ... diff --git a/src/main/java/io/github/dsheirer/module/demodulate/fm/FMDemodulatorModule.java b/src/main/java/io/github/dsheirer/module/demodulate/fm/FMDemodulatorModule.java index 72380d34..a976a3fa 100644 --- a/src/main/java/io/github/dsheirer/module/demodulate/fm/FMDemodulatorModule.java +++ b/src/main/java/io/github/dsheirer/module/demodulate/fm/FMDemodulatorModule.java @@ -1,85 +1,84 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2014-2016 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 ******************************************************************************/ package io.github.dsheirer.module.demodulate.fm; import io.github.dsheirer.dsp.filter.FilterFactory; import io.github.dsheirer.dsp.filter.Window; +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_CB_CB; import io.github.dsheirer.dsp.fm.FMDemodulator_CB; import io.github.dsheirer.module.Module; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.ComplexBuffer; import io.github.dsheirer.sample.complex.reusable.IReusableComplexBufferListener; import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; import io.github.dsheirer.sample.real.IUnFilteredRealBufferProvider; import io.github.dsheirer.sample.real.RealBuffer; +import io.github.dsheirer.source.ISourceEventListener; +import io.github.dsheirer.source.SourceEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class FMDemodulatorModule extends Module implements IReusableComplexBufferListener, IUnFilteredRealBufferProvider +public class FMDemodulatorModule extends Module implements ISourceEventListener, IReusableComplexBufferListener, + IUnFilteredRealBufferProvider { private final static Logger mLog = LoggerFactory.getLogger(FMDemodulatorModule.class); - private static final int SAMPLE_RATE = 48000; - private ComplexFIRFilter_CB_CB mIQFilter; private FMDemodulator_CB mDemodulator; - private ReusableBufferListener mReusableBufferListener = new ReusableBufferListener(); + private ReusableBufferProcessor mReusableBufferProcessor = new ReusableBufferProcessor(); + private SourceEventProcessor mSourceEventProcessor = new SourceEventProcessor(); + private int mPassFrequency; + private int mStopFrequency; /** - * FM Demodulator with I/Q filter. Demodulated output is unfiltered and - * may contain a DC component. + * FM Demodulator with I/Q filter. Demodulated output is unfiltered and may contain a DC component. * * @param pass - pass frequency for IQ filtering prior to demodulation. This - * frequency should be half of the signal bandwidth since the filter will + * frequency should be less than or equal to half of the signal bandwidth since the filter will * be applied against each of the inphase and quadrature signals and the * combined pass bandwidth will be twice this value. - * @param stop - stop frequency for IQ filtering prior to demodulation. */ public FMDemodulatorModule(int pass, int stop) { - assert (stop > pass); - - mIQFilter = new ComplexFIRFilter_CB_CB(FilterFactory.getLowPass( - SAMPLE_RATE, pass, stop, 60, Window.WindowType.HAMMING, true), 1.0f); - mDemodulator = new FMDemodulator_CB(1.0f); - mIQFilter.setListener(mDemodulator); - } - - public FMDemodulatorModule(float[] filter) - { - mIQFilter = new ComplexFIRFilter_CB_CB(filter, 1.0f); - mDemodulator = new FMDemodulator_CB(1.0f); - mIQFilter.setListener(mDemodulator); + mPassFrequency = pass; + mStopFrequency = stop; } @Override public Listener getReusableComplexBufferListener() { - return mReusableBufferListener; + return mReusableBufferProcessor; + } + + @Override + public Listener getSourceEventListener() + { + return mSourceEventProcessor; } @Override public void dispose() { - mIQFilter.dispose(); - mIQFilter = null; + if(mIQFilter != null) + { + mIQFilter.dispose(); + mIQFilter = null; + } mDemodulator.dispose(); mDemodulator = null; @@ -113,17 +112,76 @@ public class FMDemodulatorModule extends Module implements IReusableComplexBuffe { } - public class ReusableBufferListener implements Listener + /** + * Receives complex buffers and sends them through the filtering and demodulation process. Manages accounting for + * the reusable buffers. + */ + public class ReusableBufferProcessor implements Listener { @Override public void receive(ReusableComplexBuffer reusableComplexBuffer) { - float[] samples = reusableComplexBuffer.getCopyOfSamples(); + if(mIQFilter == null) + { + reusableComplexBuffer.decrementUserCount(); + throw new IllegalStateException("FM demodulator module must receive a sample rate change source " + + "event before it can process complex sample buffers"); + } - //TODO: redesign the filter chain so that we can simply pass a float array ... + float[] samples = reusableComplexBuffer.getSamplesCopy(); + + mIQFilter.receive(samples); - mIQFilter.receive(new ComplexBuffer(samples)); reusableComplexBuffer.decrementUserCount(); } } + + /** + * Monitors sample rate change source event(s) to setup the initial I/Q filter + */ + public class SourceEventProcessor implements Listener + { + @Override + public void receive(SourceEvent sourceEvent) + { + if(sourceEvent.getEvent() == SourceEvent.Event.NOTIFICATION_SAMPLE_RATE_CHANGE) + { + double sampleRate = sourceEvent.getValue().doubleValue(); + + float[] filterTaps = null; + + FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() + .sampleRate(sampleRate) + .gridDensity(16) + .oddLength(true) + .passBandCutoff(mPassFrequency) + .passBandAmplitude(1.0) + .passBandRipple(0.01) + .stopBandStart(mStopFrequency) + .stopBandAmplitude(0.0) + .stopBandRipple(0.028) //Approximately 60 dB attenuation + .build(); + + try + { + filterTaps = FilterFactory.getTaps(specification); + } + catch(FilterDesignException fde) + { + mLog.error("Couldn't design FM demodulator remez filter for sample rate [" + sampleRate + + "] pass frequency [" + mPassFrequency + "] and stop frequency [" + mStopFrequency + + "] - using sinc filter"); + } + + if(filterTaps == null) + { + filterTaps = FilterFactory.getLowPass(sampleRate, mPassFrequency, mStopFrequency, 60, + Window.WindowType.HAMMING, true); + } + + mIQFilter = new ComplexFIRFilter_CB_CB(filterTaps, 1.0f); + mIQFilter.setListener(mDemodulator); + } + } + } } 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 396d223c..b195afac 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 @@ -180,15 +180,18 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc case NOTIFICATION_SAMPLE_RATE_CHANGE: setSampleRate(sourceEvent.getValue().doubleValue()); break; - + case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_LOCKED: + case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_UNLOCKED: + case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE: + //no-op + break; //Request events from the consumer case REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE: setFrequencyCorrection(sourceEvent.getValue().longValue()); break; - default: mLog.error("Ignoring unrecognized source event: " + sourceEvent.getEvent() + " from [" + - sourceEvent.getSource().getClass() + "]"); + (sourceEvent.hasSource() ? sourceEvent.getSource().getClass() : "") + "]"); break; } } 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 64733a83..d3f2e7d8 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 @@ -181,18 +181,39 @@ public class TunerChannelSourceManager extends ChannelSourceManager long bestIntegralFrequency = getIntegralFrequency(channels); - //See if we can reuse the current frequency + //Strategy 1: reuse the current frequency if it's a good integral and the channels fit if(isIntegralSpacing(currentCenterFrequency, bestIntegralFrequency) && isValidCenterFrequency(channels, currentCenterFrequency)) { return currentCenterFrequency; } - //Start by placing the first channel at the minimum location within the tuner bandwidth double usableHalfBandwidth = mTunerController.getUsableHalfBandwidth(); + + //Strategy 2: start by placing the center frequency exactly one channel width below the first channel frequency + //and iteratively increase it one channel width at a time. This optimally places the channels nearest to the + //center of the bandwidth + long start = channels.first().getFrequency() - (int)mPolyphaseChannelManager.getChannelBandwidth(); + + if(isValidCenterFrequency(channels, start)) + { + return start; + } + + while(start - channels.first().getMinFrequency() < usableHalfBandwidth) + { + start += (int)mPolyphaseChannelManager.getChannelBandwidth(); + + if(isValidCenterFrequency(channels, start)) + { + return start; + } + } + + //Strategy 3: start by placing the first channel at the minimum location within the tuner bandwidth double startFrequency = channels.first().getMinFrequency() + usableHalfBandwidth; - //Align the test frequency to the next greater integral frequency placing to place the first channel at the + //Align the test frequency to the next greater integral frequency placing the first channel at the // minimum extreme of the available bandwidth as a starting location double delta = Math.abs(startFrequency - bestIntegralFrequency); double adjustment = delta % mPolyphaseChannelManager.getChannelBandwidth(); @@ -221,7 +242,8 @@ public class TunerChannelSourceManager extends ChannelSourceManager currentChannel++; } - //If we can't find an integral center frequency, choose a frequency that will fit the channels + //Strategy 4: brute force walk across the spectrum 1 hertz at a time looking for a center frequency that will + //fit all channels long testFrequency = channels.first().getMinFrequency() + mTunerController.getUsableHalfBandwidth(); long minimumFrequency = testFrequency - availableTestBandwidth; 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 ec1bf0d6..0638232b 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 @@ -33,8 +33,8 @@ 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 = 100000; - public static final int DC_NOISE_BANDWIDTH = 5000; - public static final double USABLE_BANDWIDTH_PERCENTAGE = .90; + public static final int DC_NOISE_BANDWIDTH = 0; + public static final double USABLE_BANDWIDTH_PERCENTAGE = 1.00; public static final int SPECTRAL_FRAME_RATE = 20; public static final long SAMPLE_GENERATION_INTERVAL = 1000 / SPECTRAL_FRAME_RATE;