diff --git a/src/main/java/io/github/dsheirer/buffer/DoubleCircularBuffer.java b/src/main/java/io/github/dsheirer/buffer/DoubleCircularBuffer.java
index 0c9172fe..af5bd86d 100644
--- a/src/main/java/io/github/dsheirer/buffer/DoubleCircularBuffer.java
+++ b/src/main/java/io/github/dsheirer/buffer/DoubleCircularBuffer.java
@@ -1,80 +1,168 @@
/*******************************************************************************
* 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.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 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);
+ }
+
+ /**
+ * 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/dsp/filter/design/FilterViewer.java b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java
index e2e7d9ca..c129a26d 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)
+ .sampleRate((int)48000.0)
+ .passBandCutoff(6000)
.passBandAmplitude(1.0)
- .passBandRipple(0.01)
- .stopBandStart(4000)
+ .passBandRipple(0.1)
.stopBandAmplitude(0.0)
- .stopBandRipple(0.008)
+ .stopBandStart(6250)
+ .stopBandRipple(0.01)
.build();
// FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder()
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/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/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/DQPSKSymbolPhaseErrorCalculator.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolPhaseErrorCalculator.java
new file mode 100644
index 00000000..c299d122
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolPhaseErrorCalculator.java
@@ -0,0 +1,30 @@
+/*******************************************************************************
+ * 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 DQPSKSymbolPhaseErrorCalculator extends QPSKSymbolPhaseErrorCalculator
+{
+ //Negative 45 degrees rotation to offset for differential additive 45 degrees of rotation
+ public static final Complex DIFFERENTIAL_OFFSET = Complex.fromAngle(Math.PI / -4.0d);
+
+ @Override
+ public void adjust(Complex symbol)
+ {
+ symbol.multiply(DIFFERENTIAL_OFFSET);
+ }
+}
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..023b3e87
--- /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 = (previous.inphase() - current.inphase()) * middle.inphase();
+ mErrorQuadrature = (previous.quadrature() - current.quadrature()) * 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/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..5b5df5a9
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/ISymbolPhaseErrorCalculator.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;
+
+import io.github.dsheirer.sample.complex.Complex;
+
+public interface ISymbolPhaseErrorCalculator
+{
+ /**
+ * Adjusts the calculated symbol as necessary before calculating any phase error. This might be useful for
+ * differential encoding to remove a 45 degree rotational spin.
+ * @param symbol to adjust
+ */
+ void adjust(Complex symbol);
+
+ /**
+ * 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/InterpolatingSymbolBuffer.java b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer.java
new file mode 100644
index 00000000..c67a2b9a
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer.java
@@ -0,0 +1,187 @@
+/*******************************************************************************
+ * 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 InterpolatingSymbolBuffer
+{
+ private Complex mMiddleSample = new Complex(0,0);
+ private Complex mCurrentSample = new Complex(0,0);
+
+ private float[] mDelayLineInphase;
+ private float[] mDelayLineQuadrature;
+ private float mMu; //Sample Counter
+ private float mGainMu = 0.05f; //Sample Counter Gain
+ private float mOmega;
+ private float mGainOmega = 0.1f * mGainMu * mGainMu;
+ private float mOmegaRel = 0.005f;
+ private float mOmegaMid;
+
+ private int mDelayLinePointer = 0;
+ private int mTwiceSamplesPerSymbol;
+ private RealInterpolator mInterpolator = new RealInterpolator(1.0f);
+
+ public InterpolatingSymbolBuffer(float samplesPerSymbol)
+ {
+ mMu = samplesPerSymbol;
+ mTwiceSamplesPerSymbol = (int)Math.floor(2.0 * samplesPerSymbol);
+ mDelayLineInphase = new float[2 * mTwiceSamplesPerSymbol];
+ mDelayLineQuadrature = new float[2 * mTwiceSamplesPerSymbol];
+ mOmega = samplesPerSymbol;
+ mOmegaMid = samplesPerSymbol;
+
+ }
+
+ public void receive(Complex currentSample)
+ {
+ mMu--;
+
+ //Fill up the delay line to use with the interpolator
+ mDelayLineInphase[mDelayLinePointer] = currentSample.inphase();
+ mDelayLineInphase[mDelayLinePointer + mTwiceSamplesPerSymbol] = currentSample.inphase();
+ mDelayLineQuadrature[mDelayLinePointer] = currentSample.quadrature();
+ mDelayLineQuadrature[mDelayLinePointer + mTwiceSamplesPerSymbol] = currentSample.quadrature();
+
+ //Increment pointer and keep pointer in bounds
+ mDelayLinePointer = (mDelayLinePointer + 1) % 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)
+ {
+ mMu += 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)
+ {
+ //mOmega is samples per symbol and is constrained to floating between +/- .005 of the nominal samples per
+ //symbol
+ mOmega = mOmega + mGainOmega * symbolTimingError;
+ mOmega = mOmegaMid + clip(mOmega - mOmegaMid, mOmegaRel);
+
+ //Add another symbol's worth of samples to the counter and adjust timing based on gardner error
+ increaseSampleCounter((mOmega + (mGainMu * symbolTimingError)));
+ }
+
+ /**
+ * Indicates if this buffer has accumulated a enough samples to represent a full symbol
+ */
+ public boolean hasSymbol()
+ {
+ return mMu < 1.0f;
+ }
+
+ /**
+ * 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 getMiddleSample()
+ {
+ /* Calculate interpolated middle sample and current sample */
+ mMiddleSample.setValues(getInphase(mMu), getQuadrature(mMu));
+
+ return mMiddleSample;
+ }
+
+ /**
+ * Interpolated current sample for the symbol. This sample represents the ideal symbol sampling point.
+ * Note: this method should only be invoked after testing for a complete symbol with the hasSymbol() method.
+ */
+ public Complex getCurrentSample()
+ {
+ float half_omega = mOmega / 2.0f;
+ int half_sps = (int)Math.floor(half_omega);
+ float half_mu = mMu + half_omega - half_sps;
+
+ if(half_mu > 1.0)
+ {
+ half_mu -= 1.0;
+ half_sps += 1;
+ }
+
+ mCurrentSample.setValues(getInphase(half_mu, half_sps), getQuadrature(half_mu, half_sps));
+
+ 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, interpolation);
+ }
+
+ /**
+ * 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, int indexOffet)
+ {
+ return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer + indexOffet, 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, 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, int indexOffset)
+ {
+ return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer + indexOffset, interpolation);
+ }
+
+ /**
+ * 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/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..aba3a907 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,395 @@ 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 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/QPSKDemodulator.java b/src/main/java/io/github/dsheirer/dsp/psk/QPSKDemodulator.java
new file mode 100644
index 00000000..91d1ce76
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/QPSKDemodulator.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;
+
+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;
+
+public class QPSKDemodulator implements ComplexSampleListener
+{
+ private InterpolatingSymbolBuffer mInterpolatingSymbolBuffer;
+ private IPhaseLockedLoop mPLL;
+ private ISymbolPhaseErrorCalculator mSymbolPhaseErrorCalculator;
+ private IQPSKSymbolDecoder mQPSKSymbolDecoder;
+ private GardnerDetector mGardnerDetector = new GardnerDetector();
+ private Listener mSymbolListener;
+ private Listener mPLLErrorListener;
+ private Listener mDibitListener;
+
+ private Complex mPreviousSample = new Complex(0, 0);
+ private Complex mPreviousMiddleSample = new Complex(0, 0);
+ private Complex mCurrentSample = new Complex(0, 0);
+ private Complex mMiddleSample = new Complex(0, 0);
+ private Complex mPreviousSymbol = new Complex(0, 0);
+ private Complex mMiddleSymbol = new Complex(0, 0);
+ private Complex mCurrentSymbol = new Complex(0, 0);
+ private float mPhaseError;
+ private float mSymbolTimingError;
+
+ /**
+ * Decoder for Quaternary Phase Shift Keying (QPSK) and Differential QPSK (DQPSK). This decoder uses both a
+ * phase-locked loop and a gardner symbol timing error detector to automatically align to the incoming carrier
+ * frequency and to adjust for any changes in symbol timing.
+ *
+ * @param phaseLockedLoop for tracking carrier frequency error
+ * @param symbolPhaseErrorCalculator to calculate symbol phase errors and optionally adjust for differential encoding
+ * @param symbolDecoder to decode demodulated symbols into dibits
+ * @param sampleRate of the incoming complex sample stream
+ * @param symbolRate of the decoded QPSK symbols.
+ */
+ public QPSKDemodulator(IPhaseLockedLoop phaseLockedLoop, ISymbolPhaseErrorCalculator symbolPhaseErrorCalculator,
+ IQPSKSymbolDecoder symbolDecoder, double sampleRate, double symbolRate)
+ {
+ if(sampleRate < (symbolRate * 2))
+ {
+ throw new IllegalArgumentException("Sample rate [" + sampleRate +
+ "] must be at least 2 x symbol rate [" + symbolRate + "]");
+ }
+
+ mPLL = phaseLockedLoop;
+ mSymbolPhaseErrorCalculator = symbolPhaseErrorCalculator;
+ mQPSKSymbolDecoder = symbolDecoder;
+
+ float samplesPerSymbol = (float)(sampleRate / symbolRate);
+ mInterpolatingSymbolBuffer = new InterpolatingSymbolBuffer(samplesPerSymbol);
+ }
+
+ /**
+ * Submits a 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]);
+ }
+ }
+
+ /**
+ * Submits a complex sample for decoding.
+ * @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
+ mCurrentSample.setValues(inphase, quadrature);
+
+ //Mix current sample with costas loop to remove any rotation that is present from a mis-tuned carrier frequency
+ mCurrentSample.multiply(mPLL.incrementAndGetCurrentVector());
+
+ mInterpolatingSymbolBuffer.receive(mCurrentSample);
+
+ //Calculate the symbol once we've stored enough samples
+ if(mInterpolatingSymbolBuffer.hasSymbol())
+ {
+ //Get middle and current samples from the interpolating buffer
+ mMiddleSample = mInterpolatingSymbolBuffer.getMiddleSample();
+ mCurrentSample = mInterpolatingSymbolBuffer.getCurrentSample();
+
+ //Calculate middle and current symbols as the delta between the previous and current samples
+ mMiddleSymbol.setInphase(Complex.multiplyInphase(mMiddleSample.inphase(), mMiddleSample.quadrature(), mPreviousMiddleSample.inphase(), -mPreviousMiddleSample.quadrature()));
+ mMiddleSymbol.setQuadrature(Complex.multiplyQuadrature(mMiddleSample.inphase(), mMiddleSample.quadrature(), mPreviousMiddleSample.inphase(), -mPreviousMiddleSample.quadrature()));
+ mCurrentSymbol.setInphase(Complex.multiplyInphase(mCurrentSample.inphase(), mCurrentSample.quadrature(), mPreviousSample.inphase(), -mPreviousSample.quadrature()));
+ mCurrentSymbol.setQuadrature(Complex.multiplyQuadrature(mCurrentSample.inphase(), mCurrentSample.quadrature(), mPreviousSample.inphase(), -mPreviousSample.quadrature()));
+
+ //Set gain to unity before we calculate the error value
+ mMiddleSymbol.normalize();
+ mCurrentSymbol.normalize();
+
+ //Symbol timing error calculations
+ mSymbolTimingError = mGardnerDetector.getError(mPreviousSymbol, mMiddleSymbol, mCurrentSymbol);
+ mInterpolatingSymbolBuffer.resetAndAdjust(mSymbolTimingError);
+
+ //Store current samples/symbols to use for the next period
+ mPreviousSample.setValues(mCurrentSample);
+ mPreviousMiddleSample.setValues(mMiddleSample);
+ mPreviousSymbol.setValues(mCurrentSymbol);
+
+ //Adjust the current symbol as necessary (e.g. for differential encoding)
+ mSymbolPhaseErrorCalculator.adjust(mCurrentSymbol);
+
+ //Calculate the phase error of the current symbol relative to the expected constellation and provide
+ //feedback to the PLL
+ mPhaseError = mSymbolPhaseErrorCalculator.getPhaseError(mCurrentSymbol);
+ mPLL.adjust(mPhaseError);
+
+ if(mPLLErrorListener != null)
+ {
+ mPLLErrorListener.receive((double)mPhaseError);
+ }
+
+ //Decode the dibit from the symbol and send to the listener
+ if(mDibitListener != null)
+ {
+ mDibitListener.receive(mQPSKSymbolDecoder.decode(mCurrentSymbol));
+ }
+
+ //Send to an external constellation symbol listener when registered
+ if(mSymbolListener != null)
+ {
+ mSymbolListener.receive(mCurrentSymbol);
+ }
+
+ //TODO: Assemble dibits here and broadcast
+ }
+ }
+
+ /**
+ * Registers the listener to receive decoded QPSK symbols
+ */
+ public void setSymbolListener(Listener listener)
+ {
+ mSymbolListener = listener;
+ }
+
+ /**
+ * Registers the listener to receive decoded QPSK dibits
+ */
+ public void setDibitListener(Listener listener)
+ {
+ mDibitListener = listener;
+ }
+
+ /**
+ * Registers the listener to receive PLL error values
+ */
+ public void setPLLErrorListener(Listener listener)
+ {
+ mPLLErrorListener = listener;
+ }
+}
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..d7c94323
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/QPSKSymbolPhaseErrorCalculator.java
@@ -0,0 +1,63 @@
+/*******************************************************************************
+ * 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 void adjust(Complex symbol)
+ {
+ //No-op - since there is no differential, we don't have to adjust
+ }
+
+ @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/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..71d07392
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/pll/CostasLoop.java
@@ -0,0 +1,337 @@
+/*******************************************************************************
+ * 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.buffer.DoubleCircularBuffer;
+import io.github.dsheirer.dsp.mixer.Oscillator;
+import io.github.dsheirer.sample.complex.Complex;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.text.DecimalFormat;
+
+/**
+ * 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
+{
+ private final static Logger mLog = LoggerFactory.getLogger(CostasLoop.class);
+ public static final double TWO_PI = 2.0 * Math.PI;
+ private DoubleCircularBuffer mErrorBuffer = new DoubleCircularBuffer(10);
+ private double mDamping = Math.sqrt(2.0) / 2.0;
+ private double mAlphaGain;
+ private double mBetaGain;
+ private double mLoopPhase = 0.0;
+ private double mLoopFrequency = 0.0;
+ private double mMaximumLoopFrequency;
+ private Complex mCurrentVector = new Complex(0, 0);
+
+ private Tracking mTracking = Tracking.SEARCHING;
+ private ITrackingStateListener mTrackingStateListener;
+ private boolean mAutomaticTrackingEnabled = true;
+
+ /**
+ * 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;
+
+ if(mLoopFrequency > mMaximumLoopFrequency)
+ {
+ mLoopFrequency -= 2.0 * mMaximumLoopFrequency;
+ }
+
+ if(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();
+
+ mLog.debug("Tracking: " + tracking.name());
+
+ if(mTrackingStateListener != null)
+ {
+ mTrackingStateListener.trackingStateChanged(tracking);
+ }
+ }
+
+ /**
+ * Sets the listener to receive notifications when the tracking state changes
+ */
+ public void setTrackingStateListener(ITrackingStateListener listener)
+ {
+ mTrackingStateListener = listener;
+ }
+
+ /**
+ * Enables or disables automatic tracking state updates. When enabled, the loop will automatically increase or
+ * decrease the loop alpha/beta gain values based on the variance of the error tracking values submitted to the
+ * loop control. As the variance decreases, so will the gain values, and vice-versa. Setting this to disabled
+ * will force the tracker to retain the gain values associated with either the default or user-specified tracking
+ * value.
+ *
+ * @param enabled true (default) to enable automatic gain tracking
+ */
+ public void setAutomaticTracking(boolean enabled)
+ {
+ mAutomaticTrackingEnabled = enabled;
+ }
+
+ /**
+ * Increments the phase of the loop for each sample received at the sample rate.
+ */
+ public void increment()
+ {
+ mLoopPhase += mLoopFrequency;
+
+ /* Keep the loop phase in bounds */
+ normalizePhase();
+ }
+
+ /**
+ * 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;
+ }
+
+ public double getMaximumLoopFrequency()
+ {
+ return mMaximumLoopFrequency;
+ }
+
+ /**
+ * 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);
+
+ /* Maintain phase between +/- 2 * PI */
+ normalizePhase();
+
+ /* Limit frequency to +/- maximum loop frequency */
+ limitFrequency();
+
+ mErrorBuffer.put(phaseError);
+
+ double variance = mErrorBuffer.variance();
+
+ switch(mTracking)
+ {
+ case SEARCHING:
+ if(Tracking.COARSE.meetsThreshold(variance))
+ {
+ setTracking(Tracking.COARSE);
+ }
+ break;
+ case COARSE:
+ if(Tracking.COARSE.meetsThreshold(variance))
+ {
+ if(Tracking.FINE.meetsThreshold(variance))
+ {
+ setTracking(Tracking.FINE);
+ }
+ }
+ else
+ {
+ setTracking(Tracking.SEARCHING);
+ }
+ break;
+ case FINE:
+ if(Tracking.FINE.meetsThreshold(variance))
+ {
+ if(Tracking.LOCKED.meetsThreshold(variance))
+ {
+ setTracking(Tracking.LOCKED);
+ }
+ }
+ else
+ {
+ setTracking(Tracking.COARSE);
+ }
+ break;
+ case LOCKED:
+ if(!Tracking.LOCKED.meetsThreshold(variance))
+ {
+ setTracking(Tracking.FINE);
+ }
+ break;
+ }
+ }
+
+ public double getErrorMean()
+ {
+ return mErrorBuffer.mean();
+ }
+
+ public double getErrorVariance()
+ {
+ return mErrorBuffer.variance();
+ }
+
+ /**
+ * Normalizes the phase tracker to maintain within +/- 2 Pi
+ */
+ private void normalizePhase()
+ {
+ while(mLoopPhase > TWO_PI)
+ {
+ mLoopPhase -= TWO_PI;
+ }
+
+ while(mLoopPhase < -TWO_PI)
+ {
+ mLoopPhase += TWO_PI;
+ }
+ }
+
+ /**
+ * Constrains the frequency within the bounds of +/- loop frequency
+ */
+ private void limitFrequency()
+ {
+ if(mLoopFrequency > mMaximumLoopFrequency)
+ {
+ mLoopFrequency = mMaximumLoopFrequency;
+ }
+
+ if(mLoopFrequency < -mMaximumLoopFrequency)
+ {
+ mLoopFrequency = -mMaximumLoopFrequency;
+ }
+ }
+
+ /**
+ * Tracking state listener interface for receiving updates when the tracking state of the costas loop changes
+ */
+ public interface ITrackingStateListener
+ {
+ void trackingStateChanged(Tracking tracking);
+ }
+
+ public static void main(String[] arguments)
+ {
+ DecimalFormat decimalFormat = new DecimalFormat("0.000000");
+ double sampleRate = 25000.0;
+ int errorFrequency = 411;
+ double loopBandwidth = 50.0;
+
+ Oscillator oscillator = new Oscillator(errorFrequency, (int)sampleRate);
+ CostasLoop costasLoop = new CostasLoop(sampleRate, 4800.0);
+ costasLoop.setTrackingStateListener(new ITrackingStateListener()
+ {
+ @Override
+ public void trackingStateChanged(Tracking tracking)
+ {
+ mLog.debug("Tracking State Changed: " + tracking.name());
+ }
+ });
+
+ for(int x = 0; x < 1000; x++)
+ {
+ Complex complex = oscillator.getComplex();
+
+ oscillator.rotate();
+ costasLoop.increment();
+
+ if(x % 5 == 0)
+ {
+ complex.multiply(costasLoop.getCurrentVector().conjugate());
+
+ double phaseError = complex.angle();
+ costasLoop.adjust(phaseError);
+
+ double loopFrequency = costasLoop.getLoopFrequency() * sampleRate / TWO_PI;
+ double maxFrequency = costasLoop.getMaximumLoopFrequency() * sampleRate / TWO_PI;
+
+ mLog.debug(x + " Frequency: " + errorFrequency +
+ " Loop:" + decimalFormat.format(loopFrequency) +
+ " Max:" + maxFrequency +
+ " Detected Error:" + decimalFormat.format(phaseError) +
+ " Avg:" + decimalFormat.format(costasLoop.getErrorMean()) +
+ " Var:" + decimalFormat.format(costasLoop.getErrorVariance()));
+ }
+ }
+ }
+
+}
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..1f4f2a52
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/pll/IPhaseLockedLoop.java
@@ -0,0 +1,56 @@
+/*******************************************************************************
+ * 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();
+
+
+}
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..5cabc7ab
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/pll/Tracking.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.pll;
+
+public enum Tracking
+{
+ SEARCHING(50.0, Math.PI),
+ COARSE(100.0, 0.01),
+ FINE(150.0, 0.001),
+ LOCKED(200.0, 0.0001);
+
+ private double mLoopBandwidth;
+ private double mVarianceThreshold;
+
+ Tracking(double loopBandwidth, double varianceThreshold)
+ {
+ mLoopBandwidth = loopBandwidth;
+ mVarianceThreshold = varianceThreshold;
+ }
+
+ public double getLoopBandwidth()
+ {
+ return mLoopBandwidth;
+ }
+
+ public double getVarianceThreshold()
+ {
+ return mVarianceThreshold;
+ }
+
+ public boolean meetsThreshold(double variance)
+ {
+ return variance < getVarianceThreshold();
+ }
+}
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
index d46e5463..0046606f 100644
--- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/DemodulatorViewerFX.java
+++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/DemodulatorViewerFX.java
@@ -15,6 +15,8 @@
******************************************************************************/
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;
@@ -44,7 +46,7 @@ public class DemodulatorViewerFX extends Application
public void start(Stage primaryStage) throws Exception
{
mStage = primaryStage;
- mStage.setTitle("Demodulator Viewer FX");
+ setTitle(null);
BorderPane borderPane = new BorderPane();
borderPane.setTop(getMenuBar());
@@ -56,6 +58,18 @@ public class DemodulatorViewerFX extends Application
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)
@@ -72,9 +86,9 @@ public class DemodulatorViewerFX extends Application
{
mMenuBar = new MenuBar();
- Menu menuFile = new Menu("File");
- MenuItem menuItemOpen = new MenuItem("Open ...");
- menuItemOpen.setOnAction(new EventHandler()
+ Menu fileMenu = new Menu("File");
+ MenuItem openMenuItem = new MenuItem("Open ...");
+ openMenuItem.setOnAction(new EventHandler()
{
@Override
public void handle(ActionEvent event)
@@ -87,8 +101,8 @@ public class DemodulatorViewerFX extends Application
}
});
- MenuItem menuItemClose = new MenuItem("Close");
- menuItemClose.setOnAction(new EventHandler()
+ MenuItem closeMenuItem = new MenuItem("Close");
+ closeMenuItem.setOnAction(new EventHandler()
{
@Override
public void handle(ActionEvent event)
@@ -97,8 +111,8 @@ public class DemodulatorViewerFX extends Application
}
});
- MenuItem menuItemExit = new MenuItem("Exit");
- menuItemExit.setOnAction(new EventHandler()
+ MenuItem exitMenuItem = new MenuItem("Exit");
+ exitMenuItem.setOnAction(new EventHandler()
{
@Override
public void handle(ActionEvent event)
@@ -107,10 +121,34 @@ public class DemodulatorViewerFX extends Application
}
});
- menuFile.getItems().addAll(menuItemOpen, getRecentFilesMenu(), menuItemClose, new SeparatorMenuItem(),
- menuItemExit);
+ fileMenu.getItems().addAll(openMenuItem, getRecentFilesMenu(), closeMenuItem, new SeparatorMenuItem(),
+ exitMenuItem);
- mMenuBar.getMenus().addAll(menuFile);
+ 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;
diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/PlaybackController.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/PlaybackController.java
index 94a958f2..646f41cf 100644
--- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/PlaybackController.java
+++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/PlaybackController.java
@@ -15,6 +15,7 @@
******************************************************************************/
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;
@@ -52,6 +53,7 @@ public class PlaybackController extends HBox implements IFrameLocationListener,
private HBox mControlsBox;
private Label mFileLabel;
private EventHandler mPlayEventHandler;
+ private DecoderPane mSampleRateListener;
private IControllableFileSource mControllableFileSource;
private Broadcaster mComplexBufferBroadcaster = new Broadcaster<>();
@@ -65,6 +67,16 @@ public class PlaybackController extends HBox implements IFrameLocationListener,
setControlsEnabled(false);
}
+ public void setSampleRateListener(DecoderPane decoderPane)
+ {
+ mSampleRateListener = decoderPane;
+
+ if(mSampleRateListener != null && mControllableFileSource != null)
+ {
+ mSampleRateListener.setSampleRate(mControllableFileSource.getSampleRate());
+ }
+ }
+
public void load(File file)
{
mLog.debug("loading:" + file.getAbsolutePath());
@@ -76,6 +88,11 @@ public class PlaybackController extends HBox implements IFrameLocationListener,
mControllableFileSource.setListener(this);
((ComplexWaveSource)mControllableFileSource).setListener((Listener)this);
mControllableFileSource.open();
+
+ if(mSampleRateListener != null)
+ {
+ mSampleRateListener.setSampleRate(mControllableFileSource.getSampleRate());
+ }
}
catch(UnsupportedAudioFileException e)
{
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
index 38e84acc..bdd01f5d 100644
--- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/ViewerDesktop.java
+++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/ViewerDesktop.java
@@ -15,25 +15,22 @@
******************************************************************************/
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 javafx.scene.layout.HBox;
-import javafx.scene.layout.VBox;
import java.io.File;
public class ViewerDesktop extends BorderPane
{
private PlaybackController mPlaybackController;
- private VBox mChartBox;
- private ComplexSampleLineChart mSampleLineChart;
- private ComplexPhaseLineChart mPhaseLineChart;
+ private DecoderPane mDecoderPane = DecoderPaneFactory.getDefaultPane();
public ViewerDesktop()
{
setBottom(getPlaybackController());
- getPlaybackController().addListener(getSampleLineChart());
- getPlaybackController().addListener(getPhaseLineChart());
- setCenter(getChartBox());
+ setCenter(getDecoderPane());
}
public void load(File file)
@@ -42,6 +39,31 @@ public class ViewerDesktop extends BorderPane
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();
@@ -56,35 +78,4 @@ public class ViewerDesktop extends BorderPane
return mPlaybackController;
}
-
- private VBox getChartBox()
- {
- if(mChartBox == null)
- {
- mChartBox = new VBox();
- mChartBox.getChildren().addAll(getSampleLineChart(), getPhaseLineChart());
- }
-
- return mChartBox;
- }
-
- private ComplexSampleLineChart getSampleLineChart()
- {
- if(mSampleLineChart == null)
- {
- mSampleLineChart = new ComplexSampleLineChart(40);
- }
-
- return mSampleLineChart;
- }
-
- private ComplexPhaseLineChart getPhaseLineChart()
- {
- if(mPhaseLineChart == null)
- {
- mPhaseLineChart = new ComplexPhaseLineChart(50);
- }
-
- return mPhaseLineChart;
- }
}
diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/ComplexPhaseLineChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexPhaseLineChart.java
similarity index 94%
rename from src/main/java/io/github/dsheirer/instrument/gui/viewer/ComplexPhaseLineChart.java
rename to src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexPhaseLineChart.java
index 9ec6d77f..68d86efb 100644
--- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/ComplexPhaseLineChart.java
+++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexPhaseLineChart.java
@@ -13,7 +13,7 @@
* If not, see
*
******************************************************************************/
-package io.github.dsheirer.instrument.gui.viewer;
+package io.github.dsheirer.instrument.gui.viewer.chart;
import io.github.dsheirer.buffer.ComplexCircularBuffer;
import io.github.dsheirer.dsp.gain.ComplexGain;
@@ -28,7 +28,6 @@ import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
-import org.apache.commons.math3.complex.ComplexUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -36,7 +35,7 @@ public class ComplexPhaseLineChart extends LineChart implements Listener> mPhaseValues = FXCollections.observableArrayList();
@@ -44,8 +43,8 @@ public class ComplexPhaseLineChart extends LineChart implements Listener phaseSeries = new Series<>("Phase", mPhaseValues);
ObservableList> observableList = FXCollections.observableArrayList(phaseSeries);
diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/ComplexSampleLineChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexSampleLineChart.java
similarity index 96%
rename from src/main/java/io/github/dsheirer/instrument/gui/viewer/ComplexSampleLineChart.java
rename to src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexSampleLineChart.java
index 7044bdac..13f49d7d 100644
--- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/ComplexSampleLineChart.java
+++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexSampleLineChart.java
@@ -13,7 +13,7 @@
* If not, see
*
******************************************************************************/
-package io.github.dsheirer.instrument.gui.viewer;
+package io.github.dsheirer.instrument.gui.viewer.chart;
import io.github.dsheirer.buffer.ComplexCircularBuffer;
import io.github.dsheirer.dsp.gain.ComplexGain;
@@ -44,8 +44,8 @@ public class ComplexSampleLineChart extends LineChart implements Listener iSampleSeries = new LineChart.Series<>("Inphase", mISamples);
LineChart.Series qSampleSeries = new LineChart.Series<>("Quadrature", mQSamples);
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..3857026d
--- /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", -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)
+ {
+ 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..5d67bb7d
--- /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.0, 1.0, 0.2),
+ new NumberAxis("Q", -1.0, 1.0, 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("Current", 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..62253028
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1Pane.java
@@ -0,0 +1,141 @@
+/*******************************************************************************
+ * 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.ComplexPhaseLineChart;
+import io.github.dsheirer.instrument.gui.viewer.chart.ComplexSampleLineChart;
+import io.github.dsheirer.instrument.gui.viewer.chart.PhaseLineChart;
+import io.github.dsheirer.instrument.gui.viewer.chart.SymbolChart;
+import io.github.dsheirer.module.decode.DecoderType;
+import io.github.dsheirer.module.decode.p25.P25_C4FMDecoder2;
+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 ComplexPhaseLineChart mPhaseLineChart;
+ private HBox mDecoderChartBox;
+ private SymbolChart mSymbolChart;
+ private PhaseLineChart mPLLPhaseErrorLineChart;
+ private P25_C4FMDecoder2 mDecoder = new P25_C4FMDecoder2(null);
+
+ public P25Phase1Pane()
+ {
+ super(DecoderType.P25_PHASE1);
+ init();
+ }
+
+ private void init()
+ {
+ addListener(getSampleLineChart());
+ addListener(getPhaseLineChart());
+ addListener(getDecoder());
+
+ getDecoder().setSymbolListener(getSymbolChart());
+ getDecoder().setPLLPhaseErrorListener(getPLLPhaseErrorLineChart());
+
+ getChildren().addAll(getSampleChartBox(), getDecoderChartBox());
+ }
+
+ @Override
+ public void setSampleRate(double sampleRate)
+ {
+ mLog.debug("Configuring for sample rate: " + sampleRate);
+
+ mDecoder.setSampleRate(sampleRate);
+ }
+
+ private P25_C4FMDecoder2 getDecoder()
+ {
+ return mDecoder;
+ }
+
+ private SymbolChart getSymbolChart()
+ {
+ if(mSymbolChart == null)
+ {
+ mSymbolChart = new SymbolChart(20);
+ }
+
+ return mSymbolChart;
+ }
+
+ private HBox getDecoderChartBox()
+ {
+ if(mDecoderChartBox == null)
+ {
+ mDecoderChartBox = new HBox();
+ getSymbolChart().setMaxWidth(Double.MAX_VALUE);
+ getPLLPhaseErrorLineChart().setMaxWidth(Double.MAX_VALUE);
+ HBox.setHgrow(getSymbolChart(), Priority.ALWAYS);
+ HBox.setHgrow(getPLLPhaseErrorLineChart(), Priority.ALWAYS);
+ mDecoderChartBox.getChildren().addAll(getSymbolChart(), getPLLPhaseErrorLineChart());
+ }
+
+ return mDecoderChartBox;
+ }
+
+ private HBox getSampleChartBox()
+ {
+ if(mSampleChartBox == null)
+ {
+ mSampleChartBox = new HBox();
+ getSampleLineChart().setMaxWidth(Double.MAX_VALUE);
+ getPhaseLineChart().setMaxWidth(Double.MAX_VALUE);
+ HBox.setHgrow(getSampleLineChart(), Priority.ALWAYS);
+ HBox.setHgrow(getPhaseLineChart(), Priority.ALWAYS);
+ mSampleChartBox.getChildren().addAll(getSampleLineChart(), getPhaseLineChart());
+ }
+
+ return mSampleChartBox;
+ }
+
+ private ComplexSampleLineChart getSampleLineChart()
+ {
+ if(mSampleLineChart == null)
+ {
+ mSampleLineChart = new ComplexSampleLineChart(40);
+ }
+
+ return mSampleLineChart;
+ }
+
+ private ComplexPhaseLineChart getPhaseLineChart()
+ {
+ if(mPhaseLineChart == null)
+ {
+ mPhaseLineChart = new ComplexPhaseLineChart(50);
+ }
+
+ return mPhaseLineChart;
+ }
+
+ private PhaseLineChart getPLLPhaseErrorLineChart()
+ {
+ if(mPLLPhaseErrorLineChart == null)
+ {
+ mPLLPhaseErrorLineChart = new PhaseLineChart(80);
+ }
+
+ return mPLLPhaseErrorLineChart;
+ }
+}
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/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..c51a7adf 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
@@ -19,10 +19,7 @@
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.sample.Listener;
import io.github.dsheirer.sample.real.IFilteredRealBufferListener;
import io.github.dsheirer.sample.real.RealBuffer;
@@ -30,7 +27,6 @@ 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;
@@ -105,93 +101,6 @@ public class P25_C4FMDecoder extends P25Decoder implements IFilteredRealBufferLi
return mSymbolFilter;
}
- /**
- * Instrumentation taps for monitoring internal processing
- */
- @Override
- public List getTapGroups()
- {
- 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;
- }
-
- /**
- * Adds an instrumentation tap to monitor internal processing.
- */
- @Override
- public void registerTap( Tap tap )
- {
- 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;
- }
- }
-
- /**
- * Removes the instrumentation tap.
- */
- @Override
- public void unregisterTap( Tap tap )
- {
- 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() );
- }
- }
-
@Override
public void reset()
{
diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2.java
new file mode 100644
index 00000000..5a992365
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2.java
@@ -0,0 +1,223 @@
+/*******************************************************************************
+ * 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.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.gain.ComplexFeedForwardGainControl;
+import io.github.dsheirer.dsp.psk.DQPSKSymbolPhaseErrorCalculator;
+import io.github.dsheirer.dsp.psk.IQPSKSymbolDecoder;
+import io.github.dsheirer.dsp.psk.ISymbolPhaseErrorCalculator;
+import io.github.dsheirer.dsp.psk.QPSKDemodulator;
+import io.github.dsheirer.dsp.psk.QPSKSymbolDecoder;
+import io.github.dsheirer.dsp.psk.pll.CostasLoop;
+import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop;
+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.ComplexBufferToStreamConverter;
+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.HashMap;
+import java.util.Map;
+import java.util.concurrent.ScheduledExecutorService;
+
+public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListener, Listener
+{
+ private final static Logger mLog = LoggerFactory.getLogger(P25_C4FMDecoder2.class);
+ private static final double SYMBOL_RATE = 4800;
+ private double mSampleRate;
+
+ private Map mBasebandFilters = new HashMap<>();
+ private ComplexFIRFilter mBasebandFilter;
+ private QPSKDemodulator mQPSKDemodulator;
+ private IPhaseLockedLoop mPhaseLockedLoop;
+ private ComplexBufferToStreamConverter mStreamConverter = new ComplexBufferToStreamConverter();
+ private ComplexFeedForwardGainControl mAGC = new ComplexFeedForwardGainControl(32);
+ private QPSKSymbolDecoder mQPSKSlicer = new QPSKSymbolDecoder();
+ private P25MessageFramer mMessageFramer;
+ private ISymbolPhaseErrorCalculator mSymbolPhaseErrorCalculator = new DQPSKSymbolPhaseErrorCalculator();
+ private IQPSKSymbolDecoder mSymbolDecoder = new QPSKSymbolDecoder();
+ private Listener mSymbolListener;
+ private Listener mPLLPhaseErrorListener;
+
+ public P25_C4FMDecoder2(AliasList aliasList)
+ {
+ super(aliasList);
+
+ setSampleRate(48000.0);
+
+ mStreamConverter.setListener(mAGC);
+ }
+
+ public void setSampleRate(double sampleRate)
+ {
+ 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);
+
+ mPhaseLockedLoop = new CostasLoop(mSampleRate, SYMBOL_RATE);
+ mQPSKDemodulator = new QPSKDemodulator(mPhaseLockedLoop, mSymbolPhaseErrorCalculator, mSymbolDecoder,
+ mSampleRate, SYMBOL_RATE);
+ mQPSKDemodulator.setSymbolListener(mSymbolListener);
+ mQPSKDemodulator.setPLLErrorListener(mPLLPhaseErrorListener);
+
+ //Message framer can trigger a symbol-inversion correction to the PLL when detected
+ mMessageFramer = new P25MessageFramer(getAliasList(), mPhaseLockedLoop);
+ mMessageFramer.setListener(getMessageProcessor());
+ mQPSKDemodulator.setDibitListener(mMessageFramer);
+ }
+
+ public void setSymbolListener(Listener listener)
+ {
+ mSymbolListener = listener;
+ mQPSKDemodulator.setSymbolListener(listener);
+ }
+
+ public void setPLLPhaseErrorListener(Listener listener)
+ {
+ mPLLPhaseErrorListener = listener;
+ mQPSKDemodulator.setPLLErrorListener(listener);
+ }
+
+ @Override
+ public void receive(ComplexBuffer complexBuffer)
+ {
+ ComplexBuffer filtered = mBasebandFilter.filter(complexBuffer);
+ ComplexBuffer amplified = mAGC.filter(filtered);
+ mQPSKDemodulator.receive(amplified);
+ }
+
+ private double getSampleRate()
+ {
+ return mSampleRate;
+ }
+
+ private float[] getBasebandFilter()
+ {
+ float[] filter = mBasebandFilters.get(getSampleRate());
+
+ if(filter == null)
+ {
+ FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
+ .sampleRate((int)getSampleRate())
+ .passBandCutoff(6000)
+ .passBandAmplitude(1.0)
+ .passBandRipple(0.1)
+ .stopBandAmplitude(0.0)
+ .stopBandStart(6250)
+ .stopBandRipple(0.01)
+ .build();
+
+ try
+ {
+ filter = FilterFactory.getTaps(specification);
+ mBasebandFilters.put(getSampleRate(), filter);
+
+ }
+ catch(FilterDesignException fde)
+ {
+ mLog.error("Couldn't design low pass baseband filter for sample rate: " + getSampleRate());
+ }
+ }
+
+ return filter;
+ }
+
+ public void dispose()
+ {
+ super.dispose();
+
+ mBasebandFilter.dispose();
+ mBasebandFilter = null;
+
+ mStreamConverter.dispose();
+ mStreamConverter = null;
+
+ mAGC.dispose();
+ mAGC = null;
+
+ mQPSKSlicer.dispose();
+ mQPSKSlicer = 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)
+ {
+ //Ignored
+ }
+ };
+ }
+
+ @Override
+ public Listener getComplexBufferListener()
+ {
+ return P25_C4FMDecoder2.this;
+ }
+
+ public Modulation getModulation()
+ {
+ return Modulation.C4FM;
+ }
+
+ @Override
+ public void reset()
+ {
+ // 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
+ }
+}
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/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/complex/Complex.java b/src/main/java/io/github/dsheirer/sample/complex/Complex.java
index afeaee9f..8b98e9e8 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,412 +27,427 @@ 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);
- public float angleDegrees()
- {
- return (float)Math.toDegrees(angle());
- }
+ return new Complex(i, q);
+ }
- /**
- * 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;
- }
- }
+ 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());
- /**
- * Angle of this sample in degrees
- */
- public float polarAngle()
- {
- return (float)Math.toDegrees( angle() );
- }
+ return new Complex(i, q);
+ }
- /**
- * 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 ) );
+ 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 tap = new Complex( 1.0f, -0.1f );
+ return new Complex(i, q);
+ }
- Complex convolved = Complex.multiply( s1, tap );
-
- mLog.debug( "s: " + s1.toString() +
- " t: " + tap.toString() +
- " convolved: " + convolved.toString() );
-
- }
+ /**
+ * 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)
+ {
+ double angle = Math.PI; //180 degrees
+
+ Complex s1 = new Complex((float)Math.sin(angle), (float)Math.cos(angle));
+
+ Complex tap = new Complex(1.0f, -0.1f);
+
+ Complex convolved = Complex.multiply(s1, tap);
+
+ mLog.debug("s: " + s1.toString() +
+ " t: " + tap.toString() +
+ " convolved: " + convolved.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);
}