diff --git a/src/main/java/io/github/dsheirer/buffer/FloatCircularBuffer.java b/src/main/java/io/github/dsheirer/buffer/FloatCircularBuffer.java
index e69de29b..640e6506 100644
--- a/src/main/java/io/github/dsheirer/buffer/FloatCircularBuffer.java
+++ b/src/main/java/io/github/dsheirer/buffer/FloatCircularBuffer.java
@@ -0,0 +1,120 @@
+/*******************************************************************************
+ * 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 java.util.Arrays;
+
+/**
+ * Circular sample buffer - allocates a buffer and stores samples in a circular
+ * fashion, overwriting older samples with newly arrived samples. Initially
+ * fills buffer with 0-valued samples
+ *
+ * Can be used as a delay-type buffer, to delay samples by the 'size' amount
+ *
+ * @author denny
+ */
+public class FloatCircularBuffer
+{
+ float[] mBuffer;
+ int mBufferPointer = 0;
+
+ /**
+ * Creates a circular double buffer of the specified size and all entries filled with the specified initial value.
+ *
+ * @param size of the buffer
+ * @param initialFillValue to fill the buffer entries
+ */
+ public FloatCircularBuffer(int size, float initialFillValue)
+ {
+ mBuffer = new float[size];
+ Arrays.fill(mBuffer, initialFillValue);
+ }
+
+ /**
+ * Creates a circular double buffer of the specified size and all entries filled with an initial value of zero.
+ *
+ * @param size of the buffer
+ */
+ public FloatCircularBuffer(int size)
+ {
+ this(size, 0.0f);
+ }
+
+ public float[] getBuffer()
+ {
+ return mBuffer;
+ }
+
+ /**
+ * Puts the new value into the buffer and returns the oldest buffer value that it replaced
+ *
+ * @param newValue to add to the buffer
+ * @return oldest value that was overwritten by the new value
+ */
+ public float get(float newValue)
+ {
+ float oldestSample = mBuffer[mBufferPointer];
+
+ mBuffer[mBufferPointer] = newValue;
+
+ mBufferPointer++;
+
+ if(mBufferPointer >= mBuffer.length)
+ {
+ mBufferPointer = 0;
+ }
+
+ return oldestSample;
+ }
+
+ public float[] getAll()
+ {
+ int valuePointer = 0;
+ float[] values = new float[mBuffer.length];
+ int bufferPointer = mBufferPointer;
+
+ while(valuePointer < values.length)
+ {
+ values[valuePointer++] = mBuffer[bufferPointer++];
+
+ if(bufferPointer >= mBuffer.length)
+ {
+ bufferPointer = 0;
+ }
+ }
+
+ return values;
+ }
+
+ /**
+ * Places the new value into the buffer, overwriting the oldest value
+ *
+ * @param newValue to add to the buffer
+ */
+ public void put(float newValue)
+ {
+ mBuffer[mBufferPointer] = newValue;
+
+ mBufferPointer++;
+
+ if(mBufferPointer >= mBuffer.length)
+ {
+ mBufferPointer = 0;
+ }
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java b/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java
index 6ac9a164..64cb1d0f 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
@@ -18,10 +18,12 @@ 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;
+import io.github.dsheirer.sample.complex.reusable.ReusableBufferQueue;
+import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer;
public class ComplexFIRFilter extends FIRFilter
{
+ private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue();
private RealFIRFilter mIFilter;
private RealFIRFilter mQFilter;
@@ -61,24 +63,43 @@ public class ComplexFIRFilter extends FIRFilter
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)
+ public float[] filter(float[] samples)
{
- float[] samples = buffer.getSamples();
- float[] filtered = new float[samples.length];
+ float[] filteredSamples = 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]);
+ filteredSamples[x] = filterInphase(samples[x]);
+ filteredSamples[x + 1] = filterQuadrature(samples[x + 1]);
}
- return new ComplexBuffer(filtered);
+ return filteredSamples;
+ }
+
+ /**
+ * Filters the complex samples from the buffer and returns a new complex buffer with the filtered output
+ *
+ * @param originalBuffer with complex samples to filter
+ * @return new buffer containing filtered complex samples
+ */
+ public ReusableComplexBuffer filter(ReusableComplexBuffer originalBuffer)
+ {
+ ReusableComplexBuffer filteredBuffer = mReusableBufferQueue.getBuffer(originalBuffer.getSampleLength());
+ filteredBuffer.setTimestamp(originalBuffer.getTimestamp());
+
+ float[] samples = originalBuffer.getSamples();
+ float[] filteredSamples = filteredBuffer.getSamples();
+
+ for(int x = 0; x < originalBuffer.getSampleLength(); x += 2)
+ {
+ filteredSamples[x] = filterInphase(samples[x]);
+ filteredSamples[x + 1] = filterQuadrature(samples[x + 1]);
+ }
+
+ originalBuffer.decrementUserCount();
+ filteredBuffer.incrementUserCount();
+
+ return filteredBuffer;
}
@Override
@@ -86,5 +107,6 @@ public class ComplexFIRFilter extends FIRFilter
{
mIFilter.dispose();
mQFilter.dispose();
+ mReusableBufferQueue.dispose();
}
}
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 215d7b46..560655b4 100644
--- a/src/main/java/io/github/dsheirer/dsp/gain/ComplexFeedForwardGainControl.java
+++ b/src/main/java/io/github/dsheirer/dsp/gain/ComplexFeedForwardGainControl.java
@@ -19,6 +19,8 @@ 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;
+import io.github.dsheirer.sample.complex.reusable.ReusableBufferQueue;
+import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer;
public class ComplexFeedForwardGainControl implements ComplexSampleListener
{
@@ -26,8 +28,8 @@ public class ComplexFeedForwardGainControl implements ComplexSampleListener
public static final float MINIMUM_ENVELOPE = 0.0001f;
private ComplexSampleListener mListener;
-
private FloatCircularBuffer mEnvelopeHistory;
+ private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue();
private float mMaxEnvelope = 0.0f;
private float mGain = 1.0f;
@@ -107,19 +109,59 @@ public class ComplexFeedForwardGainControl implements ComplexSampleListener
mListener = listener;
}
- public ComplexBuffer filter(ComplexBuffer complexBuffer)
+ public float[] filter(float[] complesSamples)
{
- float[] samples = complexBuffer.getSamples();
- float[] processed = new float[samples.length];
+ mMaxEnvelope = MINIMUM_ENVELOPE;
+ float currentEnvelope;
- for(int x = 0; x < samples.length; x += 2)
+ for(int x = 0; x < complesSamples.length; x += 2)
{
- process(samples[x], samples[x + 1]);
+ currentEnvelope = Complex.envelope(complesSamples[x], complesSamples[x + 1]);
- processed[x] = samples[x] * mGain;
- processed[x + 1] = samples[x + 1] * mGain;
+ if(currentEnvelope > mMaxEnvelope)
+ {
+ mMaxEnvelope = currentEnvelope;
+ }
}
- return new ComplexBuffer(processed);
+ adjustGain();
+
+ float[] processed = new float[complesSamples.length];
+
+ for(int x = 0; x < complesSamples.length; x += 2)
+ {
+ processed[x] = complesSamples[x] * mGain;
+ processed[x + 1] = complesSamples[x + 1] * mGain;
+ }
+
+ return processed;
+ }
+
+ /**
+ * Applies gain to the complex sample buffer
+ * @param buffer to apply gain
+ * @return buffer with gain applied samples
+ */
+ public ComplexBuffer filter(ComplexBuffer buffer)
+ {
+ float[] filtered = filter(buffer.getSamples());
+ return new ComplexBuffer(filtered);
+ }
+
+ /**
+ * Applies gain to the complex sample buffer
+ * @param buffer to apply gain
+ * @return reusable buffer with the user count incremented to 1
+ */
+ public ReusableComplexBuffer filter(ReusableComplexBuffer buffer)
+ {
+ float[] filtered = filter(buffer.getSamples());
+
+ ReusableComplexBuffer filteredBuffer = mReusableBufferQueue.getBuffer(filtered.length);
+
+ filteredBuffer.reloadFrom(filtered, buffer.getTimestamp());
+ filteredBuffer.incrementUserCount();
+
+ return filteredBuffer;
}
}
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDecisionDirectedDemodulator.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDecisionDirectedDemodulator.java
new file mode 100644
index 00000000..7eebc342
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDecisionDirectedDemodulator.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.dsp.psk;
+
+import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop;
+import io.github.dsheirer.dsp.symbol.Dibit;
+import io.github.dsheirer.sample.complex.Complex;
+
+public class DQPSKDecisionDirectedDemodulator extends PSKDemodulator
+{
+ protected DQPSKDecisionDirectedSymbolEvaluator mSymbolEvaluator = new DQPSKDecisionDirectedSymbolEvaluator();
+ private Complex mPreviousPrecedingSample = new Complex(0, 0);
+ private Complex mPreviousCurrentSample = new Complex(0, 0);
+ private Complex mPrecedingSample = new Complex(0, 0);
+ private Complex mCurrentSample = new Complex(0, 0);
+ private Complex mPrecedingSymbol = new Complex(0, 0);
+ protected Complex mCurrentSymbol = new Complex(0, 0);
+
+ /**
+ * Decoder for Differential Quaternary Phase Shift Keying (DQPSK). This decoder uses both a Costas Loop (PLL) and
+ * a decision-directed DQPSK symbol timing and phase error detector to automatically align to the incoming carrier
+ * frequency and to adjust for any changes in symbol timing.
+ *
+ * This detector is optimized for constant amplitude DQPSK symbols like C4FM.
+ *
+ * @param phaseLockedLoop for tracking carrier frequency error
+ * @param interpolatingSampleBuffer to hold samples for interpolating a symbol
+ */
+ public DQPSKDecisionDirectedDemodulator(IPhaseLockedLoop phaseLockedLoop, InterpolatingSampleBuffer interpolatingSampleBuffer)
+ {
+ super(interpolatingSampleBuffer, phaseLockedLoop);
+ }
+
+ /**
+ * Calculates a symbol from the interpolating buffer
+ */
+ protected void calculateSymbol()
+ {
+ //Get preceding sample and an interpolated current sample from the interpolating buffer
+ mPrecedingSample = getInterpolatingSampleBuffer().getPrecedingSample();
+ mCurrentSample = getInterpolatingSampleBuffer().getCurrentSample();
+
+ //Differential decode preceding and current symbols by calculating the angular rotation between the previous and
+ //current samples (current sample x complex conjugate of previous sample).
+
+ //Note: preceding symbol is a preceding measurement of the current symbol that is simply used as a reference
+ //point to determine vector rotation to the current symbol -- it is not the true predecessor symbol
+ mPrecedingSymbol.setInphase(Complex.multiplyInphase(mPrecedingSample.inphase(), mPrecedingSample.quadrature(),
+ mPreviousPrecedingSample.inphase(), -mPreviousPrecedingSample.quadrature()));
+ mPrecedingSymbol.setQuadrature(Complex.multiplyQuadrature(mPrecedingSample.inphase(), mPrecedingSample.quadrature(),
+ mPreviousPrecedingSample.inphase(), -mPreviousPrecedingSample.quadrature()));
+
+ mCurrentSymbol.setInphase(Complex.multiplyInphase(mCurrentSample.inphase(), mCurrentSample.quadrature(),
+ mPreviousCurrentSample.inphase(), -mPreviousCurrentSample.quadrature()));
+ mCurrentSymbol.setQuadrature(Complex.multiplyQuadrature(mCurrentSample.inphase(), mCurrentSample.quadrature(),
+ mPreviousCurrentSample.inphase(), -mPreviousCurrentSample.quadrature()));
+
+ //Set gain to unity before we calculate the error value
+ mPrecedingSymbol.normalize();
+ mCurrentSymbol.normalize();
+
+ //Apply symbols to evaluator to calculate phase and timing error and make a symbol decision
+ mSymbolEvaluator.setSymbol(mPrecedingSymbol, mCurrentSymbol);
+
+ //Update the symbol timing error
+ getInterpolatingSampleBuffer().resetAndAdjust(mSymbolEvaluator.getTimingError());
+
+ //Update the costas loop (PLL) with any measured phase error
+ getPLL().adjust(clip(mSymbolEvaluator.getPhaseError(), 0.5f));
+
+ //Store current samples/symbols to use for the next symbol period
+ mPreviousPrecedingSample.setValues(mPrecedingSample);
+ mPreviousCurrentSample.setValues(mCurrentSample);
+
+ broadcast(mSymbolEvaluator.getSymbolDecision());
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulatorInstrumented.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDecisionDirectedDemodulatorInstrumented.java
similarity index 79%
rename from src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulatorInstrumented.java
rename to src/main/java/io/github/dsheirer/dsp/psk/DQPSKDecisionDirectedDemodulatorInstrumented.java
index cc90a742..819a9124 100644
--- a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulatorInstrumented.java
+++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDecisionDirectedDemodulatorInstrumented.java
@@ -20,11 +20,11 @@ import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.Complex;
-public class DQPSKDemodulatorInstrumented extends DQPSKDemodulator
+public class DQPSKDecisionDirectedDemodulatorInstrumented extends DQPSKDecisionDirectedDemodulator
{
private Listener mSymbolDecisionDataListener;
private Listener mSamplesPerSymbolListener;
- private Listener mSymbolListener;
+ private Listener mComplexSymbolListener;
private Listener mPLLErrorListener;
private Listener mPLLFrequencyListener;
private double mSampleRate;
@@ -40,9 +40,9 @@ public class DQPSKDemodulatorInstrumented extends DQPSKDemodulator
* @param interpolatingSampleBuffer
* @param sampleRate of the incoming complex sample stream
*/
- public DQPSKDemodulatorInstrumented(IPhaseLockedLoop phaseLockedLoop,
- InterpolatingSampleBufferInstrumented interpolatingSampleBuffer,
- double sampleRate)
+ public DQPSKDecisionDirectedDemodulatorInstrumented(IPhaseLockedLoop phaseLockedLoop,
+ InterpolatingSampleBufferInstrumented interpolatingSampleBuffer,
+ double sampleRate)
{
super(phaseLockedLoop, interpolatingSampleBuffer);
mSampleRate = sampleRate;
@@ -57,20 +57,20 @@ public class DQPSKDemodulatorInstrumented extends DQPSKDemodulator
if(mSymbolDecisionDataListener != null)
{
mSymbolDecisionDataListener.receive(
- ((InterpolatingSampleBufferInstrumented)mInterpolatingSampleBuffer).getSymbolDecisionData());
+ ((InterpolatingSampleBufferInstrumented)getInterpolatingSampleBuffer()).getSymbolDecisionData());
}
super.calculateSymbol();
if(mSamplesPerSymbolListener != null)
{
- mSamplesPerSymbolListener.receive((double)mInterpolatingSampleBuffer.getSamplingPoint());
+ mSamplesPerSymbolListener.receive((double)getInterpolatingSampleBuffer().getSamplingPoint());
}
//Send to an external constellation symbol listener when registered
- if(mSymbolListener != null)
+ if(mComplexSymbolListener != null)
{
- mSymbolListener.receive(mCurrentSymbol);
+ mComplexSymbolListener.receive(mCurrentSymbol);
}
if(mPLLErrorListener != null)
@@ -80,7 +80,7 @@ public class DQPSKDemodulatorInstrumented extends DQPSKDemodulator
if(mPLLFrequencyListener != null)
{
- double loopFrequency = ((CostasLoop)mPLL).getLoopFrequency();
+ double loopFrequency = ((CostasLoop)getPLL()).getLoopFrequency();
loopFrequency *= mSampleRate / (2.0 * Math.PI);
@@ -107,9 +107,9 @@ public class DQPSKDemodulatorInstrumented extends DQPSKDemodulator
/**
* Registers the listener to receive decoded QPSK symbols
*/
- public void setSymbolListener(Listener listener)
+ public void setComplexSymbolListener(Listener listener)
{
- mSymbolListener = listener;
+ mComplexSymbolListener = listener;
}
/**
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolEvaluator.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDecisionDirectedSymbolEvaluator.java
similarity index 95%
rename from src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolEvaluator.java
rename to src/main/java/io/github/dsheirer/dsp/psk/DQPSKDecisionDirectedSymbolEvaluator.java
index cab0fd22..077010dc 100644
--- a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolEvaluator.java
+++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDecisionDirectedSymbolEvaluator.java
@@ -18,7 +18,7 @@ package io.github.dsheirer.dsp.psk;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.sample.complex.Complex;
-public class DQPSKSymbolEvaluator implements IDQPSKSymbolEvaluator
+public class DQPSKDecisionDirectedSymbolEvaluator implements IPSKSymbolEvaluator
{
private static final Complex ROTATE_FROM_PLUS_135 = Complex.fromAngle(-3.0 * Math.PI / 4.0);
private static final Complex ROTATE_FROM_PLUS_45 = Complex.fromAngle(-1.0 * Math.PI / 4.0);
@@ -32,7 +32,7 @@ public class DQPSKSymbolEvaluator implements IDQPSKSymbolEvaluator
private Dibit mSymbolDecision = Dibit.D00_PLUS_1;
/**
- * Decision-directed symbol phase and timing error detector and symbol decision slicer.
+ * Differential QPSK Decision-directed symbol phase and timing error detector and symbol decision slicer.
*
* Symbol decision is based on the closest reference quadrant for the sampled symbol.
*
@@ -40,7 +40,7 @@ public class DQPSKSymbolEvaluator implements IDQPSKSymbolEvaluator
* symbol. Timing error is further corrected to compensate for clock-wise or counter-clock-wise vector rotation
* relative to the immediately preceding symbol.
*/
- public DQPSKSymbolEvaluator()
+ public DQPSKDecisionDirectedSymbolEvaluator()
{
}
@@ -57,7 +57,6 @@ public class DQPSKSymbolEvaluator implements IDQPSKSymbolEvaluator
* since it's only a reference
* @param current symbol to be evaluated - interpolated
*/
- @Override
public void setSymbol(Complex preceding, Complex current)
{
mSymbolError.setValues(current);
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulator.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulator.java
deleted file mode 100644
index f8b6aabd..00000000
--- a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulator.java
+++ /dev/null
@@ -1,162 +0,0 @@
-/*******************************************************************************
- * sdr-trunk
- * Copyright (C) 2014-2018 Dennis Sheirer
- *
- * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
- * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
- * later version.
- *
- * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
- * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with this program.
- * If not, see
- *
- ******************************************************************************/
-package io.github.dsheirer.dsp.psk;
-
-import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop;
-import io.github.dsheirer.dsp.symbol.Dibit;
-import io.github.dsheirer.sample.Listener;
-import io.github.dsheirer.sample.complex.Complex;
-import io.github.dsheirer.sample.complex.ComplexBuffer;
-import io.github.dsheirer.sample.complex.ComplexSampleListener;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-public class DQPSKDemodulator implements ComplexSampleListener
-{
- private final static Logger mLog = LoggerFactory.getLogger(DQPSKDemodulator.class);
- protected InterpolatingSampleBuffer mInterpolatingSampleBuffer;
- protected IPhaseLockedLoop mPLL;
- protected DQPSKSymbolEvaluator mSymbolEvaluator = new DQPSKSymbolEvaluator();
- private Complex mReceivedSample = new Complex(0, 0);
- private Complex mPreviousPrecedingSample = new Complex(0, 0);
- private Complex mPreviousCurrentSample = new Complex(0, 0);
- private Complex mPrecedingSample = new Complex(0, 0);
- private Complex mCurrentSample = new Complex(0, 0);
- private Complex mPrecedingSymbol = new Complex(0, 0);
- protected Complex mCurrentSymbol = new Complex(0, 0);
- private Listener mDibitListener;
-
- /**
- * Decoder for Differential Quaternary Phase Shift Keying (DQPSK). This decoder uses both a Costas Loop (PLL) and
- * a custom DQPSK symbol timing error detector to automatically align to the incoming carrier frequency and to
- * adjust for any changes in symbol timing.
- *
- * This detector is optimized for constant amplitude DQPSK symbols like C4FM.
- *
- * @param phaseLockedLoop for tracking carrier frequency error
- * @param interpolatingSampleBuffer to hold samples for interpolating a symbol
- */
- public DQPSKDemodulator(IPhaseLockedLoop phaseLockedLoop, InterpolatingSampleBuffer interpolatingSampleBuffer)
- {
- mPLL = phaseLockedLoop;
- mInterpolatingSampleBuffer = interpolatingSampleBuffer;
- }
-
- /**
- * Processes a (filtered) buffer containing complex samples for decoding
- * @param complexBuffer with complex samples
- */
- public void receive(ComplexBuffer complexBuffer)
- {
- float[] samples = complexBuffer.getSamples();
-
- for(int x = 0; x < samples.length; x += 2)
- {
- receive(samples[x], samples[x + 1]);
- }
- }
-
- /**
- * Processes a complex sample for decoding. Once sufficient samples are buffered, a symbol decision is made.
- * @param inphase value for the sample
- * @param quadrature value for the sample
- */
- @Override
- public void receive(float inphase, float quadrature)
- {
- //Update current sample with values
- mReceivedSample.setValues(inphase, quadrature);
-
- //Mix current sample with costas loop to remove any rotation that is present from a mis-tuned carrier frequency
- mReceivedSample.multiply(mPLL.incrementAndGetCurrentVector());
-
- //Store the sample in the interpolating buffer
- mInterpolatingSampleBuffer.receive(mReceivedSample);
-
- //Calculate the symbol once we've stored enough samples
- if(mInterpolatingSampleBuffer.hasSymbol())
- {
- calculateSymbol();
- }
- }
-
- /**
- * Calculates a symbol from the interpolating buffer
- */
- protected void calculateSymbol()
- {
- //Get preceding sample and an interpolated current sample from the interpolating buffer
- mPrecedingSample = mInterpolatingSampleBuffer.getPrecedingSample();
- mCurrentSample = mInterpolatingSampleBuffer.getCurrentSample();
-
- //Calculate preceding, current and following symbols as the delta rotation compared to the previous samples
- mPrecedingSymbol.setInphase(Complex.multiplyInphase(mPrecedingSample.inphase(), mPrecedingSample.quadrature(),
- mPreviousPrecedingSample.inphase(), -mPreviousPrecedingSample.quadrature()));
- mPrecedingSymbol.setQuadrature(Complex.multiplyQuadrature(mPrecedingSample.inphase(), mPrecedingSample.quadrature(),
- mPreviousPrecedingSample.inphase(), -mPreviousPrecedingSample.quadrature()));
-
- mCurrentSymbol.setInphase(Complex.multiplyInphase(mCurrentSample.inphase(), mCurrentSample.quadrature(),
- mPreviousCurrentSample.inphase(), -mPreviousCurrentSample.quadrature()));
- mCurrentSymbol.setQuadrature(Complex.multiplyQuadrature(mCurrentSample.inphase(), mCurrentSample.quadrature(),
- mPreviousCurrentSample.inphase(), -mPreviousCurrentSample.quadrature()));
-
- //Set gain to unity before we calculate the error value
- mPrecedingSymbol.normalize();
- mCurrentSymbol.normalize();
-
- mSymbolEvaluator.setSymbol(mPrecedingSymbol, mCurrentSymbol);
-
- mInterpolatingSampleBuffer.resetAndAdjust(mSymbolEvaluator.getTimingError());
-
- //Store current samples/symbols to use for the next period
- mPreviousPrecedingSample.setValues(mPrecedingSample);
- mPreviousCurrentSample.setValues(mCurrentSample);
-
- //Calculate the phase error of the current symbol relative to the expected constellation and provide
- //feedback to the PLL
- mPLL.adjust(clip(mSymbolEvaluator.getPhaseError(), 0.5f));
-
- if(mDibitListener != null)
- {
- mDibitListener.receive(mSymbolEvaluator.getSymbolDecision());
- }
- }
-
- /**
- * Registers the listener to receive dibit symbol decisions from this demodulator
- */
- public void setDibitListener(Listener listener)
- {
- mDibitListener = listener;
- }
-
- /**
- * Constrains value to the range of ( -maximum <> maximum )
- */
- public static float clip(float value, float maximum)
- {
- if(value > maximum)
- {
- return maximum;
- }
- else if(value < -maximum)
- {
- return -maximum;
- }
-
- return value;
- }
-}
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKGardnerDemodulator.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKGardnerDemodulator.java
new file mode 100644
index 00000000..4e8033ee
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKGardnerDemodulator.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.dsp.psk;
+
+import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop;
+import io.github.dsheirer.dsp.symbol.Dibit;
+import io.github.dsheirer.sample.complex.Complex;
+
+public class DQPSKGardnerDemodulator extends PSKDemodulator
+{
+ protected DQPSKGardnerSymbolEvaluator mSymbolEvaluator = new DQPSKGardnerSymbolEvaluator();
+ private Complex mPreviousCurrentSample = new Complex(0, 0);
+ private Complex mPreviousMiddleSample = new Complex(0, 0);
+ private Complex mMiddleSymbol = new Complex(0, 0);
+ protected Complex mCurrentSymbol = new Complex(0, 0);
+
+ /**
+ * Implements a Differential QPSK demodulator using a Costas Loop (PLL) and a Gardner timing error detector.
+ *
+ * This decoder is optimized for P25 Linear Simulcast Modulation (LSM).
+ *
+ * @param phaseLockedLoop for tracking carrier frequency error
+ * @param interpolatingSampleBuffer to hold samples for interpolating a symbol
+ */
+ public DQPSKGardnerDemodulator(IPhaseLockedLoop phaseLockedLoop, InterpolatingSampleBuffer interpolatingSampleBuffer)
+ {
+ super(interpolatingSampleBuffer, phaseLockedLoop);
+ }
+
+ public void dispose()
+ {
+ }
+
+ @Override
+ protected void calculateSymbol()
+ {
+ //Note: the interpolating sample buffer holds 2 symbols worth of samples and the current sample method points
+ //to the sample at the mid-point between those 2 symbol periods and the middle sample method points to the
+ //sample that is half a symbol period after the current sample. Since we need a middle sample and a current
+ //symbol sample for the gardner calculation, we'll treat the interpolating buffer's current sample as the
+ //gardner mid-point and we'll treat the interpolating buffer's mid-point sample as the current symbol
+ //sample (ie flip-flopped)
+ Complex middleSample = getInterpolatingSampleBuffer().getCurrentSample();
+ Complex currentSample = getInterpolatingSampleBuffer().getMiddleSample();
+
+ //Differential decode middle and current symbols by calculating the angular rotation between the previous and
+ //current samples (current sample x complex conjugate of previous sample).
+ mMiddleSymbol.setInphase(Complex.multiplyInphase(middleSample.inphase(), middleSample.quadrature(),
+ mPreviousMiddleSample.inphase(), -mPreviousMiddleSample.quadrature()));
+ mMiddleSymbol.setQuadrature(Complex.multiplyQuadrature(middleSample.inphase(), middleSample.quadrature(),
+ mPreviousMiddleSample.inphase(), -mPreviousMiddleSample.quadrature()));
+
+ mCurrentSymbol.setInphase(Complex.multiplyInphase(currentSample.inphase(), currentSample.quadrature(),
+ mPreviousCurrentSample.inphase(), -mPreviousCurrentSample.quadrature()));
+ mCurrentSymbol.setQuadrature(Complex.multiplyQuadrature(currentSample.inphase(), currentSample.quadrature(),
+ mPreviousCurrentSample.inphase(), -mPreviousCurrentSample.quadrature()));
+
+ //Set gain to unity before we calculate the error value
+ mMiddleSymbol.normalize();
+ mCurrentSymbol.normalize();
+
+ //Pass symbols to evaluator to determine timing and phase error and make symbol decision
+ mSymbolEvaluator.setSymbols(mMiddleSymbol, mCurrentSymbol);
+
+ //Update symbol timing error
+ getInterpolatingSampleBuffer().resetAndAdjust(mSymbolEvaluator.getTimingError());
+
+ //Update PLL phase error
+ getPLL().adjust(mSymbolEvaluator.getPhaseError());
+
+ //Store current samples/symbols for next symbol calculation
+ mPreviousMiddleSample.setValues(middleSample);
+ mPreviousCurrentSample.setValues(currentSample);
+
+ broadcast(mSymbolEvaluator.getSymbolDecision());
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKGardnerDemodulatorInstrumented.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKGardnerDemodulatorInstrumented.java
new file mode 100644
index 00000000..272b0534
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKGardnerDemodulatorInstrumented.java
@@ -0,0 +1,127 @@
+/*******************************************************************************
+ * sdr-trunk
+ * Copyright (C) 2014-2018 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with this program.
+ * If not, see
+ *
+ ******************************************************************************/
+package io.github.dsheirer.dsp.psk;
+
+import io.github.dsheirer.dsp.psk.pll.CostasLoop;
+import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop;
+import io.github.dsheirer.sample.Listener;
+import io.github.dsheirer.sample.complex.Complex;
+
+public class DQPSKGardnerDemodulatorInstrumented extends DQPSKGardnerDemodulator
+{
+ private Listener mSymbolDecisionDataListener;
+ private Listener mSamplesPerSymbolListener;
+ private Listener mComplexSymbolListener;
+ private Listener mPLLErrorListener;
+ private Listener mPLLFrequencyListener;
+ private double mSampleRate;
+
+ /**
+ * Implements a Differential QPSK demodulator using a Costas Loop (PLL) and a Gardner timing error detector.
+ *
+ * This decoder is optimized for P25 Linear Simulcast Modulation (LSM).
+ *
+ * @param phaseLockedLoop for tracking carrier frequency error
+ * @param interpolatingSampleBuffer to hold samples for interpolating a symbol
+ */
+ public DQPSKGardnerDemodulatorInstrumented(IPhaseLockedLoop phaseLockedLoop,
+ InterpolatingSampleBuffer interpolatingSampleBuffer,
+ double sampleRate)
+ {
+ super(phaseLockedLoop, interpolatingSampleBuffer);
+ mSampleRate = sampleRate;
+ }
+
+ /**
+ * Overrides the parent class symbol calculation to capture eye diagram data
+ */
+ protected void calculateSymbol()
+ {
+ //Eye diagram listener
+ if(mSymbolDecisionDataListener != null)
+ {
+ mSymbolDecisionDataListener.receive(
+ ((InterpolatingSampleBufferInstrumented)getInterpolatingSampleBuffer()).getSymbolDecisionData());
+ }
+
+ super.calculateSymbol();
+
+ if(mSamplesPerSymbolListener != null)
+ {
+ mSamplesPerSymbolListener.receive((double)getInterpolatingSampleBuffer().getSamplingPoint());
+ }
+
+ //Send to an external constellation symbol listener when registered
+ if(mComplexSymbolListener != null)
+ {
+ mComplexSymbolListener.receive(mCurrentSymbol);
+ }
+
+ if(mPLLErrorListener != null)
+ {
+ mPLLErrorListener.receive((double)mSymbolEvaluator.getPhaseError());
+ }
+
+ if(mPLLFrequencyListener != null)
+ {
+ double loopFrequency = ((CostasLoop)getPLL()).getLoopFrequency();
+
+ loopFrequency *= mSampleRate / (2.0 * Math.PI);
+
+ mPLLFrequencyListener.receive(loopFrequency);
+ }
+ }
+
+ /**
+ * Registers a listener to receive symbol decision data to produce an eye diagram.
+ */
+ public void setSymbolDecisionDataListener(Listener listener)
+ {
+ mSymbolDecisionDataListener = listener;
+ }
+
+ /**
+ * Registers the listener to receive PLL error values
+ */
+ public void setSamplesPerSymbolListener(Listener listener)
+ {
+ mSamplesPerSymbolListener = listener;
+ }
+
+ /**
+ * Registers the listener to receive decoded QPSK symbols
+ */
+ public void setComplexSymbolListener(Listener listener)
+ {
+ mComplexSymbolListener = listener;
+ }
+
+ /**
+ * Registers the listener to receive PLL error values
+ */
+ public void setPLLErrorListener(Listener listener)
+ {
+ mPLLErrorListener = listener;
+ }
+
+ /**
+ * Registers the listener to receive PLL error values
+ */
+ public void setPLLFrequencyListener(Listener listener)
+ {
+ mPLLFrequencyListener = listener;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKGardnerSymbolEvaluator.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKGardnerSymbolEvaluator.java
new file mode 100644
index 00000000..e54ae90a
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKGardnerSymbolEvaluator.java
@@ -0,0 +1,158 @@
+/*******************************************************************************
+ * 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 DQPSKGardnerSymbolEvaluator implements IPSKSymbolEvaluator
+{
+ private float mPhaseError = 0.0f;
+ private float mTimingError = 0.0f;
+ private Dibit mSymbolDecision = Dibit.D00_PLUS_1;
+ private Complex mPreviousSymbol = new Complex(0, 0);
+
+ /**
+ * Differential QPSK Decision-directed symbol phase and timing error detector and symbol decision slicer.
+ *
+ * Symbol decision is based on the closest reference quadrant for the sampled symbol.
+ *
+ * Phase error is calculated as the angular distance of the sampled symbol from the reference symbol.
+ *
+ * Timing error is calculated using the Gardner method by comparing the previous symbol to the current symbol and
+ * amplifying the delta between the two using the intra-symbol sample to form the timing error.
+ */
+ public DQPSKGardnerSymbolEvaluator()
+ {
+ }
+
+ /**
+ * Sets the middle and current symbols to be evaluated for phase and timing errors and to determine the
+ * transmitted symbol relative to the closest reference symbol. After invoking this method, you can access the
+ * phase and timing errors and the symbol decision via their respective accessor methods.
+ *
+ * Phase and timing error values are calculated by first determining the symbol and then calculating the phase
+ * and timing errors relative to the reference symbol. The timing error is corrected with the appropriate sign
+ * relative to the angular vector rotation so that the error value indicates the correct error direction.
+ *
+ * @param middle interpolated differentially-decoded sample that falls midway between previous/current symbols
+ * @param current interpolated differentially-decoded symbol
+ */
+ public void setSymbols(Complex middle, Complex current)
+ {
+ /* Gardner timing error calculations */
+ float errorInphase = (mPreviousSymbol.inphase() - current.inphase()) * middle.inphase();
+ float errorQuadrature = (mPreviousSymbol.quadrature() - current.quadrature()) * middle.quadrature();
+ mTimingError = normalize(errorInphase + errorQuadrature, 1.0f);
+
+ //Calculate phase error and determine the transmitted dibit from the symbol
+ if(Math.abs(current.inphase()) > Math.abs(current.quadrature()))
+ {
+ if(current.inphase() > 0)
+ {
+ mSymbolDecision = Dibit.D10_MINUS_1;
+ mPhaseError = -current.quadrature();
+ }
+ else
+ {
+ mSymbolDecision = Dibit.D01_PLUS_3;
+ mPhaseError = current.quadrature();
+ }
+ }
+ else
+ {
+ if(current.quadrature() > 0)
+ {
+ mSymbolDecision = Dibit.D00_PLUS_1;
+ mPhaseError = current.inphase();
+ }
+ else
+ {
+ mSymbolDecision = Dibit.D11_MINUS_3;
+ mPhaseError = -current.inphase();
+ }
+ }
+
+ mPreviousSymbol.setValues(current);
+ }
+
+ /**
+ * 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);
+ }
+ }
+
+
+ /**
+ * Phase error of the symbol relative to the nearest reference symbol.
+ *
+ * @return phase error in radians of distance from the reference symbol.
+ */
+ @Override
+ public float getPhaseError()
+ {
+ return mPhaseError;
+ }
+
+ /**
+ * Timing error of the symbol relative to the nearest reference symbol.
+ *
+ * @return timing error in radians of angular distance from the reference symbol recognizing that the symbol
+ * originates at zero radians and rotates toward the intended reference symbol, therefore the error value indicates
+ * if the symbol was sampled early (-) or late (+) relative to the reference symbol.
+ */
+ @Override
+ public float getTimingError()
+ {
+ return mTimingError;
+ }
+
+ /**
+ * Reference symbol that is closest to the transmitted/sampled symbol.
+ */
+ @Override
+ public Dibit getSymbolDecision()
+ {
+ return mSymbolDecision;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/IDQPSKSymbolEvaluator.java b/src/main/java/io/github/dsheirer/dsp/psk/IPSKSymbolEvaluator.java
similarity index 71%
rename from src/main/java/io/github/dsheirer/dsp/psk/IDQPSKSymbolEvaluator.java
rename to src/main/java/io/github/dsheirer/dsp/psk/IPSKSymbolEvaluator.java
index d0213a0d..fa1d9d12 100644
--- a/src/main/java/io/github/dsheirer/dsp/psk/IDQPSKSymbolEvaluator.java
+++ b/src/main/java/io/github/dsheirer/dsp/psk/IPSKSymbolEvaluator.java
@@ -15,20 +15,8 @@
******************************************************************************/
package io.github.dsheirer.dsp.psk;
-import io.github.dsheirer.dsp.symbol.Dibit;
-import io.github.dsheirer.sample.complex.Complex;
-
-
-public interface IDQPSKSymbolEvaluator
+public interface IPSKSymbolEvaluator
{
- /**
- * Sets the sampled symbol to be evaluated. Note: the symbol's gain value MUST be normalized to the unit circle.
- *
- * @param preceding symbol used to detect (counter-)clock-wise rotation
- * @param symbol currently being sampled
- */
- void setSymbol(Complex preceding, Complex symbol);
-
/**
* Phase error of the symbol relative to the closest reference symbol
* @return angular phase error in radians
@@ -43,8 +31,8 @@ public interface IDQPSKSymbolEvaluator
float getTimingError();
/**
- * Dibit decision indicating the reference symbol location closest to the symbol.
+ * Symbol decision indicating the reference symbol location closest to the symbol.
* @return symbol decision
*/
- Dibit getSymbolDecision();
+ T getSymbolDecision();
}
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBuffer.java b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBuffer.java
index 15adbd9a..5b64f338 100644
--- a/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBuffer.java
+++ b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBuffer.java
@@ -24,6 +24,7 @@ public class InterpolatingSampleBuffer
private Complex mPrecedingSample = new Complex(0,0);
private Complex mCurrentSample = new Complex(0,0);
+ private Complex mMiddleSample = new Complex(0,0);
private float[] mDelayLineInphase;
private float[] mDelayLineQuadrature;
@@ -42,8 +43,9 @@ public class InterpolatingSampleBuffer
/**
* Buffer to store complex sample data and produce interpolated samples.
* @param samplesPerSymbol
+ * @param symbolTimingGain for the symbol timing error adjustments
*/
- public InterpolatingSampleBuffer(float samplesPerSymbol)
+ public InterpolatingSampleBuffer(float samplesPerSymbol, float symbolTimingGain)
{
mSamplingPoint = samplesPerSymbol;
mDetectedSamplesPerSymbol = samplesPerSymbol;
@@ -52,6 +54,7 @@ public class InterpolatingSampleBuffer
mTwiceSamplesPerSymbol = (int)Math.floor(2.0 * samplesPerSymbol);
mDelayLineInphase = new float[2 * mTwiceSamplesPerSymbol];
mDelayLineQuadrature = new float[2 * mTwiceSamplesPerSymbol];
+ mDetectedSamplesPerSymbolGain = symbolTimingGain * mSampleCounterGain * mSampleCounterGain;
}
/**
@@ -125,18 +128,21 @@ public class InterpolatingSampleBuffer
}
/**
- * Un-interpolated sample that precedes the current interpolated sampling point. The current sampling point falls
- * somewhere between sample index 3 and index 4, therefore the preceding sample is located at index 3.
+ * Un-interpolated sample that precedes the current interpolated sampling point. The current interpolated sampling
+ * point falls somewhere between sample index 3 and index 4 and the sample is interpolated from sample
+ * indexes 0 - 7, therefore the uninterpolated sample that immediately precedes the current sample is
+ * located at index 3.
*/
public Complex getPrecedingSample()
{
- mPrecedingSample.setValues(mDelayLineInphase[mDelayLinePointer + 4], mDelayLineQuadrature[mDelayLinePointer + 3]);
+ mPrecedingSample.setValues(mDelayLineInphase[mDelayLinePointer + 3], mDelayLineQuadrature[mDelayLinePointer + 3]);
return mPrecedingSample;
}
/**
- * Interpolated middle sample for the symbol. Note: this method should only be invoked after testing for a
- * complete symbol with the hasSymbol() method.
+ * Interpolated current sample for the symbol.
+ *
+ * Note: this method should only be invoked after testing for a complete symbol with the hasSymbol() method.
*/
public Complex getCurrentSample()
{
@@ -145,6 +151,21 @@ public class InterpolatingSampleBuffer
return mCurrentSample;
}
+ /**
+ * Interpolated sample that is 1/2 symbol away from (after) the current sample.
+ *
+ * Note: this method should only be invoked after testing for a complete symbol with the hasSymbol() method.
+ */
+ public Complex getMiddleSample()
+ {
+ float halfDetectedSamplesPerSymbol = mDetectedSamplesPerSymbol / 2.0f;
+
+ //Interpolated sample that is half a symbol away from (occurred before) the current sample.
+ mMiddleSample.setValues(getInphase(halfDetectedSamplesPerSymbol),
+ getQuadrature(halfDetectedSamplesPerSymbol));
+ return mMiddleSample;
+ }
+
/**
* Returns the interpolated inphase value for the specified offset
* @param interpolation into the buffer to calculate the interpolated sample
@@ -152,7 +173,15 @@ public class InterpolatingSampleBuffer
*/
public float getInphase(float interpolation)
{
- return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer + 1, interpolation);
+ if(interpolation < 1.0f)
+ {
+ return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer, interpolation);
+ }
+ else
+ {
+ int offset = (int)Math.floor(interpolation);
+ return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer + offset, interpolation - offset);
+ }
}
/**
@@ -162,6 +191,14 @@ public class InterpolatingSampleBuffer
*/
public float getQuadrature(float interpolation)
{
- return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer + 1, interpolation);
+ if(interpolation < 1.0f)
+ {
+ return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer, interpolation);
+ }
+ else
+ {
+ int offset = (int)Math.floor(interpolation);
+ return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer + offset, interpolation - offset);
+ }
}
}
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBufferInstrumented.java b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBufferInstrumented.java
index 6c71fc25..dcdef399 100644
--- a/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBufferInstrumented.java
+++ b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBufferInstrumented.java
@@ -21,10 +21,9 @@ public class InterpolatingSampleBufferInstrumented extends InterpolatingSampleBu
{
private SymbolDecisionData mSymbolDecisionData;
- public InterpolatingSampleBufferInstrumented(float samplesPerSymbol)
+ public InterpolatingSampleBufferInstrumented(float samplesPerSymbol, float symbolTimingGain)
{
- super(samplesPerSymbol);
-
+ super(samplesPerSymbol, symbolTimingGain);
mSymbolDecisionData = new SymbolDecisionData((int)samplesPerSymbol);
}
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/LSMDemodulator.java b/src/main/java/io/github/dsheirer/dsp/psk/LSMDemodulator.java
deleted file mode 100644
index 0536f7f2..00000000
--- a/src/main/java/io/github/dsheirer/dsp/psk/LSMDemodulator.java
+++ /dev/null
@@ -1,614 +0,0 @@
-package io.github.dsheirer.dsp.psk;
-
-/*******************************************************************************
- * SDR Trunk
- * 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;
-import io.github.dsheirer.instrument.tap.TapGroup;
-import io.github.dsheirer.instrument.tap.stream.EyeDiagramData;
-import io.github.dsheirer.instrument.tap.stream.EyeDiagramDataTap;
-import io.github.dsheirer.sample.Listener;
-import io.github.dsheirer.sample.complex.Complex;
-import io.github.dsheirer.sample.complex.ComplexSampleListener;
-import io.github.dsheirer.source.ISourceEventListener;
-
-import java.util.ArrayList;
-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
- * 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);
-
- /* 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 ISourceEventListener 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( ISourceEventListener listener )
- {
- mFrequencyChangeListener = listener;
- }
-
- public void removeListener( ISourceEventListener 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 )
-// {
-// float average = mBuffer.get( frequency );
-// mLog.debug( "Avg:" + average + " Threshold:" + CORRECTION_THRESHOLD_FREQUENCY);
-//
-// boolean correctionNeeded = false;
-//
-// if( average > CORRECTION_THRESHOLD_FREQUENCY )
-// {
-// mFrequencyError--;
-//
-// correctionNeeded = true;
-// }
-// else if( average < -CORRECTION_THRESHOLD_FREQUENCY )
-// {
-// mFrequencyError++;
-//
-// correctionNeeded = true;
-// }
-//
-// if( correctionNeeded && mFrequencyChangeListener != null )
-// {
-// mLog.debug( "Issuing Frequency Correction: " + mFrequencyError );
-// mFrequencyChangeListener.process(
-// new SourceEvent( Attribute.FREQUENCY_ERROR,
-// mFrequencyError ) );
-// }
-//
-// mCounter = 0;
-// }
-// else
-// {
-// 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;
-
- private float[] mDelayLineInphase = new float[2 * TWICE_SAMPLES_PER_SYMBOL];
- private float[] mDelayLineQuadrature = new float[2 * TWICE_SAMPLES_PER_SYMBOL];
-
- 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;
-
- 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()
- {
- }
-
- @Override
- public void receive(float inphase, float quadrature)
- {
- /* Count down samples per symbol until we calculate the symbol */
- mMu--;
-
- 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;
-
- mDelayLineQuadrature[mDelayLinePointer] = derotatedQuadrature;
- mDelayLineQuadrature[mDelayLinePointer + TWICE_SAMPLES_PER_SYMBOL] = derotatedQuadrature;
-
- /* 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;
- }
-
- /* 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, IPhaseLockedLoop
- {
- 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));
-
- private float mLoopPhase = 0.0f;
-
- private float mLoopFrequency = 0.0f;
-
- private FrequencyControl mFrequencyControl = new FrequencyControl();
-
- public CostasLoop()
- {
- }
-
- /**
- * Applies a phase correction value to the current phasor. Use this
- * method to apply correction when a +/- 90, or 180 degree phase error
- * is detected in the binary output stream.
- *
- * If the supplied correction value places the loop frequency outside
- * of the max frequency, then the frequency will be corrected 360
- * degrees in the opposite direction to maintain within the max
- * frequency bounds.
- *
- * @param correction - correction value in radians
- */
- public void correctPhaseError(double correction)
- {
- mLoopFrequency += correction;
-
- if(mLoopFrequency > MAXIMUM_FREQUENCY)
- {
- mLoopFrequency -= 2.0d * MAXIMUM_FREQUENCY;
- }
-
- if(mLoopFrequency < -MAXIMUM_FREQUENCY)
- {
- mLoopFrequency += 2.0d * MAXIMUM_FREQUENCY;
- }
- }
-
- /**
- * Increments the phase of the loop
- */
- public void increment()
- {
- mLoopPhase += mLoopFrequency;
-
- /* Keep the loop phase in bounds */
- unwrapPhase();
- }
-
- @Override
- public void reset()
- {
- mLoopPhase = 0.0f;
- mLoopFrequency = 0.0f;
- }
-
- @Override
- public void adjust(double error)
- {
- adjust(error);
- }
-
- @Override
- public void correctInversion(double correction)
- {
- correctPhaseError(correction);
- }
-
- private void unwrapPhase()
- {
- while(mLoopPhase > TWO_PI)
- {
- mLoopPhase -= TWO_PI;
- }
-
- while(mLoopPhase < -TWO_PI)
- {
- mLoopPhase += TWO_PI;
- }
- }
-
- /**
- * Current vector of the loop rotated by THETA degrees.
- */
- public Complex getCurrentVector()
- {
-// return ComplexSample.fromAngle( mLoopPhase + THETA );
- return Complex.fromAngle(mLoopPhase);
- }
-
- @Override
- public 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);
- }
-
- /**
- * 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();
-
- /* 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;
- }
- }
-
- /**
- * 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;
- }
- }
-
- @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/PSKDemodulator.java b/src/main/java/io/github/dsheirer/dsp/psk/PSKDemodulator.java
new file mode 100644
index 00000000..c187a408
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/PSKDemodulator.java
@@ -0,0 +1,137 @@
+/*******************************************************************************
+ * sdr-trunk
+ * Copyright (C) 2014-2018 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with this program.
+ * If not, see
+ *
+ ******************************************************************************/
+package io.github.dsheirer.dsp.psk;
+
+import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop;
+import io.github.dsheirer.sample.Listener;
+import io.github.dsheirer.sample.complex.Complex;
+import io.github.dsheirer.sample.complex.ComplexSampleListener;
+import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer;
+
+public abstract class PSKDemodulator implements ComplexSampleListener
+{
+ private InterpolatingSampleBuffer mInterpolatingSampleBuffer;
+ private IPhaseLockedLoop mPLL;
+ private Complex mReceivedSample = new Complex(0, 0);
+ private Listener mSymbolListener;
+
+ /**
+ * Abstract Phase Shift Keyed (PSK) demodulator
+ * @param interpolatingSampleBuffer to store complex samples and provide access to interpolated samples
+ * @param phaseLockedLoop to track and remove frequency offset in the incoming samples
+ */
+ public PSKDemodulator(InterpolatingSampleBuffer interpolatingSampleBuffer, IPhaseLockedLoop phaseLockedLoop)
+ {
+ mInterpolatingSampleBuffer = interpolatingSampleBuffer;
+ mPLL = phaseLockedLoop;
+ }
+
+ /**
+ * Registers the listener to receive symbol decisions from this demodulator
+ */
+ public void setSymbolListener(Listener listener)
+ {
+ mSymbolListener = listener;
+ }
+
+ /**
+ * Broadcasts the symbol decision to the registered symbol listener
+ * @param symbol
+ */
+ protected void broadcast(T symbol)
+ {
+ if(mSymbolListener != null)
+ {
+ mSymbolListener.receive(symbol);
+ }
+ }
+
+ /**
+ * Interpolating sample buffer for receiving the incoming complex sample stream and providing access to
+ * indexed and interpolated samples
+ */
+ protected InterpolatingSampleBuffer getInterpolatingSampleBuffer()
+ {
+ return mInterpolatingSampleBuffer;
+ }
+
+ /**
+ * Phase Locked Loop (PLL)
+ */
+ protected IPhaseLockedLoop getPLL()
+ {
+ return mPLL;
+ }
+
+ /**
+ * Processes a (filtered) buffer containing complex samples for decoding
+ * @param reusableComplexBuffer with complex samples
+ */
+ public void receive(ReusableComplexBuffer reusableComplexBuffer)
+ {
+ float[] samples = reusableComplexBuffer.getSamples();
+
+ for(int x = 0; x < samples.length; x += 2)
+ {
+ receive(samples[x], samples[x + 1]);
+ }
+
+ reusableComplexBuffer.decrementUserCount();
+ }
+
+ /**
+ * Processes a complex sample for decoding. Once sufficient samples are buffered, a symbol decision is made.
+ * @param inphase value for the sample
+ * @param quadrature value for the sample
+ */
+ @Override
+ public void receive(float inphase, float quadrature)
+ {
+ //Update current sample with values
+ mReceivedSample.setValues(inphase, quadrature);
+
+ //Mix current sample with costas loop to remove any rotation that is present from a mis-tuned carrier frequency
+ mReceivedSample.multiply(mPLL.incrementAndGetCurrentVector());
+
+ //Store the sample in the interpolating buffer
+ mInterpolatingSampleBuffer.receive(mReceivedSample);
+
+ //Calculate the symbol once we've stored enough samples
+ if(mInterpolatingSampleBuffer.hasSymbol())
+ {
+ calculateSymbol();
+ }
+ }
+
+ protected abstract void calculateSymbol();
+
+ /**
+ * 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/instrument/gui/viewer/DemodulatorViewerFX.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/DemodulatorViewerFX.java
index 5e9745a6..5af21746 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
@@ -17,6 +17,7 @@ package io.github.dsheirer.instrument.gui.viewer;
import io.github.dsheirer.instrument.gui.viewer.decoder.DecoderPaneFactory;
import io.github.dsheirer.module.decode.DecoderType;
+import io.github.dsheirer.module.decode.p25.P25Decoder;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.event.ActionEvent;
@@ -50,7 +51,7 @@ public class DemodulatorViewerFX extends Application
public void start(Stage primaryStage) throws Exception
{
mStage = primaryStage;
- setTitle(null);
+ setTitle();
BorderPane borderPane = new BorderPane();
borderPane.setTop(getMenuBar());
@@ -62,6 +63,11 @@ public class DemodulatorViewerFX extends Application
mStage.show();
}
+ private void setTitle()
+ {
+ mStage.setTitle("Demodulator Viewer FX");
+ }
+
private void setTitle(DecoderType decoderType)
{
if(decoderType != null)
@@ -70,7 +76,19 @@ public class DemodulatorViewerFX extends Application
}
else
{
- mStage.setTitle("Demodulator Viewer FX");
+ setTitle();
+ }
+ }
+
+ private void setTitle(P25Decoder.Modulation modulation)
+ {
+ if(modulation != null)
+ {
+ mStage.setTitle("Demodulator Viewer FX - P25 " + modulation.getLabel());
+ }
+ else
+ {
+ setTitle();
}
}
@@ -136,19 +154,52 @@ public class DemodulatorViewerFX extends Application
{
if(DecoderPaneFactory.isSupported(decoderType))
{
- MenuItem decoderMenuItem = new MenuItem(decoderType.getShortDisplayString());
-
- decoderMenuItem.setOnAction(new EventHandler()
+ if(decoderType == DecoderType.P25_PHASE1)
{
- @Override
- public void handle(ActionEvent event)
- {
- getViewerDesktop().setDecoder(decoderType);
- setTitle(decoderType);
- }
- });
+ MenuItem c4fmDecoderMenuItem = new MenuItem("P25 Phase 1 C4FM");
- decoderMenu.getItems().add(decoderMenuItem);
+ c4fmDecoderMenuItem.setOnAction(new EventHandler()
+ {
+ @Override
+ public void handle(ActionEvent event)
+ {
+ getViewerDesktop().setP25Phase1Decoder(P25Decoder.Modulation.C4FM);
+ setTitle(decoderType);
+ }
+ });
+
+ decoderMenu.getItems().add(c4fmDecoderMenuItem);
+
+ MenuItem lsmDecoderMenuItem = new MenuItem("P25 Phase 1 LSM");
+
+ lsmDecoderMenuItem.setOnAction(new EventHandler()
+ {
+ @Override
+ public void handle(ActionEvent event)
+ {
+ getViewerDesktop().setP25Phase1Decoder(P25Decoder.Modulation.CQPSK);
+ setTitle(decoderType);
+ }
+ });
+
+ decoderMenu.getItems().add(lsmDecoderMenuItem);
+ }
+ else
+ {
+ 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);
+ }
}
}
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 1ea7a687..6462901f 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
@@ -16,9 +16,9 @@
package io.github.dsheirer.instrument.gui.viewer;
import io.github.dsheirer.instrument.gui.viewer.decoder.DecoderPane;
-import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
-import io.github.dsheirer.sample.complex.ComplexBuffer;
+import io.github.dsheirer.sample.complex.reusable.ReusableBufferBroadcaster;
+import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer;
import io.github.dsheirer.source.IControllableFileSource;
import io.github.dsheirer.source.IFrameLocationListener;
import io.github.dsheirer.source.wave.ComplexWaveSource;
@@ -40,7 +40,7 @@ import javax.sound.sampled.UnsupportedAudioFileException;
import java.io.File;
import java.io.IOException;
-public class PlaybackController extends HBox implements IFrameLocationListener, Listener
+public class PlaybackController extends HBox implements IFrameLocationListener, Listener
{
private final static Logger mLog = LoggerFactory.getLogger(PlaybackController.class);
@@ -56,7 +56,7 @@ public class PlaybackController extends HBox implements IFrameLocationListener,
private DecoderPane mSampleRateListener;
private IControllableFileSource mControllableFileSource;
- private Broadcaster mComplexBufferBroadcaster = new Broadcaster<>();
+ private ReusableBufferBroadcaster mComplexBufferBroadcaster = new ReusableBufferBroadcaster();
public PlaybackController()
{
@@ -85,7 +85,7 @@ public class PlaybackController extends HBox implements IFrameLocationListener,
{
mControllableFileSource = new ComplexWaveSource(file);
mControllableFileSource.setListener(this);
- ((ComplexWaveSource)mControllableFileSource).setListener((Listener)this);
+ ((ComplexWaveSource)mControllableFileSource).setListener((Listener)this);
mControllableFileSource.open();
if(mSampleRateListener != null)
@@ -136,7 +136,7 @@ public class PlaybackController extends HBox implements IFrameLocationListener,
/**
* Adds listener to receive complex buffers from this playback
*/
- public void addListener(Listener listener)
+ public void addListener(Listener listener)
{
mComplexBufferBroadcaster.addListener(listener);
}
@@ -144,7 +144,7 @@ public class PlaybackController extends HBox implements IFrameLocationListener,
/**
* Removes the listener from receiving complex buffers from this playback
*/
- public void removeListener(Listener listener)
+ public void removeListener(Listener listener)
{
mComplexBufferBroadcaster.removeListener(listener);
}
@@ -304,7 +304,7 @@ public class PlaybackController extends HBox implements IFrameLocationListener,
}
@Override
- public void receive(ComplexBuffer complexBuffer)
+ public void receive(ReusableComplexBuffer complexBuffer)
{
mComplexBufferBroadcaster.broadcast(complexBuffer);
}
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 bdd01f5d..fcdef3f9 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
@@ -18,6 +18,7 @@ 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 io.github.dsheirer.module.decode.p25.P25Decoder;
import javafx.scene.layout.BorderPane;
import java.io.File;
@@ -40,14 +41,22 @@ public class ViewerDesktop extends BorderPane
}
public void setDecoder(DecoderType decoderType)
+ {
+ setDecoderPane(DecoderPaneFactory.getDecoderPane(decoderType));
+ }
+
+ public void setP25Phase1Decoder(P25Decoder.Modulation modulation)
+ {
+ setDecoderPane(DecoderPaneFactory.getP25DecoderPane(modulation));
+ }
+
+ private void setDecoderPane(DecoderPane decoderPane)
{
getPlaybackController().removeListener(getDecoderPane());
getChildren().remove(getDecoderPane());
-
- mDecoderPane = DecoderPaneFactory.getDecoderPane(decoderType);
+ mDecoderPane = decoderPane;
setCenter(getDecoderPane());
getPlaybackController().addListener(getDecoderPane());
-
getPlaybackController().setSampleRateListener(getDecoderPane());
}
diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexSampleLineChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexSampleLineChart.java
index d2b25b4b..bec81eef 100644
--- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexSampleLineChart.java
+++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexSampleLineChart.java
@@ -19,7 +19,7 @@ import io.github.dsheirer.buffer.ComplexCircularBuffer;
import io.github.dsheirer.dsp.gain.ComplexGain;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.Complex;
-import io.github.dsheirer.sample.complex.ComplexBuffer;
+import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.value.ChangeListener;
@@ -32,7 +32,7 @@ import javafx.scene.chart.XYChart;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-public class ComplexSampleLineChart extends LineChart implements Listener
+public class ComplexSampleLineChart extends LineChart implements Listener
{
private final static Logger mLog = LoggerFactory.getLogger(ComplexSampleLineChart.class);
@@ -92,7 +92,7 @@ public class ComplexSampleLineChart extends LineChart implements Listener
+public class DecoderPane extends VBox implements Listener
{
- private Broadcaster mComplexBufferBroadcaster = new Broadcaster<>();
+ private ReusableBufferBroadcaster mComplexBufferBroadcaster = new ReusableBufferBroadcaster();
private DecoderType mDecoderType;
public DecoderPane(DecoderType decoderType)
@@ -41,17 +41,17 @@ public class DecoderPane extends VBox implements Listener
}
@Override
- public void receive(ComplexBuffer complexBuffer)
+ public void receive(ReusableComplexBuffer reusableComplexBuffer)
{
- mComplexBufferBroadcaster.broadcast(complexBuffer);
+ mComplexBufferBroadcaster.broadcast(reusableComplexBuffer);
}
- public void addListener(Listener listener)
+ public void addListener(Listener listener)
{
mComplexBufferBroadcaster.addListener(listener);
}
- public void removeListener(Listener listener)
+ public void removeListener(Listener listener)
{
mComplexBufferBroadcaster.removeListener(listener);
}
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
index b069204e..03590922 100644
--- 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
@@ -16,6 +16,7 @@
package io.github.dsheirer.instrument.gui.viewer.decoder;
import io.github.dsheirer.module.decode.DecoderType;
+import io.github.dsheirer.module.decode.p25.P25Decoder;
import java.util.EnumSet;
@@ -31,7 +32,23 @@ public class DecoderPaneFactory
switch(decoderType)
{
case P25_PHASE1:
- return new P25Phase1Pane();
+ throw new IllegalArgumentException("Use the getP25DecoderPane() method for P25 decoder type");
+ }
+
+ return getDefaultPane();
+ }
+
+ /**
+ * Creates a decoder pane for the P25 decoder type
+ */
+ public static DecoderPane getP25DecoderPane(P25Decoder.Modulation modulation)
+ {
+ switch(modulation)
+ {
+ case C4FM:
+ return new P25Phase1C4FMPane();
+ case CQPSK:
+ return new P25Phase1LSMPane();
}
return getDefaultPane();
diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1C4FMPane.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1C4FMPane.java
new file mode 100644
index 00000000..66ac5153
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1C4FMPane.java
@@ -0,0 +1,194 @@
+/*******************************************************************************
+ * sdr-trunk
+ * Copyright (C) 2014-2018 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with this program.
+ * If not, see
+ *
+ ******************************************************************************/
+package io.github.dsheirer.instrument.gui.viewer.decoder;
+
+import io.github.dsheirer.instrument.gui.viewer.chart.ComplexSampleLineChart;
+import io.github.dsheirer.instrument.gui.viewer.chart.DoubleLineChart;
+import io.github.dsheirer.instrument.gui.viewer.chart.EyeDiagramChart;
+import io.github.dsheirer.instrument.gui.viewer.chart.PhaseLineChart;
+import io.github.dsheirer.instrument.gui.viewer.chart.SymbolChart;
+import io.github.dsheirer.message.Message;
+import io.github.dsheirer.module.decode.DecoderType;
+import io.github.dsheirer.module.decode.p25.P25DecoderC4FMInstrumented;
+import io.github.dsheirer.sample.Listener;
+import io.github.dsheirer.sample.complex.reusable.ReusableBufferBroadcaster;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class P25Phase1C4FMPane extends DecoderPane
+{
+ private final static Logger mLog = LoggerFactory.getLogger(P25Phase1C4FMPane.class);
+
+ private HBox mSampleChartBox;
+ private ComplexSampleLineChart mSampleLineChart;
+ private EyeDiagramChart mEyeDiagramChart;
+ private HBox mDecoderChartBox;
+ private SymbolChart mSymbolChart;
+ private PhaseLineChart mPLLPhaseErrorLineChart;
+ private DoubleLineChart mPLLFrequencyLineChart;
+ private DoubleLineChart mSamplesPerSymbolLineChart;
+ private ReusableBufferBroadcaster mFilteredBufferBroadcaster = new ReusableBufferBroadcaster();
+ private P25DecoderC4FMInstrumented mDecoder = new P25DecoderC4FMInstrumented(null);
+
+ public P25Phase1C4FMPane()
+ {
+ super(DecoderType.P25_PHASE1);
+ init();
+ }
+
+ private void init()
+ {
+ addListener(getDecoder());
+
+ getDecoder().setFilteredBufferListener(mFilteredBufferBroadcaster);
+ getDecoder().setComplexSymbolListener(getSymbolChart());
+ getDecoder().setPLLPhaseErrorListener(getPLLPhaseErrorLineChart());
+ getDecoder().setPLLFrequencyListener(getPLLFrequencyLineChart());
+ getDecoder().setSymbolDecisionDataListener(getEyeDiagramChart());
+ getDecoder().setSamplesPerSymbolListener(getSamplesPerSymbolLineChart());
+ mFilteredBufferBroadcaster.addListener(getSampleLineChart());
+
+ HBox.setHgrow(getSampleChartBox(), Priority.ALWAYS);
+ HBox.setHgrow(getDecoderChartBox(), Priority.ALWAYS);
+ getChildren().addAll(getSampleChartBox(), getDecoderChartBox());
+
+ mDecoder.setMessageListener(new Listener()
+ {
+ @Override
+ public void receive(Message message)
+ {
+ mLog.debug(message.getMessage());
+ }
+ });
+
+ }
+
+ @Override
+ public void setSampleRate(double sampleRate)
+ {
+ mLog.debug("Configuring for sample rate: " + sampleRate);
+
+ mDecoder.setSampleRate(sampleRate);
+ double samplesPerSymbol = sampleRate / 4800.0;
+
+ getSampleLineChart().setSamplesPerSymbol((int)samplesPerSymbol);
+ }
+
+ private P25DecoderC4FMInstrumented getDecoder()
+ {
+ return mDecoder;
+ }
+
+ private SymbolChart getSymbolChart()
+ {
+ if(mSymbolChart == null)
+ {
+ mSymbolChart = new SymbolChart(10);
+ }
+
+ return mSymbolChart;
+ }
+
+ private HBox getDecoderChartBox()
+ {
+ if(mDecoderChartBox == null)
+ {
+ mDecoderChartBox = new HBox();
+ mDecoderChartBox.setMaxHeight(Double.MAX_VALUE);
+ getSymbolChart().setMaxWidth(Double.MAX_VALUE);
+ getPLLPhaseErrorLineChart().setMaxWidth(Double.MAX_VALUE);
+ getPLLFrequencyLineChart().setMaxWidth(Double.MAX_VALUE);
+ HBox.setHgrow(getSymbolChart(), Priority.ALWAYS);
+ HBox.setHgrow(getPLLPhaseErrorLineChart(), Priority.ALWAYS);
+ HBox.setHgrow(getPLLFrequencyLineChart(), Priority.ALWAYS);
+ mDecoderChartBox.getChildren().addAll(getSymbolChart(), getPLLPhaseErrorLineChart(),
+ getPLLFrequencyLineChart());
+ }
+
+ return mDecoderChartBox;
+ }
+
+ private HBox getSampleChartBox()
+ {
+ if(mSampleChartBox == null)
+ {
+ mSampleChartBox = new HBox();
+ mSampleChartBox.setMaxHeight(Double.MAX_VALUE);
+ getSampleLineChart().setMaxWidth(Double.MAX_VALUE);
+ getEyeDiagramChart().setMaxWidth(Double.MAX_VALUE);
+ getSamplesPerSymbolLineChart().setMaxWidth(Double.MAX_VALUE);
+ HBox.setHgrow(getSampleLineChart(), Priority.ALWAYS);
+ HBox.setHgrow(getEyeDiagramChart(), Priority.ALWAYS);
+ HBox.setHgrow(getSamplesPerSymbolLineChart(), Priority.ALWAYS);
+ mSampleChartBox.getChildren().addAll(getSampleLineChart(), getEyeDiagramChart(),
+ getSamplesPerSymbolLineChart());
+ }
+
+ return mSampleChartBox;
+ }
+
+ private ComplexSampleLineChart getSampleLineChart()
+ {
+ if(mSampleLineChart == null)
+ {
+ mSampleLineChart = new ComplexSampleLineChart(100, 10);
+ }
+
+ return mSampleLineChart;
+ }
+
+ private EyeDiagramChart getEyeDiagramChart()
+ {
+ if(mEyeDiagramChart == null)
+ {
+ mEyeDiagramChart = new EyeDiagramChart(10);
+ }
+
+ return mEyeDiagramChart;
+ }
+
+ private PhaseLineChart getPLLPhaseErrorLineChart()
+ {
+ if(mPLLPhaseErrorLineChart == null)
+ {
+ mPLLPhaseErrorLineChart = new PhaseLineChart(40);
+ }
+
+ return mPLLPhaseErrorLineChart;
+ }
+
+ private DoubleLineChart getPLLFrequencyLineChart()
+ {
+ if(mPLLFrequencyLineChart == null)
+ {
+ mPLLFrequencyLineChart = new DoubleLineChart( "PLL Frequency", -500, 500, 50, 40);
+ }
+
+ return mPLLFrequencyLineChart;
+ }
+
+ private DoubleLineChart getSamplesPerSymbolLineChart()
+ {
+ if(mSamplesPerSymbolLineChart == null)
+ {
+ mSamplesPerSymbolLineChart = new DoubleLineChart( "Sample Point", 9.5, 11.5, 0.1, 40);
+ }
+
+ return mSamplesPerSymbolLineChart;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1Pane.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1LSMPane.java
similarity index 91%
rename from src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1Pane.java
rename to src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1LSMPane.java
index 526d3e7f..a69538a7 100644
--- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1Pane.java
+++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1LSMPane.java
@@ -22,18 +22,17 @@ import io.github.dsheirer.instrument.gui.viewer.chart.PhaseLineChart;
import io.github.dsheirer.instrument.gui.viewer.chart.SymbolChart;
import io.github.dsheirer.message.Message;
import io.github.dsheirer.module.decode.DecoderType;
-import io.github.dsheirer.module.decode.p25.P25_C4FMDecoderInstrumented;
-import io.github.dsheirer.sample.Broadcaster;
+import io.github.dsheirer.module.decode.p25.P25DecoderLSMInstrumented;
import io.github.dsheirer.sample.Listener;
-import io.github.dsheirer.sample.complex.ComplexBuffer;
+import io.github.dsheirer.sample.complex.reusable.ReusableBufferBroadcaster;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-public class P25Phase1Pane extends DecoderPane
+public class P25Phase1LSMPane extends DecoderPane
{
- private final static Logger mLog = LoggerFactory.getLogger(P25Phase1Pane.class);
+ private final static Logger mLog = LoggerFactory.getLogger(P25Phase1LSMPane.class);
private HBox mSampleChartBox;
private ComplexSampleLineChart mSampleLineChart;
@@ -43,10 +42,10 @@ public class P25Phase1Pane extends DecoderPane
private PhaseLineChart mPLLPhaseErrorLineChart;
private DoubleLineChart mPLLFrequencyLineChart;
private DoubleLineChart mSamplesPerSymbolLineChart;
- private Broadcaster mFilteredBufferBroadcaster = new Broadcaster<>();
- private P25_C4FMDecoderInstrumented mDecoder = new P25_C4FMDecoderInstrumented(null);
+ private ReusableBufferBroadcaster mFilteredBufferBroadcaster = new ReusableBufferBroadcaster();
+ private P25DecoderLSMInstrumented mDecoder = new P25DecoderLSMInstrumented(null);
- public P25Phase1Pane()
+ public P25Phase1LSMPane()
{
super(DecoderType.P25_PHASE1);
init();
@@ -57,7 +56,7 @@ public class P25Phase1Pane extends DecoderPane
addListener(getDecoder());
getDecoder().setFilteredBufferListener(mFilteredBufferBroadcaster);
- getDecoder().setSymbolListener(getSymbolChart());
+ getDecoder().setComplexSymbolListener(getSymbolChart());
getDecoder().setPLLPhaseErrorListener(getPLLPhaseErrorLineChart());
getDecoder().setPLLFrequencyListener(getPLLFrequencyLineChart());
getDecoder().setSymbolDecisionDataListener(getEyeDiagramChart());
@@ -90,7 +89,7 @@ public class P25Phase1Pane extends DecoderPane
getSampleLineChart().setSamplesPerSymbol((int)samplesPerSymbol);
}
- private P25_C4FMDecoderInstrumented getDecoder()
+ private P25DecoderLSMInstrumented getDecoder()
{
return mDecoder;
}
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 f8f4953b..00859c78 100644
--- a/src/main/java/io/github/dsheirer/module/decode/Decoder.java
+++ b/src/main/java/io/github/dsheirer/module/decode/Decoder.java
@@ -41,6 +41,21 @@ public abstract class Decoder extends Module implements IMessageProvider
mMessageListener = null;
}
+ public void start()
+ {
+ //no-op
+ }
+
+ public void stop()
+ {
+ //no-op
+ }
+
+ public void reset()
+ {
+ //no-op
+ }
+
/**
* Identifies the decoder type (ie protocol)
*/
diff --git a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java
index 0c19e47c..146e8f3a 100644
--- a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java
+++ b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java
@@ -75,10 +75,10 @@ import io.github.dsheirer.module.decode.nbfm.NBFMDecoder;
import io.github.dsheirer.module.decode.nbfm.NBFMDecoderEditor;
import io.github.dsheirer.module.decode.p25.DecodeConfigP25Phase1;
import io.github.dsheirer.module.decode.p25.P25Decoder.Modulation;
+import io.github.dsheirer.module.decode.p25.P25DecoderC4FM;
import io.github.dsheirer.module.decode.p25.P25DecoderEditor;
+import io.github.dsheirer.module.decode.p25.P25DecoderLSM;
import io.github.dsheirer.module.decode.p25.P25DecoderState;
-import io.github.dsheirer.module.decode.p25.P25_C4FMDecoder;
-import io.github.dsheirer.module.decode.p25.P25_LSMDecoder;
import io.github.dsheirer.module.decode.p25.audio.P25AudioModule;
import io.github.dsheirer.module.decode.p25.message.filter.P25MessageFilterSet;
import io.github.dsheirer.module.decode.passport.DecodeConfigPassport;
@@ -247,12 +247,12 @@ public class DecoderFactory
switch(modulation)
{
case C4FM:
- modules.add(new P25_C4FMDecoder(aliasList));
- modules.add(new P25DecoderState(aliasList, channelType, P25Decoder.Modulation.C4FM,
+ modules.add(new P25DecoderC4FM(aliasList));
+ modules.add(new P25DecoderState(aliasList, channelType, Modulation.C4FM,
p25Config.getIgnoreDataCalls()));
break;
case CQPSK:
- modules.add(new P25_LSMDecoder(aliasList));
+ modules.add(new P25DecoderLSM(aliasList));
modules.add(new P25DecoderState(aliasList, channelType, Modulation.CQPSK, p25Config.getIgnoreDataCalls()));
break;
default:
diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/DecodeConfigP25Phase1.java b/src/main/java/io/github/dsheirer/module/decode/p25/DecodeConfigP25Phase1.java
index 03aec81b..d0dc5620 100644
--- a/src/main/java/io/github/dsheirer/module/decode/p25/DecodeConfigP25Phase1.java
+++ b/src/main/java/io/github/dsheirer/module/decode/p25/DecodeConfigP25Phase1.java
@@ -25,7 +25,7 @@ import io.github.dsheirer.module.decode.config.DecodeConfiguration;
public class DecodeConfigP25Phase1 extends DecodeConfiguration
{
- private P25_LSMDecoder.Modulation mModulation = P25Decoder.Modulation.C4FM;
+ private P25DecoderLSM.Modulation mModulation = P25Decoder.Modulation.C4FM;
private int mCallTimeout = 1;
private int mTrafficChannelPoolSize = TRAFFIC_CHANNEL_LIMIT_DEFAULT;
@@ -48,12 +48,12 @@ public class DecodeConfigP25Phase1 extends DecodeConfiguration
}
@JacksonXmlProperty(isAttribute = true, localName = "modulation")
- public P25_LSMDecoder.Modulation getModulation()
+ public P25DecoderLSM.Modulation getModulation()
{
return mModulation;
}
- public void setModulation(P25_LSMDecoder.Modulation modulation)
+ public void setModulation(P25DecoderLSM.Modulation modulation)
{
mModulation = modulation;
}
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 e85dec3c..dfcf230b 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
@@ -18,26 +18,70 @@ package io.github.dsheirer.module.decode.p25;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.module.decode.Decoder;
import io.github.dsheirer.module.decode.DecoderType;
+import io.github.dsheirer.sample.Listener;
+import io.github.dsheirer.sample.complex.reusable.IReusableComplexBufferListener;
+import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer;
import io.github.dsheirer.source.ISourceEventListener;
import io.github.dsheirer.source.ISourceEventProvider;
+import io.github.dsheirer.source.SourceEvent;
-public abstract class P25Decoder extends Decoder implements ISourceEventListener, ISourceEventProvider, Instrumentable
+public abstract class P25Decoder extends Decoder implements ISourceEventListener, ISourceEventProvider,
+ IReusableComplexBufferListener, Listener
{
+ private double mSampleRate;
private P25MessageProcessor mMessageProcessor;
private AliasList mAliasList;
+ private Listener mSourceEventListener;
+ private double mSymbolRate;
- public P25Decoder(AliasList aliasList)
+ public P25Decoder(double symbolRate, AliasList aliasList)
{
+ mSymbolRate = symbolRate;
mAliasList = aliasList;
mMessageProcessor = new P25MessageProcessor(mAliasList);
mMessageProcessor.setMessageListener(getMessageListener());
}
+ protected double getSymbolRate()
+ {
+ return mSymbolRate;
+ }
+
protected AliasList getAliasList()
{
return mAliasList;
}
+ /**
+ * Current sample rate for this decoder
+ */
+ protected double getSampleRate()
+ {
+ return mSampleRate;
+ }
+
+ /**
+ * Sets current sample rate for this decoder
+ */
+ public void setSampleRate(double sampleRate)
+ {
+ if(sampleRate <= getSymbolRate() * 2)
+ {
+ throw new IllegalArgumentException("Sample rate [" + sampleRate + "] must be >9600 (2 * " +
+ getSymbolRate() + " symbol rate)");
+ }
+
+ mSampleRate = sampleRate;
+ }
+
+ /**
+ * Samples per symbol based on current sample rate and symbol rate.
+ */
+ public float getSamplesPerSymbol()
+ {
+ return (float)(getSampleRate() / getSymbolRate());
+ }
+
public void dispose()
{
super.dispose();
@@ -46,6 +90,83 @@ public abstract class P25Decoder extends Decoder implements ISourceEventListener
mMessageProcessor = null;
}
+ /**
+ * Sets the source event listener to receive source events from this decoder.
+ */
+ @Override
+ public void setSourceEventListener(Listener listener)
+ {
+ mSourceEventListener = listener;
+ }
+
+ /**
+ * Removes a registered source event listener from receiving source events from this decoder
+ */
+ @Override
+ public void removeSourceEventListener()
+ {
+ mSourceEventListener = null;
+ }
+
+ @Override
+ public Listener getSourceEventListener()
+ {
+ return new Listener()
+ {
+ @Override
+ public void receive(SourceEvent sourceEvent)
+ {
+ process(sourceEvent);
+ }
+ };
+ }
+
+ /**
+ * Sub-class processing of received source events
+ * @param sourceEvent
+ */
+ protected abstract void process(SourceEvent sourceEvent);
+
+ /**
+ * Broadcasts the source event to an optional registered listener. This method should primarily be used to
+ * issue frequency correction requests to the channel source.
+ * @param sourceEvent to broadcast
+ */
+ protected void broadcast(SourceEvent sourceEvent)
+ {
+ if(mSourceEventListener != null)
+ {
+ mSourceEventListener.receive(sourceEvent);
+ }
+ }
+
+ /**
+ * Listener interface to receive reusable complex buffers
+ */
+ @Override
+ public Listener getReusableComplexBufferListener()
+ {
+ return P25Decoder.this;
+ }
+
+ /**
+ * Starts the decoder
+ */
+ @Override
+ public void start()
+ {
+ //No-op
+ }
+
+ /**
+ * Stops the decoder
+ */
+ @Override
+ public void stop()
+ {
+ //No-op
+ }
+
@Override
public DecoderType getDecoderType()
{
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/P25DecoderC4FM.java
similarity index 56%
rename from src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder.java
rename to src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FM.java
index 6923bc37..cc787e5a 100644
--- a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder.java
+++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FM.java
@@ -20,79 +20,88 @@ import io.github.dsheirer.dsp.filter.FilterFactory;
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter;
-import io.github.dsheirer.dsp.psk.DQPSKDemodulator;
+import io.github.dsheirer.dsp.psk.DQPSKDecisionDirectedDemodulator;
import io.github.dsheirer.dsp.psk.InterpolatingSampleBuffer;
-import io.github.dsheirer.dsp.psk.InterpolatingSampleBufferInstrumented;
import io.github.dsheirer.dsp.psk.pll.CostasLoop;
-import io.github.dsheirer.sample.Listener;
-import io.github.dsheirer.sample.complex.ComplexBuffer;
-import io.github.dsheirer.sample.complex.IComplexBufferListener;
-import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent;
+import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer;
+import io.github.dsheirer.source.SourceEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
-import java.util.concurrent.ScheduledExecutorService;
-public class P25_C4FMDecoder extends P25Decoder implements IComplexBufferListener, Listener
+public class P25DecoderC4FM extends P25Decoder
{
- private final static Logger mLog = LoggerFactory.getLogger(P25_C4FMDecoder.class);
- protected static final double SYMBOL_RATE = 4800;
+ private final static Logger mLog = LoggerFactory.getLogger(P25DecoderC4FM.class);
+ protected static final float SYMBOL_TIMING_GAIN = 0.1f;
protected InterpolatingSampleBuffer mInterpolatingSampleBuffer;
- protected DQPSKDemodulator mQPSKDemodulator;
+ protected DQPSKDecisionDirectedDemodulator mQPSKDemodulator;
protected CostasLoop mCostasLoop;
protected P25MessageFramer mMessageFramer;
- protected double mSampleRate;
private Map mBasebandFilters = new HashMap<>();
private ComplexFIRFilter mBasebandFilter;
- public P25_C4FMDecoder(AliasList aliasList)
+ /**
+ * P25 Phase 1 - standard C4FM modulation decoder. Uses Differential QPSK decoding with a Costas PLL and a
+ * decision-directed phase and timing error detector.
+ *
+ * @param aliasList
+ */
+ public P25DecoderC4FM(AliasList aliasList)
{
- super(aliasList);
+ super(4800.0, aliasList);
setSampleRate(48000.0);
}
public void setSampleRate(double sampleRate)
{
- if(sampleRate <= SYMBOL_RATE * 2)
- {
- throw new IllegalArgumentException("Sample rate must be at least twice the symbol rate [4800]");
- }
+ super.setSampleRate(sampleRate);
- mSampleRate = sampleRate;
mBasebandFilter = new ComplexFIRFilter(getBasebandFilter(), 1.0f);
- mCostasLoop = new CostasLoop(mSampleRate, SYMBOL_RATE);
+ mCostasLoop = new CostasLoop(getSampleRate(), getSymbolRate());
+ mInterpolatingSampleBuffer = new InterpolatingSampleBuffer(getSamplesPerSymbol(), SYMBOL_TIMING_GAIN);
- mInterpolatingSampleBuffer = new InterpolatingSampleBufferInstrumented((float)(sampleRate / SYMBOL_RATE));
- mQPSKDemodulator = new DQPSKDemodulator(mCostasLoop, mInterpolatingSampleBuffer);
+ mQPSKDemodulator = new DQPSKDecisionDirectedDemodulator(mCostasLoop, mInterpolatingSampleBuffer);
- //Message framer can trigger a symbol-inversion correction to the PLL when detected
+ //Message framer can issue a symbol-inversion correction request to the PLL when detected
mMessageFramer = new P25MessageFramer(getAliasList(), mCostasLoop);
mMessageFramer.setListener(getMessageProcessor());
- mQPSKDemodulator.setDibitListener(mMessageFramer);
+ mQPSKDemodulator.setSymbolListener(mMessageFramer);
}
+ /**
+ * Primary method for processing incoming complex sample buffers
+ * @param reusableComplexBuffer containing channelized complex samples
+ */
@Override
- public void receive(ComplexBuffer complexBuffer)
+ public void receive(ReusableComplexBuffer reusableComplexBuffer)
{
- ComplexBuffer filtered = filter(complexBuffer);
+ //User accounting of the incoming buffer is handled by the filter
+ ReusableComplexBuffer filtered = filter(reusableComplexBuffer);
+
+ //User accounting of the filtered buffer is handled by the demodulator
mQPSKDemodulator.receive(filtered);
}
- protected ComplexBuffer filter(ComplexBuffer complexBuffer)
+ /**
+ * Filters the complex buffer and returns a new reusable complex buffer with the filtered contents.
+ * @param reusableComplexBuffer to filter
+ * @return filtered complex buffer
+ */
+ protected ReusableComplexBuffer filter(ReusableComplexBuffer reusableComplexBuffer)
{
- return mBasebandFilter.filter(complexBuffer);
- }
-
- private double getSampleRate()
- {
- return mSampleRate;
+ //User accounting of the incoming buffer is handled by the filter
+ return mBasebandFilter.filter(reusableComplexBuffer);
}
+ /**
+ * Constructs a baseband filter for this decoder using the current sample rate
+ */
private float[] getBasebandFilter()
{
+ //Attempt to reuse a cached (ie already-designed) filter if available
float[] filter = mBasebandFilters.get(getSampleRate());
if(filter == null)
@@ -110,19 +119,19 @@ public class P25_C4FMDecoder extends P25Decoder implements IComplexBufferListene
try
{
filter = FilterFactory.getTaps(specification);
-
}
catch(FilterDesignException fde)
{
mLog.error("Couldn't design low pass baseband filter for sample rate: " + getSampleRate());
}
+
if(filter != null)
{
mBasebandFilters.put(getSampleRate(), filter);
}
else
{
- throw new IllegalStateException("Couldn't design a C4FM symbol filter for sample rate: " + mSampleRate);
+ throw new IllegalStateException("Couldn't design a C4FM baseband filter for sample rate: " + getSampleRate());
}
}
@@ -141,60 +150,36 @@ public class P25_C4FMDecoder extends P25Decoder implements IComplexBufferListene
}
@Override
- public void setFrequencyChangeListener(Listener listener)
+ protected void process(SourceEvent sourceEvent)
{
- }
-
- @Override
- public void removeFrequencyChangeListener()
- {
- }
-
- @Override
- public Listener getFrequencyChangeListener()
- {
- return new Listener()
+ switch(sourceEvent.getEvent())
{
- @Override
- public void receive(FrequencyChangeEvent frequencyChangeEvent)
- {
- switch(frequencyChangeEvent.getEvent())
- {
- case NOTIFICATION_FREQUENCY_CHANGE:
- case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
- case NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE:
- case NOTIFICATION_SAMPLE_RATE_CHANGE:
- mCostasLoop.reset();
- break;
- }
- }
- };
- }
-
- @Override
- public Listener getComplexBufferListener()
- {
- return P25_C4FMDecoder.this;
+ case NOTIFICATION_FREQUENCY_CHANGE:
+ case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
+ case NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE:
+ mCostasLoop.reset();
+ break;
+ case NOTIFICATION_SAMPLE_RATE_CHANGE:
+ mCostasLoop.reset();
+ setSampleRate(sourceEvent.getValue().doubleValue());
+ break;
+ }
}
+ /**
+ * P25 modulation supported by this decoder
+ */
public Modulation getModulation()
{
return Modulation.C4FM;
}
+ /**
+ * Resets this decoder to prepare for processing a new channel
+ */
@Override
public void reset()
{
mCostasLoop.reset();
}
-
- @Override
- public void start(ScheduledExecutorService executor)
- {
- }
-
- @Override
- public void stop()
- {
- }
}
diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FMInstrumented.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FMInstrumented.java
new file mode 100644
index 00000000..c7b329bc
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FMInstrumented.java
@@ -0,0 +1,121 @@
+/*******************************************************************************
+ * sdr-trunk
+ * Copyright (C) 2014-2018 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with this program.
+ * If not, see
+ *
+ ******************************************************************************/
+package io.github.dsheirer.module.decode.p25;
+
+import io.github.dsheirer.alias.AliasList;
+import io.github.dsheirer.dsp.psk.DQPSKDecisionDirectedDemodulatorInstrumented;
+import io.github.dsheirer.dsp.psk.InterpolatingSampleBufferInstrumented;
+import io.github.dsheirer.dsp.psk.SymbolDecisionData;
+import io.github.dsheirer.sample.Listener;
+import io.github.dsheirer.sample.complex.Complex;
+import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer;
+
+public class P25DecoderC4FMInstrumented extends P25DecoderC4FM
+{
+ private Listener mPLLPhaseErrorListener;
+ private Listener mPLLFrequencyListener;
+ private Listener mSamplesPerSymbolListener;
+ private Listener mComplexSymbolListener;
+ private Listener mFilteredSymbolListener;
+ private Listener mSymbolDecisionDataListener;
+
+ /**
+ * Instrumented version of the P25 C4FM decoder that supports registering listeners to provide access to data as
+ * it is being processed by the decoder.
+ *
+ * @param aliasList
+ */
+ public P25DecoderC4FMInstrumented(AliasList aliasList)
+ {
+ super(aliasList);
+ }
+
+ /**
+ * Overrides the filter method so that we can capture the filtered samples for instrumentation
+ */
+ protected ReusableComplexBuffer filter(ReusableComplexBuffer reusableComplexBuffer)
+ {
+ ReusableComplexBuffer filtered = super.filter(reusableComplexBuffer);
+
+ //Increment user count before we hand to another listener or return from this method
+ filtered.incrementUserCount();
+
+ if(mFilteredSymbolListener != null)
+ {
+ filtered.incrementUserCount();
+ mFilteredSymbolListener.receive(filtered);
+ }
+
+ return filtered;
+ }
+
+ /**
+ * Overrides this method so we can correctly configure for instrumented operations
+ */
+ public void setSampleRate(double sampleRate)
+ {
+ super.setSampleRate(sampleRate);
+
+ InterpolatingSampleBufferInstrumented instrumentedBuffer =
+ new InterpolatingSampleBufferInstrumented(getSamplesPerSymbol(), SYMBOL_TIMING_GAIN);
+ mInterpolatingSampleBuffer = instrumentedBuffer;
+
+ DQPSKDecisionDirectedDemodulatorInstrumented instrumented = new DQPSKDecisionDirectedDemodulatorInstrumented(mCostasLoop, instrumentedBuffer, getSampleRate());
+ mQPSKDemodulator = instrumented;
+
+ instrumented.setComplexSymbolListener(mComplexSymbolListener);
+ instrumented.setPLLErrorListener(mPLLPhaseErrorListener);
+ instrumented.setPLLFrequencyListener(mPLLFrequencyListener);
+ instrumented.setSymbolDecisionDataListener(mSymbolDecisionDataListener);
+ instrumented.setSamplesPerSymbolListener(mSamplesPerSymbolListener);
+ instrumented.setSymbolListener(mMessageFramer);
+ }
+
+ public void setComplexSymbolListener(Listener listener)
+ {
+ mComplexSymbolListener = listener;
+ ((DQPSKDecisionDirectedDemodulatorInstrumented)mQPSKDemodulator).setComplexSymbolListener(listener);
+ }
+
+ public void setPLLPhaseErrorListener(Listener listener)
+ {
+ mPLLPhaseErrorListener = listener;
+ ((DQPSKDecisionDirectedDemodulatorInstrumented)mQPSKDemodulator).setPLLErrorListener(listener);
+ }
+
+ public void setPLLFrequencyListener(Listener listener)
+ {
+ mPLLFrequencyListener = listener;
+ ((DQPSKDecisionDirectedDemodulatorInstrumented)mQPSKDemodulator).setPLLFrequencyListener(listener);
+ }
+
+ public void setFilteredBufferListener(Listener listener)
+ {
+ mFilteredSymbolListener = listener;
+ }
+
+ public void setSymbolDecisionDataListener(Listener listener)
+ {
+ mSymbolDecisionDataListener = listener;
+ ((DQPSKDecisionDirectedDemodulatorInstrumented)mQPSKDemodulator).setSymbolDecisionDataListener(listener);
+ }
+
+ public void setSamplesPerSymbolListener(Listener listener)
+ {
+ mSamplesPerSymbolListener = listener;
+ ((DQPSKDecisionDirectedDemodulatorInstrumented)mQPSKDemodulator).setSamplesPerSymbolListener(listener);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderEditor.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderEditor.java
index e283a708..d6d4df83 100644
--- a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderEditor.java
+++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderEditor.java
@@ -31,7 +31,11 @@ import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import javax.swing.*;
+import javax.swing.DefaultComboBoxModel;
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JSlider;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.event.ActionEvent;
@@ -43,7 +47,7 @@ public class P25DecoderEditor extends ValidatingEditor
private static final long serialVersionUID = 1L;
- private JComboBox mComboModulation;
+ private JComboBox mComboModulation;
private JCheckBox mIgnoreDataCalls;
private JLabel mTrafficChannelPoolSizeLabel;
private JSlider mTrafficChannelPoolSize;
@@ -57,9 +61,9 @@ public class P25DecoderEditor extends ValidatingEditor
{
setLayout( new MigLayout( "insets 0 0 0 0,wrap 3", "[right][grow,fill][]", "" ) );
- mComboModulation = new JComboBox();
- mComboModulation.setModel( new DefaultComboBoxModel(
- P25_LSMDecoder.Modulation.values() ) );
+ mComboModulation = new JComboBox();
+ mComboModulation.setModel( new DefaultComboBoxModel(
+ P25DecoderLSM.Modulation.values() ) );
mComboModulation.setEnabled( false );
mComboModulation.addActionListener( new ActionListener()
{
diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSM.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSM.java
new file mode 100644
index 00000000..74a8cceb
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSM.java
@@ -0,0 +1,177 @@
+/*******************************************************************************
+ * 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.Window.WindowType;
+import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter;
+import io.github.dsheirer.dsp.gain.ComplexFeedForwardGainControl;
+import io.github.dsheirer.dsp.psk.DQPSKGardnerDemodulator;
+import io.github.dsheirer.dsp.psk.InterpolatingSampleBuffer;
+import io.github.dsheirer.dsp.psk.pll.CostasLoop;
+import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer;
+import io.github.dsheirer.source.SourceEvent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class P25DecoderLSM extends P25Decoder
+{
+ private final static Logger mLog = LoggerFactory.getLogger(P25DecoderLSM.class);
+ protected static final float SYMBOL_TIMING_GAIN = 0.1f;
+
+ private Map mBasebandFilters = new HashMap<>();
+ private ComplexFIRFilter mBasebandFilter;
+ private ComplexFeedForwardGainControl mAGC = new ComplexFeedForwardGainControl(32);
+ protected DQPSKGardnerDemodulator mQPSKDemodulator;
+ protected P25MessageFramer mMessageFramer;
+ protected CostasLoop mCostasLoop;
+ protected InterpolatingSampleBuffer mInterpolatingSampleBuffer;
+
+ /**
+ * P25 Phase 1 - linear simulcast modulation (LSM) decoder. Uses Differential QPSK decoding with a Costas PLL and
+ * a gardner timing error detector.
+ *
+ * @param aliasList
+ */
+ public P25DecoderLSM(AliasList aliasList)
+ {
+ super(4800.0, aliasList);
+ setSampleRate(48000.0);
+ }
+
+ /**
+ * Sets or changes the channel sample rate
+ * @param sampleRate in hertz
+ */
+ public void setSampleRate(double sampleRate)
+ {
+ super.setSampleRate(sampleRate);
+
+ mBasebandFilter = new ComplexFIRFilter(getBasebandFilter(), 1.0f);
+
+ mCostasLoop = new CostasLoop(getSampleRate(), getSymbolRate());
+ mInterpolatingSampleBuffer = new InterpolatingSampleBuffer(getSamplesPerSymbol(), SYMBOL_TIMING_GAIN);
+
+ mQPSKDemodulator = new DQPSKGardnerDemodulator(mCostasLoop, mInterpolatingSampleBuffer);
+
+ //Message framer can issue a symbol-inversion correction request to the PLL when detected
+ mMessageFramer = new P25MessageFramer(getAliasList(), mCostasLoop);
+ mMessageFramer.setListener(getMessageProcessor());
+ mQPSKDemodulator.setSymbolListener(mMessageFramer);
+ }
+
+ /**
+ * Primary method for receiving incoming channel samples
+ */
+ @Override
+ public void receive(ReusableComplexBuffer reusableComplexBuffer)
+ {
+ //The filter will decrement the user count when finished
+ ReusableComplexBuffer basebandFiltered = filter(reusableComplexBuffer);
+
+ //AGC will decrement the user count when finished
+ ReusableComplexBuffer gainApplied = mAGC.filter(basebandFiltered);
+
+ //Decoder will decrement the user count when finished
+ mQPSKDemodulator.receive(gainApplied);
+ }
+
+ /**
+ * Filters the complex buffer and returns a new reusable complex buffer with the filtered contents.
+ * @param reusableComplexBuffer to filter
+ * @return filtered complex buffer
+ */
+ protected ReusableComplexBuffer filter(ReusableComplexBuffer reusableComplexBuffer)
+ {
+ //User accounting of the incoming buffer is handled by the filter
+ return mBasebandFilter.filter(reusableComplexBuffer);
+ }
+
+ /**
+ * Processes source events - updates buffer and decoder when sample rate is established/changed and resets the
+ * PLL after frequency changes/corrections
+ */
+ @Override
+ protected void process(SourceEvent sourceEvent)
+ {
+ switch(sourceEvent.getEvent())
+ {
+ case NOTIFICATION_FREQUENCY_CHANGE:
+ case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
+ case NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE:
+ mCostasLoop.reset();
+ break;
+ case NOTIFICATION_SAMPLE_RATE_CHANGE:
+ mCostasLoop.reset();
+ setSampleRate(sourceEvent.getValue().doubleValue());
+ break;
+ }
+ }
+
+ /**
+ * Resets this decoder to prepare for processing a new channel
+ */
+ @Override
+ public void reset()
+ {
+ mCostasLoop.reset();
+ }
+
+ public void dispose()
+ {
+ super.dispose();
+
+ mBasebandFilter.dispose();
+ mBasebandFilter = null;
+
+ mAGC.dispose();
+ mAGC = null;
+
+ mQPSKDemodulator.dispose();
+ mQPSKDemodulator = null;
+
+ mMessageFramer.dispose();
+ mMessageFramer = null;
+ }
+
+ public Modulation getModulation()
+ {
+ return Modulation.CQPSK;
+ }
+
+ /**
+ * Constructs a baseband filter for this decoder using the current sample rate
+ */
+ private float[] getBasebandFilter()
+ {
+ //Attempt to reuse a cached (ie already-designed) filter if available
+ float[] filter = mBasebandFilters.get(getSampleRate());
+
+ if(filter == null)
+ {
+ filter = FilterFactory.getLowPass(48000, 7250, 8000, 60,
+ WindowType.HANN, true);
+
+ mBasebandFilters.put(getSampleRate(), filter);
+ }
+
+ return filter;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoderInstrumented.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSMInstrumented.java
similarity index 59%
rename from src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoderInstrumented.java
rename to src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSMInstrumented.java
index 903396a7..179d3503 100644
--- a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoderInstrumented.java
+++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSMInstrumented.java
@@ -16,23 +16,31 @@
package io.github.dsheirer.module.decode.p25;
import io.github.dsheirer.alias.AliasList;
-import io.github.dsheirer.dsp.psk.DQPSKDemodulatorInstrumented;
+import io.github.dsheirer.dsp.psk.DQPSKGardnerDemodulatorInstrumented;
import io.github.dsheirer.dsp.psk.InterpolatingSampleBufferInstrumented;
import io.github.dsheirer.dsp.psk.SymbolDecisionData;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.Complex;
-import io.github.dsheirer.sample.complex.ComplexBuffer;
+import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer;
-public class P25_C4FMDecoderInstrumented extends P25_C4FMDecoder
+public class P25DecoderLSMInstrumented extends P25DecoderLSM
{
private Listener mPLLPhaseErrorListener;
private Listener mPLLFrequencyListener;
private Listener mSamplesPerSymbolListener;
- private Listener mSymbolListener;
- private Listener mFilteredSymbolListener;
+ private Listener mComplexSymbolListener;
+ private Listener mFilteredSymbolListener;
private Listener mSymbolDecisionDataListener;
- public P25_C4FMDecoderInstrumented(AliasList aliasList)
+ /**
+ * P25 Phase 1 - linear simulcast modulation (LSM) decoder. Uses Differential QPSK decoding with a Costas PLL and
+ * a gardner timing error detector.
+ *
+ * Instrumented decoder instance.
+ *
+ * @param aliasList
+ */
+ public P25DecoderLSMInstrumented(AliasList aliasList)
{
super(aliasList);
}
@@ -40,12 +48,16 @@ public class P25_C4FMDecoderInstrumented extends P25_C4FMDecoder
/**
* Overrides the filter method so that we can capture the filtered samples for instrumentation
*/
- protected ComplexBuffer filter(ComplexBuffer complexBuffer)
+ protected ReusableComplexBuffer filter(ReusableComplexBuffer reusableComplexBuffer)
{
- ComplexBuffer filtered = super.filter(complexBuffer);
+ ReusableComplexBuffer filtered = super.filter(reusableComplexBuffer);
+
+ //Increment user count before we hand to another listener or return from this method
+ filtered.incrementUserCount();
if(mFilteredSymbolListener != null)
{
+ filtered.incrementUserCount();
mFilteredSymbolListener.receive(filtered);
}
@@ -58,39 +70,40 @@ public class P25_C4FMDecoderInstrumented extends P25_C4FMDecoder
public void setSampleRate(double sampleRate)
{
super.setSampleRate(sampleRate);
- InterpolatingSampleBufferInstrumented instrumentedBuffer = new InterpolatingSampleBufferInstrumented((float)(sampleRate / SYMBOL_RATE));
+
+ InterpolatingSampleBufferInstrumented instrumentedBuffer = new InterpolatingSampleBufferInstrumented(getSamplesPerSymbol(), SYMBOL_TIMING_GAIN);
mInterpolatingSampleBuffer = instrumentedBuffer;
- DQPSKDemodulatorInstrumented instrumented = new DQPSKDemodulatorInstrumented(mCostasLoop, instrumentedBuffer, mSampleRate);
+ DQPSKGardnerDemodulatorInstrumented instrumented = new DQPSKGardnerDemodulatorInstrumented(mCostasLoop, instrumentedBuffer, getSampleRate());
mQPSKDemodulator = instrumented;
- instrumented.setSymbolListener(mSymbolListener);
+ instrumented.setComplexSymbolListener(mComplexSymbolListener);
instrumented.setPLLErrorListener(mPLLPhaseErrorListener);
instrumented.setPLLFrequencyListener(mPLLFrequencyListener);
instrumented.setSymbolDecisionDataListener(mSymbolDecisionDataListener);
instrumented.setSamplesPerSymbolListener(mSamplesPerSymbolListener);
- instrumented.setDibitListener(mMessageFramer);
+ instrumented.setSymbolListener(mMessageFramer);
}
- public void setSymbolListener(Listener listener)
+ public void setComplexSymbolListener(Listener listener)
{
- mSymbolListener = listener;
- ((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setSymbolListener(listener);
+ mComplexSymbolListener = listener;
+ ((DQPSKGardnerDemodulatorInstrumented)mQPSKDemodulator).setComplexSymbolListener(listener);
}
public void setPLLPhaseErrorListener(Listener listener)
{
mPLLPhaseErrorListener = listener;
- ((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setPLLErrorListener(listener);
+ ((DQPSKGardnerDemodulatorInstrumented)mQPSKDemodulator).setPLLErrorListener(listener);
}
public void setPLLFrequencyListener(Listener listener)
{
mPLLFrequencyListener = listener;
- ((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setPLLFrequencyListener(listener);
+ ((DQPSKGardnerDemodulatorInstrumented)mQPSKDemodulator).setPLLFrequencyListener(listener);
}
- public void setFilteredBufferListener(Listener listener)
+ public void setFilteredBufferListener(Listener listener)
{
mFilteredSymbolListener = listener;
}
@@ -98,12 +111,12 @@ public class P25_C4FMDecoderInstrumented extends P25_C4FMDecoder
public void setSymbolDecisionDataListener(Listener listener)
{
mSymbolDecisionDataListener = listener;
- ((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setSymbolDecisionDataListener(listener);
+ ((DQPSKGardnerDemodulatorInstrumented)mQPSKDemodulator).setSymbolDecisionDataListener(listener);
}
public void setSamplesPerSymbolListener(Listener listener)
{
mSamplesPerSymbolListener = listener;
- ((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setSamplesPerSymbolListener(listener);
+ ((DQPSKGardnerDemodulatorInstrumented)mQPSKDemodulator).setSamplesPerSymbolListener(listener);
}
}
\ No newline at end of file
diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25_LSMDecoder.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25_LSMDecoder.java
deleted file mode 100644
index 20460d76..00000000
--- a/src/main/java/io/github/dsheirer/module/decode/p25/P25_LSMDecoder.java
+++ /dev/null
@@ -1,240 +0,0 @@
-/*******************************************************************************
- * 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.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.QPSKSymbolDecoder;
-import io.github.dsheirer.instrument.tap.TapGroup;
-import io.github.dsheirer.sample.Listener;
-import io.github.dsheirer.sample.complex.ComplexBuffer;
-import io.github.dsheirer.sample.complex.ComplexBufferToStreamConverter;
-import io.github.dsheirer.sample.complex.reusable.IReusableComplexBufferListener;
-import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer;
-import io.github.dsheirer.source.SourceEvent;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.List;
-
-public class P25_LSMDecoder extends P25Decoder implements IReusableComplexBufferListener
-{
- 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 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;
- private ReusableBufferListener mReusableBufferListener = new ReusableBufferListener();
-
- public P25_LSMDecoder(AliasList aliasList)
- {
- super(aliasList);
-
- mBasebandFilter = new ComplexFIRFilter_CB_CB(FilterFactory.getLowPass(
- 48000, 7250, 8000, 60, WindowType.HANN, true), 1.0f);
-
- mBasebandFilter.setListener(mStreamConverter);
-
- mStreamConverter.setListener(mAGC);
-
- 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;
-
- mLSMDemodulator.dispose();
- mLSMDemodulator = null;
-
- mQPSKSlicer.dispose();
- mQPSKSlicer = null;
-
- mMessageFramer.dispose();
- mMessageFramer = null;
- }
-
- @Override
- public Listener getReusableComplexBufferListener()
- {
- return mReusableBufferListener;
- }
-
- public Modulation getModulation()
- {
- return Modulation.CQPSK;
- }
-
- /**
- * Provides a list of instrumentation taps for monitoring internal processing
- */
- @Override
- public List getTapGroups()
- {
- 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;
- }
-
- /**
- * Adds the instrumentation tap
- */
- @Override
- public void registerTap(Tap tap)
- {
- 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());
- }
- }
-
- /**
- * Removes the instrumentation tap
- */
- @Override
- public void unregisterTap(Tap tap)
- {
- 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());
- }
- }
-
- @Override
- public Listener getSourceEventListener()
- {
- return null;
- }
-
- @Override
- public void setSourceEventListener(Listener listener)
- {
-
- }
-
- @Override
- public void removeSourceEventListener()
- {
-
- }
-
- public class ReusableBufferListener implements Listener
- {
- @Override
- public void receive(ReusableComplexBuffer reusableComplexBuffer)
- {
- float[] samples = reusableComplexBuffer.getSamplesCopy();
-
- //TODO: redesign the filter chain so that we can simply pass a float array ...
-
- mBasebandFilter.receive(new ComplexBuffer(samples));
- reusableComplexBuffer.decrementUserCount();
- }
- }
-}
diff --git a/src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableBufferQueue.java b/src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableBufferQueue.java
index 655c6967..89f7e81b 100644
--- a/src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableBufferQueue.java
+++ b/src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableBufferQueue.java
@@ -38,6 +38,11 @@ public class ReusableBufferQueue implements IReusableBufferDisposedListener
}
+ public void dispose()
+ {
+ mReusableBufferQueue.clear();
+ }
+
/**
* Implements buffer disposed listener interface. Disposed (ie user count = 0) buffers will automatically callback
* to this method to indicate when they are disposed.
diff --git a/src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableComplexBuffer.java b/src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableComplexBuffer.java
index 00345429..151852af 100644
--- a/src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableComplexBuffer.java
+++ b/src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableComplexBuffer.java
@@ -30,6 +30,10 @@ public class ReusableComplexBuffer extends ComplexBuffer
/**
* Creates a reusable, timestamped complex buffer using the specified time in milliseconds.
*
+ * NOTE: reusability of this buffer requires strict user count tracking. Each component that receives this
+ * buffer should not modify the buffer contents. Each component should also increment the user count before
+ * sending this buffer to another component and should decrement the user count when finished using this buffer.
+ *
* @param disposalListener to be notified when all consumers/users are finished using the buffer
* @param samples of data
* @param timestamp in millis for the buffer
@@ -46,6 +50,10 @@ public class ReusableComplexBuffer extends ComplexBuffer
/**
* Constructs a timestamped complex buffer using the current system time in milliseconds.
*
+ * NOTE: reusability of this buffer requires strict user count tracking. Each component that receives this
+ * buffer should not modify the buffer contents. Each component should also increment the user count before
+ * sending this buffer to another component and should decrement the user count when finished using this buffer.
+ *
* @param disposalListener to be notified when all consumers are finished using the buffer
* @param samples of data
*/
@@ -82,6 +90,11 @@ public class ReusableComplexBuffer extends ComplexBuffer
return mTimestamp;
}
+ public void setTimestamp(long timestamp)
+ {
+ mTimestamp = timestamp;
+ }
+
/**
* Reloads this buffer by copying the sample array into this buffer. Internal buffer length will be automatically
* resized to fit the new sample array length.
diff --git a/src/main/java/io/github/dsheirer/source/IControllableFileSource.java b/src/main/java/io/github/dsheirer/source/IControllableFileSource.java
index 59868d66..583b8007 100644
--- a/src/main/java/io/github/dsheirer/source/IControllableFileSource.java
+++ b/src/main/java/io/github/dsheirer/source/IControllableFileSource.java
@@ -40,7 +40,7 @@ public interface IControllableFileSource
public long getFrameCount() throws IOException;
- public int getSampleRate();
+ public double getSampleRate();
public void setListener(IFrameLocationListener listener);
diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerModel.java b/src/main/java/io/github/dsheirer/source/tuner/TunerModel.java
index a88db737..615e20bb 100644
--- a/src/main/java/io/github/dsheirer/source/tuner/TunerModel.java
+++ b/src/main/java/io/github/dsheirer/source/tuner/TunerModel.java
@@ -77,11 +77,11 @@ public class TunerModel extends AbstractTableModel implements Listener Tuners )
- {
- for( Tuner tuner: Tuners )
- {
- addTuner( tuner );
- }
- }
+ public void addTuners(List Tuners)
+ {
+ for(Tuner tuner : Tuners)
+ {
+ addTuner(tuner);
+ }
+ }
/**
* Adds the Tuner to this model
@@ -284,17 +284,17 @@ public class TunerModel extends AbstractTableModel implements Listener it = mTuners.iterator();
-
- Tuner tuner;
+ TunerChannelSource retVal = null;
- if(config.hasPreferredTuner())
+ TunerChannel tunerChannel = config.getTunerChannel();
+
+ tunerChannel.setBandwidth(bandwidth);
+
+ Iterator it = mTuners.iterator();
+
+ Tuner tuner;
+
+ if(config.hasPreferredTuner())
{
tuner = getTuner(config.getPreferredTuner());
@@ -302,7 +302,7 @@ public class TunerModel extends AbstractTableModel implements Listener
setLayout(new MigLayout("insets 0 0 0 0", "[left]", ""));
mFrequencyControl = new JFrequencyControl();
mFrequencyControl.setEnabled(false);
- mFrequencyControl.addListener(new IFrequencyChangeProcessor()
+ mFrequencyControl.addListener(new ISourceEventProcessor()
{
@Override
- public void frequencyChanged(FrequencyChangeEvent event)
+ public void process(SourceEvent event) throws SourceException
{
setModified(true);
}
diff --git a/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java b/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java
index 63453767..0abc2824 100644
--- a/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java
+++ b/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java
@@ -217,7 +217,9 @@ public class ComplexWaveSource extends ComplexSource implements IControllableFil
float[] samples = ConversionUtils.convertFromSigned16BitSamples(buffer);
ReusableComplexBuffer reusableBuffer = mReusableBufferQueue.getBuffer(samples.length);
- mListener.receive(reusableBuffer.incrementUserCount());
+ reusableBuffer.reloadFrom(samples, System.currentTimeMillis());
+ reusableBuffer.incrementUserCount();
+ mListener.receive(reusableBuffer);
}
}
}
diff --git a/src/main/java/io/github/dsheirer/spectrum/ChannelSpectrumPanel.java b/src/main/java/io/github/dsheirer/spectrum/ChannelSpectrumPanel.java
index 879a7963..0431ff26 100644
--- a/src/main/java/io/github/dsheirer/spectrum/ChannelSpectrumPanel.java
+++ b/src/main/java/io/github/dsheirer/spectrum/ChannelSpectrumPanel.java
@@ -1,6 +1,6 @@
/*******************************************************************************
* sdr-trunk
- * Copyright (C) 2014-2017 Dennis Sheirer
+ * Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
@@ -26,15 +26,15 @@ import io.github.dsheirer.dsp.filter.smoothing.SmoothingFilter.SmoothingType;
import io.github.dsheirer.module.ProcessingChain;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.SampleType;
-import io.github.dsheirer.sample.complex.ComplexBuffer;
+import io.github.dsheirer.sample.complex.reusable.ReusableBufferQueue;
+import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer;
import io.github.dsheirer.sample.real.RealBuffer;
import io.github.dsheirer.settings.ColorSetting.ColorSettingName;
import io.github.dsheirer.settings.ColorSettingMenuItem;
import io.github.dsheirer.settings.Setting;
import io.github.dsheirer.settings.SettingChangeListener;
import io.github.dsheirer.settings.SettingsManager;
-import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent;
-import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent.Event;
+import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.spectrum.converter.DFTResultsConverter;
import io.github.dsheirer.spectrum.converter.RealDecibelConverter;
import io.github.dsheirer.spectrum.menu.AveragingItem;
@@ -45,8 +45,13 @@ import io.github.dsheirer.spectrum.menu.SmoothingItem;
import io.github.dsheirer.spectrum.menu.SmoothingTypeItem;
import net.miginfocom.swing.MigLayout;
-import javax.swing.*;
-import java.awt.*;
+import javax.swing.JLayeredPane;
+import javax.swing.JMenu;
+import javax.swing.JPanel;
+import javax.swing.JPopupMenu;
+import javax.swing.JSeparator;
+import javax.swing.SwingUtilities;
+import java.awt.Component;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.MouseEvent;
@@ -54,22 +59,20 @@ import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.concurrent.atomic.AtomicBoolean;
-public class ChannelSpectrumPanel extends JPanel
- implements ChannelEventListener,
- Listener,
- SettingChangeListener,
- SpectralDisplayAdjuster
+public class ChannelSpectrumPanel extends JPanel implements ChannelEventListener,
+ Listener, SettingChangeListener, SpectralDisplayAdjuster
{
- private static final long serialVersionUID = 1L;
+ private static final long serialVersionUID = 1L;
- private DFTProcessor mDFTProcessor = new DFTProcessor( SampleType.REAL );
+ private DFTProcessor mDFTProcessor = new DFTProcessor(SampleType.REAL);
private DFTResultsConverter mDFTConverter = new RealDecibelConverter();
private JLayeredPane mLayeredPane;
private SpectrumPanel mSpectrumPanel;
private ChannelOverlayPanel mOverlayPanel;
-
+ private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue();
+
private Channel mCurrentChannel;
-
+
private int mSampleBufferSize = 2400;
private HalfBandFilter_RB_RB mDecimatingFilter = new HalfBandFilter_RB_RB(
@@ -98,11 +101,10 @@ public class ChannelSpectrumPanel extends JPanel
mDFTProcessor.addConverter( mDFTConverter );
mDFTConverter.addListener( mSpectrumPanel );
- /* Set the DFTProcessor to the decimated 24kHz sample rate */
- mDFTProcessor.frequencyChanged(
- new FrequencyChangeEvent( Event.NOTIFICATION_SAMPLE_RATE_CHANGE, 24000 ) );
-
- initGui();
+ /* Set the DFTProcessor to the decimated 24kHz sample rate */
+ mDFTProcessor.process(SourceEvent.sampleRateChange(24000.0));
+
+ initGui();
}
public void dispose()
@@ -272,9 +274,11 @@ public class ChannelSpectrumPanel extends JPanel
{
RealBuffer decimated = mDecimatingFilter.filter( buffer );
- //Hack: we're placing real samples in a complex buffer that the DFT
- //processor is expecting.
- mDFTProcessor.receive( new ComplexBuffer( decimated.getSamples() ) );
+ //Hack: we're placing real samples in a complex buffer that the DFT
+ //processor is expecting.
+ ReusableComplexBuffer reusableComplexBuffer = mReusableBufferQueue.getBuffer(decimated.getSamples().length);
+ reusableComplexBuffer.reloadFrom(decimated.getSamples(), System.currentTimeMillis());
+ mDFTProcessor.receive(reusableComplexBuffer.incrementUserCount());
}
/**