From dda37a1198d58015516edb9d0fb6e50111b718d5 Mon Sep 17 00:00:00 2001 From: Denny Date: Sun, 18 Feb 2018 16:20:45 -0500 Subject: [PATCH] 330 WIP - updates the demodulator viewer with an eye diagram and symbol chart. Testing various configurations to see which yields the best PLL and symbol timing locks. --- .../dsheirer/buffer/DoubleCircularBuffer.java | 12 + .../dsp/filter/design/FilterView.java | 278 +++++++++--------- .../dsp/filter/design/FilterViewer.java | 9 +- .../github/dsheirer/dsp/fm/FMDemodulator.java | 133 +++++---- .../dsheirer/dsp/fm/FMDemodulator_CB.java | 3 +- .../dsp/psk/InterpolatingSymbolBuffer.java | 21 +- .../dsheirer/dsp/psk/QPSKDemodulator.java | 48 ++- .../dsheirer/dsp/psk/SymbolDecisionData.java | 70 +++++ .../dsheirer/dsp/psk/pll/CostasLoop.java | 16 +- .../github/dsheirer/dsp/psk/pll/Tracking.java | 45 ++- .../gui/viewer/PlaybackController.java | 3 +- .../gui/viewer/chart/EyeDiagramChart.java | 118 ++++++++ .../viewer/chart/FMDemodulatedLineChart.java | 119 ++++++++ .../gui/viewer/chart/FrequencyLineChart.java | 73 +++++ .../gui/viewer/chart/PhaseLineChart.java | 4 +- .../gui/viewer/chart/SymbolChart.java | 30 +- .../gui/viewer/decoder/P25Phase1Pane.java | 45 ++- .../module/decode/p25/P25MessageFramer.java | 3 +- .../module/decode/p25/P25_C4FMDecoder2.java | 86 ++++-- .../source/wave/ComplexWaveSource.java | 7 +- 20 files changed, 844 insertions(+), 279 deletions(-) create mode 100644 src/main/java/io/github/dsheirer/dsp/psk/SymbolDecisionData.java create mode 100644 src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/EyeDiagramChart.java create mode 100644 src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/FMDemodulatedLineChart.java create mode 100644 src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/FrequencyLineChart.java diff --git a/src/main/java/io/github/dsheirer/buffer/DoubleCircularBuffer.java b/src/main/java/io/github/dsheirer/buffer/DoubleCircularBuffer.java index af5bd86d..b65f2f67 100644 --- a/src/main/java/io/github/dsheirer/buffer/DoubleCircularBuffer.java +++ b/src/main/java/io/github/dsheirer/buffer/DoubleCircularBuffer.java @@ -18,6 +18,7 @@ package io.github.dsheirer.buffer; import org.apache.commons.math3.stat.descriptive.moment.Mean; +import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation; import org.apache.commons.math3.stat.descriptive.moment.Variance; import org.apache.commons.math3.stat.descriptive.rank.Max; import org.apache.commons.math3.stat.descriptive.rank.Min; @@ -30,6 +31,7 @@ import java.util.Arrays; */ public class DoubleCircularBuffer { + private StandardDeviation mStandardDeviation; private Variance mVariance; private Max mMax; private Mean mMean; @@ -140,6 +142,16 @@ public class DoubleCircularBuffer return mMin.evaluate(mBuffer, 0, mBuffer.length); } + public double standardDeviation() + { + if(mStandardDeviation == null) + { + mStandardDeviation = new StandardDeviation(); + } + + return mStandardDeviation.evaluate(mBuffer); + } + /** * Calculates the variance of the values contained in this buffer */ diff --git a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterView.java b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterView.java index edaa4289..591da465 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterView.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterView.java @@ -12,155 +12,169 @@ import org.jtransforms.fft.FloatFFT_1D; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.net.URL; + public class FilterView extends BorderPane { - private final static Logger mLog = LoggerFactory.getLogger( FilterView.class ); + private final static Logger mLog = LoggerFactory.getLogger(FilterView.class); - private CurveFittedAreaChart mImpulseChart; - private NumberAxis mImpulseXAxis; - private NumberAxis mImpulseYAxis; - private CurveFittedAreaChart mDFTChart; - private NumberAxis mDFTXAxis; - private NumberAxis mDFTYAxis; - private static final int FFT_SIZE = 4096; - private FloatFFT_1D mFFT = new FloatFFT_1D( FFT_SIZE ); - - public FilterView( float[] taps ) - { - init(); + private CurveFittedAreaChart mImpulseChart; + private NumberAxis mImpulseXAxis; + private NumberAxis mImpulseYAxis; + private CurveFittedAreaChart mDFTChart; + private NumberAxis mDFTXAxis; + private NumberAxis mDFTYAxis; + private static final int FFT_SIZE = 4096; + private FloatFFT_1D mFFT = new FloatFFT_1D(FFT_SIZE); - if( taps != null ) - { - setImpulseTaps( taps ); + public FilterView(float[] taps) + { + init(); - float[] dft = new float[ FFT_SIZE ]; - System.arraycopy( taps, 0, dft, 0, taps.length ); - - mFFT.realForward( dft ); - - setDFTTaps( convertDFTToDecibel( dft, taps.length ) ); - } - } - - private void init() - { - HBox hbox = new HBox(); - - mImpulseXAxis = new NumberAxis( 0.0, 0.5, 1.0 ); - mImpulseYAxis = new NumberAxis( -0.2, 1.2, .1 ); - - mImpulseChart = new CurveFittedAreaChart( mImpulseXAxis, mImpulseYAxis ); - mImpulseChart.setLegendVisible( false ); - mImpulseChart.setHorizontalGridLinesVisible( false ); - mImpulseChart.setVerticalGridLinesVisible(false); + if(taps != null) + { + setImpulseTaps(taps); + + float[] dft = new float[FFT_SIZE]; + System.arraycopy(taps, 0, dft, 0, taps.length); + + mFFT.realForward(dft); + + setDFTTaps(convertDFTToDecibel(dft, taps.length)); + } + } + + private void init() + { + HBox hbox = new HBox(); + + mImpulseXAxis = new NumberAxis(0.0, 0.5, 1.0); + mImpulseYAxis = new NumberAxis(-0.2, 1.2, .1); + + mImpulseChart = new CurveFittedAreaChart(mImpulseXAxis, mImpulseYAxis); + mImpulseChart.setLegendVisible(false); + mImpulseChart.setHorizontalGridLinesVisible(false); + mImpulseChart.setVerticalGridLinesVisible(false); mImpulseChart.setAlternativeColumnFillVisible(false); mImpulseChart.setAlternativeRowFillVisible(false); - - String css = FilterView.class.getResource( "FilterView.css" ).toExternalForm(); - mImpulseChart.getStylesheets().add( css ); - hbox.getChildren().add( mImpulseChart ); - HBox.setHgrow( mImpulseChart, Priority.ALWAYS ); - - mDFTXAxis = new NumberAxis( 0.0, FFT_SIZE / 2, FFT_SIZE / 8 ); - mDFTYAxis = new NumberAxis( -200.0, 10.0, 10.0 ); - - mDFTChart = new CurveFittedAreaChart( mDFTXAxis, mDFTYAxis ); - mDFTChart.setLegendVisible( false ); - mDFTChart.setHorizontalGridLinesVisible( false ); - mDFTChart.setVerticalGridLinesVisible(false); + URL cssURL = FilterView.class.getResource("FilterView.css"); + + String css = null; + + if(cssURL != null) + { + css = cssURL.toExternalForm(); + + } + + hbox.getChildren().add(mImpulseChart); + HBox.setHgrow(mImpulseChart, Priority.ALWAYS); + + mDFTXAxis = new NumberAxis(0.0, FFT_SIZE / 2, FFT_SIZE / 8); + mDFTYAxis = new NumberAxis(-200.0, 10.0, 10.0); + + mDFTChart = new CurveFittedAreaChart(mDFTXAxis, mDFTYAxis); + mDFTChart.setLegendVisible(false); + mDFTChart.setHorizontalGridLinesVisible(false); + mDFTChart.setVerticalGridLinesVisible(false); mDFTChart.setAlternativeColumnFillVisible(false); mDFTChart.setAlternativeRowFillVisible(false); - mDFTChart.getStylesheets().add( css ); - - hbox.getChildren().add( mDFTChart ); - HBox.setHgrow( mDFTChart, Priority.ALWAYS ); - setCenter( hbox ); - } - - public void setImpulseTaps( double[] taps ) - { - float[] convertedTaps = new float[ taps.length ]; - - for( int x = 0; x < taps.length; x++ ) - { - convertedTaps[ x ] = (float)taps[ x ]; - } - - setImpulseTaps( convertedTaps ); - } - - public void setImpulseTaps( float[] taps ) - { - double smallest = 0.0; - double largest = 0.0; - - final XYChart.Series series = new XYChart.Series<>(); - - for( int x = 0; x < taps.length; x++ ) + if(css != null) { - if( taps[ x ] < smallest ) - { - smallest = taps[ x ]; - } - - if( taps[ x ] > largest ) - { - largest = taps[ x ]; - } - - series.getData().add( new XYChart.Data( x, taps[ x ] ) ); + mImpulseChart.getStylesheets().add(css); + mDFTChart.getStylesheets().add(css); } - - mImpulseXAxis.setLowerBound( 0.0 ); - mImpulseXAxis.setUpperBound( taps.length - 1 ); - mImpulseXAxis.setTickUnit( 1.0 ); - - double padding = Math.abs( largest - smallest ) * .1; - - mImpulseYAxis.setLowerBound( smallest - padding ); - mImpulseYAxis.setUpperBound( largest + padding ); - mImpulseYAxis.setTickUnit( padding ); + + hbox.getChildren().add(mDFTChart); + HBox.setHgrow(mDFTChart, Priority.ALWAYS); + + setCenter(hbox); + } + + public void setImpulseTaps(double[] taps) + { + float[] convertedTaps = new float[taps.length]; + + for(int x = 0; x < taps.length; x++) + { + convertedTaps[x] = (float)taps[x]; + } + + setImpulseTaps(convertedTaps); + } + + public void setImpulseTaps(float[] taps) + { + double smallest = 0.0; + double largest = 0.0; + + final XYChart.Series series = new XYChart.Series<>(); + + for(int x = 0; x < taps.length; x++) + { + if(taps[x] < smallest) + { + smallest = taps[x]; + } + + if(taps[x] > largest) + { + largest = taps[x]; + } + + series.getData().add(new XYChart.Data(x, taps[x])); + } + + mImpulseXAxis.setLowerBound(0.0); + mImpulseXAxis.setUpperBound(taps.length - 1); + mImpulseXAxis.setTickUnit(1.0); + + double padding = Math.abs(largest - smallest) * .1; + + mImpulseYAxis.setLowerBound(smallest - padding); + mImpulseYAxis.setUpperBound(largest + padding); + mImpulseYAxis.setTickUnit(padding); ObservableList> list = mImpulseChart.getData(); - list.add( series ); - } - - public void setDFTTaps( float[] taps ) - { - double smallest = 0.0; - - final XYChart.Series series = new XYChart.Series<>(); - - for( int x = 0; x < taps.length; x++ ) - { - if( taps[ x ] < smallest ) - { - smallest = taps[ x ]; - } - - series.getData().add( new XYChart.Data( x, taps[ x ] ) ); - } - - ObservableList> list = mDFTChart.getData(); - list.add( series ); - } - - public float[] convertDFTToDecibel( float[] dft, int tapLength ) + list.add(series); + } + + public void setDFTTaps(float[] taps) { - float[] decibels = new float[ dft.length / 2 ]; + double smallest = 0.0; - int index = 0; - - for( int x = 0; x < decibels.length; x ++ ) - { - index = x * 2; - - decibels[ x ] = 20.0f * (float)Math.log10( ( ( dft[ index ] * dft[ index ] ) + - ( dft[ index + 1 ] * dft[ index + 1 ] ) ) ); - } + final XYChart.Series series = new XYChart.Series<>(); - return decibels; + for(int x = 0; x < taps.length; x++) + { + if(taps[x] < smallest) + { + smallest = taps[x]; + } + + series.getData().add(new XYChart.Data(x, taps[x])); + } + + ObservableList> list = mDFTChart.getData(); + list.add(series); + } + + public float[] convertDFTToDecibel(float[] dft, int tapLength) + { + float[] decibels = new float[dft.length / 2]; + + int index = 0; + + for(int x = 0; x < decibels.length; x++) + { + index = x * 2; + + decibels[x] = 20.0f * (float)Math.log10(((dft[index] * dft[index]) + + (dft[index + 1] * dft[index + 1]))); + } + + return decibels; } } 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 c129a26d..ff42ad85 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 @@ -51,11 +51,11 @@ public class FilterViewer extends Application // .build(); FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() .sampleRate((int)48000.0) - .passBandCutoff(6000) + .passBandCutoff(2880) .passBandAmplitude(1.0) - .passBandRipple(0.1) + .passBandRipple(0.01) .stopBandAmplitude(0.0) - .stopBandStart(6250) + .stopBandStart(3600) .stopBandRipple(0.01) .build(); @@ -81,6 +81,9 @@ public class FilterViewer extends Application float[] taps = null; +// taps = FilterFactory.getSinc(6000, 1920, 47, Window.WindowType.BLACKMAN); + +// taps = FilterFactory.getRootRaisedCosine(10, 10, 0.2f); // taps = FilterFactory.getLowPass(48000, 6750, 7500, 60, // Window.WindowType.HAMMING, true); diff --git a/src/main/java/io/github/dsheirer/dsp/fm/FMDemodulator.java b/src/main/java/io/github/dsheirer/dsp/fm/FMDemodulator.java index 30049836..6a53a8ff 100644 --- a/src/main/java/io/github/dsheirer/dsp/fm/FMDemodulator.java +++ b/src/main/java/io/github/dsheirer/dsp/fm/FMDemodulator.java @@ -1,78 +1,103 @@ /******************************************************************************* * SDR Trunk * Copyright (C) 2014,2015 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.fm; +import io.github.dsheirer.sample.complex.Complex; + public abstract class FMDemodulator { - private float mPreviousI = 0.0f; - private float mPreviousQ = 0.0f; - protected float mGain; + private float mPreviousI = 0.0f; + private float mPreviousQ = 0.0f; + protected float mGain; - public FMDemodulator( float gain ) - { - mGain = gain; - } + public FMDemodulator(float gain) + { + mGain = gain; + } - public float demodulate( float currentI, float currentQ ) - { - /** - * Multiply the current sample against the complex conjugate of the - * previous sample to derive the phase delta between the two samples - * - * Negating the previous sample quadrature produces the conjugate - */ - double inphase = ( currentI * mPreviousI ) - ( currentQ * -mPreviousQ ); - double quadrature = ( currentQ * mPreviousI ) + ( currentI * -mPreviousQ ); - - double angle = 0.0f; - - //Check for divide by zero - if( inphase != 0 ) - { - /** - * Use the arc-tangent of quadrature divided by inphase to - * get the phase angle (+/-) which was directly manipulated by the - * original message waveform during the modulation. This value now - * serves as the instantaneous amplitude of the demodulated signal - */ - double denominator = 1.0d / inphase; - angle = Math.atan( (double)quadrature * denominator ); - } - - /** - * Store the current sample to use during the next iteration - */ - mPreviousI = currentI; - mPreviousQ = currentQ; - - return (float)( angle * mGain ); - } + public float demodulate(float currentI, float currentQ) + { + /** + * Multiply the current sample against the complex conjugate of the + * previous sample to derive the phase delta between the two samples + * + * Negating the previous sample quadrature produces the conjugate + */ + double inphase = (currentI * mPreviousI) - (currentQ * -mPreviousQ); + double quadrature = (currentQ * mPreviousI) + (currentI * -mPreviousQ); - public abstract void dispose(); + double angle = 0.0f; - public void reset() - { - mPreviousI = 0.0f; - mPreviousQ = 0.0f; - } + //Check for divide by zero + if(inphase != 0) + { + /** + * Use the arc-tangent of quadrature divided by inphase to + * get the phase angle (+/-) which was directly manipulated by the + * original message waveform during the modulation. This value now + * serves as the instantaneous amplitude of the demodulated signal + */ + double denominator = 1.0d / inphase; + angle = Math.atan((double)quadrature * denominator); + } - public void setGain( float gain ) - { - mGain = gain; - } + /** + * Store the current sample to use during the next iteration + */ + mPreviousI = currentI; + mPreviousQ = currentQ; + + return (float)(angle * mGain); + } + + public static double demodulate(Complex previous, Complex current) + { + double inphase = (current.inphase() * previous.inphase()) - (current.quadrature() * -previous.quadrature()); + double quadrature = (current.quadrature() * previous.inphase()) + (current.inphase() * -previous.quadrature()); + + double angle = 0.0f; + + //Check for divide by zero + if(inphase != 0) + { + /** + * Use the arc-tangent of quadrature divided by inphase to + * get the phase angle (+/-) which was directly manipulated by the + * original message waveform during the modulation. This value now + * serves as the instantaneous amplitude of the demodulated signal + */ + double denominator = 1.0d / inphase; + angle = Math.atan((double)quadrature * denominator); + } + + return angle; + } + + public abstract void dispose(); + + public void reset() + { + mPreviousI = 0.0f; + mPreviousQ = 0.0f; + } + + public void setGain(float gain) + { + mGain = gain; + } } \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/dsp/fm/FMDemodulator_CB.java b/src/main/java/io/github/dsheirer/dsp/fm/FMDemodulator_CB.java index a11620a2..b5cd4380 100644 --- a/src/main/java/io/github/dsheirer/dsp/fm/FMDemodulator_CB.java +++ b/src/main/java/io/github/dsheirer/dsp/fm/FMDemodulator_CB.java @@ -26,8 +26,7 @@ import org.slf4j.LoggerFactory; public class FMDemodulator_CB extends FMDemodulator implements Listener { - private final static Logger mLog = - LoggerFactory.getLogger( FMDemodulator_CB.class ); + private final static Logger mLog = LoggerFactory.getLogger( FMDemodulator_CB.class ); private Listener mListener; diff --git a/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer.java b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer.java index c67a2b9a..b949efe2 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer.java @@ -33,11 +33,13 @@ public class InterpolatingSymbolBuffer private float mOmegaMid; private int mDelayLinePointer = 0; + private float mSamplesPerSymbol; private int mTwiceSamplesPerSymbol; private RealInterpolator mInterpolator = new RealInterpolator(1.0f); public InterpolatingSymbolBuffer(float samplesPerSymbol) { + mSamplesPerSymbol = samplesPerSymbol; mMu = samplesPerSymbol; mTwiceSamplesPerSymbol = (int)Math.floor(2.0 * samplesPerSymbol); mDelayLineInphase = new float[2 * mTwiceSamplesPerSymbol]; @@ -58,7 +60,8 @@ public class InterpolatingSymbolBuffer mDelayLineQuadrature[mDelayLinePointer + mTwiceSamplesPerSymbol] = currentSample.quadrature(); //Increment pointer and keep pointer in bounds - mDelayLinePointer = (mDelayLinePointer + 1) % mTwiceSamplesPerSymbol; + mDelayLinePointer++; + mDelayLinePointer = mDelayLinePointer % mTwiceSamplesPerSymbol; } /** @@ -80,11 +83,11 @@ public class InterpolatingSymbolBuffer { //mOmega is samples per symbol and is constrained to floating between +/- .005 of the nominal samples per //symbol - mOmega = mOmega + mGainOmega * symbolTimingError; + mOmega = mOmega + mGainOmega * -symbolTimingError; mOmega = mOmegaMid + clip(mOmega - mOmegaMid, mOmegaRel); //Add another symbol's worth of samples to the counter and adjust timing based on gardner error - increaseSampleCounter((mOmega + (mGainMu * symbolTimingError))); + increaseSampleCounter((mOmega + (mGainMu * -symbolTimingError))); } /** @@ -128,6 +131,18 @@ public class InterpolatingSymbolBuffer return mCurrentSample; } + /** + * Contents of the interpolating buffer and the current buffer index and symbol decision offset. This data can + * be used to support an external eye-diagram chart. + * @return symbol decision data. + */ + public SymbolDecisionData getSymbolDecisionData() + { + //TODO: I think that delay line pointer needs to have +4 added to it since the filter is 8 taps long + return new SymbolDecisionData(mDelayLineInphase, mDelayLineQuadrature, + mSamplesPerSymbol, mDelayLinePointer, mMu); + } + /** * Returns the interpolated inphase value for the specified offset * @param interpolation into the buffer to calculate the interpolated sample diff --git a/src/main/java/io/github/dsheirer/dsp/psk/QPSKDemodulator.java b/src/main/java/io/github/dsheirer/dsp/psk/QPSKDemodulator.java index 91d1ce76..38194543 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/QPSKDemodulator.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/QPSKDemodulator.java @@ -15,6 +15,7 @@ ******************************************************************************/ package io.github.dsheirer.dsp.psk; +import io.github.dsheirer.dsp.psk.pll.CostasLoop; import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop; import io.github.dsheirer.dsp.symbol.Dibit; import io.github.dsheirer.sample.Listener; @@ -31,7 +32,9 @@ public class QPSKDemodulator implements ComplexSampleListener private GardnerDetector mGardnerDetector = new GardnerDetector(); private Listener mSymbolListener; private Listener mPLLErrorListener; + private Listener mPLLFrequencyListener; private Listener mDibitListener; + private Listener mSymbolDecisionDataListener; private Complex mPreviousSample = new Complex(0, 0); private Complex mPreviousMiddleSample = new Complex(0, 0); @@ -42,6 +45,7 @@ public class QPSKDemodulator implements ComplexSampleListener private Complex mCurrentSymbol = new Complex(0, 0); private float mPhaseError; private float mSymbolTimingError; + private double mSampleRate; /** * Decoder for Quaternary Phase Shift Keying (QPSK) and Differential QPSK (DQPSK). This decoder uses both a @@ -63,6 +67,7 @@ public class QPSKDemodulator implements ComplexSampleListener "] must be at least 2 x symbol rate [" + symbolRate + "]"); } + mSampleRate = sampleRate; mPLL = phaseLockedLoop; mSymbolPhaseErrorCalculator = symbolPhaseErrorCalculator; mQPSKSymbolDecoder = symbolDecoder; @@ -108,6 +113,12 @@ public class QPSKDemodulator implements ComplexSampleListener mMiddleSample = mInterpolatingSymbolBuffer.getMiddleSample(); mCurrentSample = mInterpolatingSymbolBuffer.getCurrentSample(); + //Eye diagram listener + if(mSymbolDecisionDataListener != null) + { + mSymbolDecisionDataListener.receive(mInterpolatingSymbolBuffer.getSymbolDecisionData()); + } + //Calculate middle and current symbols as the delta between the previous and current samples mMiddleSymbol.setInphase(Complex.multiplyInphase(mMiddleSample.inphase(), mMiddleSample.quadrature(), mPreviousMiddleSample.inphase(), -mPreviousMiddleSample.quadrature())); mMiddleSymbol.setQuadrature(Complex.multiplyQuadrature(mMiddleSample.inphase(), mMiddleSample.quadrature(), mPreviousMiddleSample.inphase(), -mPreviousMiddleSample.quadrature())); @@ -127,12 +138,20 @@ public class QPSKDemodulator implements ComplexSampleListener mPreviousMiddleSample.setValues(mMiddleSample); mPreviousSymbol.setValues(mCurrentSymbol); + //Send to an external constellation symbol listener when registered + if(mSymbolListener != null) + { + mSymbolListener.receive(mCurrentSymbol); + } + //Adjust the current symbol as necessary (e.g. for differential encoding) mSymbolPhaseErrorCalculator.adjust(mCurrentSymbol); //Calculate the phase error of the current symbol relative to the expected constellation and provide //feedback to the PLL mPhaseError = mSymbolPhaseErrorCalculator.getPhaseError(mCurrentSymbol); + + mPhaseError = GardnerDetector.clip(mPhaseError, 0.2f); mPLL.adjust(mPhaseError); if(mPLLErrorListener != null) @@ -140,18 +159,21 @@ public class QPSKDemodulator implements ComplexSampleListener mPLLErrorListener.receive((double)mPhaseError); } + if(mPLLFrequencyListener != null) + { + double loopFrequency = ((CostasLoop)mPLL).getLoopFrequency(); + + loopFrequency *= mSampleRate / (2.0 * Math.PI); + + mPLLFrequencyListener.receive(loopFrequency); + } + //Decode the dibit from the symbol and send to the listener if(mDibitListener != null) { mDibitListener.receive(mQPSKSymbolDecoder.decode(mCurrentSymbol)); } - //Send to an external constellation symbol listener when registered - if(mSymbolListener != null) - { - mSymbolListener.receive(mCurrentSymbol); - } - //TODO: Assemble dibits here and broadcast } } @@ -179,4 +201,18 @@ public class QPSKDemodulator implements ComplexSampleListener { mPLLErrorListener = listener; } + + /** + * Registers the listener to receive PLL error values + */ + public void setPLLFrequencyListener(Listener listener) + { + mPLLFrequencyListener = listener; + } + + public void setSymbolDecisionDataListener(Listener listener) + { + mSymbolDecisionDataListener = listener; + } + } diff --git a/src/main/java/io/github/dsheirer/dsp/psk/SymbolDecisionData.java b/src/main/java/io/github/dsheirer/dsp/psk/SymbolDecisionData.java new file mode 100644 index 00000000..75a70332 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/SymbolDecisionData.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.psk; + + +public class SymbolDecisionData +{ + private float[] mSamplesInphase; + private float[] mSamplesQuadrature; + private float mSamplesPerSymbol; + private int mSampleIndex; + private float mSampleIndexOffset; + + /** + * Symbol decision data - samples and timing decision for a single symbol decision instant. + * + * @param samplesInphase + * @param samplesQuadrature + * @param samplesPerSymbol + * @param sampleIndex + * @param sampleIndexOffset + */ + public SymbolDecisionData(float[] samplesInphase, float[] samplesQuadrature, float samplesPerSymbol, + int sampleIndex, float sampleIndexOffset) + { + mSamplesInphase = samplesInphase; + mSamplesQuadrature = samplesQuadrature; + mSamplesPerSymbol = samplesPerSymbol; + mSampleIndex = sampleIndex; + mSampleIndexOffset = sampleIndexOffset; + } + + public float[] getInphaseSamples() + { + return mSamplesInphase; + } + + public float[] getQuadratureSamples() + { + return mSamplesQuadrature; + } + + public float getSamplesPerSymbol() + { + return mSamplesPerSymbol; + } + + public int getSampleIndex() + { + return mSampleIndex; + } + + public float getSampleIndexOffset() + { + return mSampleIndexOffset; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/psk/pll/CostasLoop.java b/src/main/java/io/github/dsheirer/dsp/psk/pll/CostasLoop.java index 71d07392..22726f09 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/pll/CostasLoop.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/pll/CostasLoop.java @@ -194,20 +194,21 @@ public class CostasLoop implements IPhaseLockedLoop mErrorBuffer.put(phaseError); - double variance = mErrorBuffer.variance(); + double standardDeviation = mErrorBuffer.standardDeviation(); switch(mTracking) { case SEARCHING: - if(Tracking.COARSE.meetsThreshold(variance)) + if(Tracking.COARSE.contains(standardDeviation)) { setTracking(Tracking.COARSE); } break; case COARSE: - if(Tracking.COARSE.meetsThreshold(variance)) + if(Tracking.COARSE.contains(standardDeviation)) { - if(Tracking.FINE.meetsThreshold(variance)) + //Promote if possible - otherwise remain + if(Tracking.FINE.contains(standardDeviation)) { setTracking(Tracking.FINE); } @@ -218,9 +219,10 @@ public class CostasLoop implements IPhaseLockedLoop } break; case FINE: - if(Tracking.FINE.meetsThreshold(variance)) + if(Tracking.FINE.contains(standardDeviation)) { - if(Tracking.LOCKED.meetsThreshold(variance)) + //Promote if possible - otherwise remain + if(Tracking.LOCKED.contains(standardDeviation)) { setTracking(Tracking.LOCKED); } @@ -231,7 +233,7 @@ public class CostasLoop implements IPhaseLockedLoop } break; case LOCKED: - if(!Tracking.LOCKED.meetsThreshold(variance)) + if(!Tracking.LOCKED.contains(standardDeviation)) { setTracking(Tracking.FINE); } diff --git a/src/main/java/io/github/dsheirer/dsp/psk/pll/Tracking.java b/src/main/java/io/github/dsheirer/dsp/psk/pll/Tracking.java index 5cabc7ab..a9f07ad8 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/pll/Tracking.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/pll/Tracking.java @@ -17,32 +17,55 @@ package io.github.dsheirer.dsp.psk.pll; public enum Tracking { - SEARCHING(50.0, Math.PI), - COARSE(100.0, 0.01), - FINE(150.0, 0.001), - LOCKED(200.0, 0.0001); +// SEARCHING(50.0, 0.01, Math.PI), +// COARSE(100.0, 0.001, 0.01), +// FINE(150.0, 0.0001, 0.001), +// LOCKED(200.0, 0.0, 0.0001); + SEARCHING(100.0, 0.4, Math.PI), + COARSE(150.0, 0.2, 0.4), + FINE(175.0, 0.1, 0.2), + LOCKED(200.0, 0.0, 0.1); private double mLoopBandwidth; - private double mVarianceThreshold; + private double mThresholdMinimum; + private double mThresholdMaximum; - Tracking(double loopBandwidth, double varianceThreshold) + Tracking(double loopBandwidth, double thresholdMinimum, double thresholdMaximum) { mLoopBandwidth = loopBandwidth; - mVarianceThreshold = varianceThreshold; + mThresholdMinimum = thresholdMinimum; + mThresholdMaximum = thresholdMaximum; } + /** + * Loop bandwidth setting + */ public double getLoopBandwidth() { return mLoopBandwidth; } - public double getVarianceThreshold() + /** + * Minimum error value for this entry + */ + public double getThresholdMinimum() { - return mVarianceThreshold; + return mThresholdMinimum; } - public boolean meetsThreshold(double variance) + /** + * Maximum error value for this entry + */ + public double getThresholdMaximum() { - return variance < getVarianceThreshold(); + return mThresholdMaximum; + } + + /** + * Indicates if the error value is within the range for this entry + */ + public boolean contains(double standardDeviation) + { + return getThresholdMinimum() <= standardDeviation && standardDeviation < getThresholdMaximum(); } } diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/PlaybackController.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/PlaybackController.java index 646f41cf..1ea7a687 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/PlaybackController.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/PlaybackController.java @@ -79,7 +79,6 @@ public class PlaybackController extends HBox implements IFrameLocationListener, public void load(File file) { - mLog.debug("loading:" + file.getAbsolutePath()); if(file != null && file.isFile()) { try @@ -104,7 +103,7 @@ public class PlaybackController extends HBox implements IFrameLocationListener, } catch(IOException ioe) { - mLog.error("Error opening file [" + (file != null ? file.getAbsolutePath() : "null") + "]"); + mLog.error("Error opening file [" + (file != null ? file.getAbsolutePath() : "null") + "]", ioe); Alert alert = new Alert(Alert.AlertType.ERROR, "There was an error opening the file", ButtonType.OK); diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/EyeDiagramChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/EyeDiagramChart.java new file mode 100644 index 00000000..f1dde889 --- /dev/null +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/EyeDiagramChart.java @@ -0,0 +1,118 @@ +/******************************************************************************* + * 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.instrument.gui.viewer.chart; + +import io.github.dsheirer.dsp.fm.FMDemodulator; +import io.github.dsheirer.dsp.psk.SymbolDecisionData; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.complex.Complex; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.chart.LineChart; +import javafx.scene.chart.NumberAxis; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class EyeDiagramChart extends LineChart implements Listener +{ + private final static Logger mLog = LoggerFactory.getLogger(EyeDiagramChart.class); + + private ObservableList> mData = FXCollections.observableArrayList(); + private int mSeriesCount; + private int mSeriesPointer; + private int mSeriesLength; + + public EyeDiagramChart(int seriesCount) + { + super(new NumberAxis("Symbol Timing", 1.0, 10.0, 1.0), + new NumberAxis("Value", -1.0, 1.0, 0.25)); + + mSeriesCount = seriesCount; + init(); + } + + private void init() + { + for(int x = 0; x < mSeriesCount; x++) + { + ObservableList> series = FXCollections.observableArrayList(); + mData.add(new Series<>(series)); + } + + setData(mData); + } + + private ObservableList> getSeries(int series, int length) + { + ObservableList> seriesData = mData.get(series).getData(); + + while(seriesData.size() > length) + { + seriesData.remove(0); + } + + while(seriesData.size() < length) + { + seriesData.add(new Data(0.0,0.0)); + } + + return seriesData; + } + + private void checkChartLength(int length) + { + if(mSeriesLength != length) + { + ((NumberAxis)getXAxis()).setUpperBound(length); + mSeriesLength = length; + } + } + + @Override + public void receive(SymbolDecisionData symbolDecisionData) + { + checkChartLength((int)symbolDecisionData.getSamplesPerSymbol()); + int length = (int)symbolDecisionData.getSamplesPerSymbol() + 1; + + + ObservableList> series = getSeries(mSeriesPointer++, length + 1); + + if(mSeriesPointer >= mSeriesCount) + { + mSeriesPointer = 0; + } + + int index = symbolDecisionData.getSampleIndex(); + + Complex previous = new Complex(symbolDecisionData.getInphaseSamples()[index], + symbolDecisionData.getQuadratureSamples()[index]); + + for(int x = 1; x <= length; x++) + { + Complex current = new Complex(symbolDecisionData.getInphaseSamples()[index + x], + symbolDecisionData.getQuadratureSamples()[index + x]); + + double demodulated = FMDemodulator.demodulate(previous, current); + demodulated *= 6; + + Data dataPoint = series.get(x); + dataPoint.setXValue((double)x - symbolDecisionData.getSampleIndexOffset()); + dataPoint.setYValue(demodulated); + + previous = current; + } + } +} diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/FMDemodulatedLineChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/FMDemodulatedLineChart.java new file mode 100644 index 00000000..3ccf8854 --- /dev/null +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/FMDemodulatedLineChart.java @@ -0,0 +1,119 @@ +/******************************************************************************* + * 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.instrument.gui.viewer.chart; + +import io.github.dsheirer.buffer.DoubleCircularBuffer; +import io.github.dsheirer.dsp.fm.FMDemodulator; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.complex.Complex; +import io.github.dsheirer.sample.complex.ComplexBuffer; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.chart.LineChart; +import javafx.scene.chart.NumberAxis; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class FMDemodulatedLineChart extends LineChart implements Listener +{ + private final static Logger mLog = LoggerFactory.getLogger(FMDemodulatedLineChart.class); + + private DoubleCircularBuffer mDoubleCircularBuffer; + private int mSamplesPerSymbol; + private int mTwiceSamplesPerSymbol; + private int mSeriesCount; + private ObservableList> mData = FXCollections.observableArrayList(); + private Complex mPreviousSample = new Complex(0,0); + private int mSeriesPointer; + private int mSamplePointer; + + public FMDemodulatedLineChart(int samplesPerSymbol, int seriesCount) + { + super(new NumberAxis("Time", 1, samplesPerSymbol * 2, 2), + new NumberAxis("Demodulated", -1.0, 1.0, 0.25)); + + mSamplesPerSymbol = samplesPerSymbol; + mTwiceSamplesPerSymbol = mSamplesPerSymbol * 2; + mSeriesCount = seriesCount; + + init(); + } + + private void init() + { + int seriesLength = 2 * mSamplesPerSymbol; + mDoubleCircularBuffer = new DoubleCircularBuffer(seriesLength * mSeriesCount); + + for(int x = 0; x < mSeriesCount; x++) + { + ObservableList> series = FXCollections.observableArrayList(); + + for(int y = 0; y < seriesLength; y++) + { + series.add(new Data(y + 1, 0.0)); + } + + mData.add(new Series<>(series)); + } + + setData(mData); + } + + @Override + public void receive(ComplexBuffer complexBuffer) + { + float[] samples = complexBuffer.getSamples(); + + Complex sample; + + for(int x = 0; x < samples.length; x += 2) + { + sample = new Complex(samples[x], samples[x + 1]); + double demodulated = FMDemodulator.demodulate(mPreviousSample, sample); + demodulated *= 2.0; + + if(demodulated > 1.0) + { + demodulated = 1.0; + } + else if(demodulated < -1.0) + { + demodulated = -1.0; + } + mDoubleCircularBuffer.put(demodulated); + mPreviousSample = sample; + updateChart(demodulated); + } + } + + private void updateChart(double value) + { + Series series = mData.get(mSeriesPointer); + Data data = series.getData().get(mSamplePointer++); + data.setYValue(value); + + if(mSamplePointer >= mTwiceSamplesPerSymbol) + { + mSamplePointer = 0; + mSeriesPointer++; + + if(mSeriesPointer >= mSeriesCount) + { + mSeriesPointer = 0; + } + } + } +} diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/FrequencyLineChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/FrequencyLineChart.java new file mode 100644 index 00000000..b5cd0248 --- /dev/null +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/FrequencyLineChart.java @@ -0,0 +1,73 @@ +/******************************************************************************* + * 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.instrument.gui.viewer.chart; + +import io.github.dsheirer.buffer.DoubleCircularBuffer; +import io.github.dsheirer.sample.Listener; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.chart.LineChart; +import javafx.scene.chart.NumberAxis; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class FrequencyLineChart extends LineChart implements Listener +{ + private final static Logger mLog = LoggerFactory.getLogger(FrequencyLineChart.class); + + private DoubleCircularBuffer mDoubleCircularBuffer; + private ObservableList> mPhaseValues = FXCollections.observableArrayList(); + + public FrequencyLineChart(double maxFrequency, int length) + { + super(new NumberAxis("Time", 1, length, 2), + new NumberAxis("Frequency", -maxFrequency, maxFrequency, maxFrequency / 5.0)); + + Series phaseSeries = new Series<>("PLL Frequency", mPhaseValues); + ObservableList> observableList = FXCollections.observableArrayList(phaseSeries); + + setData(observableList); + init(length); + } + + private void init(int length) + { + mDoubleCircularBuffer = new DoubleCircularBuffer(length); + + for(int x = 1; x <= length; x++) + { + mPhaseValues.add(new Data(x, 0.0)); + } + } + + @Override + public void receive(Double phase) + { + mDoubleCircularBuffer.put(phase); + + updateChart(); + } + + private void updateChart() + { + double[] values = mDoubleCircularBuffer.getAll(); + + for(int x = 0; x < values.length; x++) + { + mPhaseValues.get(x).setYValue(values[x]); + } + } +} diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/PhaseLineChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/PhaseLineChart.java index 3857026d..df7ff76a 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/PhaseLineChart.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/PhaseLineChart.java @@ -34,9 +34,9 @@ public class PhaseLineChart extends LineChart implements Listener public PhaseLineChart(int length) { super(new NumberAxis("Time", 1, length, 2), - new NumberAxis("Phase", -Math.PI, Math.PI, Math.PI / 2.0)); + new NumberAxis("Phase", -0.8, 0.8, 0.1)); - Series phaseSeries = new Series<>("Phase", mPhaseValues); + Series phaseSeries = new Series<>("PLL Phase Error", mPhaseValues); ObservableList> observableList = FXCollections.observableArrayList(phaseSeries); setData(observableList); diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/SymbolChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/SymbolChart.java index 5d67bb7d..70b9d4ce 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/SymbolChart.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/SymbolChart.java @@ -30,8 +30,8 @@ public class SymbolChart extends ScatterChart implements Listener private final static Logger mLog = LoggerFactory.getLogger(SymbolChart.class); private ObservableList mCurrentPoints = FXCollections.observableArrayList(); - private ObservableList mPreviousPoints = FXCollections.observableArrayList(); - private ObservableList mOldestPoints = FXCollections.observableArrayList(); +// private ObservableList mPreviousPoints = FXCollections.observableArrayList(); +// private ObservableList mOldestPoints = FXCollections.observableArrayList(); private int mDataPointer; private int mHistory; @@ -43,22 +43,22 @@ public class SymbolChart extends ScatterChart implements Listener */ public SymbolChart(int history) { - super(new NumberAxis("I", -1.0, 1.0, 0.2), - new NumberAxis("Q", -1.0, 1.0, 0.2)); + super(new NumberAxis("I", -1.1, 1.1, 0.2), + new NumberAxis("Q", -1.1, 1.1, 0.2)); mHistory = history; for(int x = 0; x < mHistory; x++) { mCurrentPoints.add(new XYChart.Data<>(0.0, 0.0)); - mPreviousPoints.add(new XYChart.Data<>(0.0, 0.0)); - mOldestPoints.add(new XYChart.Data<>(0.0, 0.0)); +// mPreviousPoints.add(new XYChart.Data<>(0.0, 0.0)); +// mOldestPoints.add(new XYChart.Data<>(0.0, 0.0)); } ObservableList data = FXCollections.observableArrayList(); data.add(new ScatterChart.Series("Current", mCurrentPoints)); - data.add(new ScatterChart.Series("Previous", mPreviousPoints)); - data.add(new ScatterChart.Series("Oldest", mOldestPoints)); +// data.add(new ScatterChart.Series("Previous", mPreviousPoints)); +// data.add(new ScatterChart.Series("Oldest", mOldestPoints)); this.setData(data); } @@ -67,15 +67,15 @@ public class SymbolChart extends ScatterChart implements Listener public void receive(Complex complex) { //Transfer previous to oldest - XYChart.Data oldest = mOldestPoints.get(mDataPointer); - XYChart.Data previous = mPreviousPoints.get(mDataPointer); +// XYChart.Data oldest = mOldestPoints.get(mDataPointer); +// XYChart.Data previous = mPreviousPoints.get(mDataPointer); XYChart.Data current = mCurrentPoints.get(mDataPointer); - oldest.setXValue(previous.getXValue()); - oldest.setYValue(previous.getYValue()); - - previous.setXValue(current.getXValue()); - previous.setYValue(current.getYValue()); +// oldest.setXValue(previous.getXValue()); +// oldest.setYValue(previous.getYValue()); +// +// previous.setXValue(current.getXValue()); +// previous.setYValue(current.getYValue()); current.setXValue(complex.inphase()); current.setYValue(complex.quadrature()); diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1Pane.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1Pane.java index 62253028..c13f0a92 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1Pane.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1Pane.java @@ -15,8 +15,9 @@ ******************************************************************************/ package io.github.dsheirer.instrument.gui.viewer.decoder; -import io.github.dsheirer.instrument.gui.viewer.chart.ComplexPhaseLineChart; import io.github.dsheirer.instrument.gui.viewer.chart.ComplexSampleLineChart; +import io.github.dsheirer.instrument.gui.viewer.chart.EyeDiagramChart; +import io.github.dsheirer.instrument.gui.viewer.chart.FrequencyLineChart; import io.github.dsheirer.instrument.gui.viewer.chart.PhaseLineChart; import io.github.dsheirer.instrument.gui.viewer.chart.SymbolChart; import io.github.dsheirer.module.decode.DecoderType; @@ -32,10 +33,11 @@ public class P25Phase1Pane extends DecoderPane private HBox mSampleChartBox; private ComplexSampleLineChart mSampleLineChart; - private ComplexPhaseLineChart mPhaseLineChart; + private EyeDiagramChart mEyeDiagramChart; private HBox mDecoderChartBox; private SymbolChart mSymbolChart; private PhaseLineChart mPLLPhaseErrorLineChart; + private FrequencyLineChart mPLLFrequencyLineChart; private P25_C4FMDecoder2 mDecoder = new P25_C4FMDecoder2(null); public P25Phase1Pane() @@ -47,11 +49,12 @@ public class P25Phase1Pane extends DecoderPane private void init() { addListener(getSampleLineChart()); - addListener(getPhaseLineChart()); addListener(getDecoder()); - getDecoder().setSymbolListener(getSymbolChart()); - getDecoder().setPLLPhaseErrorListener(getPLLPhaseErrorLineChart()); + getDecoder().setDEBUGSymbolListener(getSymbolChart()); + getDecoder().setDEBUGPLLPhaseErrorListener(getPLLPhaseErrorLineChart()); + getDecoder().setDEBUGPLLFrequencyListener(getPLLFrequencyLineChart()); + getDecoder().setDEBUGSymbolDecisionDataListener(getEyeDiagramChart()); getChildren().addAll(getSampleChartBox(), getDecoderChartBox()); } @@ -73,7 +76,7 @@ public class P25Phase1Pane extends DecoderPane { if(mSymbolChart == null) { - mSymbolChart = new SymbolChart(20); + mSymbolChart = new SymbolChart(10); } return mSymbolChart; @@ -86,9 +89,11 @@ public class P25Phase1Pane extends DecoderPane mDecoderChartBox = new HBox(); getSymbolChart().setMaxWidth(Double.MAX_VALUE); getPLLPhaseErrorLineChart().setMaxWidth(Double.MAX_VALUE); + getPLLFrequencyLineChart().setMaxWidth(Double.MAX_VALUE); HBox.setHgrow(getSymbolChart(), Priority.ALWAYS); HBox.setHgrow(getPLLPhaseErrorLineChart(), Priority.ALWAYS); - mDecoderChartBox.getChildren().addAll(getSymbolChart(), getPLLPhaseErrorLineChart()); + HBox.setHgrow(getPLLFrequencyLineChart(), Priority.ALWAYS); + mDecoderChartBox.getChildren().addAll(getSymbolChart(), getPLLPhaseErrorLineChart(), getPLLFrequencyLineChart()); } return mDecoderChartBox; @@ -100,10 +105,10 @@ public class P25Phase1Pane extends DecoderPane { mSampleChartBox = new HBox(); getSampleLineChart().setMaxWidth(Double.MAX_VALUE); - getPhaseLineChart().setMaxWidth(Double.MAX_VALUE); + getEyeDiagramChart().setMaxWidth(Double.MAX_VALUE); HBox.setHgrow(getSampleLineChart(), Priority.ALWAYS); - HBox.setHgrow(getPhaseLineChart(), Priority.ALWAYS); - mSampleChartBox.getChildren().addAll(getSampleLineChart(), getPhaseLineChart()); + HBox.setHgrow(getEyeDiagramChart(), Priority.ALWAYS); + mSampleChartBox.getChildren().addAll(getSampleLineChart(), getEyeDiagramChart()); } return mSampleChartBox; @@ -119,23 +124,33 @@ public class P25Phase1Pane extends DecoderPane return mSampleLineChart; } - private ComplexPhaseLineChart getPhaseLineChart() + private EyeDiagramChart getEyeDiagramChart() { - if(mPhaseLineChart == null) + if(mEyeDiagramChart == null) { - mPhaseLineChart = new ComplexPhaseLineChart(50); + mEyeDiagramChart = new EyeDiagramChart(10); } - return mPhaseLineChart; + return mEyeDiagramChart; } private PhaseLineChart getPLLPhaseErrorLineChart() { if(mPLLPhaseErrorLineChart == null) { - mPLLPhaseErrorLineChart = new PhaseLineChart(80); + mPLLPhaseErrorLineChart = new PhaseLineChart(40); } return mPLLPhaseErrorLineChart; } + + private FrequencyLineChart getPLLFrequencyLineChart() + { + if(mPLLFrequencyLineChart == null) + { + mPLLFrequencyLineChart = new FrequencyLineChart(2400, 40); + } + + return mPLLFrequencyLineChart; + } } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25MessageFramer.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25MessageFramer.java index 54047262..4d38ed79 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25MessageFramer.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25MessageFramer.java @@ -698,7 +698,8 @@ public class P25MessageFramer implements Listener @Override public void syncDetected() { - mPhaseLockedLoop.correctInversion(mCorrection); + mLog.warn("Phase Inversion SYNC detected - applying correction: " + mCorrection); +// mPhaseLockedLoop.correctInversion(mCorrection); /* Since we detected a sync pattern, start a message assembler */ for(P25MessageAssembler assembler : mAssemblers) diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2.java index 5a992365..f18ad3f3 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2.java @@ -26,8 +26,9 @@ import io.github.dsheirer.dsp.psk.IQPSKSymbolDecoder; import io.github.dsheirer.dsp.psk.ISymbolPhaseErrorCalculator; import io.github.dsheirer.dsp.psk.QPSKDemodulator; import io.github.dsheirer.dsp.psk.QPSKSymbolDecoder; +import io.github.dsheirer.dsp.psk.SymbolDecisionData; import io.github.dsheirer.dsp.psk.pll.CostasLoop; -import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop; +import io.github.dsheirer.dsp.psk.pll.Tracking; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.complex.Complex; import io.github.dsheirer.sample.complex.ComplexBuffer; @@ -49,16 +50,21 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen private Map mBasebandFilters = new HashMap<>(); private ComplexFIRFilter mBasebandFilter; + private Map mSymbolFilters = new HashMap<>(); + private ComplexFIRFilter mSymbolFilter; + private QPSKDemodulator mQPSKDemodulator; - private IPhaseLockedLoop mPhaseLockedLoop; + private CostasLoop mCostasLoop; private ComplexBufferToStreamConverter mStreamConverter = new ComplexBufferToStreamConverter(); private ComplexFeedForwardGainControl mAGC = new ComplexFeedForwardGainControl(32); private QPSKSymbolDecoder mQPSKSlicer = new QPSKSymbolDecoder(); private P25MessageFramer mMessageFramer; private ISymbolPhaseErrorCalculator mSymbolPhaseErrorCalculator = new DQPSKSymbolPhaseErrorCalculator(); private IQPSKSymbolDecoder mSymbolDecoder = new QPSKSymbolDecoder(); - private Listener mSymbolListener; - private Listener mPLLPhaseErrorListener; + private Listener mDEBUGSymbolListener; + private Listener mDEBUGPLLPhaseErrorListener; + private Listener mDEBUGPLLFrequencyListener; + private Listener mDEBUGSymbolDecisionDataListener; public P25_C4FMDecoder2(AliasList aliasList) { @@ -78,37 +84,57 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen mSampleRate = sampleRate; mBasebandFilter = new ComplexFIRFilter(getBasebandFilter(), 1.0f); + mSymbolFilter = new ComplexFIRFilter(getSymbolFilter(), 1.0f); - mPhaseLockedLoop = new CostasLoop(mSampleRate, SYMBOL_RATE); - mQPSKDemodulator = new QPSKDemodulator(mPhaseLockedLoop, mSymbolPhaseErrorCalculator, mSymbolDecoder, + mCostasLoop = new CostasLoop(mSampleRate, SYMBOL_RATE); + mCostasLoop.setTracking(Tracking.FINE); + mCostasLoop.setAutomaticTracking(false); + + mQPSKDemodulator = new QPSKDemodulator(mCostasLoop, mSymbolPhaseErrorCalculator, mSymbolDecoder, mSampleRate, SYMBOL_RATE); - mQPSKDemodulator.setSymbolListener(mSymbolListener); - mQPSKDemodulator.setPLLErrorListener(mPLLPhaseErrorListener); + mQPSKDemodulator.setSymbolListener(mDEBUGSymbolListener); + mQPSKDemodulator.setPLLErrorListener(mDEBUGPLLPhaseErrorListener); + mQPSKDemodulator.setPLLFrequencyListener(mDEBUGPLLFrequencyListener); + mQPSKDemodulator.setSymbolDecisionDataListener(mDEBUGSymbolDecisionDataListener); //Message framer can trigger a symbol-inversion correction to the PLL when detected - mMessageFramer = new P25MessageFramer(getAliasList(), mPhaseLockedLoop); + mMessageFramer = new P25MessageFramer(getAliasList(), mCostasLoop); mMessageFramer.setListener(getMessageProcessor()); mQPSKDemodulator.setDibitListener(mMessageFramer); } - public void setSymbolListener(Listener listener) + public void setDEBUGSymbolListener(Listener listener) { - mSymbolListener = listener; + mDEBUGSymbolListener = listener; mQPSKDemodulator.setSymbolListener(listener); } - public void setPLLPhaseErrorListener(Listener listener) + public void setDEBUGPLLPhaseErrorListener(Listener listener) { - mPLLPhaseErrorListener = listener; + mDEBUGPLLPhaseErrorListener = listener; mQPSKDemodulator.setPLLErrorListener(listener); } + public void setDEBUGPLLFrequencyListener(Listener listener) + { + mDEBUGPLLFrequencyListener = listener; + mQPSKDemodulator.setPLLFrequencyListener(listener); + } + + public void setDEBUGSymbolDecisionDataListener(Listener listener) + { + mDEBUGSymbolDecisionDataListener = listener; + mQPSKDemodulator.setSymbolDecisionDataListener(listener); + } + @Override public void receive(ComplexBuffer complexBuffer) { - ComplexBuffer filtered = mBasebandFilter.filter(complexBuffer); - ComplexBuffer amplified = mAGC.filter(filtered); - mQPSKDemodulator.receive(amplified); + ComplexBuffer basebandFiltered = mBasebandFilter.filter(complexBuffer); +// ComplexBuffer symbolFiltered = mBasebandFilter.filter(basebandFiltered); +// ComplexBuffer amplified = mAGC.filter(symbolFiltered); + mQPSKDemodulator.receive(basebandFiltered); +// mQPSKDemodulator.receive(complexBuffer); } private double getSampleRate() @@ -124,24 +150,44 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen { FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() .sampleRate((int)getSampleRate()) - .passBandCutoff(6000) + .passBandCutoff(2880) .passBandAmplitude(1.0) - .passBandRipple(0.1) + .passBandRipple(0.01) .stopBandAmplitude(0.0) - .stopBandStart(6250) + .stopBandStart(3600) .stopBandRipple(0.01) .build(); try { filter = FilterFactory.getTaps(specification); - mBasebandFilters.put(getSampleRate(), filter); } catch(FilterDesignException fde) { mLog.error("Couldn't design low pass baseband filter for sample rate: " + getSampleRate()); } + if(filter != null) + { + mBasebandFilters.put(getSampleRate(), filter); + } + else + { + throw new IllegalStateException("Couldn't design a C4FM symbol filter for sample rate: " + mSampleRate); + } + } + + return filter; + } + + private float[] getSymbolFilter() + { + float[] filter = mSymbolFilters.get(getSampleRate()); + + if(filter == null) + { + filter = FilterFactory.getRootRaisedCosine(10, 10, 0.2f); + mSymbolFilters.put(getSampleRate(), filter); } return filter; diff --git a/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java b/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java index d227a199..9b825533 100644 --- a/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java +++ b/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java @@ -167,13 +167,8 @@ public class ComplexWaveSource extends ComplexSource implements IControllableFil if(mInputStream != null) { mInputStream.close(); + mInputStream = null; } - else - { - throw new IOException("Can't close wave source - was not opened"); - } - - mInputStream = null; } /**