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 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 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)); }