diff --git a/build.gradle b/build.gradle
index 5039a823..b89f9ef5 100644
--- a/build.gradle
+++ b/build.gradle
@@ -54,6 +54,8 @@ dependencies {
compile 'org.apache.axis:axis-jaxrpc:1.4'
compile 'org.apache.axis:axis:1.4'
compile 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.9.3'
+ compile 'org.kordamp.ikonli:ikonli-javafx:2.1.1'
+ compile 'org.kordamp.ikonli:ikonli-fontawesome-pack:2.1.1'
}
task wrapper(type: Wrapper) {
diff --git a/src/main/java/io/github/dsheirer/buffer/ComplexCircularBuffer.java b/src/main/java/io/github/dsheirer/buffer/ComplexCircularBuffer.java
index 7dbbed04..db56da97 100644
--- a/src/main/java/io/github/dsheirer/buffer/ComplexCircularBuffer.java
+++ b/src/main/java/io/github/dsheirer/buffer/ComplexCircularBuffer.java
@@ -1,17 +1,17 @@
/*******************************************************************************
* SDR Trunk
- * Copyright (C) 2014 Dennis Sheirer
- *
+ * 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
******************************************************************************/
@@ -25,66 +25,83 @@ import java.util.Arrays;
* Circular sample buffer - allocates a buffer and stores samples in a circular
* fashion, overwriting older samples with newly arrived samples. Initially
* fills buffer with 0-valued samples
- *
+ *
* Can be used as a delay-type buffer, to delay samples by the 'size' amount
*/
public class ComplexCircularBuffer
{
- Complex[] mBuffer;
- int mBufferPointer = 0;
-
- public ComplexCircularBuffer( int size )
- {
- mBuffer = new Complex[ size ];
-
- Arrays.fill( mBuffer, new Complex( 1.0f, 1.0f ) );
- }
+ Complex[] mBuffer;
+ int mBufferPointer = 0;
- /**
- * Returns the sample currently pointed to in the buffer and stores the
- * new sample in its place
- * @param sample
- * @return
- */
- public Complex get( Complex sample )
- {
- Complex retVal = mBuffer[ mBufferPointer ];
+ public ComplexCircularBuffer(int size)
+ {
+ mBuffer = new Complex[size];
- put( sample );
+ Arrays.fill(mBuffer, new Complex(1.0f, 1.0f));
+ }
- return retVal;
- }
-
- public void put( Complex sample )
- {
- mBuffer[ mBufferPointer ] = sample;
+ public int length()
+ {
+ return mBuffer.length;
+ }
- mBufferPointer++;
-
- if( mBufferPointer >= mBuffer.length )
- {
- mBufferPointer = 0;
- }
- }
-
- public Complex[] get( int count )
- {
- Complex[] samples = new Complex[ count ];
-
- int tempCounter = mBufferPointer;
+ /**
+ * Returns the sample currently pointed to in the buffer and stores the
+ * new sample in its place
+ *
+ * @param sample
+ * @return
+ */
+ public Complex get(Complex sample)
+ {
+ Complex retVal = mBuffer[mBufferPointer];
- for( int x = 0; x < count; x++ )
- {
- samples[ x ] = mBuffer[ tempCounter ];
-
- tempCounter++;
-
- if( tempCounter > mBuffer.length - 1 )
- {
- tempCounter = 0;
- }
- }
-
- return samples;
- }
+ put(sample);
+
+ return retVal;
+ }
+
+ public void put(Complex sample)
+ {
+ mBuffer[mBufferPointer] = sample;
+
+ mBufferPointer++;
+
+ if(mBufferPointer >= mBuffer.length)
+ {
+ mBufferPointer = 0;
+ }
+ }
+
+ /**
+ * Returns the (count) oldest samples in a time ordered array
+ */
+ public Complex[] get(int count)
+ {
+ Complex[] samples = new Complex[count];
+
+ int tempCounter = mBufferPointer;
+
+ for(int x = 0; x < count; x++)
+ {
+ samples[x] = mBuffer[tempCounter];
+
+ tempCounter++;
+
+ if(tempCounter > mBuffer.length - 1)
+ {
+ tempCounter = 0;
+ }
+ }
+
+ return samples;
+ }
+
+ /**
+ * Returns the samples in a time-ordered array
+ */
+ public Complex[] getAll()
+ {
+ return get(length());
+ }
}
diff --git a/src/main/java/io/github/dsheirer/buffer/DoubleCircularBuffer.java b/src/main/java/io/github/dsheirer/buffer/DoubleCircularBuffer.java
index 0c9172fe..b65f2f67 100644
--- a/src/main/java/io/github/dsheirer/buffer/DoubleCircularBuffer.java
+++ b/src/main/java/io/github/dsheirer/buffer/DoubleCircularBuffer.java
@@ -1,80 +1,180 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 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.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;
+
import java.util.Arrays;
/**
- * Circular sample buffer - allocates a buffer and stores samples in a circular
- * fashion, overwriting older samples with newly arrived samples. Initially
- * fills buffer with 0-valued samples
- *
- * Can be used as a delay-type buffer, to delay samples by the 'size' amount
+ * Circular sample buffer - allocates a buffer and stores samples in a circular fashion, overwriting older samples with
+ * newly arrived samples. Initially fills buffer with 0-valued (default) samples or with a specified value.
*/
public class DoubleCircularBuffer
{
- double[] mBuffer;
- int mBufferPointer = 0;
-
- public DoubleCircularBuffer( int size )
- {
- mBuffer = new double[ size ];
-
- Arrays.fill( mBuffer, 0.0 );
- }
+ private StandardDeviation mStandardDeviation;
+ private Variance mVariance;
+ private Max mMax;
+ private Mean mMean;
+ private Min mMin;
+ private double[] mBuffer;
+ private int mBufferPointer = 0;
- /**
- * Puts the new value into the buffer and returns the oldest buffer value
- * that it replaced
- * @param newValue
- * @return
- */
- public double get( double newValue )
- {
- double oldestSample = mBuffer[ mBufferPointer ];
+ /**
+ * Creates a circular double buffer of the specified size and all entries filled with the specified initial value.
+ * @param size of the buffer
+ * @param initialFillValue to fill the buffer entries
+ */
+ public DoubleCircularBuffer(int size, double initialFillValue)
+ {
+ mBuffer = new double[size];
+ Arrays.fill(mBuffer, initialFillValue);
+ }
- mBuffer[ mBufferPointer ] = newValue;
-
- mBufferPointer++;
-
- if( mBufferPointer >= mBuffer.length )
- {
- mBufferPointer = 0;
- }
-
- return oldestSample;
- }
+ /**
+ * Creates a circular double buffer of the specified size and all entries filled with an initial value of zero.
+ * @param size of the buffer
+ */
+ public DoubleCircularBuffer(int size)
+ {
+ this(size, 0.0);
+ }
- /**
- * Returns the maximum from the values currently in the buffer
- */
- public double max()
- {
- double max = 0.0d;
-
- for( int x = 0; x < mBuffer.length; x++ )
- {
- if( mBuffer[ x ] > max )
- {
- max = mBuffer[ x ];
- }
- }
-
- return max;
- }
+ /**
+ * Puts the new value into the buffer and returns the oldest buffer value that it replaced
+ *
+ * @param newValue to add to the buffer
+ * @return oldest value that was overwritten by the new value
+ */
+ public double get(double newValue)
+ {
+ double oldestSample = mBuffer[mBufferPointer];
+
+ mBuffer[mBufferPointer] = newValue;
+
+ mBufferPointer++;
+
+ if(mBufferPointer >= mBuffer.length)
+ {
+ mBufferPointer = 0;
+ }
+
+ return oldestSample;
+ }
+
+ public double[] getAll()
+ {
+ int valuePointer = 0;
+ double[] values = new double[mBuffer.length];
+ int bufferPointer = mBufferPointer;
+
+ while(valuePointer < values.length)
+ {
+ values[valuePointer++] = mBuffer[bufferPointer++];
+
+ if(bufferPointer >= mBuffer.length)
+ {
+ bufferPointer = 0;
+ }
+ }
+
+ return values;
+ }
+
+ /**
+ * Places the new value into the buffer, overwriting the oldest value
+ * @param newValue to add to the buffer
+ */
+ public void put(double newValue)
+ {
+ mBuffer[mBufferPointer] = newValue;
+
+ mBufferPointer++;
+
+ if(mBufferPointer >= mBuffer.length)
+ {
+ mBufferPointer = 0;
+ }
+ }
+
+ /**
+ * Returns the maximum from the values currently in the buffer
+ */
+ public double max()
+ {
+ if(mMax == null)
+ {
+ mMax = new Max();
+ }
+
+ return mMax.evaluate(mBuffer, 0, mBuffer.length);
+ }
+
+ /**
+ * Returns the minimum from the values currently in the buffer
+ */
+ public double min()
+ {
+ if(mMin == null)
+ {
+ mMin = new Min();
+ }
+
+ 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
+ */
+ public double variance()
+ {
+ if(mVariance == null)
+ {
+ mVariance = new Variance();
+ }
+
+ return mVariance.evaluate(mBuffer);
+ }
+
+ /**
+ * Calculates the average/mean of the values contained in this buffer
+ */
+ public double mean()
+ {
+ if(mMean == null)
+ {
+ mMean = new Mean();
+ }
+
+ return mMean.evaluate(mBuffer, 0, mBuffer.length);
+ }
}
diff --git a/src/main/java/io/github/dsheirer/buffer/FloatCircularBuffer.java b/src/main/java/io/github/dsheirer/buffer/FloatCircularBuffer.java
index ce3d0bd3..640e6506 100644
--- a/src/main/java/io/github/dsheirer/buffer/FloatCircularBuffer.java
+++ b/src/main/java/io/github/dsheirer/buffer/FloatCircularBuffer.java
@@ -1,17 +1,17 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 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
******************************************************************************/
@@ -23,96 +23,98 @@ import java.util.Arrays;
* Circular sample buffer - allocates a buffer and stores samples in a circular
* fashion, overwriting older samples with newly arrived samples. Initially
* fills buffer with 0-valued samples
- *
- * Can be used as a delay-type buffer, to delay samples by the 'size' amount
- * @author denny
*
+ * Can be used as a delay-type buffer, to delay samples by the 'size' amount
+ *
+ * @author denny
*/
public class FloatCircularBuffer
{
- float[] mBuffer;
- int mBufferPointer = 0;
-
- public FloatCircularBuffer( int size )
- {
- mBuffer = new float[ size ];
-
- Arrays.fill( mBuffer, (float)0.0 );
- }
-
- public int getSize()
- {
- return mBuffer.length;
- }
-
- public float[] getBuffer()
- {
- return mBuffer;
- }
+ float[] mBuffer;
+ int mBufferPointer = 0;
- /**
- * Puts the new value into the buffer and returns the oldest buffer value
- * that it replaced
- * @param newValue
- * @return
- */
- public float get( float newValue )
- {
- float oldestSample = mBuffer[ mBufferPointer ];
+ /**
+ * Creates a circular double buffer of the specified size and all entries filled with the specified initial value.
+ *
+ * @param size of the buffer
+ * @param initialFillValue to fill the buffer entries
+ */
+ public FloatCircularBuffer(int size, float initialFillValue)
+ {
+ mBuffer = new float[size];
+ Arrays.fill(mBuffer, initialFillValue);
+ }
- mBuffer[ mBufferPointer ] = newValue;
-
- mBufferPointer++;
-
- if( mBufferPointer >= mBuffer.length )
- {
- mBufferPointer = 0;
- }
-
- return oldestSample;
- }
+ /**
+ * Creates a circular double buffer of the specified size and all entries filled with an initial value of zero.
+ *
+ * @param size of the buffer
+ */
+ public FloatCircularBuffer(int size)
+ {
+ this(size, 0.0f);
+ }
- /**
- * Convenience method to put a new value in the buffer, while discarding
- * the old value
- */
- public void put( float newValue )
- {
- get( newValue );
- }
+ public float[] getBuffer()
+ {
+ return mBuffer;
+ }
- /**
- * Returns an array of arraySize, loaded with sampleCount of the
- * oldest samples, loaded into the array starting at index 0.
- *
- * This is useful for fetching an array filled half way with samples to
- * use in FFT processing.
- * @param arraySize - size of the array (must be larger than sampleCount)
- * @param sampleCount - number of the oldest samples in the buffer to load
- * @return
- */
- public float[] get( int arraySize, int sampleCount )
- {
- float[] samples = new float[ arraySize ];
+ /**
+ * Puts the new value into the buffer and returns the oldest buffer value that it replaced
+ *
+ * @param newValue to add to the buffer
+ * @return oldest value that was overwritten by the new value
+ */
+ public float get(float newValue)
+ {
+ float oldestSample = mBuffer[mBufferPointer];
- if( arraySize > sampleCount )
- {
- int tempCounter = mBufferPointer;
+ mBuffer[mBufferPointer] = newValue;
- for( int x = 0; x < sampleCount; x++ )
- {
- samples[ x ] = mBuffer[ tempCounter ];
-
- tempCounter++;
-
- if( tempCounter > mBuffer.length )
- {
- tempCounter = 0;
- }
- }
- }
-
- return samples;
- }
-
+ mBufferPointer++;
+
+ if(mBufferPointer >= mBuffer.length)
+ {
+ mBufferPointer = 0;
+ }
+
+ return oldestSample;
+ }
+
+ public float[] getAll()
+ {
+ int valuePointer = 0;
+ float[] values = new float[mBuffer.length];
+ int bufferPointer = mBufferPointer;
+
+ while(valuePointer < values.length)
+ {
+ values[valuePointer++] = mBuffer[bufferPointer++];
+
+ if(bufferPointer >= mBuffer.length)
+ {
+ bufferPointer = 0;
+ }
+ }
+
+ return values;
+ }
+
+ /**
+ * Places the new value into the buffer, overwriting the oldest value
+ *
+ * @param newValue to add to the buffer
+ */
+ public void put(float newValue)
+ {
+ mBuffer[mBufferPointer] = newValue;
+
+ mBufferPointer++;
+
+ if(mBufferPointer >= mBuffer.length)
+ {
+ mBufferPointer = 0;
+ }
+ }
}
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 01706df0..074448f4 100644
--- a/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java
+++ b/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java
@@ -666,11 +666,11 @@ public class FilterFactory
* @param alpha - roll-off factor
* @return - filter coefficients
*/
- public static float[] getRootRaisedCosine(int samplesPerSymbol,
+ public static float[] getRootRaisedCosine(float samplesPerSymbol,
int symbols,
float alpha)
{
- int taps = samplesPerSymbol * symbols + 1;
+ int taps = (int)(samplesPerSymbol * (float)symbols + 1.0f);
float scale = 0;
@@ -680,8 +680,8 @@ public class FilterFactory
{
float index = (float) x - ((float) taps / 2.0f);
- float x1 = (float) Math.PI * index / (float) samplesPerSymbol;
- float x2 = 4.0f * alpha * index / (float) samplesPerSymbol;
+ float x1 = (float) Math.PI * index / samplesPerSymbol;
+ float x2 = 4.0f * alpha * index / samplesPerSymbol;
float x3 = x2 * x2 - 1.0f;
float numerator, denominator;
@@ -692,7 +692,7 @@ public class FilterFactory
{
numerator = (float) Math.cos((1.0 + alpha) * x1) +
(float) Math.sin((1.0f - alpha) * x1) /
- (4.0f * alpha * index / (float) samplesPerSymbol);
+ (4.0f * alpha * index / samplesPerSymbol);
}
else
{
@@ -713,9 +713,9 @@ public class FilterFactory
x3 = (1.0f - alpha) * x1;
x2 = (1.0f + alpha) * x1;
- numerator = (float) (Math.sin(x2) * (1.0f + alpha) * Math.PI -
+ numerator = (float) (Math.sin(x2) * (1.0 + alpha) * Math.PI -
Math.cos(x3) * ((1.0 - alpha) * Math.PI *
- (double) samplesPerSymbol) / (4 * alpha * index) +
+ (double) samplesPerSymbol) / (4.0 * alpha * index) +
Math.sin(x3) * (double) samplesPerSymbol *
(double) samplesPerSymbol / (4.0 * alpha * index * index));
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 e2e7d9ca..480463ee 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
@@ -39,15 +39,24 @@ public class FilterViewer extends Application
*/
private float[] getFilter()
{
+// FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
+// .sampleRate(48000)
+// .gridDensity(16)
+// .passBandCutoff(2500)
+// .passBandAmplitude(1.0)
+// .passBandRipple(0.01)
+// .stopBandStart(4000)
+// .stopBandAmplitude(0.0)
+// .stopBandRipple(0.008)
+// .build();
FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
.sampleRate(48000)
- .gridDensity(16)
- .passBandCutoff(2500)
+ .passBandCutoff(5100)
.passBandAmplitude(1.0)
.passBandRipple(0.01)
- .stopBandStart(4000)
.stopBandAmplitude(0.0)
- .stopBandRipple(0.008)
+ .stopBandStart(6500)
+ .stopBandRipple(0.01)
.build();
// FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder()
@@ -72,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/filter/fir/complex/ComplexFIRFilter.java b/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java
index 8190b9a1..6ac9a164 100644
--- a/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java
+++ b/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java
@@ -1,54 +1,90 @@
+/*******************************************************************************
+ * sdr-trunk
+ * Copyright (C) 2014-2018 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with this program.
+ * If not, see
+ *
+ ******************************************************************************/
package io.github.dsheirer.dsp.filter.fir.complex;
import io.github.dsheirer.dsp.filter.fir.FIRFilter;
import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter;
import io.github.dsheirer.sample.complex.Complex;
+import io.github.dsheirer.sample.complex.ComplexBuffer;
public class ComplexFIRFilter extends FIRFilter
{
- private RealFIRFilter mIFilter;
- private RealFIRFilter mQFilter;
-
- /**
- * Complex FIR Filter for processing complex sample pairs. Wraps two real
- * FIR filters for processing each of the inphase and quadrature samples.
- *
- * @param coefficients - filter taps
- * @param gain - gain to apply to filtered outputs - use 1.0f for no gain
- */
- public ComplexFIRFilter( float[] coefficients, float gain )
- {
- mIFilter = new RealFIRFilter( coefficients, gain );
- mQFilter = new RealFIRFilter( coefficients, gain );
- }
-
- public float[] getCoefficients()
- {
- return mIFilter.getCoefficients();
- }
-
- public float filterInphase( float sample )
- {
- return mIFilter.filter( sample );
- }
-
- public float filterQuadrature( float sample )
- {
- return mQFilter.filter( sample );
- }
-
- public Complex filter( Complex sample )
- {
- float i = filterInphase( sample.inphase() );
- float q = filterQuadrature( sample.quadrature() );
-
- return new Complex( i, q );
- }
-
- @Override
- public void dispose()
- {
- mIFilter.dispose();
- mQFilter.dispose();
- }
+ private RealFIRFilter mIFilter;
+ private RealFIRFilter mQFilter;
+
+ /**
+ * Complex FIR Filter for processing complex sample pairs. Wraps two real
+ * FIR filters for processing each of the inphase and quadrature samples.
+ *
+ * @param coefficients - filter taps
+ * @param gain - gain to apply to filtered outputs - use 1.0f for no gain
+ */
+ public ComplexFIRFilter(float[] coefficients, float gain)
+ {
+ mIFilter = new RealFIRFilter(coefficients, gain);
+ mQFilter = new RealFIRFilter(coefficients, gain);
+ }
+
+ public float[] getCoefficients()
+ {
+ return mIFilter.getCoefficients();
+ }
+
+ public float filterInphase(float sample)
+ {
+ return mIFilter.filter(sample);
+ }
+
+ public float filterQuadrature(float sample)
+ {
+ return mQFilter.filter(sample);
+ }
+
+ public Complex filter(Complex sample)
+ {
+ float i = filterInphase(sample.inphase());
+ float q = filterQuadrature(sample.quadrature());
+
+ return new Complex(i, q);
+ }
+
+ /**
+ * Filters the complex samples from the buffer and returns a new complex buffer with the filtered output
+ *
+ * @param buffer with complex samples to filter
+ * @return new buffer containing filtered complex samples
+ */
+ public ComplexBuffer filter(ComplexBuffer buffer)
+ {
+ float[] samples = buffer.getSamples();
+ float[] filtered = new float[samples.length];
+
+ for(int x = 0; x < samples.length; x += 2)
+ {
+ filtered[x] = filterInphase(samples[x]);
+ filtered[x + 1] = filterQuadrature(samples[x + 1]);
+ }
+
+ return new ComplexBuffer(filtered);
+ }
+
+ @Override
+ public void dispose()
+ {
+ mIFilter.dispose();
+ mQFilter.dispose();
+ }
}
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter_CB_CB.java b/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter_CB_CB.java
index 33cd4862..d864d2e2 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,8 +3,7 @@ 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;
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/interpolator/RealInterpolator.java b/src/main/java/io/github/dsheirer/dsp/filter/interpolator/RealInterpolator.java
index d0b99908..ff933ed1 100644
--- a/src/main/java/io/github/dsheirer/dsp/filter/interpolator/RealInterpolator.java
+++ b/src/main/java/io/github/dsheirer/dsp/filter/interpolator/RealInterpolator.java
@@ -5,54 +5,86 @@ import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.text.DecimalFormat;
+import java.util.Arrays;
+
public class RealInterpolator extends Interpolator
{
- private final static Logger mLog = LoggerFactory.getLogger( RealInterpolator.class );
+ private final static Logger mLog = LoggerFactory.getLogger(RealInterpolator.class);
- private float mGain;
-
- public RealInterpolator( float gain )
- {
- mGain = gain;
- }
- /**
- * Calculates an interpolated value from the 8 samples beginning at the
- * offset index. Uses the mu value to determine which of 128 filter kernels
- * to use in order to closely approximate the ideal frequency response for
- * the value of mu, ranging from 0.0 to 1.0, indicating the interpolated
- * position in the sample set ranging from offset to offset + 7.
- *
- * @param samples - sample array of length at least offset + 7
- *
- * @param mu - interpolated sample position between 0 and 1.0
- *
- * @return - interpolated sample value
- */
- public float filter( float[] samples, int offset, float mu )
- {
- /* Ensure we have enough samples in the array */
- Validate.isTrue(samples.length >= offset + 7);
+ private float mGain;
- /* Identify the filter bank that corresponds to mu */
- int index = (int)( NSTEPS * mu );
-
- float accumulator = ( TAPS[ index ][ 7 ] * samples[ offset ] );
- accumulator += ( TAPS[ index ][ 6 ] * samples[ offset + 1 ] );
- accumulator += ( TAPS[ index ][ 5 ] * samples[ offset + 2 ] );
- accumulator += ( TAPS[ index ][ 4 ] * samples[ offset + 3 ] );
- accumulator += ( TAPS[ index ][ 3 ] * samples[ offset + 4 ] );
- accumulator += ( TAPS[ index ][ 2 ] * samples[ offset + 5 ] );
- accumulator += ( TAPS[ index ][ 1 ] * samples[ offset + 6 ] );
- accumulator += ( TAPS[ index ][ 0 ] * samples[ offset + 7 ] );
-
- return accumulator * mGain;
- }
-
- public Complex filter( float[] iSamples, float[] qSamples, int offset, float mu )
- {
- float i = filter( iSamples, offset, mu );
- float q = filter( qSamples, offset, mu );
-
- return new Complex( i, q );
- }
+ /**
+ * Provides an interpolated sample point along an eight-sample waveform representation using 128 filters to
+ * provide approximate interpolation with a resolution of 1/128th of a sample.
+ *
+ * @param gain to apply to the interpolated sample
+ */
+ public RealInterpolator(float gain)
+ {
+ mGain = gain;
+ }
+
+ /**
+ * Calculates an interpolated value from eight samples that start at the offset into the sample array. The
+ * interpolated sample will fall within the middle of the eight sample array, between indexes offset+3 and
+ * offset+4. The mu argument is translated into an index position between 0 and 128, where a mu of 0.0 will be
+ * converted to index zero and will be equal to the sample at index offset+3 and a mu of 1.0 will be equal to
+ * the sample at offset+4. All mu values between 0.0 and 1.0 will be converted to a 1 - 127 index and will
+ * produce an approximated value from among 127 interpolated sample points between indexes offset+3 and offset+4.
+ *
+ * @param samples - sample array of length at least offset + 7
+ * @param mu - interpolated sample position between 0.0 and 1.0
+ * @return - interpolated sample value
+ */
+ public float filter(float[] samples, int offset, float mu)
+ {
+ /* Ensure we have enough samples in the array */
+ Validate.isTrue(samples.length >= offset + 7);
+
+ /* Identify the filter bank that corresponds to mu */
+ int index = (int)(NSTEPS * mu);
+
+ float accumulator = (TAPS[index][7] * samples[offset]);
+ accumulator += (TAPS[index][6] * samples[offset + 1]);
+ accumulator += (TAPS[index][5] * samples[offset + 2]);
+ accumulator += (TAPS[index][4] * samples[offset + 3]);
+ accumulator += (TAPS[index][3] * samples[offset + 4]);
+ accumulator += (TAPS[index][2] * samples[offset + 5]);
+ accumulator += (TAPS[index][1] * samples[offset + 6]);
+ accumulator += (TAPS[index][0] * samples[offset + 7]);
+
+ return accumulator * mGain;
+ }
+
+ public Complex filter(float[] iSamples, float[] qSamples, int offset, float mu)
+ {
+ float i = filter(iSamples, offset, mu);
+ float q = filter(qSamples, offset, mu);
+
+ return new Complex(i, q);
+ }
+
+ public static void main(String[] args)
+ {
+ RealInterpolator interpolator = new RealInterpolator(1.0f);
+ DecimalFormat decimalFormat = new DecimalFormat("0.0000");
+
+ double TWO_PI = Math.PI * 2.0;
+
+ float[] samples = new float[16];
+
+ for(int x = 0; x < 16; x++)
+ {
+ samples[x] = (float)Math.sin(TWO_PI * (double)x / 8.0);
+ }
+
+ mLog.debug("Samples: " + Arrays.toString(samples));
+
+ for(float x = 0.0f; x <= 1.01f; x += 0.1f)
+ {
+ mLog.debug(decimalFormat.format(x) + ": " + decimalFormat.format(interpolator.filter(samples, 1, x)));
+ }
+
+ }
}
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/gain/ComplexFeedForwardGainControl.java b/src/main/java/io/github/dsheirer/dsp/gain/ComplexFeedForwardGainControl.java
index c939b952..215d7b46 100644
--- a/src/main/java/io/github/dsheirer/dsp/gain/ComplexFeedForwardGainControl.java
+++ b/src/main/java/io/github/dsheirer/dsp/gain/ComplexFeedForwardGainControl.java
@@ -1,112 +1,125 @@
+/*******************************************************************************
+ * 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.gain;
import io.github.dsheirer.buffer.FloatCircularBuffer;
import io.github.dsheirer.sample.complex.Complex;
+import io.github.dsheirer.sample.complex.ComplexBuffer;
import io.github.dsheirer.sample.complex.ComplexSampleListener;
-/*******************************************************************************
- * SDR Trunk
- * Copyright (C) 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
- *
- * This class is modeled using concepts detailed in the GNURadio
- * implementation of feed forward AGC class located at:
- * https://github.com/gnuradio/gnuradio/blob/master/gr-analog/lib/
- * feedforward_agc_cc_impl.cc
- *
- ******************************************************************************/
public class ComplexFeedForwardGainControl implements ComplexSampleListener
{
- public static final float OBJECTIVE_ENVELOPE = 1.0f;
- public static final float MINIMUM_ENVELOPE = 0.0001f;
+ public static final float OBJECTIVE_ENVELOPE = 1.0f;
+ public static final float MINIMUM_ENVELOPE = 0.0001f;
- private ComplexSampleListener mListener;
+ private ComplexSampleListener mListener;
- private FloatCircularBuffer mEnvelopeHistory;
-
- private float mMaxEnvelope = 0.0f;
- private float mGain = 1.0f;
-
- /**
- * Dynamic gain control for incoming sample stream to amplify or attenuate
- * all samples toward an objective unity)gain, using the maximum envelope
- * value detected in the stream history window.
- *
- * Uses the specified damping factor to limit gain swings. Damping factor
- * is applied against the delta between current gain value and a recalculated
- * gain value to limit how quickly the gain value will increase or decrease.
- *
- * @param window - history size to use in detecting maximum envelope value
- * @param damping - a value between 0 < damping <= 1.0;
- */
- public ComplexFeedForwardGainControl( int window )
- {
- mEnvelopeHistory = new FloatCircularBuffer( window );
- }
-
- public void dispose()
- {
- mListener = null;
- }
+ private FloatCircularBuffer mEnvelopeHistory;
- @Override
- public void receive( float inphase, float quadrature )
- {
- float envelope = Complex.envelope( inphase, quadrature );
+ private float mMaxEnvelope = 0.0f;
+ private float mGain = 1.0f;
- if( envelope > mMaxEnvelope )
- {
- mMaxEnvelope = envelope;
-
- adjustGain();
- }
+ /**
+ * Dynamic gain control for incoming sample stream to amplify or attenuate
+ * all samples toward an objective unity)gain, using the maximum envelope
+ * value detected in the stream history window.
+ *
+ * Uses the specified damping factor to limit gain swings. Damping factor
+ * is applied against the delta between current gain value and a recalculated
+ * gain value to limit how quickly the gain value will increase or decrease.
+ *
+ * @param window - history size to use in detecting maximum envelope value
+ */
+ public ComplexFeedForwardGainControl(int window)
+ {
+ mEnvelopeHistory = new FloatCircularBuffer(window);
+ }
- /* Replace oldest envelope value with current envelope value */
- float oldestEnvelope = mEnvelopeHistory.get( envelope );
+ public void dispose()
+ {
+ mListener = null;
+ }
- /* If the oldest envelope value was the max envelope value, then we
- * have to rediscover the max value from the envelope history */
- if( mMaxEnvelope == oldestEnvelope && mMaxEnvelope != envelope )
- {
- mMaxEnvelope = MINIMUM_ENVELOPE;
-
- for( float value: mEnvelopeHistory.getBuffer() )
- {
- if( value > mMaxEnvelope )
- {
- mMaxEnvelope = value;
- }
- }
-
- adjustGain();
- }
-
- /* Apply current gain value to the sample and send to the listener */
- if( mListener != null )
- {
- mListener.receive( inphase *= mGain, quadrature *= mGain );
- }
- }
-
- private void adjustGain()
- {
- mGain = OBJECTIVE_ENVELOPE / mMaxEnvelope;
- }
+ @Override
+ public void receive(float inphase, float quadrature)
+ {
+ process(inphase, quadrature);
- public void setListener( ComplexSampleListener listener )
- {
- mListener = listener;
- }
+ /* Apply current gain value to the sample and send to the listener */
+ if(mListener != null)
+ {
+ mListener.receive(inphase *= mGain, quadrature *= mGain);
+ }
+ }
+
+ private void process(float inphase, float quadrature)
+ {
+ float envelope = Complex.envelope(inphase, quadrature);
+
+ if(envelope > mMaxEnvelope)
+ {
+ mMaxEnvelope = envelope;
+
+ adjustGain();
+ }
+
+ /* Replace oldest envelope value with current envelope value */
+ float oldestEnvelope = mEnvelopeHistory.get(envelope);
+
+ /* If the oldest envelope value was the max envelope value, then we
+ * have to rediscover the max value from the envelope history */
+ if(mMaxEnvelope == oldestEnvelope && mMaxEnvelope != envelope)
+ {
+ mMaxEnvelope = MINIMUM_ENVELOPE;
+
+ for(float value : mEnvelopeHistory.getBuffer())
+ {
+ if(value > mMaxEnvelope)
+ {
+ mMaxEnvelope = value;
+ }
+ }
+
+ adjustGain();
+ }
+ }
+
+ private void adjustGain()
+ {
+ mGain = OBJECTIVE_ENVELOPE / mMaxEnvelope;
+ }
+
+ public void setListener(ComplexSampleListener listener)
+ {
+ mListener = listener;
+ }
+
+ public ComplexBuffer filter(ComplexBuffer complexBuffer)
+ {
+ float[] samples = complexBuffer.getSamples();
+ float[] processed = new float[samples.length];
+
+ for(int x = 0; x < samples.length; x += 2)
+ {
+ process(samples[x], samples[x + 1]);
+
+ processed[x] = samples[x] * mGain;
+ processed[x + 1] = samples[x + 1] * mGain;
+ }
+
+ return new ComplexBuffer(processed);
+ }
}
diff --git a/src/main/java/io/github/dsheirer/dsp/gain/ComplexGain.java b/src/main/java/io/github/dsheirer/dsp/gain/ComplexGain.java
index f10b571b..70dcdd0e 100644
--- a/src/main/java/io/github/dsheirer/dsp/gain/ComplexGain.java
+++ b/src/main/java/io/github/dsheirer/dsp/gain/ComplexGain.java
@@ -5,27 +5,34 @@ import io.github.dsheirer.sample.complex.Complex;
public class ComplexGain implements Listener
{
- private float mGain;
- private Listener mListener;
+ private float mGain;
+ private Listener mListener;
- public ComplexGain( float gain )
- {
- mGain = gain;
- }
-
- @Override
- public void receive( Complex sample )
- {
- if( mListener != null )
- {
- sample.multiply( mGain );
-
- mListener.receive( sample );
- }
- }
+ public ComplexGain(float gain)
+ {
+ mGain = gain;
+ }
- public void setListener( Listener listener )
- {
- mListener = listener;
- }
+ public Complex apply(Complex sample)
+ {
+ sample.multiply(mGain);
+
+ return sample;
+ }
+
+ @Override
+ public void receive(Complex sample)
+ {
+ if(mListener != null)
+ {
+ apply(sample);
+
+ mListener.receive(sample);
+ }
+ }
+
+ public void setListener(Listener listener)
+ {
+ mListener = listener;
+ }
}
diff --git a/src/main/java/io/github/dsheirer/dsp/mixer/Oscillator.java b/src/main/java/io/github/dsheirer/dsp/mixer/Oscillator.java
index 50511208..9825dfbb 100644
--- a/src/main/java/io/github/dsheirer/dsp/mixer/Oscillator.java
+++ b/src/main/java/io/github/dsheirer/dsp/mixer/Oscillator.java
@@ -51,8 +51,7 @@ public class Oscillator
*/
private void update()
{
- float anglePerSample =
- (float)( 2.0d * Math.PI * mFrequency / mSampleRate );
+ float anglePerSample = (float)( 2.0d * Math.PI * mFrequency / mSampleRate );
mAnglePerSample = Complex.fromAngle( anglePerSample );
}
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulator.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulator.java
new file mode 100644
index 00000000..f8b6aabd
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulator.java
@@ -0,0 +1,162 @@
+/*******************************************************************************
+ * 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;
+
+import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop;
+import io.github.dsheirer.dsp.symbol.Dibit;
+import io.github.dsheirer.sample.Listener;
+import io.github.dsheirer.sample.complex.Complex;
+import io.github.dsheirer.sample.complex.ComplexBuffer;
+import io.github.dsheirer.sample.complex.ComplexSampleListener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class DQPSKDemodulator implements ComplexSampleListener
+{
+ private final static Logger mLog = LoggerFactory.getLogger(DQPSKDemodulator.class);
+ protected InterpolatingSampleBuffer mInterpolatingSampleBuffer;
+ protected IPhaseLockedLoop mPLL;
+ protected DQPSKSymbolEvaluator mSymbolEvaluator = new DQPSKSymbolEvaluator();
+ private Complex mReceivedSample = new Complex(0, 0);
+ private Complex mPreviousPrecedingSample = new Complex(0, 0);
+ private Complex mPreviousCurrentSample = new Complex(0, 0);
+ private Complex mPrecedingSample = new Complex(0, 0);
+ private Complex mCurrentSample = new Complex(0, 0);
+ private Complex mPrecedingSymbol = new Complex(0, 0);
+ protected Complex mCurrentSymbol = new Complex(0, 0);
+ private Listener mDibitListener;
+
+ /**
+ * Decoder for Differential Quaternary Phase Shift Keying (DQPSK). This decoder uses both a Costas Loop (PLL) and
+ * a custom DQPSK symbol timing error detector to automatically align to the incoming carrier frequency and to
+ * adjust for any changes in symbol timing.
+ *
+ * This detector is optimized for constant amplitude DQPSK symbols like C4FM.
+ *
+ * @param phaseLockedLoop for tracking carrier frequency error
+ * @param interpolatingSampleBuffer to hold samples for interpolating a symbol
+ */
+ public DQPSKDemodulator(IPhaseLockedLoop phaseLockedLoop, InterpolatingSampleBuffer interpolatingSampleBuffer)
+ {
+ mPLL = phaseLockedLoop;
+ mInterpolatingSampleBuffer = interpolatingSampleBuffer;
+ }
+
+ /**
+ * Processes a (filtered) buffer containing complex samples for decoding
+ * @param complexBuffer with complex samples
+ */
+ public void receive(ComplexBuffer complexBuffer)
+ {
+ float[] samples = complexBuffer.getSamples();
+
+ for(int x = 0; x < samples.length; x += 2)
+ {
+ receive(samples[x], samples[x + 1]);
+ }
+ }
+
+ /**
+ * Processes a complex sample for decoding. Once sufficient samples are buffered, a symbol decision is made.
+ * @param inphase value for the sample
+ * @param quadrature value for the sample
+ */
+ @Override
+ public void receive(float inphase, float quadrature)
+ {
+ //Update current sample with values
+ mReceivedSample.setValues(inphase, quadrature);
+
+ //Mix current sample with costas loop to remove any rotation that is present from a mis-tuned carrier frequency
+ mReceivedSample.multiply(mPLL.incrementAndGetCurrentVector());
+
+ //Store the sample in the interpolating buffer
+ mInterpolatingSampleBuffer.receive(mReceivedSample);
+
+ //Calculate the symbol once we've stored enough samples
+ if(mInterpolatingSampleBuffer.hasSymbol())
+ {
+ calculateSymbol();
+ }
+ }
+
+ /**
+ * Calculates a symbol from the interpolating buffer
+ */
+ protected void calculateSymbol()
+ {
+ //Get preceding sample and an interpolated current sample from the interpolating buffer
+ mPrecedingSample = mInterpolatingSampleBuffer.getPrecedingSample();
+ mCurrentSample = mInterpolatingSampleBuffer.getCurrentSample();
+
+ //Calculate preceding, current and following symbols as the delta rotation compared to the previous samples
+ mPrecedingSymbol.setInphase(Complex.multiplyInphase(mPrecedingSample.inphase(), mPrecedingSample.quadrature(),
+ mPreviousPrecedingSample.inphase(), -mPreviousPrecedingSample.quadrature()));
+ mPrecedingSymbol.setQuadrature(Complex.multiplyQuadrature(mPrecedingSample.inphase(), mPrecedingSample.quadrature(),
+ mPreviousPrecedingSample.inphase(), -mPreviousPrecedingSample.quadrature()));
+
+ mCurrentSymbol.setInphase(Complex.multiplyInphase(mCurrentSample.inphase(), mCurrentSample.quadrature(),
+ mPreviousCurrentSample.inphase(), -mPreviousCurrentSample.quadrature()));
+ mCurrentSymbol.setQuadrature(Complex.multiplyQuadrature(mCurrentSample.inphase(), mCurrentSample.quadrature(),
+ mPreviousCurrentSample.inphase(), -mPreviousCurrentSample.quadrature()));
+
+ //Set gain to unity before we calculate the error value
+ mPrecedingSymbol.normalize();
+ mCurrentSymbol.normalize();
+
+ mSymbolEvaluator.setSymbol(mPrecedingSymbol, mCurrentSymbol);
+
+ mInterpolatingSampleBuffer.resetAndAdjust(mSymbolEvaluator.getTimingError());
+
+ //Store current samples/symbols to use for the next period
+ mPreviousPrecedingSample.setValues(mPrecedingSample);
+ mPreviousCurrentSample.setValues(mCurrentSample);
+
+ //Calculate the phase error of the current symbol relative to the expected constellation and provide
+ //feedback to the PLL
+ mPLL.adjust(clip(mSymbolEvaluator.getPhaseError(), 0.5f));
+
+ if(mDibitListener != null)
+ {
+ mDibitListener.receive(mSymbolEvaluator.getSymbolDecision());
+ }
+ }
+
+ /**
+ * Registers the listener to receive dibit symbol decisions from this demodulator
+ */
+ public void setDibitListener(Listener listener)
+ {
+ mDibitListener = listener;
+ }
+
+ /**
+ * Constrains value to the range of ( -maximum <> maximum )
+ */
+ public static float clip(float value, float maximum)
+ {
+ if(value > maximum)
+ {
+ return maximum;
+ }
+ else if(value < -maximum)
+ {
+ return -maximum;
+ }
+
+ return value;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulatorInstrumented.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulatorInstrumented.java
new file mode 100644
index 00000000..cc90a742
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulatorInstrumented.java
@@ -0,0 +1,131 @@
+/*******************************************************************************
+ * 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;
+
+import io.github.dsheirer.dsp.psk.pll.CostasLoop;
+import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop;
+import io.github.dsheirer.sample.Listener;
+import io.github.dsheirer.sample.complex.Complex;
+
+public class DQPSKDemodulatorInstrumented extends DQPSKDemodulator
+{
+ private Listener mSymbolDecisionDataListener;
+ private Listener mSamplesPerSymbolListener;
+ private Listener mSymbolListener;
+ private Listener mPLLErrorListener;
+ private Listener mPLLFrequencyListener;
+ private double mSampleRate;
+
+ /**
+ * Decoder for Differential Quaternary Phase Shift Keying (DQPSK). This decoder uses both a Costas Loop (PLL) and
+ * a custom DQPSK symbol timing error detector to automatically align to the incoming carrier frequency and to
+ * adjust for any changes in symbol timing.
+ *
+ * This detector is optimized for constant amplitude DQPSK symbols like C4FM.
+ *
+ * @param phaseLockedLoop for tracking carrier frequency error
+ * @param interpolatingSampleBuffer
+ * @param sampleRate of the incoming complex sample stream
+ */
+ public DQPSKDemodulatorInstrumented(IPhaseLockedLoop phaseLockedLoop,
+ InterpolatingSampleBufferInstrumented interpolatingSampleBuffer,
+ double sampleRate)
+ {
+ super(phaseLockedLoop, interpolatingSampleBuffer);
+ mSampleRate = sampleRate;
+ }
+
+ /**
+ * Overrides the parent class symbol calculation to capture eye diagram data
+ */
+ protected void calculateSymbol()
+ {
+ //Eye diagram listener
+ if(mSymbolDecisionDataListener != null)
+ {
+ mSymbolDecisionDataListener.receive(
+ ((InterpolatingSampleBufferInstrumented)mInterpolatingSampleBuffer).getSymbolDecisionData());
+ }
+
+ super.calculateSymbol();
+
+ if(mSamplesPerSymbolListener != null)
+ {
+ mSamplesPerSymbolListener.receive((double)mInterpolatingSampleBuffer.getSamplingPoint());
+ }
+
+ //Send to an external constellation symbol listener when registered
+ if(mSymbolListener != null)
+ {
+ mSymbolListener.receive(mCurrentSymbol);
+ }
+
+ if(mPLLErrorListener != null)
+ {
+ mPLLErrorListener.receive((double)mSymbolEvaluator.getPhaseError());
+ }
+
+ if(mPLLFrequencyListener != null)
+ {
+ double loopFrequency = ((CostasLoop)mPLL).getLoopFrequency();
+
+ loopFrequency *= mSampleRate / (2.0 * Math.PI);
+
+ mPLLFrequencyListener.receive(loopFrequency);
+ }
+ }
+
+ /**
+ * Registers a listener to receive symbol decision data to produce an eye diagram.
+ */
+ public void setSymbolDecisionDataListener(Listener listener)
+ {
+ mSymbolDecisionDataListener = listener;
+ }
+
+ /**
+ * Registers the listener to receive PLL error values
+ */
+ public void setSamplesPerSymbolListener(Listener listener)
+ {
+ mSamplesPerSymbolListener = listener;
+ }
+
+ /**
+ * Registers the listener to receive decoded QPSK symbols
+ */
+ public void setSymbolListener(Listener listener)
+ {
+ mSymbolListener = listener;
+ }
+
+ /**
+ * Registers the listener to receive PLL error values
+ */
+ public void setPLLErrorListener(Listener listener)
+ {
+ mPLLErrorListener = listener;
+ }
+
+ /**
+ * Registers the listener to receive PLL error values
+ */
+ public void setPLLFrequencyListener(Listener listener)
+ {
+ mPLLFrequencyListener = listener;
+ }
+
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolEvaluator.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolEvaluator.java
new file mode 100644
index 00000000..cab0fd22
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolEvaluator.java
@@ -0,0 +1,137 @@
+/*******************************************************************************
+ * 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;
+
+import io.github.dsheirer.dsp.symbol.Dibit;
+import io.github.dsheirer.sample.complex.Complex;
+
+public class DQPSKSymbolEvaluator implements IDQPSKSymbolEvaluator
+{
+ private static final Complex ROTATE_FROM_PLUS_135 = Complex.fromAngle(-3.0 * Math.PI / 4.0);
+ private static final Complex ROTATE_FROM_PLUS_45 = Complex.fromAngle(-1.0 * Math.PI / 4.0);
+ private static final Complex ROTATE_FROM_MINUS_45 = Complex.fromAngle(1.0 * Math.PI / 4.0);
+ private static final Complex ROTATE_FROM_MINUS_135 = Complex.fromAngle(3.0 * Math.PI / 4.0);
+
+ private Complex mSymbolError = new Complex(0, 0);
+ private float mPhaseError = 0.0f;
+ private float mTimingError = 0.0f;
+ private float mTimingErrorPolarity = 1.0f;
+ private Dibit mSymbolDecision = Dibit.D00_PLUS_1;
+
+ /**
+ * Decision-directed symbol phase and timing error detector and symbol decision slicer.
+ *
+ * Symbol decision is based on the closest reference quadrant for the sampled symbol.
+ *
+ * Phase and timing error are calculated as the angular distance of the sampled symbol from the reference
+ * symbol. Timing error is further corrected to compensate for clock-wise or counter-clock-wise vector rotation
+ * relative to the immediately preceding symbol.
+ */
+ public DQPSKSymbolEvaluator()
+ {
+ }
+
+ /**
+ * Sets the preceding and current symbols to be evaluated for phase and timing errors and to determine the
+ * transmitted symbol relative to the closest reference symbol. After invoking this method, you can access the
+ * phase and timing errors and the symbol decision via their respective accessor methods.
+ *
+ * Phase and timing error values are calculated by first determining the symbol and then calculating the phase
+ * and timing errors relative to the reference symbol. The timing error is corrected with the appropriate sign
+ * relative to the angular vector rotation so that the error value indicates the correct error direction.
+ *
+ * @param preceding differential-decoded sample/symbol used to detect rotation - does NOT need to be interpolated
+ * since it's only a reference
+ * @param current symbol to be evaluated - interpolated
+ */
+ @Override
+ public void setSymbol(Complex preceding, Complex current)
+ {
+ mSymbolError.setValues(current);
+
+ if(mSymbolError.quadrature() > 0.0f)
+ {
+ if(mSymbolError.inphase() > 0.0f)
+ {
+ mSymbolDecision = Dibit.D00_PLUS_1;
+ mTimingErrorPolarity = (preceding.quadrature() > current.quadrature() ? 1.0f : -1.0f);
+ mSymbolError.multiply(ROTATE_FROM_PLUS_45);
+ }
+ else
+ {
+ mSymbolDecision = Dibit.D01_PLUS_3;
+ mTimingErrorPolarity = (preceding.quadrature() < current.quadrature() ? 1.0f : -1.0f);
+ mSymbolError.multiply(ROTATE_FROM_PLUS_135);
+ }
+
+ }
+ else
+ {
+ if(mSymbolError.inphase() > 0.0f)
+ {
+ mSymbolDecision = Dibit.D10_MINUS_1;
+ mTimingErrorPolarity = (preceding.quadrature() > current.quadrature() ? 1.0f : -1.0f);
+ mSymbolError.multiply(ROTATE_FROM_MINUS_45);
+ }
+ else
+ {
+ mSymbolDecision = Dibit.D11_MINUS_3;
+ mTimingErrorPolarity = (preceding.quadrature() < current.quadrature() ? 1.0f : -1.0f);
+ mSymbolError.multiply(ROTATE_FROM_MINUS_135);
+ }
+ }
+
+ //Since we've rotated the error symbol back to 0 radians, the quadrature value closely approximates the
+ //arctan of the error angle relative to 0 radians and this provides our error value
+ mPhaseError = -mSymbolError.quadrature();
+
+ //Timing error is the same as phase error with the sign corrected according to the vector's angular rotation
+ mTimingError = mSymbolError.quadrature() * mTimingErrorPolarity;
+ }
+
+ /**
+ * Phase error of the symbol relative to the nearest reference symbol.
+ *
+ * @return phase error in radians of distance from the reference symbol.
+ */
+ @Override
+ public float getPhaseError()
+ {
+ return mPhaseError;
+ }
+
+ /**
+ * Timing error of the symbol relative to the nearest reference symbol.
+ *
+ * @return timing error in radians of angular distance from the reference symbol recognizing that the symbol
+ * originates at zero radians and rotates toward the intended reference symbol, therefore the error value indicates
+ * if the symbol was sampled early (-) or late (+) relative to the reference symbol.
+ */
+ @Override
+ public float getTimingError()
+ {
+ return mTimingError;
+ }
+
+ /**
+ * Reference symbol that is closest to the transmitted/sampled symbol.
+ */
+ @Override
+ public Dibit getSymbolDecision()
+ {
+ return mSymbolDecision;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/GardnerDetector.java b/src/main/java/io/github/dsheirer/dsp/psk/GardnerDetector.java
new file mode 100644
index 00000000..9b338e97
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/GardnerDetector.java
@@ -0,0 +1,82 @@
+/*******************************************************************************
+ * 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;
+
+import io.github.dsheirer.sample.complex.Complex;
+
+public class GardnerDetector
+{
+ private float mErrorInphase;
+ private float mErrorQuadrature;
+ private float mError;
+
+ /**
+ * Gardner detector for recovering symbol timing error.
+ */
+ public GardnerDetector()
+ {
+ }
+
+
+ /**
+ * Calculates the symbol timing error by evaluating the previous, middle and current symbol instants.
+ *
+ * @param previous symbol
+ * @param middle symbol (ie midway between the previous and current)
+ * @param current symbol
+ * @return symbol timing error
+ */
+ public float getError(Complex previous, Complex middle, Complex current)
+ {
+ mErrorInphase = (Math.abs(previous.inphase()) - Math.abs(current.inphase())) * Math.abs(middle.inphase());
+ mErrorQuadrature = (Math.abs(previous.quadrature()) - Math.abs(current.quadrature())) * Math.abs(middle.quadrature());
+ mError = normalize(mErrorInphase + mErrorQuadrature, 1.0f);
+
+ return mError;
+ }
+
+ /**
+ * Constrains timing error to +/- the maximum value and corrects any floating point invalid numbers
+ */
+ public static float normalize(float error, float maximum)
+ {
+ if(Float.isNaN(error))
+ {
+ return 0.0f;
+ }
+ else
+ {
+ return clip(error, maximum);
+ }
+ }
+
+ /**
+ * Constrains value to the range of ( -maximum <> maximum )
+ */
+ public static float clip(float value, float maximum)
+ {
+ if(value > maximum)
+ {
+ return maximum;
+ }
+ else if(value < -maximum)
+ {
+ return -maximum;
+ }
+
+ return value;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/IDQPSKSymbolEvaluator.java b/src/main/java/io/github/dsheirer/dsp/psk/IDQPSKSymbolEvaluator.java
new file mode 100644
index 00000000..d0213a0d
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/IDQPSKSymbolEvaluator.java
@@ -0,0 +1,50 @@
+/*******************************************************************************
+ * 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;
+
+import io.github.dsheirer.dsp.symbol.Dibit;
+import io.github.dsheirer.sample.complex.Complex;
+
+
+public interface IDQPSKSymbolEvaluator
+{
+ /**
+ * Sets the sampled symbol to be evaluated. Note: the symbol's gain value MUST be normalized to the unit circle.
+ *
+ * @param preceding symbol used to detect (counter-)clock-wise rotation
+ * @param symbol currently being sampled
+ */
+ void setSymbol(Complex preceding, Complex symbol);
+
+ /**
+ * Phase error of the symbol relative to the closest reference symbol
+ * @return angular phase error in radians
+ */
+ float getPhaseError();
+
+ /**
+ * Timing error of the symbol relative to the closest reference symbol
+ * @return angular timing error in radians where a positive value indicates early sampling and sample timing needs
+ * to be increased and a negative value indicates late sampling where sample timing needs to be decreased.
+ */
+ float getTimingError();
+
+ /**
+ * Dibit decision indicating the reference symbol location closest to the symbol.
+ * @return symbol decision
+ */
+ Dibit getSymbolDecision();
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/IQPSKSymbolDecoder.java b/src/main/java/io/github/dsheirer/dsp/psk/IQPSKSymbolDecoder.java
new file mode 100644
index 00000000..0befad75
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/IQPSKSymbolDecoder.java
@@ -0,0 +1,29 @@
+/*******************************************************************************
+ * 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;
+
+import io.github.dsheirer.dsp.symbol.Dibit;
+import io.github.dsheirer.sample.complex.Complex;
+
+public interface IQPSKSymbolDecoder
+{
+ /**
+ * Decodes the symbol and converts it into a Dibit
+ * @param symbol to decode
+ * @return decoded dibit
+ */
+ Dibit decode(Complex symbol);
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/ISymbolPhaseErrorCalculator.java b/src/main/java/io/github/dsheirer/dsp/psk/ISymbolPhaseErrorCalculator.java
new file mode 100644
index 00000000..4e413d88
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/ISymbolPhaseErrorCalculator.java
@@ -0,0 +1,28 @@
+/*******************************************************************************
+ * 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;
+
+import io.github.dsheirer.sample.complex.Complex;
+
+public interface ISymbolPhaseErrorCalculator
+{
+ /**
+ * Calculates the phase error of the symbol relative to an alignment for the constellation
+ * @param symbol to calculate error
+ * @return error value
+ */
+ float getPhaseError(Complex symbol);
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBuffer.java b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBuffer.java
new file mode 100644
index 00000000..15adbd9a
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBuffer.java
@@ -0,0 +1,167 @@
+/*******************************************************************************
+ * 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;
+
+import io.github.dsheirer.dsp.filter.interpolator.RealInterpolator;
+import io.github.dsheirer.sample.complex.Complex;
+
+public class InterpolatingSampleBuffer
+{
+ private static final float MAXIMUM_DEVIATION_SAMPLES_PER_SYMBOL = 0.02f; // +/- 2% deviation
+
+ private Complex mPrecedingSample = new Complex(0,0);
+ private Complex mCurrentSample = new Complex(0,0);
+
+ private float[] mDelayLineInphase;
+ private float[] mDelayLineQuadrature;
+ private int mDelayLinePointer = 0;
+ private int mTwiceSamplesPerSymbol;
+
+ private float mSamplingPoint;
+ private float mSampleCounterGain = 1.0f;
+ private float mDetectedSamplesPerSymbol;
+ private float mDetectedSamplesPerSymbolGain = 0.1f * mSampleCounterGain * mSampleCounterGain;
+ private float mMaximumSamplesPerSymbol;
+ private float mMinimumSamplesPerSymbol;
+
+ private RealInterpolator mInterpolator = new RealInterpolator(1.0f);
+
+ /**
+ * Buffer to store complex sample data and produce interpolated samples.
+ * @param samplesPerSymbol
+ */
+ public InterpolatingSampleBuffer(float samplesPerSymbol)
+ {
+ mSamplingPoint = samplesPerSymbol;
+ mDetectedSamplesPerSymbol = samplesPerSymbol;
+ mMaximumSamplesPerSymbol = samplesPerSymbol * (1.0f + MAXIMUM_DEVIATION_SAMPLES_PER_SYMBOL);
+ mMinimumSamplesPerSymbol = samplesPerSymbol * (1.0f - MAXIMUM_DEVIATION_SAMPLES_PER_SYMBOL);
+ mTwiceSamplesPerSymbol = (int)Math.floor(2.0 * samplesPerSymbol);
+ mDelayLineInphase = new float[2 * mTwiceSamplesPerSymbol];
+ mDelayLineQuadrature = new float[2 * mTwiceSamplesPerSymbol];
+ }
+
+ /**
+ * Stores the sample in the buffer and updates pointers.
+ * @param sample
+ */
+ public void receive(Complex sample)
+ {
+ mSamplingPoint--;
+
+ //Fill up the delay line to use with the interpolator
+ mDelayLineInphase[mDelayLinePointer] = sample.inphase();
+ mDelayLineInphase[mDelayLinePointer + mTwiceSamplesPerSymbol] = sample.inphase();
+ mDelayLineQuadrature[mDelayLinePointer] = sample.quadrature();
+ mDelayLineQuadrature[mDelayLinePointer + mTwiceSamplesPerSymbol] = sample.quadrature();
+
+ //Increment pointer and keep pointer in bounds
+ mDelayLinePointer++;
+ mDelayLinePointer = mDelayLinePointer % mTwiceSamplesPerSymbol;
+ }
+
+ /**
+ * Updates the internal sample counter with additional samples to add to the symbol sample counter.
+ * @param samplesToAdd to the sample counter
+ */
+ public void increaseSampleCounter(float samplesToAdd)
+ {
+ mSamplingPoint += samplesToAdd;
+ }
+
+ /**
+ * Adjusts samples per symbol and symbol timing counters and increments the sample counter to collect another
+ * symbol.
+ *
+ * @param symbolTimingError from a symbol timing error detector
+ */
+ public void resetAndAdjust(float symbolTimingError)
+ {
+ //Adjust detected samples per symbol based on timing error
+ mDetectedSamplesPerSymbol = mDetectedSamplesPerSymbol + (symbolTimingError * mDetectedSamplesPerSymbolGain);
+
+ //Constrain detected samples per symbol to min/max values
+ if(mDetectedSamplesPerSymbol > mMaximumSamplesPerSymbol)
+ {
+ mDetectedSamplesPerSymbol = mMaximumSamplesPerSymbol;
+ }
+
+ if(mDetectedSamplesPerSymbol < mMinimumSamplesPerSymbol)
+ {
+ mDetectedSamplesPerSymbol = mMinimumSamplesPerSymbol;
+ }
+
+ //Add another symbol's worth of samples to the counter and adjust timing based on gardner error
+ increaseSampleCounter((mDetectedSamplesPerSymbol + (symbolTimingError * mSampleCounterGain)));
+ }
+
+ /**
+ * Current value of the detected samples per symbol
+ */
+ public float getSamplingPoint()
+ {
+ return mSamplingPoint;
+ }
+
+ /**
+ * Indicates if this buffer has accumulated a enough samples to represent a full symbol
+ */
+ public boolean hasSymbol()
+ {
+ return mSamplingPoint < 1.0f;
+ }
+
+ /**
+ * Un-interpolated sample that precedes the current interpolated sampling point. The current sampling point falls
+ * somewhere between sample index 3 and index 4, therefore the preceding sample is located at index 3.
+ */
+ public Complex getPrecedingSample()
+ {
+ mPrecedingSample.setValues(mDelayLineInphase[mDelayLinePointer + 4], mDelayLineQuadrature[mDelayLinePointer + 3]);
+ return mPrecedingSample;
+ }
+
+ /**
+ * Interpolated middle sample for the symbol. Note: this method should only be invoked after testing for a
+ * complete symbol with the hasSymbol() method.
+ */
+ public Complex getCurrentSample()
+ {
+ /* Calculate interpolated current sample */
+ mCurrentSample.setValues(getInphase(mSamplingPoint), getQuadrature(mSamplingPoint));
+ return mCurrentSample;
+ }
+
+ /**
+ * Returns the interpolated inphase value for the specified offset
+ * @param interpolation into the buffer to calculate the interpolated sample
+ * @return inphase value of the interpolated sample
+ */
+ public float getInphase(float interpolation)
+ {
+ return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer + 1, interpolation);
+ }
+
+ /**
+ * Returns the interpolated quadrature value for the specified offset
+ * @param interpolation into the buffer to calculate the interpolated sample
+ * @return quadrature value of the interpolated sample
+ */
+ public float getQuadrature(float interpolation)
+ {
+ return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer + 1, interpolation);
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBufferInstrumented.java b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBufferInstrumented.java
new file mode 100644
index 00000000..6c71fc25
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBufferInstrumented.java
@@ -0,0 +1,48 @@
+/*******************************************************************************
+ * 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;
+
+import io.github.dsheirer.sample.complex.Complex;
+
+public class InterpolatingSampleBufferInstrumented extends InterpolatingSampleBuffer
+{
+ private SymbolDecisionData mSymbolDecisionData;
+
+ public InterpolatingSampleBufferInstrumented(float samplesPerSymbol)
+ {
+ super(samplesPerSymbol);
+
+ mSymbolDecisionData = new SymbolDecisionData((int)samplesPerSymbol);
+ }
+
+ public void receive(Complex sample)
+ {
+ super.receive(sample);
+ mSymbolDecisionData.receive(sample);
+ }
+
+ /**
+ * 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()
+ {
+ mSymbolDecisionData.setSamplingPoint(getSamplingPoint());
+ return mSymbolDecisionData;
+ }
+
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/LSMConstellationDecoder.java b/src/main/java/io/github/dsheirer/dsp/psk/LSMConstellationDecoder.java
new file mode 100644
index 00000000..7c500838
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/LSMConstellationDecoder.java
@@ -0,0 +1,28 @@
+/*******************************************************************************
+ * 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;
+
+import io.github.dsheirer.dsp.symbol.Dibit;
+import io.github.dsheirer.sample.complex.Complex;
+
+public class LSMConstellationDecoder implements IQPSKSymbolDecoder
+{
+ @Override
+ public Dibit decode(Complex symbol)
+ {
+ return null;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/LSMDemodulator.java b/src/main/java/io/github/dsheirer/dsp/psk/LSMDemodulator.java
index 030ef0d8..1a21ae75 100644
--- a/src/main/java/io/github/dsheirer/dsp/psk/LSMDemodulator.java
+++ b/src/main/java/io/github/dsheirer/dsp/psk/LSMDemodulator.java
@@ -2,36 +2,37 @@ package io.github.dsheirer.dsp.psk;
/*******************************************************************************
* SDR Trunk
- * Copyright (C) 2014, 2015 Dennis Sheirer
- *
+ * Copyright (C) 2014 - 2018 Dennis Sheirer
+ *
* Gardner detector, Costas loop and Interpolator derived from:
- *
+ *
* OP25 - gardner_costas_cc_impl.cc
* Copyright 2010, 2011, 2012, 2013 KA1RBI (gardner_costas_cc_impl.cc)
- *
+ *
* OP25 - scope.py
* Copyright 2008-2011 Steve Glass
* Copyright 2011, 2012, 2013 KA1RBI
- *
+ *
* GNURadio - control_loop and mpsk_receiver_cc
* Copyright 2011,2013 Free Software Foundation, Inc.
- *
+ *
* 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
******************************************************************************/
import io.github.dsheirer.buffer.FloatAveragingBuffer;
import io.github.dsheirer.dsp.filter.interpolator.RealInterpolator;
+import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.instrument.Instrumentable;
import io.github.dsheirer.instrument.tap.Tap;
@@ -48,134 +49,139 @@ import java.util.Arrays;
import java.util.List;
/**
- * Implements a LSM (Pi/4) demodulator using a Gardner Detector to determine
- * optimal sampling timing and a Costas Loop as a phase locked loop synchronized
+ * Implements a LSM (Pi/4) demodulator using a Gardner Detector to determine
+ * optimal sampling timing and a Costas Loop as a phase locked loop synchronized
* with the incoming signal carrier frequency.
- *
+ *
* Sample Rate: 48000
* Symbol Rate: 4800
*/
public class LSMDemodulator implements Instrumentable, ComplexSampleListener
{
- private List mAvailableTaps;
-
- /* 45 degree rotation angle */
- public static final float THETA = (float)( Math.PI / 4.0d );
+ private List mAvailableTaps;
- /* 45 degree point */
- public static final Complex POINT_45_DEGREES =
- new Complex( (float)Math.sin( THETA ), (float)Math.cos( THETA ) );
-
- private Listener mSymbolListener;
-
- private GardnerSymbolTiming mGardnerDetector = new GardnerSymbolTiming();
-
- private CostasLoop mCostasLoop = new CostasLoop();
-
- private IFrequencyChangeListener mFrequencyChangeListener;
-
- private EyeDiagramDataTap mEyeDiagramDataTap;
-
- public LSMDemodulator()
- {
- }
-
- public void dispose()
- {
- mSymbolListener = null;
- mFrequencyChangeListener = null;
- }
-
- public void setSymbolListener( Listener listener )
- {
- mSymbolListener = listener;
- }
-
- public void removeSymbolListener()
- {
- mSymbolListener = null;
- }
-
- @Override
- public void receive( float inphase, float quadrature )
- {
- mGardnerDetector.receive( inphase, quadrature );
- }
-
- /**
- * Applies a phase correction value to the costas loop to correct when a
- * phase lock error is detected in the binary output stream.
- *
- * @param correction - value in radians
- */
- public void correctPhaseError( double correction )
- {
- mCostasLoop.correctPhaseError( correction );
- }
-
- /**
- * Constrains value to the range of ( -maximum <> maximum )
- */
- public static float clip( float value, float maximum )
- {
- if( value > maximum )
- {
- return maximum;
- }
- else if( value < -maximum )
- {
- return -maximum;
- }
-
- return value;
- }
-
- /**
- * Constrains timing error to +/- the maximum value and corrects any
- * floating point invalid numbers
- */
- private float normalize( float error, float maximum )
- {
- if( Float.isNaN( error ) )
- {
- return 0.0f;
- }
- else
- {
- return clip( error, maximum );
- }
- }
-
- public void addListener( IFrequencyChangeListener listener )
- {
- mFrequencyChangeListener = listener;
- }
-
- public void removeListener( IFrequencyChangeListener listener )
- {
- mFrequencyChangeListener = null;
- }
+ /* 45 degree rotation angle */
+ public static final float THETA = (float)(Math.PI / 4.0d);
- /**
- * Processes the current loop frequency of the costas loop and broadcasts
- * tuner frequency offset adjustments 10 times a second as needed to keep
- * the frequency offset below half of the maximum frequency
- */
- public class FrequencyControl
- {
- /* Set the trigger threshold at half of the maximum costas loop control
- * frequency */
- private static final float CORRECTION_THRESHOLD_FREQUENCY =
- ( 2.0f * (float)Math.PI * 600.0f ) / 48000.0f;
+ /* 45 degree point */
+ public static final Complex POINT_45_DEGREES = new Complex((float)Math.sin(THETA), (float)Math.cos(THETA));
- private FloatAveragingBuffer mBuffer = new FloatAveragingBuffer( 40 );
-
- private long mFrequencyError = 0;
-
- private int mCounter = 0;
-
- public void receive( float frequency )
- {
+ private Listener mSymbolListener;
+
+ private GardnerSymbolTiming mGardnerDetector = new GardnerSymbolTiming();
+
+ private CostasLoop mCostasLoop = new CostasLoop();
+ private QPSKSymbolDecoder mQPSKSymbolDecoder = new QPSKSymbolDecoder();
+
+ private IFrequencyChangeListener mFrequencyChangeListener;
+
+ private EyeDiagramDataTap mEyeDiagramDataTap;
+
+ public LSMDemodulator()
+ {
+ }
+
+ public CostasLoop getCostasLoop()
+ {
+ return mCostasLoop;
+ }
+
+ public void dispose()
+ {
+ mSymbolListener = null;
+ mFrequencyChangeListener = null;
+ }
+
+ public void setSymbolListener(Listener listener)
+ {
+ mSymbolListener = listener;
+ }
+
+ public void removeSymbolListener()
+ {
+ mSymbolListener = null;
+ }
+
+ @Override
+ public void receive(float inphase, float quadrature)
+ {
+ mGardnerDetector.receive(inphase, quadrature);
+ }
+
+ /**
+ * Applies a phase correction value to the costas loop to correct when a
+ * phase lock error is detected in the binary output stream.
+ *
+ * @param correction - value in radians
+ */
+ public void correctPhaseError(double correction)
+ {
+ mCostasLoop.correctPhaseError(correction);
+ }
+
+ /**
+ * Constrains value to the range of ( -maximum <> maximum )
+ */
+ public static float clip(float value, float maximum)
+ {
+ if(value > maximum)
+ {
+ return maximum;
+ }
+ else if(value < -maximum)
+ {
+ return -maximum;
+ }
+
+ return value;
+ }
+
+ /**
+ * Constrains timing error to +/- the maximum value and corrects any
+ * floating point invalid numbers
+ */
+ private float normalize(float error, float maximum)
+ {
+ if(Float.isNaN(error))
+ {
+ return 0.0f;
+ }
+ else
+ {
+ return clip(error, maximum);
+ }
+ }
+
+ public void addListener(IFrequencyChangeListener listener)
+ {
+ mFrequencyChangeListener = listener;
+ }
+
+ public void removeListener(IFrequencyChangeListener listener)
+ {
+ mFrequencyChangeListener = null;
+ }
+
+ /**
+ * Processes the current loop frequency of the costas loop and broadcasts
+ * tuner frequency offset adjustments 10 times a second as needed to keep
+ * the frequency offset below half of the maximum frequency
+ */
+ public class FrequencyControl
+ {
+ /* Set the trigger threshold at half of the maximum costas loop control
+ * frequency */
+ private static final float CORRECTION_THRESHOLD_FREQUENCY =
+ (2.0f * (float)Math.PI * 600.0f) / 48000.0f;
+
+ private FloatAveragingBuffer mBuffer = new FloatAveragingBuffer(40);
+
+ private long mFrequencyError = 0;
+
+ private int mCounter = 0;
+
+ public void receive(float frequency)
+ {
// mCounter++;
//
// if( mCounter >= 480 )
@@ -212,377 +218,402 @@ public class LSMDemodulator implements Instrumentable, ComplexSampleListener
// {
// mBuffer.get( frequency );
// }
- }
- }
+ }
+ }
- /**
- * Gardner symbol timing detector
- */
- public class GardnerSymbolTiming implements ComplexSampleListener
- {
- public static final int HALF_SAMPLES_PER_SYMBOL = 5;
- public static final int SAMPLES_PER_SYMBOL = 10;
- public static final int TWICE_SAMPLES_PER_SYMBOL = 20;
+ /**
+ * Gardner symbol timing detector
+ */
+ public class GardnerSymbolTiming implements ComplexSampleListener
+ {
+ public static final int HALF_SAMPLES_PER_SYMBOL = 5;
+ public static final int SAMPLES_PER_SYMBOL = 10;
+ public static final int TWICE_SAMPLES_PER_SYMBOL = 20;
- private float[] mDelayLineInphase = new float[ 2 * TWICE_SAMPLES_PER_SYMBOL ];
- private float[] mDelayLineQuadrature = new float[ 2 * TWICE_SAMPLES_PER_SYMBOL ];
+ private float[] mDelayLineInphase = new float[2 * TWICE_SAMPLES_PER_SYMBOL];
+ private float[] mDelayLineQuadrature = new float[2 * TWICE_SAMPLES_PER_SYMBOL];
- private int mDelayLinePointer = 0;
+ private int mDelayLinePointer = 0;
- /* Sampling point */
- private float mMu = 10.0f;
- private float mGainMu = 0.05f;
-
- /* Samples per symbol */
- private float mOmega = 10.0f;
- private float mGainOmega = 0.1f * mGainMu * mGainMu;
- private float mOmegaRel = 0.005f;
- private float mOmegaMid = 10.0f;
+ /* Sampling point */
+ private float mMu = 10.0f;
+ private float mGainMu = 0.05f;
- private RealInterpolator mInterpolator = new RealInterpolator( 1.0f );
-
- private Complex mPreviousSample = new Complex( 0, 0 );
- private Complex mPreviousMiddleSample = new Complex( 0, 0 );
- private Complex mPreviousSymbol = new Complex( 0, 0 );
-
- /**
- * Provides symbol sampling timing control
- */
- public GardnerSymbolTiming()
- {
- }
+ /* Samples per symbol */
+ private float mOmega = 10.0f;
+ private float mGainOmega = 0.1f * mGainMu * mGainMu;
+ private float mOmegaRel = 0.005f;
+ private float mOmegaMid = 10.0f;
- @Override
- public void receive( float inphase, float quadrature )
- {
- /* Count down samples per symbol until we calculate the symbol */
- mMu--;
+ private RealInterpolator mInterpolator = new RealInterpolator(1.0f);
- mCostasLoop.increment();
-
- /* Mix incoming sample with costas loop to remove any rotation
- * that is present from a mis-tuned carrier frequency */
-
- Complex costas = mCostasLoop.getCurrentVector();
-
- float derotatedInphase = Complex.multiplyInphase( inphase, quadrature, costas.inphase(), costas.quadrature() );
- float derotatedQuadrature = Complex.multiplyQuadrature( inphase, quadrature, costas.inphase(), costas.quadrature() );
-
- /* Fill up the delay line to use with the interpolator */
- mDelayLineInphase[ mDelayLinePointer ] = derotatedInphase;
- mDelayLineInphase[ mDelayLinePointer + TWICE_SAMPLES_PER_SYMBOL ] = derotatedInphase;
+ private Complex mPreviousSample = new Complex(0, 0);
+ private Complex mPreviousMiddleSample = new Complex(0, 0);
+ private Complex mPreviousSymbol = new Complex(0, 0);
- mDelayLineQuadrature[ mDelayLinePointer ] = derotatedQuadrature;
- mDelayLineQuadrature[ mDelayLinePointer + TWICE_SAMPLES_PER_SYMBOL ] = derotatedQuadrature;
+ /**
+ * Provides symbol sampling timing control
+ */
+ public GardnerSymbolTiming()
+ {
+ }
- /* Increment pointer and keep pointer in bounds */
- mDelayLinePointer = ( mDelayLinePointer + 1 ) % TWICE_SAMPLES_PER_SYMBOL;
-
- /* Calculate the symbol once we've stored enough samples */
- if( mMu <= 1.0f )
- {
- float half_omega = mOmega / 2.0f;
- int half_sps = (int)Math.floor( half_omega );
- float half_mu = mMu + half_omega - (float)half_sps;
-
- if( half_mu > 1.0 )
- {
- half_mu -= 1.0;
- half_sps += 1;
- }
+ @Override
+ public void receive(float inphase, float quadrature)
+ {
+ /* Count down samples per symbol until we calculate the symbol */
+ mMu--;
- /* Calculate interpolated middle sample and current sample */
- float middleSampleInphase = mInterpolator.filter(
- mDelayLineInphase, mDelayLinePointer, mMu );
- float middleSampleQuadrature = mInterpolator.filter(
- mDelayLineQuadrature, mDelayLinePointer, mMu );
+ mCostasLoop.increment();
- int index = mDelayLinePointer + half_sps;
-
- float currentSampleInphase = mInterpolator.filter(
- mDelayLineInphase, index, half_mu );
- float currentSampleQuadrature = mInterpolator.filter(
- mDelayLineQuadrature, index, half_mu );
+ /* Mix incoming sample with costas loop to remove any rotation
+ * that is present from a mis-tuned carrier frequency */
- /* Multiply current sample and conjugate (negated quadrature) of
- * previous sample to get symbols to use for gardner error feedback */
- Complex middleSymbol = Complex.multiply( middleSampleInphase,
- middleSampleQuadrature, mPreviousMiddleSample.conjugate() );
- Complex currentSymbol = Complex.multiply( currentSampleInphase,
- currentSampleQuadrature, mPreviousSample.conjugate() );
+ Complex costas = mCostasLoop.getCurrentVector();
- /* Set gain to unity */
- middleSymbol.normalize();
- currentSymbol.normalize();
+ float derotatedInphase = Complex.multiplyInphase(inphase, quadrature, costas.inphase(), costas.quadrature());
+ float derotatedQuadrature = Complex.multiplyQuadrature(inphase, quadrature, costas.inphase(), costas.quadrature());
- /* Gardner timing error calculations */
- float errorInphase = ( mPreviousSymbol.inphase() -
- currentSymbol.inphase() ) * middleSymbol.inphase();
+ /* Fill up the delay line to use with the interpolator */
+ mDelayLineInphase[mDelayLinePointer] = derotatedInphase;
+ mDelayLineInphase[mDelayLinePointer + TWICE_SAMPLES_PER_SYMBOL] = derotatedInphase;
- float errorQuadrature = ( mPreviousSymbol.quadrature() -
- currentSymbol.quadrature() ) * middleSymbol.quadrature();
+ mDelayLineQuadrature[mDelayLinePointer] = derotatedQuadrature;
+ mDelayLineQuadrature[mDelayLinePointer + TWICE_SAMPLES_PER_SYMBOL] = derotatedQuadrature;
- float gardnerError = normalize( errorInphase + errorQuadrature, 1.0f );
-
- if( mEyeDiagramDataTap != null )
- {
- mEyeDiagramDataTap.receive(
- new EyeDiagramData(
- Arrays.copyOfRange( mDelayLineInphase, 0, 20 ),
- Arrays.copyOfRange( mDelayLineQuadrature, 0, 20 ),
- mMu, (float)half_sps + half_mu, gardnerError ) );
- }
-
- /* mOmega is samples per symbol and is constrained to floating
- * between +/- .005 of the nominal 10.0 samples per symbol */
- mOmega = mOmega + mGainOmega * gardnerError;
- mOmega = mOmegaMid + clip( mOmega - mOmegaMid, mOmegaRel );
+ /* Increment pointer and keep pointer in bounds */
+ mDelayLinePointer = (mDelayLinePointer + 1) % TWICE_SAMPLES_PER_SYMBOL;
- /* Adjust sample timing based on error of current sample */
- mMu += mOmega + ( mGainMu * gardnerError );
+ /* Calculate the symbol once we've stored enough samples */
+ if(mMu <= 1.0f)
+ {
+ float half_omega = mOmega / 2.0f;
+ int half_sps = (int)Math.floor(half_omega);
+ float half_mu = mMu + half_omega - (float)half_sps;
- /* Store current samples/symbols to use for the next period */
- mPreviousSample.setValues( currentSampleInphase, currentSampleQuadrature );
- mPreviousMiddleSample.setValues( middleSampleInphase, middleSampleQuadrature );
- mPreviousSymbol = currentSymbol;
+ if(half_mu > 1.0)
+ {
+ half_mu -= 1.0;
+ half_sps += 1;
+ }
- /* Update costas loop using phase error present in current
- * symbol. The symbol is rotated from star orientation to polar
- * orientation to simplify error calculation */
- mCostasLoop.receive( currentSymbol );
+ /* Calculate interpolated middle sample and current sample */
+ float middleSampleInphase = mInterpolator.filter(
+ mDelayLineInphase, mDelayLinePointer, mMu);
+ float middleSampleQuadrature = mInterpolator.filter(
+ mDelayLineQuadrature, mDelayLinePointer, mMu);
+
+ int index = mDelayLinePointer + half_sps;
+
+ float currentSampleInphase = mInterpolator.filter(
+ mDelayLineInphase, index, half_mu);
+ float currentSampleQuadrature = mInterpolator.filter(
+ mDelayLineQuadrature, index, half_mu);
+
+ /* Multiply current sample and conjugate (negated quadrature) of
+ * previous sample to get symbols to use for gardner error feedback */
+ Complex middleSymbol = Complex.multiply(middleSampleInphase,
+ middleSampleQuadrature, mPreviousMiddleSample.conjugate());
+ Complex currentSymbol = Complex.multiply(currentSampleInphase,
+ currentSampleQuadrature, mPreviousSample.conjugate());
+
+ /* Set gain to unity */
+ middleSymbol.normalize();
+ currentSymbol.normalize();
+
+ /* Gardner timing error calculations */
+ float errorInphase = (mPreviousSymbol.inphase() -
+ currentSymbol.inphase()) * middleSymbol.inphase();
+
+ float errorQuadrature = (mPreviousSymbol.quadrature() -
+ currentSymbol.quadrature()) * middleSymbol.quadrature();
+
+ float gardnerError = normalize(errorInphase + errorQuadrature, 1.0f);
+
+ if(mEyeDiagramDataTap != null)
+ {
+ mEyeDiagramDataTap.receive(
+ new EyeDiagramData(
+ Arrays.copyOfRange(mDelayLineInphase, 0, 20),
+ Arrays.copyOfRange(mDelayLineQuadrature, 0, 20),
+ mMu, (float)half_sps + half_mu, gardnerError));
+ }
+
+ /* mOmega is samples per symbol and is constrained to floating
+ * between +/- .005 of the nominal 10.0 samples per symbol */
+ mOmega = mOmega + mGainOmega * gardnerError;
+ mOmega = mOmegaMid + clip(mOmega - mOmegaMid, mOmegaRel);
+
+ /* Adjust sample timing based on error of current sample */
+ mMu += mOmega + (mGainMu * gardnerError);
+
+ /* Store current samples/symbols to use for the next period */
+ mPreviousSample.setValues(currentSampleInphase, currentSampleQuadrature);
+ mPreviousMiddleSample.setValues(middleSampleInphase, middleSampleQuadrature);
+ mPreviousSymbol = currentSymbol;
+
+ /* Update costas loop using phase error present in current
+ * symbol. The symbol is rotated from star orientation to polar
+ * orientation to simplify error calculation */
+ mCostasLoop.receive(currentSymbol);
// mCostasLoop.receive(
// ComplexSample.multiply( currentSymbol, POINT_45_DEGREES ) );
-
- /* Dispatch the differentiated symbol to the registered listener */
- if( mSymbolListener != null )
- {
- mSymbolListener.receive( currentSymbol );
- }
- }
- }
- }
-
- /**
- * Costas Loop - phase locked loop synchronized to the frequency offset of
- * the incoming signal to enable the signal to be mixed down to zero offset
- * for proper synchronization. The mFrequency value indicates the detected
- * frequency offset. We attempt to keep that value close to zero by issuing
- * frequency adjustments to the tuner channel source.
- *
- * Most of the costas loop code was ported from gnuradio/control_loop and
- * gnuradio/mpsk_receiver_cc using initialization values from KA1RBI's
- * OP25/cqpsk.py
- */
- public class CostasLoop implements Listener
- {
- public static final double TWO_PI = 2.0 * Math.PI;
-
- private static final float MAXIMUM_FREQUENCY =
- ( 2400.0f * (float)TWO_PI ) / 48000.0f;
-
- /* http://www.trondeau.com/blog/2011/8/13/control-loop-gain-values.html */
- private float mDamping = (float)Math.sqrt( 2.0 ) / 2.0f;
-
- /* Use denominator between 100 (most) and 400 (least) to adjust costas
- * loop control level */
- private float mLoopBandwidth = (float)( TWO_PI / 400.0d );
-
- private float mAlphaGain = ( 4.0f * mDamping * mLoopBandwidth ) /
- ( 1.0f + ( 2.0f * mDamping * mLoopBandwidth ) +
- ( mLoopBandwidth * mLoopBandwidth ) );
- private float mBetaGain = ( 4.0f * mLoopBandwidth * mLoopBandwidth ) /
- ( 1.0f + ( 2.0f * mDamping * mLoopBandwidth ) +
- ( mLoopBandwidth * mLoopBandwidth ) );
+ /* Dispatch the differentiated symbol to the registered listener */
+ if(mSymbolListener != null)
+ {
+ mSymbolListener.receive(currentSymbol);
+ }
+ }
+ }
+ }
- private float mLoopPhase = 0.0f;
-
- private float mLoopFrequency = 0.0f;
-
- private FrequencyControl mFrequencyControl = new FrequencyControl();
-
- public CostasLoop()
- {
- }
-
- /**
- * Applies a phase correction value to the current phasor. Use this
- * method to apply correction when a +/- 90, or 180 degree phase error
- * is detected in the binary output stream.
- *
- * If the supplied correction value places the loop frequency outside
- * of the max frequency, then the frequency will be corrected 360
- * degrees in the opposite direction to maintain within the max
- * frequency bounds.
- *
- * @param correction - correction value in radians
- */
- public void correctPhaseError( double correction )
- {
- mLoopFrequency += correction;
-
- if( mLoopFrequency > MAXIMUM_FREQUENCY )
- {
- mLoopFrequency -= 2.0d * MAXIMUM_FREQUENCY;
- }
-
- if( mLoopFrequency < -MAXIMUM_FREQUENCY )
- {
- mLoopFrequency += 2.0d * MAXIMUM_FREQUENCY;
- }
- }
-
- /**
- * Increments the phase of the loop
- */
- public void increment()
- {
- mLoopPhase += mLoopFrequency;
+ /**
+ * Costas Loop - phase locked loop synchronized to the frequency offset of
+ * the incoming signal to enable the signal to be mixed down to zero offset
+ * for proper synchronization. The mFrequency value indicates the detected
+ * frequency offset. We attempt to keep that value close to zero by issuing
+ * frequency adjustments to the tuner channel source.
+ *
+ * Most of the costas loop code was ported from gnuradio/control_loop and
+ * gnuradio/mpsk_receiver_cc using initialization values from KA1RBI's
+ * OP25/cqpsk.py
+ */
+ public class CostasLoop implements Listener, IPhaseLockedLoop
+ {
+ public static final double TWO_PI = 2.0 * Math.PI;
- /* Keep the loop phase in bounds */
- unwrapPhase();
- }
+ private static final float MAXIMUM_FREQUENCY =
+ (2400.0f * (float)TWO_PI) / 48000.0f;
- private void unwrapPhase()
- {
- while( mLoopPhase > TWO_PI )
- {
- mLoopPhase -= TWO_PI;
- }
-
- while( mLoopPhase < -TWO_PI )
- {
- mLoopPhase += TWO_PI;
- }
- }
-
- /**
- * Current vector of the loop rotated by THETA degrees.
- */
- public Complex getCurrentVector()
- {
+ /* http://www.trondeau.com/blog/2011/8/13/control-loop-gain-values.html */
+ private float mDamping = (float)Math.sqrt(2.0) / 2.0f;
+
+ /* Use denominator between 100 (most) and 400 (least) to adjust costas
+ * loop control level */
+ private float mLoopBandwidth = (float)(TWO_PI / 400.0d);
+
+ private float mAlphaGain = (4.0f * mDamping * mLoopBandwidth) /
+ (1.0f + (2.0f * mDamping * mLoopBandwidth) +
+ (mLoopBandwidth * mLoopBandwidth));
+
+ private float mBetaGain = (4.0f * mLoopBandwidth * mLoopBandwidth) /
+ (1.0f + (2.0f * mDamping * mLoopBandwidth) +
+ (mLoopBandwidth * mLoopBandwidth));
+
+ private float mLoopPhase = 0.0f;
+
+ private float mLoopFrequency = 0.0f;
+
+ private FrequencyControl mFrequencyControl = new FrequencyControl();
+
+ public CostasLoop()
+ {
+ }
+
+ /**
+ * Applies a phase correction value to the current phasor. Use this
+ * method to apply correction when a +/- 90, or 180 degree phase error
+ * is detected in the binary output stream.
+ *
+ * If the supplied correction value places the loop frequency outside
+ * of the max frequency, then the frequency will be corrected 360
+ * degrees in the opposite direction to maintain within the max
+ * frequency bounds.
+ *
+ * @param correction - correction value in radians
+ */
+ public void correctPhaseError(double correction)
+ {
+ mLoopFrequency += correction;
+
+ if(mLoopFrequency > MAXIMUM_FREQUENCY)
+ {
+ mLoopFrequency -= 2.0d * MAXIMUM_FREQUENCY;
+ }
+
+ if(mLoopFrequency < -MAXIMUM_FREQUENCY)
+ {
+ mLoopFrequency += 2.0d * MAXIMUM_FREQUENCY;
+ }
+ }
+
+ /**
+ * Increments the phase of the loop
+ */
+ public void increment()
+ {
+ mLoopPhase += mLoopFrequency;
+
+ /* Keep the loop phase in bounds */
+ unwrapPhase();
+ }
+
+ @Override
+ public void reset()
+ {
+ mLoopPhase = 0.0f;
+ mLoopFrequency = 0.0f;
+ }
+
+ @Override
+ public void adjust(double error)
+ {
+ adjust(error);
+ }
+
+ @Override
+ public void correctInversion(double correction)
+ {
+ correctPhaseError(correction);
+ }
+
+ private void unwrapPhase()
+ {
+ while(mLoopPhase > TWO_PI)
+ {
+ mLoopPhase -= TWO_PI;
+ }
+
+ while(mLoopPhase < -TWO_PI)
+ {
+ mLoopPhase += TWO_PI;
+ }
+ }
+
+ /**
+ * Current vector of the loop rotated by THETA degrees.
+ */
+ public Complex getCurrentVector()
+ {
// return ComplexSample.fromAngle( mLoopPhase + THETA );
- return Complex.fromAngle( mLoopPhase );
- }
-
- @Override
- public void receive( Complex complex )
- {
- /* Calculate phase error */
- float phaseError = getPhaseError( complex );
-
- Dibit d = QPSKPolarSlicer.decide( complex );
+ return Complex.fromAngle(mLoopPhase);
+ }
+
+ @Override
+ public Complex incrementAndGetCurrentVector()
+ {
+ return null;
+ }
+
+ @Override
+ public void receive(Complex complex)
+ {
+ /* Calculate phase error */
+ float phaseError = getPhaseError(complex);
+
+ Dibit d = mQPSKSymbolDecoder.decode(complex);
// mLog.debug( "Dibit: " + d.name() + " Error: " + phaseError );
- /* Adjust for phase error */
- adjust( phaseError );
- }
+ /* Adjust for phase error */
+ adjust(phaseError);
+ }
- /**
- * Updates the costas loop frequency and phase to adjust for the phase
- * error value
- *
- * @param phase_error - (-)= late and (+)= early
- */
- private void adjust( float phase_error )
- {
- mLoopFrequency += mBetaGain * phase_error;
- mLoopPhase += mLoopFrequency + mAlphaGain * phase_error;
+ /**
+ * Updates the costas loop frequency and phase to adjust for the phase
+ * error value
+ *
+ * @param phase_error - (-)= late and (+)= early
+ */
+ private void adjust(float phase_error)
+ {
+ mLoopFrequency += mBetaGain * phase_error;
+ mLoopPhase += mLoopFrequency + mAlphaGain * phase_error;
- /* Maintain phase between +/- 2 * PI */
- unwrapPhase();
+ /* Maintain phase between +/- 2 * PI */
+ unwrapPhase();
- /* Limit frequency to +/- maximum loop frequency */
- limitFrequency();
- }
+ /* Limit frequency to +/- maximum loop frequency */
+ limitFrequency();
+ }
- /**
- * Constrains the frequency within the bounds of +/- loop frequency
- */
- private void limitFrequency()
- {
- /* Check for and issue tuner offset correction */
- mFrequencyControl.receive( mLoopFrequency );
-
- if( mLoopFrequency > MAXIMUM_FREQUENCY )
- {
- mLoopFrequency = MAXIMUM_FREQUENCY;
- }
-
- if( mLoopFrequency < -MAXIMUM_FREQUENCY )
- {
- mLoopFrequency = -MAXIMUM_FREQUENCY;
- }
- }
+ /**
+ * Constrains the frequency within the bounds of +/- loop frequency
+ */
+ private void limitFrequency()
+ {
+ /* Check for and issue tuner offset correction */
+ mFrequencyControl.receive(mLoopFrequency);
- /**
- * Detects rotational error present when the costas loop is not aligned
- * with the carrier frequency. Provides error feedback to adjust
- * mixer frequency.
- */
- public float getPhaseError( Complex complex )
- {
- float phase_error = 0;
-
- if( Math.abs( complex.inphase() ) > Math.abs( complex.quadrature() ) )
- {
- if( complex.inphase() > 0 )
- {
- phase_error = -complex.quadrature();
- }
- else
- {
- phase_error = complex.quadrature();
- }
- }
- else
- {
- if( complex.quadrature() > 0 )
- {
- phase_error = complex.inphase();
- }
- else
- {
- phase_error = -complex.inphase();
- }
- }
-
- return phase_error;
- }
- }
+ if(mLoopFrequency > MAXIMUM_FREQUENCY)
+ {
+ mLoopFrequency = MAXIMUM_FREQUENCY;
+ }
- @Override
- public List getTapGroups()
- {
- if( mAvailableTaps == null )
- {
- mAvailableTaps = new ArrayList<>();
-
- TapGroup group = new TapGroup( "LSM Demodulator" );
-
- group.add( new EyeDiagramDataTap( "Eye Diagram", 0, 4800 ) );
+ if(mLoopFrequency < -MAXIMUM_FREQUENCY)
+ {
+ mLoopFrequency = -MAXIMUM_FREQUENCY;
+ }
+ }
- mAvailableTaps.add( group );
- }
-
- return mAvailableTaps;
- }
+ /**
+ * Detects rotational error present when the costas loop is not aligned
+ * with the carrier frequency. Provides error feedback to adjust
+ * mixer frequency.
+ */
+ public float getPhaseError(Complex complex)
+ {
+ float phase_error = 0;
- @Override
- public void registerTap( Tap tap )
- {
- if( tap instanceof EyeDiagramDataTap )
- {
- mEyeDiagramDataTap = (EyeDiagramDataTap)tap;
- }
- }
+ if(Math.abs(complex.inphase()) > Math.abs(complex.quadrature()))
+ {
+ if(complex.inphase() > 0)
+ {
+ phase_error = -complex.quadrature();
+ }
+ else
+ {
+ phase_error = complex.quadrature();
+ }
+ }
+ else
+ {
+ if(complex.quadrature() > 0)
+ {
+ phase_error = complex.inphase();
+ }
+ else
+ {
+ phase_error = -complex.inphase();
+ }
+ }
- @Override
- public void unregisterTap( Tap tap )
- {
- if( tap instanceof EyeDiagramDataTap )
- {
- mEyeDiagramDataTap = null;
- }
- }
+ return phase_error;
+ }
+ }
+
+ @Override
+ public List getTapGroups()
+ {
+ if(mAvailableTaps == null)
+ {
+ mAvailableTaps = new ArrayList<>();
+
+ TapGroup group = new TapGroup("LSM Demodulator");
+
+ group.add(new EyeDiagramDataTap("Eye Diagram", 0, 4800));
+
+ mAvailableTaps.add(group);
+ }
+
+ return mAvailableTaps;
+ }
+
+ @Override
+ public void registerTap(Tap tap)
+ {
+ if(tap instanceof EyeDiagramDataTap)
+ {
+ mEyeDiagramDataTap = (EyeDiagramDataTap)tap;
+ }
+ }
+
+ @Override
+ public void unregisterTap(Tap tap)
+ {
+ if(tap instanceof EyeDiagramDataTap)
+ {
+ mEyeDiagramDataTap = null;
+ }
+ }
}
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/QPSKStarSlicer.java b/src/main/java/io/github/dsheirer/dsp/psk/QPSKStarSlicer.java
index f6b23861..b8295145 100644
--- a/src/main/java/io/github/dsheirer/dsp/psk/QPSKStarSlicer.java
+++ b/src/main/java/io/github/dsheirer/dsp/psk/QPSKStarSlicer.java
@@ -1,3 +1,18 @@
+/*******************************************************************************
+ * 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;
import io.github.dsheirer.dsp.symbol.Dibit;
@@ -5,54 +20,54 @@ import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.Complex;
-public class QPSKStarSlicer implements Listener
+public class QPSKStarSlicer implements IQPSKSymbolDecoder, Listener
{
- private Broadcaster mBroadcaster = new Broadcaster();
+ private Broadcaster mBroadcaster = new Broadcaster();
- /**
- * Slices a ComplexSample representing a phase shifted symbol according to
- * the following constellation pattern:
- *
- * 00 | 01
- * ---|---
- * 10 | 11
- */
- public QPSKStarSlicer()
- {
- }
-
- public void dispose()
- {
- mBroadcaster.dispose();
- mBroadcaster = null;
- }
+ /**
+ * Slices a Complex sample representing a phase shifted symbol according to the following constellation pattern:
+ *
+ * 00 +3 | 01 +1
+ * ------|------
+ * 10 -1 | 11 -1
+ */
+ public QPSKStarSlicer()
+ {
+ }
- public void addListener( Listener listener )
- {
- mBroadcaster.addListener( listener );
- }
+ public void dispose()
+ {
+ mBroadcaster.dispose();
+ mBroadcaster = null;
+ }
- public void removeListener( Listener listener )
- {
- mBroadcaster.removeListener( listener );
- }
+ public void addListener(Listener listener)
+ {
+ mBroadcaster.addListener(listener);
+ }
+
+ public void removeListener(Listener listener)
+ {
+ mBroadcaster.removeListener(listener);
+ }
+
+ @Override
+ public void receive(Complex sample)
+ {
+ mBroadcaster.broadcast(decode(sample));
+ }
+
+ @Override
+ public Dibit decode(Complex sample)
+ {
+ if(sample.inphase() > 0)
+ {
+ return sample.quadrature() > 0 ? Dibit.D00_PLUS_1 : Dibit.D10_MINUS_1;
+ }
+ else
+ {
+ return sample.quadrature() > 0 ? Dibit.D01_PLUS_3 : Dibit.D11_MINUS_3;
+ }
+ }
- @Override
- public void receive( Complex sample )
- {
- mBroadcaster.broadcast( decide( sample ) );
- }
-
- public static Dibit decide( Complex sample )
- {
- if( sample.inphase() > 0 )
- {
- return sample.quadrature() > 0 ? Dibit.D00_PLUS_1 : Dibit.D10_MINUS_1;
- }
- else
- {
- return sample.quadrature() > 0 ? Dibit.D01_PLUS_3 : Dibit.D11_MINUS_3;
- }
- }
-
}
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/QPSKPolarSlicer.java b/src/main/java/io/github/dsheirer/dsp/psk/QPSKSymbolDecoder.java
similarity index 59%
rename from src/main/java/io/github/dsheirer/dsp/psk/QPSKPolarSlicer.java
rename to src/main/java/io/github/dsheirer/dsp/psk/QPSKSymbolDecoder.java
index 3ff0c708..42a97b6c 100644
--- a/src/main/java/io/github/dsheirer/dsp/psk/QPSKPolarSlicer.java
+++ b/src/main/java/io/github/dsheirer/dsp/psk/QPSKSymbolDecoder.java
@@ -1,3 +1,18 @@
+/*******************************************************************************
+ * 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;
import io.github.dsheirer.dsp.symbol.Dibit;
@@ -5,24 +20,7 @@ import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.Complex;
-/*******************************************************************************
- * 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
- ******************************************************************************/
-public class QPSKPolarSlicer implements Listener
+public class QPSKSymbolDecoder implements Listener, IQPSKSymbolDecoder
{
private Broadcaster mBroadcaster = new Broadcaster();
@@ -37,7 +35,7 @@ public class QPSKPolarSlicer implements Listener
* / \
* / 11 \
*/
- public QPSKPolarSlicer()
+ public QPSKSymbolDecoder()
{
}
@@ -60,10 +58,10 @@ public class QPSKPolarSlicer implements Listener
@Override
public void receive( Complex complex )
{
- mBroadcaster.broadcast( decide( complex ) );
+ mBroadcaster.broadcast( decode( complex ) );
}
- public static Dibit decide( Complex complex )
+ public Dibit decode(Complex complex )
{
if( Math.abs( complex.inphase() ) > Math.abs( complex.quadrature() ) )
{
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/QPSKSymbolPhaseErrorCalculator.java b/src/main/java/io/github/dsheirer/dsp/psk/QPSKSymbolPhaseErrorCalculator.java
new file mode 100644
index 00000000..fed580ce
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/QPSKSymbolPhaseErrorCalculator.java
@@ -0,0 +1,57 @@
+/*******************************************************************************
+ * 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;
+
+import io.github.dsheirer.sample.complex.Complex;
+
+/**
+ * Calculates the phase error for a QPSK polar-orientation constellation
+ */
+public class QPSKSymbolPhaseErrorCalculator implements ISymbolPhaseErrorCalculator
+{
+ private float mPhaseError;
+
+ @Override
+ public float getPhaseError(Complex symbol)
+ {
+ mPhaseError = 0.0f;
+
+ if(Math.abs(symbol.inphase()) > Math.abs(symbol.quadrature()))
+ {
+ if(symbol.inphase() > 0)
+ {
+ mPhaseError = -symbol.quadrature();
+ }
+ else
+ {
+ mPhaseError = symbol.quadrature();
+ }
+ }
+ else
+ {
+ if(symbol.quadrature() > 0)
+ {
+ mPhaseError = symbol.inphase();
+ }
+ else
+ {
+ mPhaseError = -symbol.inphase();
+ }
+ }
+
+ return mPhaseError;
+ }
+}
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..e06f3d22
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/SymbolDecisionData.java
@@ -0,0 +1,93 @@
+/*******************************************************************************
+ * 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;
+
+import io.github.dsheirer.buffer.ComplexCircularBuffer;
+import io.github.dsheirer.sample.complex.Complex;
+
+public class SymbolDecisionData
+{
+ //45 degrees rotation to orient the symbol to a polar axis to make error calculation easy/efficient
+ public static final Complex DIFFERENTIAL_OFFSET = Complex.fromAngle(Math.PI / 4.0d);
+
+ private ComplexCircularBuffer mBuffer;
+ private float mSamplingPoint;
+
+ /**
+ * Circular buffer for capturing two symbols worth of sample data for use in instrumentation.
+ * @param samplesPerSymbol to size the buffer
+ */
+ public SymbolDecisionData(int samplesPerSymbol)
+ {
+ mBuffer = new ComplexCircularBuffer(2 * samplesPerSymbol);
+ }
+
+ /**
+ * Stores the complex sample in the buffer
+ */
+ public void receive(float inphase, float quadrature)
+ {
+ mBuffer.put(new Complex(inphase, quadrature));
+ }
+
+ /**
+ * Stores the complex sample in the buffer
+ */
+ public void receive(Complex complex)
+ {
+ mBuffer.put(complex.copy());
+ }
+
+ /**
+ * Sets the fractional sampling point for the current symbol to use in interpolating the current symbol.
+ */
+ public void setSamplingPoint(float samplingPoint)
+ {
+ mSamplingPoint = samplingPoint;
+ }
+
+ /**
+ * Fractional sampling point
+ */
+ public float getSamplingPoint()
+ {
+ return mSamplingPoint;
+ }
+
+ /**
+ * Array of demodulated samples representing the current symbol where each current sample is demodulated against
+ * the previous symbol's sample to produce the differential demodulated sample. Samples are in time order.
+ *
+ * Note: differential decoding is on an integral samples-per-symbol basis and does not use fractional interpolation.
+ */
+ public Complex[] getDemodulated()
+ {
+ Complex[] samples = mBuffer.getAll();
+
+ int length = samples.length / 2;
+
+ Complex[] demodulated = new Complex[length];
+
+ for(int x = 0; x < samples.length / 2; x++)
+ {
+ Complex copy = samples[length + x].copy();
+ copy.multiply(samples[x].conjugate());
+ demodulated[x] = copy;
+ }
+
+ return demodulated;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/SymbolInversion.java b/src/main/java/io/github/dsheirer/dsp/psk/SymbolInversion.java
new file mode 100644
index 00000000..0b7431c9
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/SymbolInversion.java
@@ -0,0 +1,35 @@
+/*******************************************************************************
+ * 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 enum SymbolInversion
+{
+ DEGREES_90_PLUS(Math.PI / 2.0),
+ DEGREES_90_MINUS(Math.PI / -2.0),
+ DEGREES_180(Math.PI);
+
+ private double mAngleRadians;
+
+ SymbolInversion(double angleRadians)
+ {
+ mAngleRadians = angleRadians;
+ }
+
+ public double getError()
+ {
+ return mAngleRadians;
+ }
+}
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
new file mode 100644
index 00000000..38b8d101
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/pll/CostasLoop.java
@@ -0,0 +1,182 @@
+/*******************************************************************************
+ * 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.pll;
+
+import io.github.dsheirer.sample.complex.Complex;
+
+/**
+ * Costas Loop - phase locked loop designed to automatically synchronize to the incoming carrier frequency in order to
+ * zeroize any frequency offset inherent in the signal due to either mis-tuning or carrier frequency drift.
+ *
+ * Costas loop structure was modeled from KA1RBI's OP25/cqpsk.py
+ *
+ * Loop bandwidth and alpha/beta gain formulas derived from:
+ * http://www.trondeau.com/blog/2011/8/13/control-loop-gain-values.html
+ */
+public class CostasLoop implements IPhaseLockedLoop
+{
+ public static final double TWO_PI = 2.0 * Math.PI;
+
+ private Complex mCurrentVector = new Complex(0, 0);
+ private double mLoopPhase = 0.0;
+ private double mLoopFrequency = 0.0;
+ private double mMaximumLoopFrequency;
+ private double mDamping = Math.sqrt(2.0) / 2.0;
+ private double mAlphaGain;
+ private double mBetaGain;
+ private Tracking mTracking = Tracking.NORMAL;
+
+ /**
+ * Costas Loop for tracking and correcting frequency error in a received carrier signal.
+ *
+ * @param sampleRate of the incoming samples
+ * @param symbolRate of the digital signal
+ */
+ public CostasLoop(double sampleRate, double symbolRate)
+ {
+ mMaximumLoopFrequency = TWO_PI * (symbolRate / 2.0) / sampleRate;
+ updateLoopBandwidth();
+ }
+
+ /**
+ * Corrects the current phase tracker when an inverted output is detected. The inversion will take the form of
+ * +/- 90 degrees or +/- 180 degrees, although the latter correction is equal regardless of the sign.
+ *
+ * If the supplied correction value places the loop frequency outside of the max frequency, then the frequency will
+ * be corrected 360 degrees in the opposite direction to maintain within the max frequency bounds.
+ *
+ * @param correction as measured in radians
+ */
+ public void correctInversion(double correction)
+ {
+ mLoopFrequency += correction;
+
+ while(mLoopFrequency > mMaximumLoopFrequency)
+ {
+ mLoopFrequency -= 2.0 * mMaximumLoopFrequency;
+ }
+
+ while(mLoopFrequency < -mMaximumLoopFrequency)
+ {
+ mLoopFrequency += 2.0 * mMaximumLoopFrequency;
+ }
+ }
+
+ /**
+ * Updates the loop bandwidth and alpha/beta gains according to the current loop synchronization state.
+ */
+ private void updateLoopBandwidth()
+ {
+ double bandwidth = TWO_PI / mTracking.getLoopBandwidth();
+
+ mAlphaGain = (4.0 * mDamping * bandwidth) / (1.0 + (2.0 * mDamping * bandwidth) + (bandwidth * bandwidth));
+ mBetaGain = (4.0 * bandwidth * bandwidth) / (1.0 + (2.0 * mDamping * bandwidth) + (bandwidth * bandwidth));
+ }
+
+ /**
+ * Sets the tracking state for this costas loop. The tracking state affects the aggressiveness of the alpha/beta
+ * gain values in synchronizing with the signal carrier.
+ *
+ * @param tracking
+ */
+ public void setTracking(Tracking tracking)
+ {
+ mTracking = tracking;
+ updateLoopBandwidth();
+ }
+
+ /**
+ * Increments the phase of the loop for each sample received at the sample rate.
+ */
+ public void increment()
+ {
+ mLoopPhase += mLoopFrequency;
+
+ /* Normalize phase between +/- 2 * PI */
+ if(mLoopPhase > TWO_PI)
+ {
+ mLoopPhase -= TWO_PI;
+ }
+
+ if(mLoopPhase < -TWO_PI)
+ {
+ mLoopPhase += TWO_PI;
+ }
+ }
+
+ /**
+ * Current vector of the loop. Note: this value is updated for the current angle in radians each time this method
+ * is invoked.
+ */
+ public Complex getCurrentVector()
+ {
+ mCurrentVector.setAngle(mLoopPhase);
+ return mCurrentVector;
+ }
+
+ @Override
+ public Complex incrementAndGetCurrentVector()
+ {
+ increment();
+ return getCurrentVector();
+ }
+
+ public double getLoopFrequency()
+ {
+ return mLoopFrequency;
+ }
+
+ /**
+ * Updates the costas loop frequency and phase to adjust for the phase
+ * error value
+ *
+ * @param phaseError - (-)= late and (+)= early
+ */
+ public void adjust(double phaseError)
+ {
+ mLoopFrequency += (mBetaGain * phaseError);
+ mLoopPhase += mLoopFrequency + (mAlphaGain * phaseError);
+
+ /* Normalize phase between +/- 2 * PI */
+ if(mLoopPhase > TWO_PI)
+ {
+ mLoopPhase -= TWO_PI;
+ }
+
+ if(mLoopPhase < -TWO_PI)
+ {
+ mLoopPhase += TWO_PI;
+ }
+
+ /* Limit frequency to +/- maximum loop frequency */
+ if(mLoopFrequency > mMaximumLoopFrequency)
+ {
+ mLoopFrequency = mMaximumLoopFrequency;
+ }
+
+ if(mLoopFrequency < -mMaximumLoopFrequency)
+ {
+ mLoopFrequency = -mMaximumLoopFrequency;
+ }
+ }
+
+ @Override
+ public void reset()
+ {
+ mLoopPhase = 0.0;
+ mLoopFrequency = 0.0;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/pll/IPhaseLockedLoop.java b/src/main/java/io/github/dsheirer/dsp/psk/pll/IPhaseLockedLoop.java
new file mode 100644
index 00000000..631a772c
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/pll/IPhaseLockedLoop.java
@@ -0,0 +1,59 @@
+/*******************************************************************************
+ * 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.pll;
+
+import io.github.dsheirer.sample.complex.Complex;
+
+/**
+ * Phase-Locked Loop (PLL) interface
+ */
+public interface IPhaseLockedLoop
+{
+ /**
+ * Increments the PLL by one sample period
+ */
+ void increment();
+
+ /**
+ * Applies an error value to the PLL.
+ *
+ * @param error value in the range of -PI <> +PI
+ */
+ void adjust(double error);
+
+ /**
+ * Adjusts the current phase of the loop to account for +/-90 or 180 degree inverted phase locks when detected
+ * @param correction to apply to the phase of the loop
+ */
+ void correctInversion(double correction);
+
+ /**
+ * Current vector of the PLL to use in de-spinning the incoming samples.
+ * @return complex vector
+ */
+ Complex getCurrentVector();
+
+ /**
+ * Increments the phase rotation and gets the current vector of the PLL to use in de-spinning the incoming samples.
+ * @return complex vector
+ */
+ Complex incrementAndGetCurrentVector();
+
+ /**
+ * Reset tracking to 0
+ */
+ void reset();
+}
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
new file mode 100644
index 00000000..a4611d20
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/pll/Tracking.java
@@ -0,0 +1,42 @@
+/*******************************************************************************
+ * 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.pll;
+
+/**
+ * Phase Locked Loop (PLL) tracking bandwidth - controls how fast/slow the PLL tracks the carrier frequency
+ */
+public enum Tracking
+{
+ FASTEST(50.0),
+ FAST(100.0),
+ NORMAL(200.0),
+ SLOW(400.0);
+
+ private double mLoopBandwidth;
+
+ Tracking(double loopBandwidth)
+ {
+ mLoopBandwidth = loopBandwidth;
+ }
+
+ /**
+ * Loop bandwidth setting
+ */
+ public double getLoopBandwidth()
+ {
+ return mLoopBandwidth;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/instrument/gui/DecoderSelectionFrame.java b/src/main/java/io/github/dsheirer/instrument/gui/DecoderSelectionFrame.java
index 00df6945..bc3f28d4 100644
--- a/src/main/java/io/github/dsheirer/instrument/gui/DecoderSelectionFrame.java
+++ b/src/main/java/io/github/dsheirer/instrument/gui/DecoderSelectionFrame.java
@@ -2,15 +2,19 @@ package io.github.dsheirer.instrument.gui;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.audio.broadcast.BroadcastModel;
+import io.github.dsheirer.controller.channel.Channel;
import io.github.dsheirer.controller.channel.ChannelModel;
import io.github.dsheirer.controller.channel.map.ChannelMapModel;
import io.github.dsheirer.module.decode.DecodeConfigurationEditor;
+import io.github.dsheirer.module.decode.config.DecodeConfiguration;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.source.IControllableFileSource;
import net.miginfocom.swing.MigLayout;
-import javax.swing.*;
-import java.awt.*;
+import javax.swing.JButton;
+import javax.swing.JDesktopPane;
+import javax.swing.JInternalFrame;
+import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
@@ -18,14 +22,14 @@ public class DecoderSelectionFrame extends JInternalFrame
{
private static final long serialVersionUID = 1L;
+ private Channel mChannel = new Channel();
private DecodeConfigurationEditor mDecodeEditor = new DecodeConfigurationEditor(null);
private PlaylistManager mPlaylistManager;
private IControllableFileSource mSource;
private JDesktopPane mDesktop;
- public DecoderSelectionFrame(JDesktopPane desktop,
- IControllableFileSource source)
+ public DecoderSelectionFrame(JDesktopPane desktop, IControllableFileSource source)
{
AliasModel aliasModel = new AliasModel();
BroadcastModel broadcastModel = new BroadcastModel(null);
@@ -37,6 +41,7 @@ public class DecoderSelectionFrame extends JInternalFrame
mDesktop = desktop;
mSource = source;
+ mDecodeEditor.setItem(mChannel);
initGUI();
}
@@ -72,17 +77,23 @@ public class DecoderSelectionFrame extends JInternalFrame
@Override
public void actionPerformed(ActionEvent arg0)
{
-// DecodeConfiguration config = mDecodeEditor.getDecodeConfig();
-//
-// if( config != null )
-// {
-// DecoderViewFrame decoderFrame = new DecoderViewFrame(
-// mPlaylistManager, null, mSource );
-//
-// decoderFrame.setVisible( true );
-//
-// mDesktop.add( decoderFrame );
-// }
+ mDecodeEditor.save();
+
+ Channel channel = mDecodeEditor.getItem();
+
+ if(channel != null)
+ {
+ DecodeConfiguration config = channel.getDecodeConfiguration();
+
+ if( config != null )
+ {
+ DecoderViewFrame decoderFrame = new DecoderViewFrame(mPlaylistManager, channel, mSource );
+
+ decoderFrame.setVisible( true );
+
+ mDesktop.add( decoderFrame );
+ }
+ }
}
});
}
diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/DemodulatorViewerFX.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/DemodulatorViewerFX.java
new file mode 100644
index 00000000..5e9745a6
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/DemodulatorViewerFX.java
@@ -0,0 +1,183 @@
+/*******************************************************************************
+ * 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;
+
+import io.github.dsheirer.instrument.gui.viewer.decoder.DecoderPaneFactory;
+import io.github.dsheirer.module.decode.DecoderType;
+import javafx.application.Application;
+import javafx.application.Platform;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.scene.Scene;
+import javafx.scene.control.Menu;
+import javafx.scene.control.MenuBar;
+import javafx.scene.control.MenuItem;
+import javafx.scene.control.SeparatorMenuItem;
+import javafx.scene.layout.BorderPane;
+import javafx.stage.FileChooser;
+import javafx.stage.Stage;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+
+/**
+ * Application for viewing and working with demodulators and recording files.
+ */
+public class DemodulatorViewerFX extends Application
+{
+ private final static Logger mLog = LoggerFactory.getLogger(DemodulatorViewerFX.class);
+
+ private Stage mStage;
+ private MenuBar mMenuBar;
+ private RecentFilesMenu mRecentFilesMenu;
+ private ViewerDesktop mViewerDesktop;
+
+ @Override
+ public void start(Stage primaryStage) throws Exception
+ {
+ mStage = primaryStage;
+ setTitle(null);
+
+ BorderPane borderPane = new BorderPane();
+ borderPane.setTop(getMenuBar());
+ borderPane.setCenter(getViewerDesktop());
+
+ Scene scene = new Scene(borderPane, 1500, 900);
+
+ mStage.setScene(scene);
+ mStage.show();
+ }
+
+ private void setTitle(DecoderType decoderType)
+ {
+ if(decoderType != null)
+ {
+ mStage.setTitle("Demodulator Viewer FX - " + decoderType.getDisplayString());
+ }
+ else
+ {
+ mStage.setTitle("Demodulator Viewer FX");
+ }
+ }
+
+ private ViewerDesktop getViewerDesktop()
+ {
+ if(mViewerDesktop == null)
+ {
+ mViewerDesktop = new ViewerDesktop();
+ }
+
+ return mViewerDesktop;
+ }
+
+ private MenuBar getMenuBar()
+ {
+ if(mMenuBar == null)
+ {
+ mMenuBar = new MenuBar();
+
+ Menu fileMenu = new Menu("File");
+ MenuItem openMenuItem = new MenuItem("Open ...");
+ openMenuItem.setOnAction(new EventHandler()
+ {
+ @Override
+ public void handle(ActionEvent event)
+ {
+ FileChooser fileChooser = new FileChooser();
+ fileChooser.setTitle("Open an I/Q recording file");
+ File file = fileChooser.showOpenDialog(mStage);
+ getViewerDesktop().load(file);
+ getRecentFilesMenu().add(file);
+ }
+ });
+
+ MenuItem closeMenuItem = new MenuItem("Close");
+ closeMenuItem.setOnAction(new EventHandler()
+ {
+ @Override
+ public void handle(ActionEvent event)
+ {
+ getViewerDesktop().close();
+ }
+ });
+
+ MenuItem exitMenuItem = new MenuItem("Exit");
+ exitMenuItem.setOnAction(new EventHandler()
+ {
+ @Override
+ public void handle(ActionEvent event)
+ {
+ Platform.exit();
+ }
+ });
+
+ fileMenu.getItems().addAll(openMenuItem, getRecentFilesMenu(), closeMenuItem, new SeparatorMenuItem(),
+ exitMenuItem);
+
+ mMenuBar.getMenus().add(fileMenu);
+
+ Menu decoderMenu = new Menu("Decoder");
+
+ for(DecoderType decoderType: DecoderType.values())
+ {
+ if(DecoderPaneFactory.isSupported(decoderType))
+ {
+ MenuItem decoderMenuItem = new MenuItem(decoderType.getShortDisplayString());
+
+ decoderMenuItem.setOnAction(new EventHandler()
+ {
+ @Override
+ public void handle(ActionEvent event)
+ {
+ getViewerDesktop().setDecoder(decoderType);
+ setTitle(decoderType);
+ }
+ });
+
+ decoderMenu.getItems().add(decoderMenuItem);
+ }
+ }
+
+ mMenuBar.getMenus().add(decoderMenu);
+ }
+
+ return mMenuBar;
+ }
+
+ private RecentFilesMenu getRecentFilesMenu()
+ {
+ if(mRecentFilesMenu == null)
+ {
+ mRecentFilesMenu = new RecentFilesMenu("Recent","demodulator.viewer", 5);
+ mRecentFilesMenu.setFileSelectionListener(new RecentFilesMenu.IFileSelectionListener()
+ {
+ @Override
+ public void fileSelected(File file)
+ {
+ getViewerDesktop().load(file);
+ }
+ });
+ }
+
+ return mRecentFilesMenu;
+ }
+
+ public static void main(String[] args)
+ {
+ launch(args);
+ }
+}
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
new file mode 100644
index 00000000..1ea7a687
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/PlaybackController.java
@@ -0,0 +1,311 @@
+/*******************************************************************************
+ * 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;
+
+import io.github.dsheirer.instrument.gui.viewer.decoder.DecoderPane;
+import io.github.dsheirer.sample.Broadcaster;
+import io.github.dsheirer.sample.Listener;
+import io.github.dsheirer.sample.complex.ComplexBuffer;
+import io.github.dsheirer.source.IControllableFileSource;
+import io.github.dsheirer.source.IFrameLocationListener;
+import io.github.dsheirer.source.wave.ComplexWaveSource;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.geometry.Pos;
+import javafx.scene.control.Alert;
+import javafx.scene.control.Button;
+import javafx.scene.control.ButtonType;
+import javafx.scene.control.Label;
+import javafx.scene.control.TextField;
+import javafx.scene.layout.HBox;
+import org.kordamp.ikonli.fontawesome.FontAwesome;
+import org.kordamp.ikonli.javafx.FontIcon;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.sound.sampled.UnsupportedAudioFileException;
+import java.io.File;
+import java.io.IOException;
+
+public class PlaybackController extends HBox implements IFrameLocationListener, Listener
+{
+ private final static Logger mLog = LoggerFactory.getLogger(PlaybackController.class);
+
+ private Button mRewindButton;
+ private TextField mPlaybackPositionText;
+ private Button mPlay1Button;
+ private Button mPlay10Button;
+ private Button mPlay100Button;
+ private Button mPlay1000Button;
+ private HBox mControlsBox;
+ private Label mFileLabel;
+ private EventHandler mPlayEventHandler;
+ private DecoderPane mSampleRateListener;
+
+ private IControllableFileSource mControllableFileSource;
+ private Broadcaster mComplexBufferBroadcaster = new Broadcaster<>();
+
+ public PlaybackController()
+ {
+ super(10);
+
+ getChildren().addAll(getControlsBox(), getFileLabel());
+
+ setControlsEnabled(false);
+ }
+
+ public void setSampleRateListener(DecoderPane decoderPane)
+ {
+ mSampleRateListener = decoderPane;
+
+ if(mSampleRateListener != null && mControllableFileSource != null)
+ {
+ mSampleRateListener.setSampleRate(mControllableFileSource.getSampleRate());
+ }
+ }
+
+ public void load(File file)
+ {
+ if(file != null && file.isFile())
+ {
+ try
+ {
+ mControllableFileSource = new ComplexWaveSource(file);
+ mControllableFileSource.setListener(this);
+ ((ComplexWaveSource)mControllableFileSource).setListener((Listener)this);
+ mControllableFileSource.open();
+
+ if(mSampleRateListener != null)
+ {
+ mSampleRateListener.setSampleRate(mControllableFileSource.getSampleRate());
+ }
+ }
+ catch(UnsupportedAudioFileException e)
+ {
+ mLog.error("Unsupported Audio File Type [" + (file != null ? file.getAbsolutePath() : "null") + "]");
+
+ Alert alert = new Alert(Alert.AlertType.ERROR, "The file type is unsupported.", ButtonType.OK);
+ alert.show();
+ return;
+ }
+ catch(IOException ioe)
+ {
+ 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);
+ alert.show();
+ return;
+ }
+
+ getFileLabel().setText(file.getName());
+ setControlsEnabled(true);
+ }
+ }
+
+ public void close()
+ {
+ if(mControllableFileSource != null)
+ {
+ try
+ {
+ mControllableFileSource.close();
+ }
+ catch(IOException ioe)
+ {
+ mLog.error("Error closing file", ioe);
+ }
+ }
+
+ setControlsEnabled(false);
+ }
+
+ /**
+ * Adds listener to receive complex buffers from this playback
+ */
+ public void addListener(Listener listener)
+ {
+ mComplexBufferBroadcaster.addListener(listener);
+ }
+
+ /**
+ * Removes the listener from receiving complex buffers from this playback
+ */
+ public void removeListener(Listener listener)
+ {
+ mComplexBufferBroadcaster.removeListener(listener);
+ }
+
+ /**
+ * Sets all of the playback controls to the specified enabled state.
+ */
+ private void setControlsEnabled(boolean enabled)
+ {
+ getRewindButton().setDisable(!enabled);
+ getPlaybackPositionText().setDisable(!enabled);
+ getPlay1Button().setDisable(!enabled);
+ getPlay10Button().setDisable(!enabled);
+ getPlay100Button().setDisable(!enabled);
+ getPlay1000Button().setDisable(!enabled);
+ }
+
+ private HBox getControlsBox()
+ {
+ if(mControlsBox == null)
+ {
+ mControlsBox = new HBox();
+
+ mControlsBox.getChildren().addAll(getRewindButton(), getPlaybackPositionText(), getPlay1Button(),
+ getPlay10Button(), getPlay100Button(), getPlay1000Button());
+ }
+
+ return mControlsBox;
+ }
+
+
+ private Button getRewindButton()
+ {
+ if(mRewindButton == null)
+ {
+ mRewindButton = new Button();
+ mRewindButton.setGraphic(new FontIcon(FontAwesome.FAST_BACKWARD));
+ }
+
+ return mRewindButton;
+ }
+
+ private Button getPlay1Button()
+ {
+ if(mPlay1Button == null)
+ {
+ mPlay1Button = new Button("1");
+ mPlay1Button.setUserData(1);
+ mPlay1Button.setOnAction(getPlaybackEventHandler());
+ mPlay1Button.setGraphic(new FontIcon(FontAwesome.PLAY));
+ }
+
+ return mPlay1Button;
+ }
+
+ private Button getPlay10Button()
+ {
+ if(mPlay10Button == null)
+ {
+ mPlay10Button = new Button("10");
+ mPlay10Button.setUserData(10);
+ mPlay10Button.setOnAction(getPlaybackEventHandler());
+ mPlay10Button.setGraphic(new FontIcon(FontAwesome.FAST_FORWARD));
+ }
+
+ return mPlay10Button;
+ }
+
+ private Button getPlay100Button()
+ {
+ if(mPlay100Button == null)
+ {
+ mPlay100Button = new Button("100");
+ mPlay100Button.setUserData(100);
+ mPlay100Button.setOnAction(getPlaybackEventHandler());
+ mPlay100Button.setGraphic(new FontIcon(FontAwesome.FAST_FORWARD));
+ }
+
+ return mPlay100Button;
+ }
+
+ private Button getPlay1000Button()
+ {
+ if(mPlay1000Button == null)
+ {
+ mPlay1000Button = new Button("1000");
+ mPlay1000Button.setUserData(1000);
+ mPlay1000Button.setOnAction(getPlaybackEventHandler());
+ mPlay1000Button.setGraphic(new FontIcon(FontAwesome.FAST_FORWARD));
+ }
+
+ return mPlay1000Button;
+ }
+
+ private EventHandler getPlaybackEventHandler()
+ {
+ if(mPlayEventHandler == null)
+ {
+ mPlayEventHandler = new EventHandler()
+ {
+ @Override
+ public void handle(ActionEvent event)
+ {
+ int count = (int)((Button)event.getSource()).getUserData();
+
+ if(mControllableFileSource != null)
+ {
+ try
+ {
+ mControllableFileSource.next(count, true);
+ }
+ catch(IOException ioe)
+ {
+ mLog.error("Error while playing samples");
+ }
+ }
+
+ }
+ };
+ }
+
+ return mPlayEventHandler;
+ }
+
+ private TextField getPlaybackPositionText()
+ {
+ if(mPlaybackPositionText == null)
+ {
+ mPlaybackPositionText = new TextField("0");
+ mPlaybackPositionText.setAlignment(Pos.CENTER);
+ mPlaybackPositionText.setPrefWidth(80);
+ }
+
+ return mPlaybackPositionText;
+ }
+
+ private Label getFileLabel()
+ {
+ if(mFileLabel == null)
+ {
+ mFileLabel = new Label();
+ }
+
+ return mFileLabel;
+ }
+
+ @Override
+ public void frameLocationUpdated(int location)
+ {
+ getPlaybackPositionText().setText(String.valueOf(location));
+ }
+
+ @Override
+ public void frameLocationReset()
+ {
+ mLog.info("Frame location reset ... ignoring?");
+ }
+
+ @Override
+ public void receive(ComplexBuffer complexBuffer)
+ {
+ mComplexBufferBroadcaster.broadcast(complexBuffer);
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/RecentFilesMenu.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/RecentFilesMenu.java
new file mode 100644
index 00000000..873babf7
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/RecentFilesMenu.java
@@ -0,0 +1,192 @@
+package io.github.dsheirer.instrument.gui.viewer;
+
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.scene.control.Menu;
+import javafx.scene.control.MenuItem;
+import javafx.scene.control.SeparatorMenuItem;
+
+import java.io.File;
+import java.util.prefs.Preferences;
+
+public class RecentFilesMenu extends Menu implements EventHandler
+{
+ private Preferences mPreferences = Preferences.userNodeForPackage(RecentFilesMenu.class);
+ private IFileSelectionListener mFileSelectionListener;
+ private String mIdentifier;
+ private int mSize;
+
+ /**
+ * Recently used files menu. Uses java Preferences to store recently accessed files and provides access to those
+ * files via child menu items to this menu.
+ *
+ * @param label to use for this menu.
+ * @param uniqueIdentifier to use in addition to the class path of this class to uniquely identify recently accessed
+ * files in a java Preferences data store.
+ * @param size of the maximum number of recent files to present to the user
+ */
+ public RecentFilesMenu(String label, String uniqueIdentifier, int size)
+ {
+ super(label);
+ mIdentifier = uniqueIdentifier;
+ mSize = size;
+
+ load();
+ }
+
+ /**
+ * Registers the listener to receive file selection events when the user selects a recently used file.
+ * @param listener to receive file selection events
+ */
+ public void setFileSelectionListener(IFileSelectionListener listener)
+ {
+ mFileSelectionListener = listener;
+ }
+
+ /**
+ * Adds the file to the recently accessed files list. If this file is already included in the recently files list,
+ * it will be promoted to the first menu item and all other recently used files will be demoted.
+ *
+ * @param file to add to the recently used files list.
+ */
+ public void add(File file)
+ {
+ if(file != null)
+ {
+ String filePath = file.getAbsolutePath();
+
+ int index = mSize - 1;
+
+ //Check to see if this file is already in the recent files list
+ for(int x = mSize - 1; x >= 0; x--)
+ {
+ String existingItem = mPreferences.get(getKey(x), null);
+
+ if(existingItem != null && existingItem.equals(filePath))
+ {
+ index = x;
+ break;
+ }
+ }
+
+ //Shift all recent files down to make room for the newly added file at the top of the list
+ while(index > 0)
+ {
+ String newerItem = mPreferences.get(getKey(index - 1), null);
+
+ if(newerItem != null)
+ {
+ mPreferences.put(getKey(index), newerItem);
+ }
+
+ index--;
+ }
+
+ mPreferences.put(getKey(0), file.getAbsolutePath());
+ }
+
+ load();
+ }
+
+ /**
+ * Creates a unique key value for each numbered recently used file item in the preferences.
+ *
+ * @param index of recent file item.
+ * @return unique key to use with the preferences instance.
+ */
+ private String getKey(int index)
+ {
+ return mIdentifier + ".recent.file." + index;
+ }
+
+ /**
+ * Clears and (re)loads all child recent file menu items.
+ */
+ private void load()
+ {
+ getItems().clear();
+
+ for(int x = 0; x < mSize; x++)
+ {
+ String path = mPreferences.get(getKey(x), null);
+
+ if(path != null)
+ {
+ File file = new File(path);
+
+ if(file.isFile())
+ {
+ MenuItem menuItem = new MenuItem(file.getName());
+ menuItem.setUserData(file);
+ menuItem.setOnAction(this);
+ getItems().add(menuItem);
+ }
+ }
+ }
+
+ if(getItems().size() > 0)
+ {
+ setDisable(false);
+ getItems().add(new SeparatorMenuItem());
+
+ MenuItem clearMenuItem = new MenuItem("Clear History");
+ clearMenuItem.setOnAction(new EventHandler()
+ {
+ @Override
+ public void handle(ActionEvent event)
+ {
+ clearHistory();
+ }
+ });
+ getItems().add(clearMenuItem);
+ }
+ else
+ {
+ setDisable(true);
+ }
+ }
+
+ /**
+ * Clears the recently used files history and removes all items from the preferences instance
+ */
+ private void clearHistory()
+ {
+ for(int x = 0; x < mSize; x++)
+ {
+ mPreferences.remove(getKey(x));
+ }
+
+ load();
+ }
+
+ /**
+ * Event handler for all recent file child menu items. Extracts the selected file stored as the user data object
+ * in each of the menu items and notifies the listener that the recent file has been selected.
+ *
+ * @param event from a recent file menu item child of this menu.
+ */
+ @Override
+ public void handle(ActionEvent event)
+ {
+ if(mFileSelectionListener != null)
+ {
+ if(event.getSource() instanceof MenuItem)
+ {
+ Object fileObject = ((MenuItem)event.getSource()).getUserData();
+
+ if(fileObject instanceof File)
+ {
+ mFileSelectionListener.fileSelected((File)fileObject);
+ }
+ }
+ }
+ }
+
+ /**
+ * Listener interface for file selection events.
+ */
+ public interface IFileSelectionListener
+ {
+ void fileSelected(File file);
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/ViewerDesktop.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/ViewerDesktop.java
new file mode 100644
index 00000000..bdd01f5d
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/ViewerDesktop.java
@@ -0,0 +1,81 @@
+/*******************************************************************************
+ * 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;
+
+import io.github.dsheirer.instrument.gui.viewer.decoder.DecoderPane;
+import io.github.dsheirer.instrument.gui.viewer.decoder.DecoderPaneFactory;
+import io.github.dsheirer.module.decode.DecoderType;
+import javafx.scene.layout.BorderPane;
+
+import java.io.File;
+
+public class ViewerDesktop extends BorderPane
+{
+ private PlaybackController mPlaybackController;
+ private DecoderPane mDecoderPane = DecoderPaneFactory.getDefaultPane();
+
+ public ViewerDesktop()
+ {
+ setBottom(getPlaybackController());
+ setCenter(getDecoderPane());
+ }
+
+ public void load(File file)
+ {
+ close();
+ getPlaybackController().load(file);
+ }
+
+ public void setDecoder(DecoderType decoderType)
+ {
+ getPlaybackController().removeListener(getDecoderPane());
+ getChildren().remove(getDecoderPane());
+
+ mDecoderPane = DecoderPaneFactory.getDecoderPane(decoderType);
+ setCenter(getDecoderPane());
+ getPlaybackController().addListener(getDecoderPane());
+
+ getPlaybackController().setSampleRateListener(getDecoderPane());
+ }
+
+ /**
+ * Decoder pane
+ */
+ public DecoderPane getDecoderPane()
+ {
+ if(mDecoderPane == null)
+ {
+ mDecoderPane = DecoderPaneFactory.getDefaultPane();
+ }
+
+ return mDecoderPane;
+ }
+
+ public void close()
+ {
+ getPlaybackController().close();
+ }
+
+ private PlaybackController getPlaybackController()
+ {
+ if(mPlaybackController == null)
+ {
+ mPlaybackController = new PlaybackController();
+ }
+
+ return mPlaybackController;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexPhaseLineChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexPhaseLineChart.java
new file mode 100644
index 00000000..68d86efb
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexPhaseLineChart.java
@@ -0,0 +1,121 @@
+/*******************************************************************************
+ * 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.ComplexCircularBuffer;
+import io.github.dsheirer.dsp.gain.ComplexGain;
+import io.github.dsheirer.sample.Listener;
+import io.github.dsheirer.sample.complex.Complex;
+import io.github.dsheirer.sample.complex.ComplexBuffer;
+import javafx.beans.property.IntegerProperty;
+import javafx.beans.property.SimpleIntegerProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+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 ComplexPhaseLineChart extends LineChart implements Listener
+{
+ private final static Logger mLog = LoggerFactory.getLogger(ComplexPhaseLineChart.class);
+
+ private static final Complex ANGLE_OFFSET_45_DEGREES = Complex.fromAngle(Math.PI / 4.0);
+ private ComplexCircularBuffer mComplexCircularBuffer;
+ private ComplexGain mComplexGain = new ComplexGain(600.0f);
+ private ObservableList> mPhaseValues = FXCollections.observableArrayList();
+ private IntegerProperty mLengthProperty = new SimpleIntegerProperty(40);
+
+ public ComplexPhaseLineChart(int length)
+ {
+ super(new NumberAxis("Time", 1, length - 10, 2),
+ new NumberAxis("Phase", -Math.PI, Math.PI, Math.PI / 2.0));
+
+ Series phaseSeries = new Series<>("Phase", mPhaseValues);
+ ObservableList> observableList = FXCollections.observableArrayList(phaseSeries);
+
+ setData(observableList);
+ init(length);
+ }
+
+ private void init(int length)
+ {
+ mLengthProperty.addListener(new ChangeListener()
+ {
+ @Override
+ public void changed(ObservableValue extends Number> observable, Number oldValue, Number newValue)
+ {
+ mComplexCircularBuffer = new ComplexCircularBuffer(newValue.intValue());
+ }
+ });
+ mComplexCircularBuffer = new ComplexCircularBuffer(length);
+ length().setValue(length);
+
+ for(int x = 10; x <= length; x++)
+ {
+ mPhaseValues.add(new Data(x - 10, 0.0f));
+ }
+ }
+
+ /**
+ * Length of the x-axis for displaying samples
+ */
+ public IntegerProperty length()
+ {
+ return mLengthProperty;
+ }
+
+ @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]);
+ Complex copy = sample.copy();
+ mComplexGain.apply(copy);
+ mComplexCircularBuffer.put(copy);
+ }
+
+ updateChart();
+ }
+
+ private void updateChart()
+ {
+ Complex[] samples = mComplexCircularBuffer.getAll();
+
+ for(int x = 10; x < samples.length; x++)
+ {
+ Complex previous = samples[x - 10].copy();
+ Complex sample = samples[x].copy();
+
+ if(previous != null && sample != null)
+ {
+// previous.normalize();
+// sample.normalize();
+ previous.add(ANGLE_OFFSET_45_DEGREES);
+ sample.multiply(previous.conjugate());
+// sample.normalize();
+ mPhaseValues.get(x - 9).setYValue(sample.angle());
+ }
+ }
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexSampleLineChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexSampleLineChart.java
new file mode 100644
index 00000000..d2b25b4b
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexSampleLineChart.java
@@ -0,0 +1,131 @@
+/*******************************************************************************
+ * 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.ComplexCircularBuffer;
+import io.github.dsheirer.dsp.gain.ComplexGain;
+import io.github.dsheirer.sample.Listener;
+import io.github.dsheirer.sample.complex.Complex;
+import io.github.dsheirer.sample.complex.ComplexBuffer;
+import javafx.beans.property.IntegerProperty;
+import javafx.beans.property.SimpleIntegerProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.scene.chart.LineChart;
+import javafx.scene.chart.NumberAxis;
+import javafx.scene.chart.XYChart;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ComplexSampleLineChart extends LineChart implements Listener
+{
+ private final static Logger mLog = LoggerFactory.getLogger(ComplexSampleLineChart.class);
+
+ private ComplexCircularBuffer mComplexCircularBuffer;
+ private ComplexCircularBuffer mDemodulationCircularBuffer;
+ private ComplexGain mComplexGain = new ComplexGain(500.0f);
+ private ObservableList> mISamples = FXCollections.observableArrayList();
+ private ObservableList> mQSamples = FXCollections.observableArrayList();
+ private IntegerProperty mLengthProperty = new SimpleIntegerProperty(40);
+
+ public ComplexSampleLineChart(int length, int samplesPerSymbol)
+ {
+ super(new NumberAxis("Differential-Demodulated Samples", 0, length, 10),
+ new NumberAxis("Value", -1.0, 1.0, 0.25));
+
+ LineChart.Series iSampleSeries = new LineChart.Series<>("Inphase", mISamples);
+ LineChart.Series qSampleSeries = new LineChart.Series<>("Quadrature", mQSamples);
+ ObservableList> observableList =
+ FXCollections.observableArrayList(iSampleSeries, qSampleSeries);
+
+ setData(observableList);
+ init(length, samplesPerSymbol);
+ }
+
+ public void setSamplesPerSymbol(int samplesPerSymbol)
+ {
+ mDemodulationCircularBuffer = new ComplexCircularBuffer(samplesPerSymbol);
+ }
+
+ private void init(int length, int samplesPerSymbol)
+ {
+ mLengthProperty.addListener(new ChangeListener()
+ {
+ @Override
+ public void changed(ObservableValue extends Number> observable, Number oldValue, Number newValue)
+ {
+ mComplexCircularBuffer = new ComplexCircularBuffer(newValue.intValue());
+ }
+ });
+ mComplexCircularBuffer = new ComplexCircularBuffer(length);
+ mDemodulationCircularBuffer = new ComplexCircularBuffer(samplesPerSymbol);
+ length().setValue(length);
+
+ for(int x = 1; x <= length; x++)
+ {
+ mISamples.add(new Data(x, 0.0f));
+ mQSamples.add(new Data(x, 0.0f));
+ }
+ }
+
+ /**
+ * Length of the x-axis for displaying samples
+ */
+ public IntegerProperty length()
+ {
+ return mLengthProperty;
+ }
+
+ @Override
+ public void receive(ComplexBuffer complexBuffer)
+ {
+ float[] samples = complexBuffer.getSamples();
+
+ Complex sample = new Complex(0,0);
+
+ for(int x = 0; x < samples.length; x += 2)
+ {
+ sample.setValues(samples[x], samples[x + 1]);
+// mComplexGain.apply(sample);
+ sample.normalize();
+
+ Complex previous = mDemodulationCircularBuffer.get(sample.copy());
+ sample.multiply(previous.conjugate());
+ mComplexCircularBuffer.put(sample.copy());
+ }
+
+ updateChart();
+ }
+
+ private void updateChart()
+ {
+ Complex[] samples = mComplexCircularBuffer.getAll();
+
+ for(int x = 0; x < samples.length; x++)
+ {
+ Complex sample = samples[x];
+
+ if(sample != null)
+ {
+ sample.normalize();
+ mISamples.get(x).setYValue(sample.inphase());
+ mQSamples.get(x).setYValue(sample.quadrature());
+ }
+ }
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/DoubleLineChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/DoubleLineChart.java
new file mode 100644
index 00000000..54811999
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/DoubleLineChart.java
@@ -0,0 +1,72 @@
+/*******************************************************************************
+ * 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 DoubleLineChart extends LineChart implements Listener
+{
+ private final static Logger mLog = LoggerFactory.getLogger(DoubleLineChart.class);
+
+ private DoubleCircularBuffer mDoubleCircularBuffer;
+ private ObservableList> mPhaseValues = FXCollections.observableArrayList();
+
+ public DoubleLineChart(String seriesLabel, double min, double max, double tick, int length)
+ {
+ super(new NumberAxis(1, length, 2), new NumberAxis(min, max, tick));
+
+ Series phaseSeries = new Series<>(seriesLabel, 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/EyeDiagramChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/EyeDiagramChart.java
new file mode 100644
index 00000000..5764cc8e
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/EyeDiagramChart.java
@@ -0,0 +1,121 @@
+/*******************************************************************************
+ * 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.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 (5 = center)", 1.0, 10.0, 1.0),
+ new NumberAxis("Value", -1.25, 1.25, 0.25));
+
+ mSeriesCount = seriesCount * 2;
+ 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)
+ {
+ Complex[] demodulated = symbolDecisionData.getDemodulated();
+
+ int length = demodulated.length;
+
+ checkChartLength(length);
+
+
+ ObservableList> iSeries = getSeries(mSeriesPointer++, length + 1);
+
+ if(mSeriesPointer >= mSeriesCount)
+ {
+ mSeriesPointer = 0;
+ }
+
+ ObservableList> qSeries = getSeries(mSeriesPointer++, length + 1);
+
+ if(mSeriesPointer >= mSeriesCount)
+ {
+ mSeriesPointer = 0;
+ }
+
+ for(int x = 0; x < length; x++)
+ {
+ demodulated[x].normalize();
+ Data iDataPoint = iSeries.get(x);
+ iDataPoint.setXValue((double)x + 1 - symbolDecisionData.getSamplingPoint());
+ double iValue = demodulated[x].inphase();
+ iDataPoint.setYValue(iValue);
+
+ Data qDataPoint = qSeries.get(x);
+ qDataPoint.setXValue((double)x + 1 - symbolDecisionData.getSamplingPoint());
+ double qValue = demodulated[x].quadrature();
+ qDataPoint.setYValue(qValue);
+ }
+ }
+}
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/PhaseLineChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/PhaseLineChart.java
new file mode 100644
index 00000000..df7ff76a
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/PhaseLineChart.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 PhaseLineChart extends LineChart implements Listener
+{
+ private final static Logger mLog = LoggerFactory.getLogger(PhaseLineChart.class);
+
+ private DoubleCircularBuffer mDoubleCircularBuffer;
+ private ObservableList> mPhaseValues = FXCollections.observableArrayList();
+
+ public PhaseLineChart(int length)
+ {
+ super(new NumberAxis("Time", 1, length, 2),
+ new NumberAxis("Phase", -0.8, 0.8, 0.1));
+
+ Series phaseSeries = new Series<>("PLL Phase Error", 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/SymbolChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/SymbolChart.java
new file mode 100644
index 00000000..6f99f042
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/SymbolChart.java
@@ -0,0 +1,90 @@
+/*******************************************************************************
+ * 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.sample.Listener;
+import io.github.dsheirer.sample.complex.Complex;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.scene.chart.NumberAxis;
+import javafx.scene.chart.ScatterChart;
+import javafx.scene.chart.XYChart;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+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 int mDataPointer;
+ private int mHistory;
+
+ /**
+ * Symbol chart for plotting a series of constellation symbols over time. Maintains three history sets for
+ * current, previous, and oldest symbols.
+ *
+ * @param history size for each of the history sets
+ */
+ public SymbolChart(int history)
+ {
+ 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));
+ }
+
+ ObservableList data = FXCollections.observableArrayList();
+ data.add(new ScatterChart.Series("Constellation", mCurrentPoints));
+// data.add(new ScatterChart.Series("Previous", mPreviousPoints));
+// data.add(new ScatterChart.Series("Oldest", mOldestPoints));
+
+ this.setData(data);
+ }
+
+ @Override
+ public void receive(Complex complex)
+ {
+ //Transfer previous to oldest
+// 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());
+
+ current.setXValue(complex.inphase());
+ current.setYValue(complex.quadrature());
+
+ mDataPointer++;
+
+ if(mDataPointer >= mHistory)
+ {
+ mDataPointer = 0;
+ }
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPane.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPane.java
new file mode 100644
index 00000000..70c39c2e
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPane.java
@@ -0,0 +1,62 @@
+/*******************************************************************************
+ * 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.decoder;
+
+import io.github.dsheirer.module.decode.DecoderType;
+import io.github.dsheirer.sample.Broadcaster;
+import io.github.dsheirer.sample.Listener;
+import io.github.dsheirer.sample.complex.ComplexBuffer;
+import javafx.scene.layout.VBox;
+import javafx.scene.text.Text;
+
+public class DecoderPane extends VBox implements Listener
+{
+ private Broadcaster mComplexBufferBroadcaster = new Broadcaster<>();
+ private DecoderType mDecoderType;
+
+ public DecoderPane(DecoderType decoderType)
+ {
+ mDecoderType = decoderType;
+ }
+
+ /**
+ * Package private - used by factory class to create an empty pane
+ */
+ DecoderPane()
+ {
+ getChildren().add(new Text("Please select a decoder from the menu"));
+ }
+
+ @Override
+ public void receive(ComplexBuffer complexBuffer)
+ {
+ mComplexBufferBroadcaster.broadcast(complexBuffer);
+ }
+
+ public void addListener(Listener listener)
+ {
+ mComplexBufferBroadcaster.addListener(listener);
+ }
+
+ public void removeListener(Listener listener)
+ {
+ mComplexBufferBroadcaster.removeListener(listener);
+ }
+
+ public void setSampleRate(double sampleRate)
+ {
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPaneFactory.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPaneFactory.java
new file mode 100644
index 00000000..b069204e
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPaneFactory.java
@@ -0,0 +1,55 @@
+/*******************************************************************************
+ * 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.decoder;
+
+import io.github.dsheirer.module.decode.DecoderType;
+
+import java.util.EnumSet;
+
+public class DecoderPaneFactory
+{
+ private static final EnumSet SUPPORTED_DECODER_TYPES = EnumSet.of(DecoderType.P25_PHASE1);
+
+ /**
+ * Creates a decoder pane for the decoder type
+ */
+ public static DecoderPane getDecoderPane(DecoderType decoderType)
+ {
+ switch(decoderType)
+ {
+ case P25_PHASE1:
+ return new P25Phase1Pane();
+ }
+
+ return getDefaultPane();
+ }
+
+ /**
+ * Creates an empty decoder pane
+ */
+ public static DecoderPane getDefaultPane()
+ {
+ return new DecoderPane();
+ }
+
+ /**
+ * Indicates if the decoder type is supported by this factory
+ */
+ public static boolean isSupported(DecoderType decoderType)
+ {
+ return SUPPORTED_DECODER_TYPES.contains(decoderType);
+ }
+}
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
new file mode 100644
index 00000000..526d3e7f
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1Pane.java
@@ -0,0 +1,195 @@
+/*******************************************************************************
+ * 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.decoder;
+
+import io.github.dsheirer.instrument.gui.viewer.chart.ComplexSampleLineChart;
+import io.github.dsheirer.instrument.gui.viewer.chart.DoubleLineChart;
+import io.github.dsheirer.instrument.gui.viewer.chart.EyeDiagramChart;
+import io.github.dsheirer.instrument.gui.viewer.chart.PhaseLineChart;
+import io.github.dsheirer.instrument.gui.viewer.chart.SymbolChart;
+import io.github.dsheirer.message.Message;
+import io.github.dsheirer.module.decode.DecoderType;
+import io.github.dsheirer.module.decode.p25.P25_C4FMDecoderInstrumented;
+import io.github.dsheirer.sample.Broadcaster;
+import io.github.dsheirer.sample.Listener;
+import io.github.dsheirer.sample.complex.ComplexBuffer;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class P25Phase1Pane extends DecoderPane
+{
+ private final static Logger mLog = LoggerFactory.getLogger(P25Phase1Pane.class);
+
+ private HBox mSampleChartBox;
+ private ComplexSampleLineChart mSampleLineChart;
+ private EyeDiagramChart mEyeDiagramChart;
+ private HBox mDecoderChartBox;
+ private SymbolChart mSymbolChart;
+ private PhaseLineChart mPLLPhaseErrorLineChart;
+ private DoubleLineChart mPLLFrequencyLineChart;
+ private DoubleLineChart mSamplesPerSymbolLineChart;
+ private Broadcaster mFilteredBufferBroadcaster = new Broadcaster<>();
+ private P25_C4FMDecoderInstrumented mDecoder = new P25_C4FMDecoderInstrumented(null);
+
+ public P25Phase1Pane()
+ {
+ super(DecoderType.P25_PHASE1);
+ init();
+ }
+
+ private void init()
+ {
+ addListener(getDecoder());
+
+ getDecoder().setFilteredBufferListener(mFilteredBufferBroadcaster);
+ getDecoder().setSymbolListener(getSymbolChart());
+ getDecoder().setPLLPhaseErrorListener(getPLLPhaseErrorLineChart());
+ getDecoder().setPLLFrequencyListener(getPLLFrequencyLineChart());
+ getDecoder().setSymbolDecisionDataListener(getEyeDiagramChart());
+ getDecoder().setSamplesPerSymbolListener(getSamplesPerSymbolLineChart());
+ mFilteredBufferBroadcaster.addListener(getSampleLineChart());
+
+ HBox.setHgrow(getSampleChartBox(), Priority.ALWAYS);
+ HBox.setHgrow(getDecoderChartBox(), Priority.ALWAYS);
+ getChildren().addAll(getSampleChartBox(), getDecoderChartBox());
+
+ mDecoder.setMessageListener(new Listener()
+ {
+ @Override
+ public void receive(Message message)
+ {
+ mLog.debug(message.getMessage());
+ }
+ });
+
+ }
+
+ @Override
+ public void setSampleRate(double sampleRate)
+ {
+ mLog.debug("Configuring for sample rate: " + sampleRate);
+
+ mDecoder.setSampleRate(sampleRate);
+ double samplesPerSymbol = sampleRate / 4800.0;
+
+ getSampleLineChart().setSamplesPerSymbol((int)samplesPerSymbol);
+ }
+
+ private P25_C4FMDecoderInstrumented getDecoder()
+ {
+ return mDecoder;
+ }
+
+ private SymbolChart getSymbolChart()
+ {
+ if(mSymbolChart == null)
+ {
+ mSymbolChart = new SymbolChart(10);
+ }
+
+ return mSymbolChart;
+ }
+
+ private HBox getDecoderChartBox()
+ {
+ if(mDecoderChartBox == null)
+ {
+ mDecoderChartBox = new HBox();
+ mDecoderChartBox.setMaxHeight(Double.MAX_VALUE);
+ 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);
+ HBox.setHgrow(getPLLFrequencyLineChart(), Priority.ALWAYS);
+ mDecoderChartBox.getChildren().addAll(getSymbolChart(), getPLLPhaseErrorLineChart(),
+ getPLLFrequencyLineChart());
+ }
+
+ return mDecoderChartBox;
+ }
+
+ private HBox getSampleChartBox()
+ {
+ if(mSampleChartBox == null)
+ {
+ mSampleChartBox = new HBox();
+ mSampleChartBox.setMaxHeight(Double.MAX_VALUE);
+ getSampleLineChart().setMaxWidth(Double.MAX_VALUE);
+ getEyeDiagramChart().setMaxWidth(Double.MAX_VALUE);
+ getSamplesPerSymbolLineChart().setMaxWidth(Double.MAX_VALUE);
+ HBox.setHgrow(getSampleLineChart(), Priority.ALWAYS);
+ HBox.setHgrow(getEyeDiagramChart(), Priority.ALWAYS);
+ HBox.setHgrow(getSamplesPerSymbolLineChart(), Priority.ALWAYS);
+ mSampleChartBox.getChildren().addAll(getSampleLineChart(), getEyeDiagramChart(),
+ getSamplesPerSymbolLineChart());
+ }
+
+ return mSampleChartBox;
+ }
+
+ private ComplexSampleLineChart getSampleLineChart()
+ {
+ if(mSampleLineChart == null)
+ {
+ mSampleLineChart = new ComplexSampleLineChart(100, 10);
+ }
+
+ return mSampleLineChart;
+ }
+
+ private EyeDiagramChart getEyeDiagramChart()
+ {
+ if(mEyeDiagramChart == null)
+ {
+ mEyeDiagramChart = new EyeDiagramChart(10);
+ }
+
+ return mEyeDiagramChart;
+ }
+
+ private PhaseLineChart getPLLPhaseErrorLineChart()
+ {
+ if(mPLLPhaseErrorLineChart == null)
+ {
+ mPLLPhaseErrorLineChart = new PhaseLineChart(40);
+ }
+
+ return mPLLPhaseErrorLineChart;
+ }
+
+ private DoubleLineChart getPLLFrequencyLineChart()
+ {
+ if(mPLLFrequencyLineChart == null)
+ {
+ mPLLFrequencyLineChart = new DoubleLineChart( "PLL Frequency", -500, 500, 50, 40);
+ }
+
+ return mPLLFrequencyLineChart;
+ }
+
+ private DoubleLineChart getSamplesPerSymbolLineChart()
+ {
+ if(mSamplesPerSymbolLineChart == null)
+ {
+ mSamplesPerSymbolLineChart = new DoubleLineChart( "Sample Point", 9.5, 11.5, 0.1, 40);
+ }
+
+ return mSamplesPerSymbolLineChart;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/module/decode/Decoder.java b/src/main/java/io/github/dsheirer/module/decode/Decoder.java
index 9efeb0e7..f8f4953b 100644
--- a/src/main/java/io/github/dsheirer/module/decode/Decoder.java
+++ b/src/main/java/io/github/dsheirer/module/decode/Decoder.java
@@ -1,17 +1,17 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014-2016 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
******************************************************************************/
@@ -22,39 +22,39 @@ import io.github.dsheirer.message.Message;
import io.github.dsheirer.module.Module;
import io.github.dsheirer.sample.Listener;
-public abstract class Decoder extends Module
- implements IMessageProvider, Listener
+public abstract class Decoder extends Module implements IMessageProvider
{
- /* This has to be a broadcaster in order for references to persist */
- protected Listener mMessageListener;
+ /* This has to be a broadcaster in order for references to persist */
+ private Listener mMessageDistributor = new MessageDistributor();
+ protected Listener mMessageListener;
- /**
- * Decoder - parent class for all decoders, demodulators and components.
- */
- public Decoder()
- {
- }
+ /**
+ * Decoder - parent class for all decoders, demodulators and components.
+ */
+ public Decoder()
+ {
+ }
- @Override
- public void dispose()
- {
- mMessageListener = null;
- }
+ @Override
+ public void dispose()
+ {
+ mMessageListener = null;
+ }
- /**
- * Identifies the decoder type (ie protocol)
- */
- public abstract DecoderType getDecoderType();
+ /**
+ * Identifies the decoder type (ie protocol)
+ */
+ public abstract DecoderType getDecoderType();
/**
* Adds a listener for receiving decoded messages from this decoder
*/
@Override
- public void setMessageListener( Listener listener )
+ public void setMessageListener(Listener listener)
{
- mMessageListener = listener;
- }
+ mMessageListener = listener;
+ }
/**
* Removes the listener from receiving decoded messages from all attached
@@ -63,23 +63,29 @@ public abstract class Decoder extends Module
@Override
public void removeMessageListener()
{
- mMessageListener = null;
+ mMessageListener = null;
}
/**
- * Broadcasts the message to the registered message listener
+ * Message listener for receiving the output from this decoder
*/
- public void broadcast( Message message )
+ protected Listener getMessageListener()
{
- if( mMessageListener != null )
- {
- mMessageListener.receive( message );
- }
+ return mMessageDistributor;
}
- @Override
- public void receive( Message message )
- {
- broadcast( message );
- }
+ /**
+ * Distributes/forwards messages from sub-class decoder implementations to the registered message listener.
+ */
+ public class MessageDistributor implements Listener
+ {
+ @Override
+ public void receive(Message message)
+ {
+ if(mMessageListener != null)
+ {
+ mMessageListener.receive(message);
+ }
+ }
+ }
}
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 29dbcf5b..94ec4ae7 100644
--- a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java
+++ b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java
@@ -246,9 +246,7 @@ public class DecoderFactory
switch(modulation)
{
case C4FM:
- 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 P25_C4FMDecoder(aliasList));
modules.add(new P25DecoderState(aliasList, channelType, P25Decoder.Modulation.C4FM,
p25Config.getIgnoreDataCalls()));
break;
diff --git a/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2Decoder.java b/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2Decoder.java
index 072d28f4..629d5f5f 100644
--- a/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2Decoder.java
+++ b/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2Decoder.java
@@ -90,7 +90,7 @@ public class Fleetsync2Decoder extends Decoder implements IFilteredRealBufferLis
mMessageProcessor = new Fleetsync2MessageProcessor( aliasList );
mMessageFramer.addMessageListener( mMessageProcessor );
- mMessageProcessor.setMessageListener( this );
+ mMessageProcessor.setMessageListener( getMessageListener() );
}
public void dispose()
diff --git a/src/main/java/io/github/dsheirer/module/decode/lj1200/LJ1200Decoder.java b/src/main/java/io/github/dsheirer/module/decode/lj1200/LJ1200Decoder.java
index fb19a8d2..a1ed1899 100644
--- a/src/main/java/io/github/dsheirer/module/decode/lj1200/LJ1200Decoder.java
+++ b/src/main/java/io/github/dsheirer/module/decode/lj1200/LJ1200Decoder.java
@@ -100,7 +100,7 @@ public class LJ1200Decoder extends Decoder implements IFilteredRealBufferListene
mMessageProcessor = new LJ1200MessageProcessor( aliasList );
mTowerMessageFramer.addMessageListener( mMessageProcessor );
mTransponderMessageFramer.addMessageListener( mMessageProcessor );
- mMessageProcessor.setMessageListener( this );
+ mMessageProcessor.setMessageListener( getMessageListener() );
}
@Override
diff --git a/src/main/java/io/github/dsheirer/module/decode/ltrnet/LTRNetDecoder.java b/src/main/java/io/github/dsheirer/module/decode/ltrnet/LTRNetDecoder.java
index 08041b72..f8e234b9 100644
--- a/src/main/java/io/github/dsheirer/module/decode/ltrnet/LTRNetDecoder.java
+++ b/src/main/java/io/github/dsheirer/module/decode/ltrnet/LTRNetDecoder.java
@@ -74,7 +74,7 @@ public class LTRNetDecoder extends Decoder
mLTRMessageFramer.addMessageListener( mLTRMessageProcessor );
- mLTRMessageProcessor.setMessageListener( this );
+ mLTRMessageProcessor.setMessageListener( getMessageListener() );
}
@Override
diff --git a/src/main/java/io/github/dsheirer/module/decode/ltrstandard/LTRStandardDecoder.java b/src/main/java/io/github/dsheirer/module/decode/ltrstandard/LTRStandardDecoder.java
index 0a96486b..6836aabf 100644
--- a/src/main/java/io/github/dsheirer/module/decode/ltrstandard/LTRStandardDecoder.java
+++ b/src/main/java/io/github/dsheirer/module/decode/ltrstandard/LTRStandardDecoder.java
@@ -64,7 +64,7 @@ public class LTRStandardDecoder extends Decoder implements IUnFilteredRealBuffer
mLTRMessageProcessor = new LTRStandardMessageProcessor( direction, aliasList );
mLTRMessageFramer.addMessageListener( mLTRMessageProcessor );
- mLTRMessageProcessor.setMessageListener( this );
+ mLTRMessageProcessor.setMessageListener( getMessageListener() );
}
@Override
diff --git a/src/main/java/io/github/dsheirer/module/decode/mdc1200/MDCDecoder.java b/src/main/java/io/github/dsheirer/module/decode/mdc1200/MDCDecoder.java
index beaf51da..85cb9657 100644
--- a/src/main/java/io/github/dsheirer/module/decode/mdc1200/MDCDecoder.java
+++ b/src/main/java/io/github/dsheirer/module/decode/mdc1200/MDCDecoder.java
@@ -102,7 +102,7 @@ public class MDCDecoder extends Decoder implements IFilteredRealBufferListener,
/* Message Processor */
mMessageProcessor = new MDCMessageProcessor( aliasList );
mMessageFramer.addMessageListener( mMessageProcessor );
- mMessageProcessor.addMessageListener( this );
+ mMessageProcessor.addMessageListener(getMessageListener());
}
public void dispose()
diff --git a/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327Decoder.java b/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327Decoder.java
index d78d3670..ecd67c24 100644
--- a/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327Decoder.java
+++ b/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327Decoder.java
@@ -148,7 +148,7 @@ public class MPT1327Decoder extends Decoder implements IFilteredRealBufferListen
/* Fully decoded and framed messages processor */
mMessageProcessor = new MPT1327MessageProcessor(aliasList);
- mMessageProcessor.setMessageListener(this);
+ mMessageProcessor.setMessageListener(getMessageListener());
mControlMessageFramer.addMessageListener(mMessageProcessor);
mTrafficMessageFramer.addMessageListener(mMessageProcessor);
}
diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25Decoder.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25Decoder.java
index 38e3fdfc..7851230d 100644
--- a/src/main/java/io/github/dsheirer/module/decode/p25/P25Decoder.java
+++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25Decoder.java
@@ -1,70 +1,91 @@
+/*******************************************************************************
+ * 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.module.decode.p25;
import io.github.dsheirer.alias.AliasList;
-import io.github.dsheirer.instrument.Instrumentable;
import io.github.dsheirer.module.decode.Decoder;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeListener;
import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeProvider;
-public abstract class P25Decoder extends Decoder implements IFrequencyChangeListener, IFrequencyChangeProvider, Instrumentable
+public abstract class P25Decoder extends Decoder implements IFrequencyChangeListener, IFrequencyChangeProvider
{
- private P25MessageProcessor mMessageProcessor;
-
- public P25Decoder( AliasList aliasList )
- {
- mMessageProcessor = new P25MessageProcessor( aliasList );
- mMessageProcessor.setMessageListener( this );
- }
-
- public void dispose()
- {
- super.dispose();
-
- mMessageProcessor.dispose();
- mMessageProcessor = null;
- }
+ private P25MessageProcessor mMessageProcessor;
+ private AliasList mAliasList;
- @Override
- public DecoderType getDecoderType()
+ public P25Decoder(AliasList aliasList)
{
- return DecoderType.P25_PHASE1;
+ mAliasList = aliasList;
+ mMessageProcessor = new P25MessageProcessor(mAliasList);
+ mMessageProcessor.setMessageListener(getMessageListener());
}
- protected P25MessageProcessor getMessageProcessor()
- {
- return mMessageProcessor;
- }
-
- public abstract Modulation getModulation();
-
- public enum Modulation
- {
- CQPSK( "Simulcast (LSM)", "LSM" ),
- C4FM( "Normal (C4FM)", "C4FM" );
-
- private String mLabel;
- private String mShortLabel;
-
- private Modulation( String label, String shortLabel )
- {
- mLabel = label;
- mShortLabel = shortLabel;
- }
-
- public String getLabel()
- {
- return mLabel;
- }
-
- public String getShortLabel()
- {
- return mShortLabel;
- }
-
- public String toString()
- {
- return getLabel();
- }
- }
+ protected AliasList getAliasList()
+ {
+ return mAliasList;
+ }
+
+ public void dispose()
+ {
+ super.dispose();
+
+ mMessageProcessor.dispose();
+ mMessageProcessor = null;
+ }
+
+ @Override
+ public DecoderType getDecoderType()
+ {
+ return DecoderType.P25_PHASE1;
+ }
+
+ protected P25MessageProcessor getMessageProcessor()
+ {
+ return mMessageProcessor;
+ }
+
+ public abstract Modulation getModulation();
+
+ public enum Modulation
+ {
+ CQPSK("Simulcast (LSM)", "LSM"),
+ C4FM("Normal (C4FM)", "C4FM");
+
+ private String mLabel;
+ private String mShortLabel;
+
+ private Modulation(String label, String shortLabel)
+ {
+ mLabel = label;
+ mShortLabel = shortLabel;
+ }
+
+ public String getLabel()
+ {
+ return mLabel;
+ }
+
+ public String getShortLabel()
+ {
+ return mShortLabel;
+ }
+
+ public String toString()
+ {
+ return getLabel();
+ }
+ }
}
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 8a0d4397..54047262 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
@@ -7,7 +7,7 @@ import io.github.dsheirer.bits.ISyncDetectListener;
import io.github.dsheirer.bits.MultiSyncPatternMatcher;
import io.github.dsheirer.bits.SoftSyncDetector;
import io.github.dsheirer.bits.SyncDetector;
-import io.github.dsheirer.dsp.psk.LSMDemodulator;
+import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.dsp.symbol.FrameSync;
import io.github.dsheirer.edac.BCH_63_16_11;
@@ -39,619 +39,619 @@ import java.util.ArrayList;
public class P25MessageFramer implements Listener
{
- private final static Logger mLog =
- LoggerFactory.getLogger( P25MessageFramer.class );
+ private final static Logger mLog =
+ LoggerFactory.getLogger(P25MessageFramer.class);
- /* Determines the threshold for sync pattern soft matching */
- private static final int SYNC_MATCH_THRESHOLD = 2;
- private static final int SYNC_IN_CALL_THRESHOLD = 4;
+ /* Determines the threshold for sync pattern soft matching */
+ private static final int SYNC_MATCH_THRESHOLD = 2;
+ private static final int SYNC_IN_CALL_THRESHOLD = 4;
- /* Costas Loop phase lock error correction values. A phase lock error of
- * 90 degrees requires a correction of 1/4 of the symbol rate (1200Hz). An
- * error of 180 degrees requires a correction of 1/2 of the symbol rate */
- public static final double SYMBOL_FREQUENCY = 2.0d * Math.PI * 4800.0d / 48000.0d;
- public static final double PHASE_CORRECTION_90_DEGREES = SYMBOL_FREQUENCY / 4.0d;
- public static final double PHASE_CORRECTION_180_DEGREES = SYMBOL_FREQUENCY / 2.0d;
- public static final int TSBK_BEGIN = 64;
- public static final int TSBK_CRC_START = 144;
- public static final int TSBK_END = 260;
- public static final int TSBK_DECODED_END = 160;
+ /* Costas Loop phase lock error correction values. A phase lock error of
+ * 90 degrees requires a correction of 1/4 of the symbol rate (1200Hz). An
+ * error of 180 degrees requires a correction of 1/2 of the symbol rate */
+ public static final double SYMBOL_FREQUENCY = 2.0d * Math.PI * 4800.0d / 48000.0d;
+ public static final double PHASE_CORRECTION_90_DEGREES = SYMBOL_FREQUENCY / 4.0d;
+ public static final double PHASE_CORRECTION_180_DEGREES = SYMBOL_FREQUENCY / 2.0d;
+ public static final int TSBK_BEGIN = 64;
+ public static final int TSBK_CRC_START = 144;
+ public static final int TSBK_END = 260;
+ public static final int TSBK_DECODED_END = 160;
- public static final int PDU0_BEGIN = 64;
- public static final int PDU0_CRC_BEGIN = 144;
- public static final int PDU0_END = 260;
- public static final int PDU1_BEGIN = 160;
- public static final int PDU1_END = 356;
- public static final int PDU2_BEGIN = 256;
- public static final int PDU2_END = 452;
- public static final int PDU3_BEGIN = 352;
- public static final int PDU3_END = 548;
- public static final int PDU3_DECODED_END = 448;
-
- private SoftSyncDetector mPrimarySyncDetector = new SoftSyncDetector(
- FrameSync.P25_PHASE1_NORMAL.getSync(), SYNC_MATCH_THRESHOLD );
+ public static final int PDU0_BEGIN = 64;
+ public static final int PDU0_CRC_BEGIN = 144;
+ public static final int PDU0_END = 260;
+ public static final int PDU1_BEGIN = 160;
+ public static final int PDU1_END = 356;
+ public static final int PDU2_BEGIN = 256;
+ public static final int PDU2_END = 452;
+ public static final int PDU3_BEGIN = 352;
+ public static final int PDU3_END = 548;
+ public static final int PDU3_DECODED_END = 448;
- private MultiSyncPatternMatcher mMatcher = new MultiSyncPatternMatcher( 48 );
- private ArrayList mAssemblers =
- new ArrayList();
+ private SoftSyncDetector mPrimarySyncDetector = new SoftSyncDetector(
+ FrameSync.P25_PHASE1_NORMAL.getSync(), SYNC_MATCH_THRESHOLD);
- private Listener mListener;
- private AliasList mAliasList;
+ private MultiSyncPatternMatcher mMatcher = new MultiSyncPatternMatcher(48);
+ private ArrayList mAssemblers =
+ new ArrayList();
- private Trellis_1_2_Rate mHalfRate = new Trellis_1_2_Rate();
- private Trellis_3_4_Rate mThreeQuarterRate = new Trellis_3_4_Rate();
- private BCH_63_16_11 mNIDDecoder = new BCH_63_16_11();
-
- /**
- * Constructs a P25 message framer to receive a stream of symbols and
- * detect the sync pattern then capture the following stream of symbols up
- * to the message length, and then broadcast that bit buffer to the registered
- * listener.
- */
- public P25MessageFramer( AliasList aliasList )
- {
- mAliasList = aliasList;
-
- mPrimarySyncDetector.setListener( new ISyncDetectListener()
- {
- @Override
- public void syncDetected()
- {
- for( P25MessageAssembler assembler: mAssemblers )
- {
- if( !assembler.isActive() )
- {
- assembler.setActive( true );
- break;
- }
- }
- }
- } );
-
- mMatcher.add( mPrimarySyncDetector );
+ private Listener mListener;
+ private AliasList mAliasList;
- /**
- * We use two message assemblers to catch any sync detections, so that
- * if we have a false trigger then we still have a chance to catch the
- * valid message. If we have two false triggers before the arrival of
- * the actual message sync, then the third or subsequent real or false
- * sync pattern will produce a debug message indicating no assemblers
- * are available..
- */
- mAssemblers.add( new P25MessageAssembler() );
- mAssemblers.add( new P25MessageAssembler() );
- }
-
- public P25MessageFramer( AliasList aliasList, LSMDemodulator demodulator )
- {
- this( aliasList );
+ private Trellis_1_2_Rate mHalfRate = new Trellis_1_2_Rate();
+ private Trellis_3_4_Rate mThreeQuarterRate = new Trellis_3_4_Rate();
+ private BCH_63_16_11 mNIDDecoder = new BCH_63_16_11();
- if( demodulator != null )
- {
- /* For CQPSK, we include 3 additional sync detectors to watch for and
- * correct +/-90 and 180 degree costas loop phase lock errors */
- mMatcher.add( new CostasPhaseErrorDetector( FrameSync.P25_PHASE1_ERROR_90_CCW,
- demodulator, PHASE_CORRECTION_90_DEGREES ) );
-
- mMatcher.add( new CostasPhaseErrorDetector( FrameSync.P25_PHASE1_ERROR_90_CW,
- demodulator, -PHASE_CORRECTION_90_DEGREES ) );
-
- mMatcher.add( new CostasPhaseErrorDetector( FrameSync.P25_PHASE1_ERROR_180,
- demodulator, PHASE_CORRECTION_180_DEGREES ) );
- }
- }
-
- public void dispose()
- {
- for( P25MessageAssembler a: mAssemblers )
- {
- a.dispose();
- }
-
- mAssemblers.clear();
-
- mListener = null;
- mAliasList = null;
-
- mMatcher.dispose();
- mMatcher = null;
-
- mPrimarySyncDetector.dispose();
- mPrimarySyncDetector = null;
-
- }
-
- private void dispatch( Message message )
- {
- if( mListener != null )
- {
- mListener.receive( message );
- }
- }
-
- @Override
- public void receive( Dibit symbol )
+ /**
+ * Constructs a P25 message framer to receive a stream of symbols and
+ * detect the sync pattern then capture the following stream of symbols up
+ * to the message length, and then broadcast that bit buffer to the registered
+ * listener.
+ */
+ public P25MessageFramer(AliasList aliasList)
{
- for( P25MessageAssembler assembler: mAssemblers )
- {
- if( assembler.isActive() )
- {
- assembler.receive( symbol );
-
- if( assembler.complete() )
- {
- assembler.reset();
- }
- }
- }
-
- mMatcher.receive( symbol.getBit1(), symbol.getBit2() );
+ mAliasList = aliasList;
+
+ mPrimarySyncDetector.setListener(new ISyncDetectListener()
+ {
+ @Override
+ public void syncDetected()
+ {
+ for(P25MessageAssembler assembler : mAssemblers)
+ {
+ if(!assembler.isActive())
+ {
+ assembler.setActive(true);
+ break;
+ }
+ }
+ }
+ });
+
+ mMatcher.add(mPrimarySyncDetector);
+
+ /**
+ * We use two message assemblers to catch any sync detections, so that
+ * if we have a false trigger then we still have a chance to catch the
+ * valid message. If we have two false triggers before the arrival of
+ * the actual message sync, then the third or subsequent real or false
+ * sync pattern will produce a debug message indicating no assemblers
+ * are available..
+ */
+ mAssemblers.add(new P25MessageAssembler());
+ mAssemblers.add(new P25MessageAssembler());
}
- public void setListener( Listener listener )
+ public P25MessageFramer(AliasList aliasList, IPhaseLockedLoop phaseLockedLoop)
{
- mListener = listener;
+ this(aliasList);
+
+ if(phaseLockedLoop != null)
+ {
+ //Add additional sync pattern detectors to detect when we get 90/180 degree out of phase sync pattern
+ //detections so that we can apply correction to the phase locked loop
+ mMatcher.add(new PLLPhaseInversionDetector(FrameSync.P25_PHASE1_ERROR_90_CCW,
+ phaseLockedLoop, PHASE_CORRECTION_90_DEGREES));
+
+ mMatcher.add(new PLLPhaseInversionDetector(FrameSync.P25_PHASE1_ERROR_90_CW,
+ phaseLockedLoop, -PHASE_CORRECTION_90_DEGREES));
+
+ mMatcher.add(new PLLPhaseInversionDetector(FrameSync.P25_PHASE1_ERROR_180,
+ phaseLockedLoop, PHASE_CORRECTION_180_DEGREES));
+ }
}
- public void removeListener( Listener listener )
+ public void dispose()
{
- mListener = null;
+ for(P25MessageAssembler a : mAssemblers)
+ {
+ a.dispose();
+ }
+
+ mAssemblers.clear();
+
+ mListener = null;
+ mAliasList = null;
+
+ mMatcher.dispose();
+ mMatcher = null;
+
+ mPrimarySyncDetector.dispose();
+ mPrimarySyncDetector = null;
+
}
-
+
+ private void dispatch(Message message)
+ {
+ if(mListener != null)
+ {
+ mListener.receive(message);
+ }
+ }
+
+ @Override
+ public void receive(Dibit symbol)
+ {
+ for(P25MessageAssembler assembler : mAssemblers)
+ {
+ if(assembler.isActive())
+ {
+ assembler.receive(symbol);
+
+ if(assembler.complete())
+ {
+ assembler.reset();
+ }
+ }
+ }
+
+ mMatcher.receive(symbol.getBit1(), symbol.getBit2());
+ }
+
+ public void setListener(Listener listener)
+ {
+ mListener = listener;
+ }
+
+ public void removeListener(Listener listener)
+ {
+ mListener = null;
+ }
+
private class P25MessageAssembler
{
- /* Starting position of the status symbol counter is 24 symbols to
- * account for the 48-bit sync pattern which is not included in message */
- private int mStatusSymbolPointer = 24;
- private BinaryMessage mMessage;
+ /* Starting position of the status symbol counter is 24 symbols to
+ * account for the 48-bit sync pattern which is not included in message */
+ private int mStatusSymbolPointer = 24;
+ private BinaryMessage mMessage;
private int mMessageLength;
private boolean mComplete = false;
private boolean mActive = false;
private DataUnitID mDUID = DataUnitID.NID;
-
+
public P25MessageAssembler()
{
- mMessageLength = mDUID.getMessageLength();
- mMessage = new BinaryMessage( mMessageLength );
- reset();
+ mMessageLength = mDUID.getMessageLength();
+ mMessage = new BinaryMessage(mMessageLength);
+ reset();
}
-
- public void receive( Dibit dibit )
+
+ public void receive(Dibit dibit)
{
- if( mActive )
- {
- if( mStatusSymbolPointer == 35 )
- {
- mStatusSymbolPointer = 0;
- }
- else
- {
- mStatusSymbolPointer++;
+ if(mActive)
+ {
+ if(mStatusSymbolPointer == 35)
+ {
+ mStatusSymbolPointer = 0;
+ }
+ else
+ {
+ mStatusSymbolPointer++;
try
{
- mMessage.add( dibit.getBit1() );
- mMessage.add( dibit.getBit2() );
+ mMessage.add(dibit.getBit1());
+ mMessage.add(dibit.getBit2());
}
- catch( BitSetFullException e )
+ catch(BitSetFullException e)
{
- mComplete = true;
+ mComplete = true;
}
-
+
/* Check the message for complete */
- if( mMessage.isFull() )
+ if(mMessage.isFull())
{
- checkComplete();
+ checkComplete();
}
- }
- }
+ }
+ }
}
public void reset()
{
- mDUID = DataUnitID.NID;
- mMessage.setSize( mDUID.getMessageLength() );
- mMessage.clear();
- mStatusSymbolPointer = 24;
+ mDUID = DataUnitID.NID;
+ mMessage.setSize(mDUID.getMessageLength());
+ mMessage.clear();
+ mStatusSymbolPointer = 24;
mComplete = false;
mActive = false;
}
-
- public void setActive( boolean active )
+
+ public void setActive(boolean active)
{
- mActive = active;
+ mActive = active;
}
- private void setDUID( DataUnitID id )
+ private void setDUID(DataUnitID id)
{
- mDUID = id;
- mMessageLength = id.getMessageLength();
- mMessage.setSize( mMessageLength );
+ mDUID = id;
+ mMessageLength = id.getMessageLength();
+ mMessage.setSize(mMessageLength);
}
private void checkComplete()
{
- switch( mDUID )
- {
- case NID:
- mMessage = mNIDDecoder.correctNID( mMessage );
-
- if( mMessage.getCRC() != CRC.FAILED_CRC )
- {
- int value = mMessage.getInt( P25Message.DUID );
-
- DataUnitID duid = DataUnitID.fromValue( value );
-
- if( duid != DataUnitID.UNKN )
- {
- setDUID( duid );
- }
- else
- {
- mComplete = true;
- dispatch( new P25Message( mMessage.copy(), mDUID, mAliasList ) );
- }
- }
- else
- {
- mComplete = true;
- }
- break;
- case HDU:
- mComplete = true;
- dispatch( new HDUMessage( mMessage.copy(), mDUID, mAliasList ) );
-
- /* We're in a call now, lower the sync match threshold */
- mPrimarySyncDetector.setThreshold( SYNC_IN_CALL_THRESHOLD );
- break;
- case LDU1:
- mComplete = true;
-
- LDU1Message ldu1 = new LDU1Message( mMessage.copy(),
- mDUID, mAliasList );
+ switch(mDUID)
+ {
+ case NID:
+ mMessage = mNIDDecoder.correctNID(mMessage);
- /* Convert the LDU1 message into a link control LDU1 message */
- dispatch( LDULCMessageFactory.getMessage( ldu1 ) );
+ if(mMessage.getCRC() != CRC.FAILED_CRC)
+ {
+ int value = mMessage.getInt(P25Message.DUID);
+
+ DataUnitID duid = DataUnitID.fromValue(value);
+
+ if(duid != DataUnitID.UNKN)
+ {
+ setDUID(duid);
+ }
+ else
+ {
+ mComplete = true;
+ dispatch(new P25Message(mMessage.copy(), mDUID, mAliasList));
+ }
+ }
+ else
+ {
+ mComplete = true;
+ }
+ break;
+ case HDU:
+ mComplete = true;
+ dispatch(new HDUMessage(mMessage.copy(), mDUID, mAliasList));
/* We're in a call now, lower the sync match threshold */
- mPrimarySyncDetector.setThreshold( SYNC_IN_CALL_THRESHOLD );
- break;
- case LDU2:
- mComplete = true;
- dispatch( new LDU2Message( mMessage.copy(), mDUID, mAliasList ) );
+ mPrimarySyncDetector.setThreshold(SYNC_IN_CALL_THRESHOLD);
+ break;
+ case LDU1:
+ mComplete = true;
+
+ LDU1Message ldu1 = new LDU1Message(mMessage.copy(),
+ mDUID, mAliasList);
+
+ /* Convert the LDU1 message into a link control LDU1 message */
+ dispatch(LDULCMessageFactory.getMessage(ldu1));
/* We're in a call now, lower the sync match threshold */
- mPrimarySyncDetector.setThreshold( SYNC_IN_CALL_THRESHOLD );
- break;
- case PDU0:
+ mPrimarySyncDetector.setThreshold(SYNC_IN_CALL_THRESHOLD);
+ break;
+ case LDU2:
+ mComplete = true;
+ dispatch(new LDU2Message(mMessage.copy(), mDUID, mAliasList));
- /* Remove interleaving */
- P25Interleave.deinterleaveData( mMessage, PDU0_BEGIN, PDU0_END );
-
- /* Remove trellis encoding - abort processing if we have an
- * unsuccessful decode due to excessive errors */
- if( mHalfRate.decode( mMessage, PDU0_BEGIN, PDU0_END ) )
- {
- mMessage = CRCP25.correctCCITT80( mMessage,
- PDU0_BEGIN, PDU0_CRC_BEGIN );
-
+ /* We're in a call now, lower the sync match threshold */
+ mPrimarySyncDetector.setThreshold(SYNC_IN_CALL_THRESHOLD);
+ break;
+ case PDU0:
+
+ /* Remove interleaving */
+ P25Interleave.deinterleaveData(mMessage, PDU0_BEGIN, PDU0_END);
+
+ /* Remove trellis encoding - abort processing if we have an
+ * unsuccessful decode due to excessive errors */
+ if(mHalfRate.decode(mMessage, PDU0_BEGIN, PDU0_END))
+ {
+ mMessage = CRCP25.correctCCITT80(mMessage,
+ PDU0_BEGIN, PDU0_CRC_BEGIN);
+
+
+ if(mMessage.getCRC() != CRC.FAILED_CRC)
+ {
+ boolean confirmed = mMessage.get(
+ PDUMessage.CONFIRMATION_REQUIRED_INDICATOR);
+
+ if(confirmed)
+ {
+ setDUID(DataUnitID.PDUC);
+ }
+ else
+ {
+ setDUID(DataUnitID.PDU1);
+ }
+
+ mMessage.setPointer(PDU1_BEGIN);
+ }
+ else
+ {
+ mComplete = true;
+ }
+ }
+ else
+ {
+ mComplete = true;
+ }
- if( mMessage.getCRC() != CRC.FAILED_CRC )
- {
- boolean confirmed = mMessage.get(
- PDUMessage.CONFIRMATION_REQUIRED_INDICATOR );
-
- if( confirmed )
- {
- setDUID( DataUnitID.PDUC );
- }
- else
- {
- setDUID( DataUnitID.PDU1 );
- }
-
- mMessage.setPointer( PDU1_BEGIN );
- }
- else
- {
- mComplete = true;
- }
- }
- else
- {
- mComplete = true;
- }
-
/* Set sync match threshold to normal */
- mPrimarySyncDetector.setThreshold( SYNC_MATCH_THRESHOLD );
-
- break;
- case PDU1:
- /* Remove interleaving */
- P25Interleave.deinterleaveData( mMessage, PDU1_BEGIN, PDU1_END );
-
- /* Remove trellis encoding - abort processing if we have an
- * unsuccessful decode due to excessive errors */
- if( mHalfRate.decode( mMessage, PDU1_BEGIN, PDU1_END ) )
- {
- if( mMessage.getInt( PDUMessage.BLOCKS_TO_FOLLOW ) == 1 )
- {
- mMessage.setSize( PDU2_BEGIN );
-
- PDUMessage pduMessage1 = PDUMessageFactory.getMessage(
- mMessage.copy(), DataUnitID.PDU1, mAliasList );
+ mPrimarySyncDetector.setThreshold(SYNC_MATCH_THRESHOLD);
- dispatch( pduMessage1 );
-
- mComplete = true;
- }
- else
- {
- setDUID( DataUnitID.PDU2 );
-
- mMessage.setPointer( PDU2_BEGIN );
- }
- }
- else
- {
- mComplete = true;
- }
- break;
- case PDU2:
- /* Remove interleaving */
- P25Interleave.deinterleaveData( mMessage, PDU2_BEGIN, PDU2_END );
-
- /* Remove trellis encoding - abort processing if we have an
- * unsuccessful decode due to excessive errors */
- if( mHalfRate.decode( mMessage, PDU2_BEGIN, PDU2_END ) )
- {
- if( mMessage.getInt( PDUMessage.BLOCKS_TO_FOLLOW ) == 2 )
- {
- mMessage.setSize( PDU3_BEGIN );
-
- PDUMessage pduMessage2 = PDUMessageFactory.getMessage(
- mMessage.copy(), DataUnitID.PDU2, mAliasList );
+ break;
+ case PDU1:
+ /* Remove interleaving */
+ P25Interleave.deinterleaveData(mMessage, PDU1_BEGIN, PDU1_END);
- dispatch( pduMessage2 );
-
- mComplete = true;
- }
- else
- {
- setDUID( DataUnitID.PDU3 );
-
- mMessage.setPointer( PDU3_BEGIN );
- }
- }
- else
- {
- mComplete = true;
- }
+ /* Remove trellis encoding - abort processing if we have an
+ * unsuccessful decode due to excessive errors */
+ if(mHalfRate.decode(mMessage, PDU1_BEGIN, PDU1_END))
+ {
+ if(mMessage.getInt(PDUMessage.BLOCKS_TO_FOLLOW) == 1)
+ {
+ mMessage.setSize(PDU2_BEGIN);
- /* Set sync match threshold to normal */
- mPrimarySyncDetector.setThreshold( SYNC_MATCH_THRESHOLD );
- break;
- case PDU3:
- /* Remove interleaving */
- P25Interleave.deinterleaveData( mMessage, PDU3_BEGIN, PDU3_END );
-
- /* Remove trellis encoding - abort processing if we have an
- * unsuccessful decode due to excessive errors */
- if( mHalfRate.decode( mMessage, PDU3_BEGIN, PDU3_END ) )
- {
- mMessage.setSize( PDU3_DECODED_END );
-
- PDUMessage pduMessage3 = PDUMessageFactory.getMessage(mMessage.copy(), DataUnitID.PDU3, mAliasList );
+ PDUMessage pduMessage1 = PDUMessageFactory.getMessage(
+ mMessage.copy(), DataUnitID.PDU1, mAliasList);
- dispatch( pduMessage3 );
- }
-
- mComplete = true;
- break;
- case PDUC:
- /* De-interleave the latest block*/
- P25Interleave.deinterleaveData( mMessage,
- mMessage.size() - 196, mMessage.size() );
+ dispatch(pduMessage1);
- /* Decode 3/4 rate convolutional encoding from latest block */
- if( mThreeQuarterRate.decode( mMessage,
- mMessage.size() - 196, mMessage.size() ) )
- {
- /* Resize the message and adjust the message pointer
- * to account for removing 48 + 4 parity bits */
- mMessage.setSize( mMessage.size() - 52 );
- mMessage.adjustPointer( -52 );
-
- int blocks = mMessage.getInt( PDUMessage.BLOCKS_TO_FOLLOW );
-
- int current = ( mMessage.size() - 160 ) / 144;
-
- if( current < blocks )
- {
- mMessage.setSize( mMessage.size() + 196 );
- mMessageLength = mMessage.size();
- }
- else
- {
- PDUConfirmedMessage pducm =
- new PDUConfirmedMessage( mMessage.copy(),
- mAliasList );
+ mComplete = true;
+ }
+ else
+ {
+ setDUID(DataUnitID.PDU2);
- /* Translate into correct subclass */
- dispatch( PDUConfirmedMessageFactory.getMessage( pducm ) );
-
- mComplete = true;
- }
- break;
- }
- else
- {
- PDUConfirmedMessage pducm =
- new PDUConfirmedMessage( mMessage.copy(),
- mAliasList );
+ mMessage.setPointer(PDU2_BEGIN);
+ }
+ }
+ else
+ {
+ mComplete = true;
+ }
+ break;
+ case PDU2:
+ /* Remove interleaving */
+ P25Interleave.deinterleaveData(mMessage, PDU2_BEGIN, PDU2_END);
+
+ /* Remove trellis encoding - abort processing if we have an
+ * unsuccessful decode due to excessive errors */
+ if(mHalfRate.decode(mMessage, PDU2_BEGIN, PDU2_END))
+ {
+ if(mMessage.getInt(PDUMessage.BLOCKS_TO_FOLLOW) == 2)
+ {
+ mMessage.setSize(PDU3_BEGIN);
+
+ PDUMessage pduMessage2 = PDUMessageFactory.getMessage(
+ mMessage.copy(), DataUnitID.PDU2, mAliasList);
+
+ dispatch(pduMessage2);
+
+ mComplete = true;
+ }
+ else
+ {
+ setDUID(DataUnitID.PDU3);
+
+ mMessage.setPointer(PDU3_BEGIN);
+ }
+ }
+ else
+ {
+ mComplete = true;
+ }
- /* Translate into correct subclass */
- dispatch( PDUConfirmedMessageFactory.getMessage( pducm ) );
-
- mComplete = true;
- }
-
/* Set sync match threshold to normal */
- mPrimarySyncDetector.setThreshold( SYNC_MATCH_THRESHOLD );
- break;
- case TDU:
- dispatch( new TDUMessage( mMessage.copy(), mDUID, mAliasList ) );
- mComplete = true;
+ mPrimarySyncDetector.setThreshold(SYNC_MATCH_THRESHOLD);
+ break;
+ case PDU3:
+ /* Remove interleaving */
+ P25Interleave.deinterleaveData(mMessage, PDU3_BEGIN, PDU3_END);
- /* Set sync match threshold to normal */
- mPrimarySyncDetector.setThreshold( SYNC_MATCH_THRESHOLD );
- break;
- case TDULC:
- TDULinkControlMessage tdulc = new TDULinkControlMessage(
- mMessage.copy(), mDUID, mAliasList );
+ /* Remove trellis encoding - abort processing if we have an
+ * unsuccessful decode due to excessive errors */
+ if(mHalfRate.decode(mMessage, PDU3_BEGIN, PDU3_END))
+ {
+ mMessage.setSize(PDU3_DECODED_END);
- /* Convert to an appropriate link control message */
- tdulc = TDULCMessageFactory.getMessage( tdulc );
+ PDUMessage pduMessage3 = PDUMessageFactory.getMessage(mMessage.copy(), DataUnitID.PDU3, mAliasList);
- dispatch( tdulc );
- mComplete = true;
+ dispatch(pduMessage3);
+ }
- /* Set sync match threshold to normal */
- mPrimarySyncDetector.setThreshold( SYNC_MATCH_THRESHOLD );
- break;
- case TSBK1:
- /* Remove interleaving */
- P25Interleave.deinterleaveData( mMessage, TSBK_BEGIN, TSBK_END );
-
- /* Remove trellis encoding - abort processing if we have an
- * unsuccessful decode due to excessive errors */
-
- if( mHalfRate.decode( mMessage, TSBK_BEGIN, TSBK_END ) )
- {
- mMessage = CRCP25.correctCCITT80( mMessage,
- TSBK_BEGIN, TSBK_CRC_START );
+ mComplete = true;
+ break;
+ case PDUC:
+ /* De-interleave the latest block*/
+ P25Interleave.deinterleaveData(mMessage,
+ mMessage.size() - 196, mMessage.size());
- if( mMessage.getCRC() != CRC.FAILED_CRC )
- {
- BinaryMessage tsbkBuffer1 = mMessage.copy();
- tsbkBuffer1.setSize( TSBK_DECODED_END );
-
- TSBKMessage tsbkMessage1 = TSBKMessageFactory.getMessage(
- tsbkBuffer1, DataUnitID.TSBK1, mAliasList );
+ /* Decode 3/4 rate convolutional encoding from latest block */
+ if(mThreeQuarterRate.decode(mMessage,
+ mMessage.size() - 196, mMessage.size()))
+ {
+ /* Resize the message and adjust the message pointer
+ * to account for removing 48 + 4 parity bits */
+ mMessage.setSize(mMessage.size() - 52);
+ mMessage.adjustPointer(-52);
- if( tsbkMessage1.isLastBlock() )
- {
- mComplete = true;
- }
- else
- {
- setDUID( DataUnitID.TSBK2 );
- mMessage.setPointer( TSBK_BEGIN );
- }
-
- dispatch( tsbkMessage1 );
- }
- else
- {
- mComplete = true;
- }
- }
- else
- {
- mComplete = true;
- }
+ int blocks = mMessage.getInt(PDUMessage.BLOCKS_TO_FOLLOW);
- /* Set sync match threshold to normal */
- mPrimarySyncDetector.setThreshold( SYNC_MATCH_THRESHOLD );
- break;
- case TSBK2:
- /* Remove interleaving */
- P25Interleave.deinterleaveData( mMessage, TSBK_BEGIN, TSBK_END );
+ int current = (mMessage.size() - 160) / 144;
- /* Remove trellis encoding - abort processing if we have an
- * unsuccessful decode due to excessive errors */
- if( mHalfRate.decode( mMessage, TSBK_BEGIN, TSBK_END ) )
- {
- mMessage = CRCP25.correctCCITT80( mMessage,
- TSBK_BEGIN, TSBK_CRC_START );
-
- if( mMessage.getCRC() != CRC.FAILED_CRC )
- {
- BinaryMessage tsbkBuffer2 = mMessage.copy();
- tsbkBuffer2.setSize( TSBK_DECODED_END );
-
- TSBKMessage tsbkMessage2 = TSBKMessageFactory.getMessage(
- tsbkBuffer2, DataUnitID.TSBK2, mAliasList );
+ if(current < blocks)
+ {
+ mMessage.setSize(mMessage.size() + 196);
+ mMessageLength = mMessage.size();
+ }
+ else
+ {
+ PDUConfirmedMessage pducm =
+ new PDUConfirmedMessage(mMessage.copy(),
+ mAliasList);
- if( tsbkMessage2.isLastBlock() )
- {
- mComplete = true;
- }
- else
- {
- setDUID( DataUnitID.TSBK3 );
- mMessage.setPointer( TSBK_BEGIN );
- }
-
- dispatch( tsbkMessage2 );
- }
- else
- {
- mComplete = true;
- }
- }
- else
- {
- mComplete = true;
- }
- break;
- case TSBK3:
- /* Remove interleaving */
- P25Interleave.deinterleaveData( mMessage, TSBK_BEGIN, TSBK_END );
-
- /* Remove trellis encoding - abort processing if we have an
- * unsuccessful decode due to excessive errors */
- if( mHalfRate.decode( mMessage, TSBK_BEGIN, TSBK_END ) )
- {
- mMessage = CRCP25.correctCCITT80( mMessage,
- TSBK_BEGIN, TSBK_CRC_START );
-
- if( mMessage.getCRC() != CRC.FAILED_CRC )
- {
- BinaryMessage tsbkBuffer3 = mMessage.copy();
- tsbkBuffer3.setSize( TSBK_DECODED_END );
-
- TSBKMessage tsbkMessage3 = TSBKMessageFactory.getMessage(
- tsbkBuffer3, DataUnitID.TSBK3, mAliasList );
+ /* Translate into correct subclass */
+ dispatch(PDUConfirmedMessageFactory.getMessage(pducm));
- dispatch( tsbkMessage3 );
- }
- }
+ mComplete = true;
+ }
+ break;
+ }
+ else
+ {
+ PDUConfirmedMessage pducm =
+ new PDUConfirmedMessage(mMessage.copy(),
+ mAliasList);
- mComplete = true;
- break;
- case VSELP1:
- mComplete = true;
- dispatch( new VSELP1Message( mMessage.copy(), mDUID, mAliasList ) );
- break;
- case VSELP2:
- mComplete = true;
- dispatch( new VSELP2Message( mMessage.copy(), mDUID, mAliasList ) );
- break;
- case UNKN:
- mComplete = true;
- dispatch( new P25Message( mMessage.copy(), mDUID, mAliasList ) );
- break;
- default:
- mComplete = true;
- break;
- }
+ /* Translate into correct subclass */
+ dispatch(PDUConfirmedMessageFactory.getMessage(pducm));
+
+ mComplete = true;
+ }
+
+ /* Set sync match threshold to normal */
+ mPrimarySyncDetector.setThreshold(SYNC_MATCH_THRESHOLD);
+ break;
+ case TDU:
+ dispatch(new TDUMessage(mMessage.copy(), mDUID, mAliasList));
+ mComplete = true;
+
+ /* Set sync match threshold to normal */
+ mPrimarySyncDetector.setThreshold(SYNC_MATCH_THRESHOLD);
+ break;
+ case TDULC:
+ TDULinkControlMessage tdulc = new TDULinkControlMessage(
+ mMessage.copy(), mDUID, mAliasList);
+
+ /* Convert to an appropriate link control message */
+ tdulc = TDULCMessageFactory.getMessage(tdulc);
+
+ dispatch(tdulc);
+ mComplete = true;
+
+ /* Set sync match threshold to normal */
+ mPrimarySyncDetector.setThreshold(SYNC_MATCH_THRESHOLD);
+ break;
+ case TSBK1:
+ /* Remove interleaving */
+ P25Interleave.deinterleaveData(mMessage, TSBK_BEGIN, TSBK_END);
+
+ /* Remove trellis encoding - abort processing if we have an
+ * unsuccessful decode due to excessive errors */
+
+ if(mHalfRate.decode(mMessage, TSBK_BEGIN, TSBK_END))
+ {
+ mMessage = CRCP25.correctCCITT80(mMessage,
+ TSBK_BEGIN, TSBK_CRC_START);
+
+ if(mMessage.getCRC() != CRC.FAILED_CRC)
+ {
+ BinaryMessage tsbkBuffer1 = mMessage.copy();
+ tsbkBuffer1.setSize(TSBK_DECODED_END);
+
+ TSBKMessage tsbkMessage1 = TSBKMessageFactory.getMessage(
+ tsbkBuffer1, DataUnitID.TSBK1, mAliasList);
+
+ if(tsbkMessage1.isLastBlock())
+ {
+ mComplete = true;
+ }
+ else
+ {
+ setDUID(DataUnitID.TSBK2);
+ mMessage.setPointer(TSBK_BEGIN);
+ }
+
+ dispatch(tsbkMessage1);
+ }
+ else
+ {
+ mComplete = true;
+ }
+ }
+ else
+ {
+ mComplete = true;
+ }
+
+ /* Set sync match threshold to normal */
+ mPrimarySyncDetector.setThreshold(SYNC_MATCH_THRESHOLD);
+ break;
+ case TSBK2:
+ /* Remove interleaving */
+ P25Interleave.deinterleaveData(mMessage, TSBK_BEGIN, TSBK_END);
+
+ /* Remove trellis encoding - abort processing if we have an
+ * unsuccessful decode due to excessive errors */
+ if(mHalfRate.decode(mMessage, TSBK_BEGIN, TSBK_END))
+ {
+ mMessage = CRCP25.correctCCITT80(mMessage,
+ TSBK_BEGIN, TSBK_CRC_START);
+
+ if(mMessage.getCRC() != CRC.FAILED_CRC)
+ {
+ BinaryMessage tsbkBuffer2 = mMessage.copy();
+ tsbkBuffer2.setSize(TSBK_DECODED_END);
+
+ TSBKMessage tsbkMessage2 = TSBKMessageFactory.getMessage(
+ tsbkBuffer2, DataUnitID.TSBK2, mAliasList);
+
+ if(tsbkMessage2.isLastBlock())
+ {
+ mComplete = true;
+ }
+ else
+ {
+ setDUID(DataUnitID.TSBK3);
+ mMessage.setPointer(TSBK_BEGIN);
+ }
+
+ dispatch(tsbkMessage2);
+ }
+ else
+ {
+ mComplete = true;
+ }
+ }
+ else
+ {
+ mComplete = true;
+ }
+ break;
+ case TSBK3:
+ /* Remove interleaving */
+ P25Interleave.deinterleaveData(mMessage, TSBK_BEGIN, TSBK_END);
+
+ /* Remove trellis encoding - abort processing if we have an
+ * unsuccessful decode due to excessive errors */
+ if(mHalfRate.decode(mMessage, TSBK_BEGIN, TSBK_END))
+ {
+ mMessage = CRCP25.correctCCITT80(mMessage,
+ TSBK_BEGIN, TSBK_CRC_START);
+
+ if(mMessage.getCRC() != CRC.FAILED_CRC)
+ {
+ BinaryMessage tsbkBuffer3 = mMessage.copy();
+ tsbkBuffer3.setSize(TSBK_DECODED_END);
+
+ TSBKMessage tsbkMessage3 = TSBKMessageFactory.getMessage(
+ tsbkBuffer3, DataUnitID.TSBK3, mAliasList);
+
+ dispatch(tsbkMessage3);
+ }
+ }
+
+ mComplete = true;
+ break;
+ case VSELP1:
+ mComplete = true;
+ dispatch(new VSELP1Message(mMessage.copy(), mDUID, mAliasList));
+ break;
+ case VSELP2:
+ mComplete = true;
+ dispatch(new VSELP2Message(mMessage.copy(), mDUID, mAliasList));
+ break;
+ case UNKN:
+ mComplete = true;
+ dispatch(new P25Message(mMessage.copy(), mDUID, mAliasList));
+ break;
+ default:
+ mComplete = true;
+ break;
+ }
}
-
+
public void dispose()
{
- mMessage = null;
- mHalfRate.dispose();
+ mMessage = null;
+ mHalfRate.dispose();
}
/**
@@ -663,55 +663,54 @@ public class P25MessageFramer implements Listener
{
return mComplete;
}
-
+
public boolean isActive()
{
- return mActive;
+ return mActive;
}
}
-
+
/**
- * Sync pattern detector to listen for costas loop phase lock errors and
- * apply a phase correction to the costas loop so that we don't miss any
+ * Sync pattern detector to listen for costas loop phase lock errors and
+ * apply a phase correction to the costas loop so that we don't miss any
* messages.
- *
- * When the costas loop locks with a +/- 90 degree or 180 degree phase
+ *
+ * When the costas loop locks with a +/- 90 degree or 180 degree phase
* error, the slicer will incorrectly apply the symbol pattern rotated left
* or right by the phase error. However, we can detect these rotated sync
* patterns and apply immediate phase correction so that message processing
- * can continue.
+ * can continue.
*/
- public class CostasPhaseErrorDetector extends SyncDetector
+ public class PLLPhaseInversionDetector extends SyncDetector
{
- private LSMDemodulator mDemodulator;
- private double mCorrection;
-
- public CostasPhaseErrorDetector( FrameSync frameSync, LSMDemodulator demodulator, double correction )
- {
- super( frameSync.getSync() );
+ private IPhaseLockedLoop mPhaseLockedLoop;
+ private double mCorrection;
- mDemodulator = demodulator;
-
- mCorrection = correction;
+ public PLLPhaseInversionDetector(FrameSync frameSync, IPhaseLockedLoop phaseLockedLoop, double correction)
+ {
+ super(frameSync.getSync());
- setListener( new ISyncDetectListener()
- {
- @Override
- public void syncDetected()
- {
- mDemodulator.correctPhaseError( mCorrection );
+ mPhaseLockedLoop = phaseLockedLoop;
+ mCorrection = correction;
- /* Since we detected a sync pattern, start a message assembler */
- for( P25MessageAssembler assembler: mAssemblers )
- {
- if( !assembler.isActive() )
- {
- assembler.setActive( true );
- break;
- }
- }
- }
- } );
- }
+ setListener(new ISyncDetectListener()
+ {
+ @Override
+ public void syncDetected()
+ {
+ mPhaseLockedLoop.correctInversion(mCorrection);
+
+ /* Since we detected a sync pattern, start a message assembler */
+ for(P25MessageAssembler assembler : mAssemblers)
+ {
+ if(!assembler.isActive())
+ {
+ assembler.setActive(true);
+ break;
+ }
+ }
+ }
+ });
+ }
}
}
diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder.java
index 13512400..6923bc37 100644
--- a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder.java
+++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder.java
@@ -1,244 +1,200 @@
/*******************************************************************************
- * sdrtrunk
- * Copyright (C) 2014-2017 Dennis Sheirer
+ * sdr-trunk
+ * Copyright (C) 2014-2018 Dennis Sheirer
*
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
+ * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
*
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see
+ * You should have received a copy of the GNU General Public License along with this program.
+ * If not, see
*
******************************************************************************/
package io.github.dsheirer.module.decode.p25;
import io.github.dsheirer.alias.AliasList;
-import io.github.dsheirer.instrument.tap.Tap;
-import io.github.dsheirer.instrument.tap.TapGroup;
-import io.github.dsheirer.instrument.tap.stream.DibitTap;
-import io.github.dsheirer.instrument.tap.stream.FloatTap;
+import io.github.dsheirer.dsp.filter.FilterFactory;
+import io.github.dsheirer.dsp.filter.design.FilterDesignException;
+import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
+import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter;
+import io.github.dsheirer.dsp.psk.DQPSKDemodulator;
+import io.github.dsheirer.dsp.psk.InterpolatingSampleBuffer;
+import io.github.dsheirer.dsp.psk.InterpolatingSampleBufferInstrumented;
+import io.github.dsheirer.dsp.psk.pll.CostasLoop;
import io.github.dsheirer.sample.Listener;
-import io.github.dsheirer.sample.real.IFilteredRealBufferListener;
-import io.github.dsheirer.sample.real.RealBuffer;
+import io.github.dsheirer.sample.complex.ComplexBuffer;
+import io.github.dsheirer.sample.complex.IComplexBufferListener;
import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.util.ArrayList;
-import java.util.List;
+import java.util.HashMap;
+import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
-public class P25_C4FMDecoder extends P25Decoder implements IFilteredRealBufferListener
+public class P25_C4FMDecoder extends P25Decoder implements IComplexBufferListener, Listener
{
- private final static Logger mLog = LoggerFactory.getLogger( P25_C4FMDecoder.class );
-
- /* Instrumentation Taps */
- private static final String INSTRUMENT_C4FM_SYMBOL_FILTER_OUTPUT = "Tap Point: Symbol Filter Output";
- private static final String INSTRUMENT_C4FM_SLICER_OUTPUT = "Tap Point: C4FM Slicer Output";
+ private final static Logger mLog = LoggerFactory.getLogger(P25_C4FMDecoder.class);
+ protected static final double SYMBOL_RATE = 4800;
+ protected InterpolatingSampleBuffer mInterpolatingSampleBuffer;
+ protected DQPSKDemodulator mQPSKDemodulator;
+ protected CostasLoop mCostasLoop;
+ protected P25MessageFramer mMessageFramer;
+ protected double mSampleRate;
+ private Map mBasebandFilters = new HashMap<>();
+ private ComplexFIRFilter mBasebandFilter;
- private List mAvailableTaps;
- private C4FMSymbolFilter mSymbolFilter;
- private C4FMSlicer mC4FMSlicer;
- private P25MessageFramer mMessageFramer;
-
- /**
- * P25 Phase 1 C4FM Decoder processes real buffers of un-filtered,
- * demodulated audio and produces decoded P25 Phase 1 Messages.
- *
- * Provides Frequency Control to steer external tuner source and incorporates
- * internal gain control.
- *
- * Note: use the P25AudioModule to convert the decoded messages into audio.
- *
- * @param aliasList - optional (can be null) list of alias values for network
- * infrastructure and subscriber identities that will be included in each
- * decoded message
- */
- public P25_C4FMDecoder(AliasList aliasList, int frequencyCorrectionMaximum )
- {
- super( aliasList );
-
- /* Shape gain and frequency offsets to optimize sample stream */
- mSymbolFilter = new C4FMSymbolFilter( frequencyCorrectionMaximum );
-
- /* Convert samples to symbols */
- mC4FMSlicer = new C4FMSlicer();
- mSymbolFilter.setListener( mC4FMSlicer );
-
- /* Sync pattern detection and message construction */
- mMessageFramer = new P25MessageFramer( aliasList );
- mC4FMSlicer.addListener( mMessageFramer );
-
- /* Process and broadcast messages */
- mMessageFramer.setListener( getMessageProcessor() );
- }
-
- @Override
- public void dispose()
- {
- super.dispose();
-
- mSymbolFilter.dispose();
- mSymbolFilter = null;
-
- mMessageFramer.dispose();
- mMessageFramer = null;
-
- mC4FMSlicer.dispose();
- mC4FMSlicer = null;
- }
-
- public Modulation getModulation()
- {
- return Modulation.C4FM;
- }
-
- @Override
- public Listener getFilteredRealBufferListener()
- {
- return mSymbolFilter;
- }
-
- /**
- * Instrumentation taps for monitoring internal processing
- */
- @Override
- public List getTapGroups()
+ public P25_C4FMDecoder(AliasList aliasList)
{
- if( mAvailableTaps == null )
- {
- mAvailableTaps = new ArrayList<>();
-
- TapGroup group = new TapGroup( "P25 C4FM Decoder" );
-
- group.add( new FloatTap( INSTRUMENT_C4FM_SYMBOL_FILTER_OUTPUT, 0, 0.1f ) );
- group.add( new DibitTap( INSTRUMENT_C4FM_SLICER_OUTPUT, 0, 0.1f ) );
-
- mAvailableTaps.add( group );
-
- if( mSymbolFilter != null )
- {
- mAvailableTaps.addAll( mSymbolFilter.getTapGroups() );
- }
- }
-
- return mAvailableTaps;
+ super(aliasList);
+ setSampleRate(48000.0);
}
- /**
- * Adds an instrumentation tap to monitor internal processing.
- */
- @Override
- public void registerTap( Tap tap )
+ public void setSampleRate(double sampleRate)
{
- if( mSymbolFilter != null )
- {
- mSymbolFilter.registerTap( tap );
- }
-
- switch( tap.getName() )
- {
- case INSTRUMENT_C4FM_SYMBOL_FILTER_OUTPUT:
- FloatTap symbolTap = (FloatTap)tap;
- mSymbolFilter.setListener( symbolTap );
- symbolTap.setListener( mC4FMSlicer );
- break;
- case INSTRUMENT_C4FM_SLICER_OUTPUT:
- DibitTap slicerTap = (DibitTap)tap;
-
- if( mC4FMSlicer != null )
- {
- mC4FMSlicer.addListener( slicerTap );
- }
- break;
- default:
- break;
- }
+ if(sampleRate <= SYMBOL_RATE * 2)
+ {
+ throw new IllegalArgumentException("Sample rate must be at least twice the symbol rate [4800]");
+ }
+
+ mSampleRate = sampleRate;
+ mBasebandFilter = new ComplexFIRFilter(getBasebandFilter(), 1.0f);
+
+ mCostasLoop = new CostasLoop(mSampleRate, SYMBOL_RATE);
+
+ mInterpolatingSampleBuffer = new InterpolatingSampleBufferInstrumented((float)(sampleRate / SYMBOL_RATE));
+ mQPSKDemodulator = new DQPSKDemodulator(mCostasLoop, mInterpolatingSampleBuffer);
+
+ //Message framer can trigger a symbol-inversion correction to the PLL when detected
+ mMessageFramer = new P25MessageFramer(getAliasList(), mCostasLoop);
+ mMessageFramer.setListener(getMessageProcessor());
+ mQPSKDemodulator.setDibitListener(mMessageFramer);
}
- /**
- * Removes the instrumentation tap.
- */
- @Override
- public void unregisterTap( Tap tap )
+ @Override
+ public void receive(ComplexBuffer complexBuffer)
{
- if( mSymbolFilter != null )
- {
- mSymbolFilter.unregisterTap( tap );
- }
-
- switch( tap.getName() )
- {
- case INSTRUMENT_C4FM_SYMBOL_FILTER_OUTPUT:
- mSymbolFilter.setListener( mC4FMSlicer );
- break;
- case INSTRUMENT_C4FM_SLICER_OUTPUT:
- DibitTap slicerTap = (DibitTap)tap;
-
- if( mC4FMSlicer != null )
- {
- mC4FMSlicer.removeListener( slicerTap );
- }
- break;
- default:
- throw new IllegalArgumentException( "Unrecognized tap: " +
- tap.getName() );
- }
+ ComplexBuffer filtered = filter(complexBuffer);
+ mQPSKDemodulator.receive(filtered);
}
- @Override
- public void reset()
- {
- // TODO Auto-generated method stub
-
- }
+ protected ComplexBuffer filter(ComplexBuffer complexBuffer)
+ {
+ return mBasebandFilter.filter(complexBuffer);
+ }
- @Override
- public void start( ScheduledExecutorService executor )
- {
- // TODO Auto-generated method stub
-
- }
+ private double getSampleRate()
+ {
+ return mSampleRate;
+ }
- @Override
- public void stop()
- {
- // TODO Auto-generated method stub
-
- }
+ private float[] getBasebandFilter()
+ {
+ float[] filter = mBasebandFilters.get(getSampleRate());
- @Override
- public void setFrequencyChangeListener( Listener listener )
- {
- if( mSymbolFilter != null )
- {
- mSymbolFilter.setFrequencyChangeListener( listener );
- }
- }
+ if(filter == null)
+ {
+ FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
+ .sampleRate((int)getSampleRate())
+ .passBandCutoff(5100)
+ .passBandAmplitude(1.0)
+ .passBandRipple(0.01)
+ .stopBandAmplitude(0.0)
+ .stopBandStart(6500)
+ .stopBandRipple(0.01)
+ .build();
- @Override
- public void removeFrequencyChangeListener()
- {
- if( mSymbolFilter != null )
- {
- mSymbolFilter.setFrequencyChangeListener( null );
- }
- }
+ try
+ {
+ filter = FilterFactory.getTaps(specification);
- @Override
- public Listener getFrequencyChangeListener()
- {
- if( mSymbolFilter != null )
- {
- return mSymbolFilter.getFrequencyChangeListener();
- }
-
- return null;
- }
+ }
+ 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;
+ }
+
+ public void dispose()
+ {
+ super.dispose();
+
+ mBasebandFilter.dispose();
+ mBasebandFilter = null;
+
+ mMessageFramer.dispose();
+ mMessageFramer = null;
+ }
+
+ @Override
+ public void setFrequencyChangeListener(Listener listener)
+ {
+ }
+
+ @Override
+ public void removeFrequencyChangeListener()
+ {
+ }
+
+ @Override
+ public Listener getFrequencyChangeListener()
+ {
+ return new Listener()
+ {
+ @Override
+ public void receive(FrequencyChangeEvent frequencyChangeEvent)
+ {
+ switch(frequencyChangeEvent.getEvent())
+ {
+ case NOTIFICATION_FREQUENCY_CHANGE:
+ case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
+ case NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE:
+ case NOTIFICATION_SAMPLE_RATE_CHANGE:
+ mCostasLoop.reset();
+ break;
+ }
+ }
+ };
+ }
+
+ @Override
+ public Listener getComplexBufferListener()
+ {
+ return P25_C4FMDecoder.this;
+ }
+
+ public Modulation getModulation()
+ {
+ return Modulation.C4FM;
+ }
+
+ @Override
+ public void reset()
+ {
+ mCostasLoop.reset();
+ }
+
+ @Override
+ public void start(ScheduledExecutorService executor)
+ {
+ }
+
+ @Override
+ public void stop()
+ {
+ }
}
diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoderInstrumented.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoderInstrumented.java
new file mode 100644
index 00000000..903396a7
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoderInstrumented.java
@@ -0,0 +1,109 @@
+/*******************************************************************************
+ * 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.module.decode.p25;
+
+import io.github.dsheirer.alias.AliasList;
+import io.github.dsheirer.dsp.psk.DQPSKDemodulatorInstrumented;
+import io.github.dsheirer.dsp.psk.InterpolatingSampleBufferInstrumented;
+import io.github.dsheirer.dsp.psk.SymbolDecisionData;
+import io.github.dsheirer.sample.Listener;
+import io.github.dsheirer.sample.complex.Complex;
+import io.github.dsheirer.sample.complex.ComplexBuffer;
+
+public class P25_C4FMDecoderInstrumented extends P25_C4FMDecoder
+{
+ private Listener mPLLPhaseErrorListener;
+ private Listener mPLLFrequencyListener;
+ private Listener mSamplesPerSymbolListener;
+ private Listener mSymbolListener;
+ private Listener mFilteredSymbolListener;
+ private Listener mSymbolDecisionDataListener;
+
+ public P25_C4FMDecoderInstrumented(AliasList aliasList)
+ {
+ super(aliasList);
+ }
+
+ /**
+ * Overrides the filter method so that we can capture the filtered samples for instrumentation
+ */
+ protected ComplexBuffer filter(ComplexBuffer complexBuffer)
+ {
+ ComplexBuffer filtered = super.filter(complexBuffer);
+
+ if(mFilteredSymbolListener != null)
+ {
+ mFilteredSymbolListener.receive(filtered);
+ }
+
+ return filtered;
+ }
+
+ /**
+ * Overrides this method so we can correctly configure for instrumented operations
+ */
+ public void setSampleRate(double sampleRate)
+ {
+ super.setSampleRate(sampleRate);
+ InterpolatingSampleBufferInstrumented instrumentedBuffer = new InterpolatingSampleBufferInstrumented((float)(sampleRate / SYMBOL_RATE));
+ mInterpolatingSampleBuffer = instrumentedBuffer;
+
+ DQPSKDemodulatorInstrumented instrumented = new DQPSKDemodulatorInstrumented(mCostasLoop, instrumentedBuffer, mSampleRate);
+ mQPSKDemodulator = instrumented;
+
+ instrumented.setSymbolListener(mSymbolListener);
+ instrumented.setPLLErrorListener(mPLLPhaseErrorListener);
+ instrumented.setPLLFrequencyListener(mPLLFrequencyListener);
+ instrumented.setSymbolDecisionDataListener(mSymbolDecisionDataListener);
+ instrumented.setSamplesPerSymbolListener(mSamplesPerSymbolListener);
+ instrumented.setDibitListener(mMessageFramer);
+ }
+
+ public void setSymbolListener(Listener listener)
+ {
+ mSymbolListener = listener;
+ ((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setSymbolListener(listener);
+ }
+
+ public void setPLLPhaseErrorListener(Listener listener)
+ {
+ mPLLPhaseErrorListener = listener;
+ ((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setPLLErrorListener(listener);
+ }
+
+ public void setPLLFrequencyListener(Listener listener)
+ {
+ mPLLFrequencyListener = listener;
+ ((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setPLLFrequencyListener(listener);
+ }
+
+ public void setFilteredBufferListener(Listener listener)
+ {
+ mFilteredSymbolListener = listener;
+ }
+
+ public void setSymbolDecisionDataListener(Listener listener)
+ {
+ mSymbolDecisionDataListener = listener;
+ ((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setSymbolDecisionDataListener(listener);
+ }
+
+ public void setSamplesPerSymbolListener(Listener listener)
+ {
+ mSamplesPerSymbolListener = listener;
+ ((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setSamplesPerSymbolListener(listener);
+ }
+}
\ No newline at end of file
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 806b20e6..00bb781e 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
@@ -1,17 +1,17 @@
/*******************************************************************************
* 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
******************************************************************************/
@@ -23,13 +23,8 @@ import io.github.dsheirer.dsp.filter.Window.WindowType;
import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter_CB_CB;
import io.github.dsheirer.dsp.gain.ComplexFeedForwardGainControl;
import io.github.dsheirer.dsp.psk.LSMDemodulator;
-import io.github.dsheirer.dsp.psk.QPSKPolarSlicer;
-import io.github.dsheirer.instrument.tap.Tap;
+import io.github.dsheirer.dsp.psk.QPSKSymbolDecoder;
import io.github.dsheirer.instrument.tap.TapGroup;
-import io.github.dsheirer.instrument.tap.stream.ComplexSampleTap;
-import io.github.dsheirer.instrument.tap.stream.ComplexTap;
-import io.github.dsheirer.instrument.tap.stream.DibitTap;
-import io.github.dsheirer.instrument.tap.stream.QPSKTap;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.ComplexBuffer;
import io.github.dsheirer.sample.complex.ComplexBufferToStreamConverter;
@@ -38,73 +33,72 @@ import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
public class P25_LSMDecoder extends P25Decoder implements IComplexBufferListener
{
- private final static Logger mLog = LoggerFactory.getLogger( P25_LSMDecoder.class );
-
+ private final static Logger mLog = LoggerFactory.getLogger(P25_LSMDecoder.class);
+
/* Instrumentation Taps */
- private static final String INSTRUMENT_BASEBAND_FILTER_OUTPUT = "Tap Point: Baseband Filter Output";
- private static final String INSTRUMENT_AGC_OUTPUT = "Tap Point: AGC Output";
- private static final String INSTRUMENT_LSM_DEMODULATOR_OUTPUT = "Tap Point: LSM Demodulator Output";
- private static final String INSTRUMENT_QPSK_SLICER_OUTPUT = "Tap Point: QPSK Slicer Output";
+ private static final String INSTRUMENT_BASEBAND_FILTER_OUTPUT = "Tap Point: Baseband Filter Output";
+ private static final String INSTRUMENT_AGC_OUTPUT = "Tap Point: AGC Output";
+ private static final String INSTRUMENT_LSM_DEMODULATOR_OUTPUT = "Tap Point: LSM Demodulator Output";
+ private static final String INSTRUMENT_QPSK_SLICER_OUTPUT = "Tap Point: QPSK Slicer Output";
- private List mAvailableTaps;
-
- private ComplexFIRFilter_CB_CB mBasebandFilter;
- private ComplexBufferToStreamConverter mStreamConverter = new ComplexBufferToStreamConverter();
- private ComplexFeedForwardGainControl mAGC =
- new ComplexFeedForwardGainControl( 32 );
- private LSMDemodulator mLSMDemodulator = new LSMDemodulator();
- private QPSKPolarSlicer mQPSKSlicer = new QPSKPolarSlicer();
- private P25MessageFramer mMessageFramer;
-
- public P25_LSMDecoder( AliasList aliasList )
- {
- super( aliasList );
-
- mBasebandFilter = new ComplexFIRFilter_CB_CB( FilterFactory.getLowPass(
- 48000, 7250, 8000, 60, WindowType.HANNING, true ), 1.0f );
-
- mBasebandFilter.setListener( mStreamConverter );
-
- mStreamConverter.setListener( mAGC );
+ private List mAvailableTaps;
- mAGC.setListener( mLSMDemodulator );
-
- mLSMDemodulator.setSymbolListener( mQPSKSlicer );
-
- mMessageFramer = new P25MessageFramer( aliasList, mLSMDemodulator );
- mQPSKSlicer.addListener( mMessageFramer );
-
- mMessageFramer.setListener( getMessageProcessor() );
- }
-
- public void dispose()
- {
- super.dispose();
-
- mBasebandFilter.dispose();
- mBasebandFilter = null;
-
- mStreamConverter.dispose();
- mStreamConverter = null;
-
- mAGC.dispose();
- mAGC = null;
+ private ComplexFIRFilter_CB_CB mBasebandFilter;
+ private ComplexBufferToStreamConverter mStreamConverter = new ComplexBufferToStreamConverter();
+ private ComplexFeedForwardGainControl mAGC =
+ new ComplexFeedForwardGainControl(32);
+ private LSMDemodulator mLSMDemodulator = new LSMDemodulator();
+ private QPSKSymbolDecoder mQPSKSlicer = new QPSKSymbolDecoder();
+ private P25MessageFramer mMessageFramer;
- mLSMDemodulator.dispose();
- mLSMDemodulator = null;
-
- mQPSKSlicer.dispose();
- mQPSKSlicer = null;
-
- mMessageFramer.dispose();
- mMessageFramer = null;
- }
+ public P25_LSMDecoder(AliasList aliasList)
+ {
+ super(aliasList);
+
+ mBasebandFilter = new ComplexFIRFilter_CB_CB(FilterFactory.getLowPass(
+ 48000, 7250, 8000, 60, WindowType.HANNING, true), 1.0f);
+
+ mBasebandFilter.setListener(mStreamConverter);
+
+ mStreamConverter.setListener(mAGC);
+
+ mAGC.setListener(mLSMDemodulator);
+
+ mLSMDemodulator.setSymbolListener(mQPSKSlicer);
+
+ mMessageFramer = new P25MessageFramer(aliasList, mLSMDemodulator.getCostasLoop());
+ mQPSKSlicer.addListener(mMessageFramer);
+
+ mMessageFramer.setListener(getMessageProcessor());
+ }
+
+ public void dispose()
+ {
+ super.dispose();
+
+ mBasebandFilter.dispose();
+ mBasebandFilter = null;
+
+ mStreamConverter.dispose();
+ mStreamConverter = null;
+
+ mAGC.dispose();
+ mAGC = null;
+
+ mLSMDemodulator.dispose();
+ mLSMDemodulator = null;
+
+ mQPSKSlicer.dispose();
+ mQPSKSlicer = null;
+
+ mMessageFramer.dispose();
+ mMessageFramer = null;
+ }
@Override
public void setFrequencyChangeListener(Listener listener)
@@ -130,130 +124,34 @@ public class P25_LSMDecoder extends P25Decoder implements IComplexBufferListener
}
@Override
- public Listener getComplexBufferListener()
- {
- return mBasebandFilter;
- }
-
- public Modulation getModulation()
- {
- return Modulation.CQPSK;
- }
-
- /**
- * Provides a list of instrumentation taps for monitoring internal processing
- */
- @Override
- public List getTapGroups()
+ public Listener getComplexBufferListener()
{
- if( mAvailableTaps == null )
- {
- mAvailableTaps = new ArrayList<>();
-
- TapGroup group = new TapGroup( "P25 LSM Decoder" );
-
- group.add( new ComplexTap( INSTRUMENT_BASEBAND_FILTER_OUTPUT, 0, 1.0f ) );
- group.add( new ComplexTap( INSTRUMENT_AGC_OUTPUT, 0, 1.0f ) );
- group.add( new QPSKTap( INSTRUMENT_LSM_DEMODULATOR_OUTPUT, 0, 1.0f ) );
- group.add( new DibitTap( INSTRUMENT_QPSK_SLICER_OUTPUT, 0, 0.1f ) );
-
- mAvailableTaps.add( group );
-
- if( mLSMDemodulator != null )
- {
- mAvailableTaps.addAll( mLSMDemodulator.getTapGroups() );
- }
- }
-
- return mAvailableTaps;
+ return mBasebandFilter;
}
- /**
- * Adds the instrumentation tap
- */
- @Override
- public void registerTap( Tap tap )
+ public Modulation getModulation()
{
- if( mLSMDemodulator != null )
- {
- mLSMDemodulator.registerTap( tap );
- }
-
- switch( tap.getName() )
- {
- case INSTRUMENT_BASEBAND_FILTER_OUTPUT:
- ComplexSampleTap baseband = (ComplexSampleTap)tap;
- mStreamConverter.setListener( baseband );
- baseband.setListener( mAGC );
- break;
- case INSTRUMENT_AGC_OUTPUT:
- ComplexSampleTap agcSymbol = (ComplexSampleTap)tap;
- mAGC.setListener( agcSymbol );
- agcSymbol.setListener( mLSMDemodulator );
- break;
- case INSTRUMENT_LSM_DEMODULATOR_OUTPUT:
- QPSKTap qpsk = (QPSKTap)tap;
- mLSMDemodulator.setSymbolListener( qpsk );
- qpsk.setListener( mQPSKSlicer );
- break;
- case INSTRUMENT_QPSK_SLICER_OUTPUT:
- mQPSKSlicer.addListener( (DibitTap)tap );
- break;
- default:
- throw new IllegalArgumentException( "Unrecognized tap: " +
- tap.getName() );
- }
+ return Modulation.CQPSK;
}
- /**
- * Removes the instrumentation tap
- */
- @Override
- public void unregisterTap( Tap tap )
+ @Override
+ public void reset()
{
- if( mLSMDemodulator != null )
- {
- mLSMDemodulator.unregisterTap( tap );
- }
-
- switch( tap.getName() )
- {
- case INSTRUMENT_BASEBAND_FILTER_OUTPUT:
- mStreamConverter.setListener( mAGC );
- break;
- case INSTRUMENT_AGC_OUTPUT:
- mAGC.setListener( mLSMDemodulator );
- break;
- case INSTRUMENT_LSM_DEMODULATOR_OUTPUT:
- mLSMDemodulator.setSymbolListener( mQPSKSlicer );
- break;
- case INSTRUMENT_QPSK_SLICER_OUTPUT:
- mQPSKSlicer.removeListener( (DibitTap)tap );
- break;
- default:
- throw new IllegalArgumentException( "Unrecognized tap: " +
- tap.getName() );
- }
+ // TODO Auto-generated method stub
+
}
- @Override
- public void reset()
- {
- // TODO Auto-generated method stub
-
- }
+ @Override
+ public void start(ScheduledExecutorService executor)
+ {
+ // TODO Auto-generated method stub
- @Override
- public void start( ScheduledExecutorService executor )
- {
- // TODO Auto-generated method stub
-
- }
+ }
- @Override
- public void stop()
- {
- // TODO Auto-generated method stub
-
- }
+ @Override
+ public void stop()
+ {
+ // TODO Auto-generated method stub
+
+ }
}
diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/message/tsbk/osp/control/SyncBroadcast.java b/src/main/java/io/github/dsheirer/module/decode/p25/message/tsbk/osp/control/SyncBroadcast.java
index 2f8f1988..e7ed4a93 100644
--- a/src/main/java/io/github/dsheirer/module/decode/p25/message/tsbk/osp/control/SyncBroadcast.java
+++ b/src/main/java/io/github/dsheirer/module/decode/p25/message/tsbk/osp/control/SyncBroadcast.java
@@ -78,7 +78,7 @@ public class SyncBroadcast extends TSBKMessage
if( isMicroslotsLockedToMinuteRollover() )
{
- sb.append( " MICROSLOT-MINUTE ROLLOVER:LOCKED" );
+ sb.append( " MICROSLOT-MINUTE ROLLOVER:SLOW" );
}
else
{
diff --git a/src/main/java/io/github/dsheirer/module/decode/passport/PassportDecoder.java b/src/main/java/io/github/dsheirer/module/decode/passport/PassportDecoder.java
index 72ba45a2..9f7980b2 100644
--- a/src/main/java/io/github/dsheirer/module/decode/passport/PassportDecoder.java
+++ b/src/main/java/io/github/dsheirer/module/decode/passport/PassportDecoder.java
@@ -63,7 +63,7 @@ public class PassportDecoder extends Decoder
mPassportMessageProcessor = new PassportMessageProcessor( aliasList );
mPassportMessageFramer.addMessageListener( mPassportMessageProcessor );
- mPassportMessageProcessor.setMessageListener( this );
+ mPassportMessageProcessor.setMessageListener( getMessageListener() );
}
@Override
diff --git a/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200Decoder.java b/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200Decoder.java
index 9ccae831..848bb85c 100644
--- a/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200Decoder.java
+++ b/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200Decoder.java
@@ -104,8 +104,8 @@ public class Tait1200Decoder extends Decoder implements IFilteredRealBufferListe
mMessageFramerGPS.addMessageListener( mMessageAProcessor );
mMessageFramerANI.addMessageListener( mMessageBProcessor );
- mMessageAProcessor.setMessageListener( this );
- mMessageBProcessor.setMessageListener( this );
+ mMessageAProcessor.setMessageListener(getMessageListener());
+ mMessageBProcessor.setMessageListener(getMessageListener());
}
public void dispose()
diff --git a/src/main/java/io/github/dsheirer/record/RecorderManager.java b/src/main/java/io/github/dsheirer/record/RecorderManager.java
index 738ffe3f..c5845cdc 100644
--- a/src/main/java/io/github/dsheirer/record/RecorderManager.java
+++ b/src/main/java/io/github/dsheirer/record/RecorderManager.java
@@ -42,6 +42,7 @@ public class RecorderManager implements Listener
private static final Logger mLog = LoggerFactory.getLogger(RecorderManager.class);
public static final int AUDIO_SAMPLE_RATE = 8000;
+ public static final int BASEBAND_SAMPLE_RATE = 48000;
public static final long IDLE_RECORDER_REMOVAL_THRESHOLD = 6000; //6 seconds
private Map mRecorders = new HashMap<>();
@@ -229,7 +230,7 @@ public class RecorderManager implements Listener
sb.append(SystemProperties.getInstance().getApplicationFolder("recordings"));
sb.append(File.separator).append(channelName).append("_baseband");
- return new ComplexBufferWaveRecorder(AUDIO_SAMPLE_RATE, sb.toString());
+ return new ComplexBufferWaveRecorder(BASEBAND_SAMPLE_RATE, sb.toString());
}
/**
diff --git a/src/main/java/io/github/dsheirer/sample/ConversionUtils.java b/src/main/java/io/github/dsheirer/sample/ConversionUtils.java
index 95d72bcb..7ad5c27c 100644
--- a/src/main/java/io/github/dsheirer/sample/ConversionUtils.java
+++ b/src/main/java/io/github/dsheirer/sample/ConversionUtils.java
@@ -21,14 +21,13 @@ public class ConversionUtils
*/
public static float[] convertFromSigned16BitSamples( ByteBuffer buffer )
{
- ShortBuffer byteBuffer =
- buffer.order( ByteOrder.LITTLE_ENDIAN ).asShortBuffer();
+ ShortBuffer byteBuffer = buffer.order( ByteOrder.LITTLE_ENDIAN ).asShortBuffer();
float[] samples = new float[ buffer.limit() / 2 ];
for( int x = 0; x < samples.length; x++ )
{
- samples[ x ] = (float)byteBuffer.get() / 32767.0f;
+ samples[ x ] = (float)byteBuffer.get() / (float)Short.MAX_VALUE;
}
return samples;
diff --git a/src/main/java/io/github/dsheirer/sample/complex/Complex.java b/src/main/java/io/github/dsheirer/sample/complex/Complex.java
index e9a7fdc0..93c15627 100644
--- a/src/main/java/io/github/dsheirer/sample/complex/Complex.java
+++ b/src/main/java/io/github/dsheirer/sample/complex/Complex.java
@@ -1,17 +1,17 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 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
******************************************************************************/
@@ -27,407 +27,424 @@ import java.io.Serializable;
*/
public class Complex implements Serializable
{
- private final static Logger mLog =
- LoggerFactory.getLogger( Complex.class );
+ private final static Logger mLog = LoggerFactory.getLogger(Complex.class);
- private static final long serialVersionUID = 1L;
+ private static final long serialVersionUID = 1L;
private float mLeft;
- private float mRight;
-
- public Complex( float left, float right )
- {
- mLeft = left;
- mRight = right;
- }
-
- public Complex()
- {
- this( 0.0f, 0.0f );
- }
-
- public void setInphase( float inphase )
- {
- mLeft = inphase;
- }
-
- public void setQuadrature( float quadrature )
- {
- mRight = quadrature;
- }
-
- public void setValues( float inphase, float quadrature )
- {
- mLeft = inphase;
- mRight = quadrature;
- }
+ private float mRight;
- public Complex copy()
- {
- return new Complex( mLeft, mRight );
- }
-
- public String toString()
- {
- StringBuilder sb = new StringBuilder();
-
- sb.append( "I:" );
- sb.append( mLeft );
- sb.append( " Q:" );
- sb.append( mRight );
-
- return sb.toString();
- }
-
- /**
- * Returns a new sample representing the conjugate of this one
- */
- public Complex conjugate()
- {
- return new Complex( mLeft, -mRight );
- }
+ public Complex(float left, float right)
+ {
+ mLeft = left;
+ mRight = right;
+ }
- /**
- * Multiplies this sample by the scalor value
- */
- public void multiply( float scalor )
- {
- mLeft *= scalor;
- mRight *= scalor;
- }
-
- public static Complex multiply( Complex sample, float scalor )
- {
- return new Complex( sample.left() * scalor,
- sample.right() * scalor );
- }
-
- /**
- * Calculates the inphase component of multiplying two complex numbers, A and B.
- */
- public static float multiplyInphase( float inphaseA, float quadratureA, float inphaseB, float quadratureB )
- {
- return ( inphaseA * inphaseB ) - ( quadratureA * quadratureB );
- }
-
- /**
- * Calculates the quadrature component of multiplying two complex numbers, A and B.
- */
- public static float multiplyQuadrature( float inphaseA, float quadratureA, float inphaseB, float quadratureB )
- {
- return ( quadratureA * inphaseB ) + ( inphaseA * quadratureB );
- }
-
- /**
- * Multiplies this sample by the multiplier sample
- */
- public void multiply( Complex multiplier )
- {
- float inphase = multiplyInphase( inphase(), quadrature(),
- multiplier.inphase(), multiplier.quadrature() );
- float quadrature = multiplyQuadrature( inphase(), quadrature(),
- multiplier.inphase(), multiplier.quadrature() );
-
- mLeft = inphase;
- mRight = quadrature;
- }
-
- /**
- * Multiplies both samples returning a new sample with the results
- */
- public static Complex multiply( Complex sample1, Complex sample2 )
- {
- float inphase = multiplyInphase( sample1.inphase(), sample1.quadrature(),
- sample2.inphase(), sample2.quadrature() );
- float quadrature = multiplyQuadrature( sample1.inphase(), sample1.quadrature(),
- sample2.inphase(), sample2.quadrature() );
+ public Complex()
+ {
+ this(0.0f, 0.0f);
+ }
- return new Complex( inphase, quadrature );
- }
+ public void setInphase(float inphase)
+ {
+ mLeft = inphase;
+ }
- public static Complex multiply( Complex sample,
- Float inphase, Float quadrature )
- {
- float i = multiplyInphase( sample.inphase(), sample.quadrature(),
- inphase, quadrature );
- float q = multiplyQuadrature( sample.inphase(), sample.quadrature(),
- inphase, quadrature );
-
- return new Complex( i, q );
- }
-
- public static Complex multiply( Float inphase, Float quadrature, Complex sample )
- {
- float i = multiplyInphase( inphase, quadrature, sample.inphase(), sample.quadrature() );
- float q = multiplyQuadrature( inphase, quadrature, sample.inphase(), sample.quadrature() );
-
- return new Complex( i, q );
- }
+ public void setQuadrature(float quadrature)
+ {
+ mRight = quadrature;
+ }
- public static Complex multiply( float inphaseA, float quadratureA, float inphaseB, float quadratureB )
- {
- float i = multiplyInphase( inphaseA, quadratureA, inphaseB, quadratureB );
- float q = multiplyQuadrature( inphaseA, quadratureA, inphaseB, quadratureB );
-
- return new Complex( i, q );
- }
-
- /**
- * Adds the adder sample value to this sample
- */
- public void add( Complex adder )
- {
- mLeft += adder.left();
- mRight += adder.right();
- }
+ public void setValues(float inphase, float quadrature)
+ {
+ mLeft = inphase;
+ mRight = quadrature;
+ }
- /**
- * Adds the two complex samples returning a new complex sample with the result
- */
- public static Complex add( Complex first, Complex second )
- {
- return new Complex( first.left() + second.left(),
- first.right() + second.right() );
- }
+ public void setValues(Complex sampleToCopy)
+ {
+ mLeft = sampleToCopy.left();
+ mRight = sampleToCopy.right();
+ }
- /**
- * Adds the two complex samples returning a new complex sample with the result
- */
- public static Complex subtract( Complex first, Complex second )
- {
- return new Complex( first.left() - second.left(),
- first.right() - second.right() );
- }
+ public Complex copy()
+ {
+ return new Complex(mLeft, mRight);
+ }
- /**
- * Magnitude of this sample
- */
- public float magnitude()
- {
- return (float)Math.sqrt( magnitudeSquared() );
- }
-
- public static float magnitude( float inphase, float quadrature )
- {
- return (float)Math.sqrt( ( inphase * inphase ) + ( quadrature * quadrature ) );
- }
+ public String toString()
+ {
+ StringBuilder sb = new StringBuilder();
- /**
- * Magnitude squared of this sample
- */
- public float magnitudeSquared()
- {
- return norm();
- }
+ sb.append("I:");
+ sb.append(mLeft);
+ sb.append(" Q:");
+ sb.append(mRight);
- /**
- * Norm of this sample = ( i * i ) + ( q * q )
- */
- public float norm()
- {
- return (float)( ( inphase() * inphase() ) +
- ( quadrature() * quadrature() ) );
- }
-
- /**
- * Returns the vector length to 1 (unit circle)
- */
- public void normalize()
- {
- float magnitude = magnitude();
-
- if( magnitude != 0 )
- {
- multiply( (float)( 1.0f / magnitude() ) );
- }
- }
+ return sb.toString();
+ }
- /**
- * Returns the vector length to 1 (unit circle),
- * avoiding square root multiplication
- */
- public void fastNormalize()
- {
- multiply( (float)( 1.95f - magnitudeSquared() ) );
- }
-
- public float left()
- {
- return mLeft;
- }
-
- public float right()
- {
- return mRight;
- }
+ /**
+ * Returns a new sample representing the conjugate of this one
+ */
+ public Complex conjugate()
+ {
+ return new Complex(mLeft, -mRight);
+ }
- public float inphase()
- {
- return mLeft;
- }
-
- /**
- * Absolute value of in-phase component
- */
- public float inPhaseAbsolute()
- {
- return Math.abs( mLeft );
- }
+ /**
+ * Multiplies this sample by the scalor value
+ */
+ public void multiply(float scalor)
+ {
+ mLeft *= scalor;
+ mRight *= scalor;
+ }
- public float quadrature()
- {
- return mRight;
- }
-
- /**
- * Absolute value of quadrature component
- */
- public float quadratureAbsolute()
- {
- return Math.abs( mRight );
- }
+ public static Complex multiply(Complex sample, float scalor)
+ {
+ return new Complex(sample.left() * scalor,
+ sample.right() * scalor);
+ }
- public float x()
- {
- return mLeft;
- }
-
- public float y()
- {
- return mRight;
- }
-
- public float real()
- {
- return mLeft;
- }
+ /**
+ * Calculates the inphase component of multiplying two complex numbers, A and B.
+ */
+ public static float multiplyInphase(float inphaseA, float quadratureA, float inphaseB, float quadratureB)
+ {
+ return (inphaseA * inphaseB) - (quadratureA * quadratureB);
+ }
- /**
- * Absolute value of real component
- */
- public float realAbsolute()
- {
- return Math.abs( mLeft );
- }
+ /**
+ * Calculates the quadrature component of multiplying two complex numbers, A and B.
+ */
+ public static float multiplyQuadrature(float inphaseA, float quadratureA, float inphaseB, float quadratureB)
+ {
+ return (quadratureA * inphaseB) + (inphaseA * quadratureB);
+ }
- public float imaginary()
- {
- return mRight;
- }
+ /**
+ * Multiplies this sample by the multiplier sample
+ */
+ public void multiply(Complex multiplier)
+ {
+ float inphase = multiplyInphase(inphase(), quadrature(),
+ multiplier.inphase(), multiplier.quadrature());
+ float quadrature = multiplyQuadrature(inphase(), quadrature(),
+ multiplier.inphase(), multiplier.quadrature());
- /**
- * Absolute value of imaginary component
- */
- public float imaginaryAbsolute()
- {
- return Math.abs( mRight );
- }
+ mLeft = inphase;
+ mRight = quadrature;
+ }
- /**
- * Returns the greater absolute value between left and right values
- */
- public float maximumAbsolute()
- {
- if( Math.abs( mLeft ) > Math.abs( mRight ) )
- {
- return Math.abs( mLeft );
- }
- else
- {
- return Math.abs( mRight );
- }
- }
+ /**
+ * Multiplies both samples returning a new sample with the results
+ */
+ public static Complex multiply(Complex sample1, Complex sample2)
+ {
+ float inphase = multiplyInphase(sample1.inphase(), sample1.quadrature(),
+ sample2.inphase(), sample2.quadrature());
+ float quadrature = multiplyQuadrature(sample1.inphase(), sample1.quadrature(),
+ sample2.inphase(), sample2.quadrature());
- /**
- * Creates a new complex sample representing the angle with unit circle
- * magnitude
- * @param angle in radians
- * @return
- */
- public static Complex fromAngle( float angle )
- {
- return new Complex( (float)Math.cos( angle ),
- (float)Math.sin( angle ) );
- }
+ return new Complex(inphase, quadrature);
+ }
- /**
- * Angle of this sample in radians
- */
- public float angle()
- {
- return (float)Math.atan2( y(), x() );
- }
+ public static Complex multiply(Complex sample,
+ Float inphase, Float quadrature)
+ {
+ float i = multiplyInphase(sample.inphase(), sample.quadrature(),
+ inphase, quadrature);
+ float q = multiplyQuadrature(sample.inphase(), sample.quadrature(),
+ inphase, quadrature);
- /**
- * Constrains the i and q quantities to +/- value.
- *
- * @param value - maximum absolute value
- */
- public void clip( float value )
- {
- if( mLeft > value )
- {
- mLeft = value;
- }
- else if( mLeft < -value )
- {
- mLeft = -value;
- }
-
- if( mRight > value )
- {
- mRight = value;
- }
- else if( mRight < -value )
- {
- mRight = -value;
- }
- }
+ return new Complex(i, q);
+ }
- /**
- * Angle of this sample in degrees
- */
- public float polarAngle()
- {
- return (float)Math.toDegrees( angle() );
- }
+ public static Complex multiply(Float inphase, Float quadrature, Complex sample)
+ {
+ float i = multiplyInphase(inphase, quadrature, sample.inphase(), sample.quadrature());
+ float q = multiplyQuadrature(inphase, quadrature, sample.inphase(), sample.quadrature());
- /**
- * Provides an approximate magnitude value for this sample.
- */
- public float envelope()
- {
- return envelope( mLeft, mRight );
- }
-
- public static float envelope( float inphase, float quadrature )
- {
- float inphaseAbsolute = Math.abs( inphase );
- float quadratureAbsolute = Math.abs( quadrature );
-
- if( inphaseAbsolute > quadratureAbsolute )
- {
- return inphaseAbsolute + ( 0.4f * quadratureAbsolute );
- }
- else
- {
- return quadratureAbsolute + ( 0.4f * inphaseAbsolute );
- }
- }
-
- public static void main( String[] args )
- {
- double angle = Math.PI; //180 degrees
-
- Complex s1 = new Complex( (float)Math.sin( angle ), (float)Math.cos( angle ) );
+ return new Complex(i, q);
+ }
- Complex tap = new Complex( 1.0f, -0.1f );
+ public static Complex multiply(float inphaseA, float quadratureA, float inphaseB, float quadratureB)
+ {
+ float i = multiplyInphase(inphaseA, quadratureA, inphaseB, quadratureB);
+ float q = multiplyQuadrature(inphaseA, quadratureA, inphaseB, quadratureB);
- Complex convolved = Complex.multiply( s1, tap );
-
- mLog.debug( "s: " + s1.toString() +
- " t: " + tap.toString() +
- " convolved: " + convolved.toString() );
-
- }
+ return new Complex(i, q);
+ }
+
+ /**
+ * Adds the adder sample value to this sample
+ */
+ public void add(Complex adder)
+ {
+ mLeft += adder.left();
+ mRight += adder.right();
+ }
+
+ /**
+ * Adds the two complex samples returning a new complex sample with the result
+ */
+ public static Complex add(Complex first, Complex second)
+ {
+ return new Complex(first.left() + second.left(),
+ first.right() + second.right());
+ }
+
+ /**
+ * Adds the two complex samples returning a new complex sample with the result
+ */
+ public static Complex subtract(Complex first, Complex second)
+ {
+ return new Complex(first.left() - second.left(),
+ first.right() - second.right());
+ }
+
+ /**
+ * Magnitude of this sample
+ */
+ public float magnitude()
+ {
+ return (float)Math.sqrt(magnitudeSquared());
+ }
+
+ public static float magnitude(float inphase, float quadrature)
+ {
+ return (float)Math.sqrt((inphase * inphase) + (quadrature * quadrature));
+ }
+
+ /**
+ * Magnitude squared of this sample
+ */
+ public float magnitudeSquared()
+ {
+ return norm();
+ }
+
+ /**
+ * Norm of this sample = ( i * i ) + ( q * q )
+ */
+ public float norm()
+ {
+ return (float)((inphase() * inphase()) +
+ (quadrature() * quadrature()));
+ }
+
+ /**
+ * Returns the vector length to 1 (unit circle)
+ */
+ public void normalize()
+ {
+ float magnitude = magnitude();
+
+ if(magnitude != 0)
+ {
+ multiply((float)(1.0f / magnitude()));
+ }
+ }
+
+ /**
+ * Returns the vector length to 1 (unit circle),
+ * avoiding square root multiplication
+ */
+ public void fastNormalize()
+ {
+ multiply((float)(1.95f - magnitudeSquared()));
+ }
+
+ public float left()
+ {
+ return mLeft;
+ }
+
+ public float right()
+ {
+ return mRight;
+ }
+
+ public float inphase()
+ {
+ return mLeft;
+ }
+
+ /**
+ * Absolute value of in-phase component
+ */
+ public float inPhaseAbsolute()
+ {
+ return Math.abs(mLeft);
+ }
+
+ public float quadrature()
+ {
+ return mRight;
+ }
+
+ /**
+ * Absolute value of quadrature component
+ */
+ public float quadratureAbsolute()
+ {
+ return Math.abs(mRight);
+ }
+
+ public float x()
+ {
+ return mLeft;
+ }
+
+ public float y()
+ {
+ return mRight;
+ }
+
+ public float real()
+ {
+ return mLeft;
+ }
+
+ /**
+ * Absolute value of real component
+ */
+ public float realAbsolute()
+ {
+ return Math.abs(mLeft);
+ }
+
+ public float imaginary()
+ {
+ return mRight;
+ }
+
+ /**
+ * Absolute value of imaginary component
+ */
+ public float imaginaryAbsolute()
+ {
+ return Math.abs(mRight);
+ }
+
+ /**
+ * Returns the greater absolute value between left and right values
+ */
+ public float maximumAbsolute()
+ {
+ if(Math.abs(mLeft) > Math.abs(mRight))
+ {
+ return Math.abs(mLeft);
+ }
+ else
+ {
+ return Math.abs(mRight);
+ }
+ }
+
+ /**
+ * Creates a new complex sample representing the angle with unit circle
+ * magnitude
+ *
+ * @param angle in radians
+ * @return
+ */
+ public static Complex fromAngle(double angle)
+ {
+ return new Complex((float)Math.cos(angle), (float)Math.sin(angle));
+ }
+
+ /**
+ * Sets this complex instance with the specified angle in radians.
+ * @param angle in radians
+ */
+ public void setAngle(double angle)
+ {
+ setInphase((float)Math.cos(angle));
+ setQuadrature((float)Math.sin(angle));
+ }
+
+ /**
+ * Angle of this sample in radians
+ */
+ public float angle()
+ {
+ return (float)Math.atan2(y(), x());
+ }
+
+ public float angleDegrees()
+ {
+ return (float)Math.toDegrees(angle());
+ }
+
+ /**
+ * Constrains the i and q quantities to +/- value.
+ *
+ * @param value - maximum absolute value
+ */
+ public void clip(float value)
+ {
+ if(mLeft > value)
+ {
+ mLeft = value;
+ }
+ else if(mLeft < -value)
+ {
+ mLeft = -value;
+ }
+
+ if(mRight > value)
+ {
+ mRight = value;
+ }
+ else if(mRight < -value)
+ {
+ mRight = -value;
+ }
+ }
+
+ /**
+ * Angle of this sample in degrees
+ */
+ public float polarAngle()
+ {
+ return (float)Math.toDegrees(angle());
+ }
+
+ /**
+ * Provides an approximate magnitude value for this sample.
+ */
+ public float envelope()
+ {
+ return envelope(mLeft, mRight);
+ }
+
+ public static float envelope(float inphase, float quadrature)
+ {
+ float inphaseAbsolute = Math.abs(inphase);
+ float quadratureAbsolute = Math.abs(quadrature);
+
+ if(inphaseAbsolute > quadratureAbsolute)
+ {
+ return inphaseAbsolute + (0.4f * quadratureAbsolute);
+ }
+ else
+ {
+ return quadratureAbsolute + (0.4f * inphaseAbsolute);
+ }
+ }
+
+ public static void main(String[] args)
+ {
+ Complex ROTATE_FROM_MINUS_45 = Complex.fromAngle(1.0 * Math.PI / 4.0);
+ Complex ROTATE_FROM_PLUS_135 = Complex.fromAngle(-3.0 * Math.PI / 4.0);
+ mLog.debug("REF: " + ROTATE_FROM_MINUS_45.toString());
+
+ Complex symbol = new Complex(0.7665009f, -0.6422432f);
+ mLog.debug("SYM: " + symbol.toString());
+
+ symbol.multiply(ROTATE_FROM_MINUS_45);
+ mLog.debug("DER: " + symbol.toString());
+ }
}
diff --git a/src/main/java/io/github/dsheirer/source/IControllableFileSource.java b/src/main/java/io/github/dsheirer/source/IControllableFileSource.java
index 13270141..59868d66 100644
--- a/src/main/java/io/github/dsheirer/source/IControllableFileSource.java
+++ b/src/main/java/io/github/dsheirer/source/IControllableFileSource.java
@@ -1,3 +1,18 @@
+/*******************************************************************************
+ * sdr-trunk
+ * Copyright (C) 2014-2018 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with this program.
+ * If not, see
+ *
+ ******************************************************************************/
package io.github.dsheirer.source;
import javax.sound.sampled.UnsupportedAudioFileException;
@@ -10,22 +25,24 @@ import java.io.IOException;
*/
public interface IControllableFileSource
{
- /**
- * Opens the file source
- */
- public void open() throws IOException, UnsupportedAudioFileException;
-
- public void close() throws IOException;
-
- public File getFile();
-
- public void next( int frames ) throws IOException;
-
- public void next( int frames, boolean broadcast ) throws IOException;
-
- public long getFrameCount() throws IOException;
-
- public void setListener( IFrameLocationListener listener );
-
- public void removeListener( IFrameLocationListener listener );
+ /**
+ * Opens the file source
+ */
+ public void open() throws IOException, UnsupportedAudioFileException;
+
+ public void close() throws IOException;
+
+ public File getFile();
+
+ public void next(int frames) throws IOException;
+
+ public void next(int frames, boolean broadcast) throws IOException;
+
+ public long getFrameCount() throws IOException;
+
+ public int getSampleRate();
+
+ public void setListener(IFrameLocationListener listener);
+
+ public void removeListener(IFrameLocationListener listener);
}
diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerChannelSource.java b/src/main/java/io/github/dsheirer/source/tuner/TunerChannelSource.java
index c577789e..6804d8d7 100644
--- a/src/main/java/io/github/dsheirer/source/tuner/TunerChannelSource.java
+++ b/src/main/java/io/github/dsheirer/source/tuner/TunerChannelSource.java
@@ -469,6 +469,7 @@ public class TunerChannelSource extends ComplexSource implements IFrequencyChang
{
//Frequency correction requests are the only change requests supported from downstream components
case REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE:
+ mLog.debug("Got a freq corr change request: " + event.getValue().intValue());
setFrequencyCorrection(event.getValue().intValue());
break;
}
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 8ece68b6..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;
}
/**
@@ -239,8 +234,7 @@ public class ComplexWaveSource extends ComplexSource implements IControllableFil
buffer = Arrays.copyOf(buffer, samplesRead);
}
- float[] samples = ConversionUtils
- .convertFromSigned16BitSamples(buffer);
+ float[] samples = ConversionUtils.convertFromSigned16BitSamples(buffer);
mListener.receive(new ComplexBuffer(samples));
}