mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-11 08:07:24 -05:00
39 - WIP - incorporates the latest P25 C4FM decoder from master and
updates the LSM decoder to follow the same design pattern. Adds LSM decoder support to the demodulator viewer application. The current symbol timing gain for LSM is set too high -- is using the C4FM gain and causes too much fluctuation in the timing error.
This commit is contained in:
@@ -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 <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
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<Dibit>
|
||||
{
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -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<SymbolDecisionData> mSymbolDecisionDataListener;
|
||||
private Listener<Double> mSamplesPerSymbolListener;
|
||||
private Listener<Complex> mSymbolListener;
|
||||
private Listener<Complex> mComplexSymbolListener;
|
||||
private Listener<Double> mPLLErrorListener;
|
||||
private Listener<Double> 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<Complex> listener)
|
||||
public void setComplexSymbolListener(Listener<Complex> listener)
|
||||
{
|
||||
mSymbolListener = listener;
|
||||
mComplexSymbolListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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<Dibit>
|
||||
{
|
||||
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);
|
||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
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<Dibit> 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<Dibit> 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;
|
||||
}
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
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<Dibit>
|
||||
{
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
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<SymbolDecisionData> mSymbolDecisionDataListener;
|
||||
private Listener<Double> mSamplesPerSymbolListener;
|
||||
private Listener<Complex> mComplexSymbolListener;
|
||||
private Listener<Double> mPLLErrorListener;
|
||||
private Listener<Double> 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<SymbolDecisionData> listener)
|
||||
{
|
||||
mSymbolDecisionDataListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive PLL error values
|
||||
*/
|
||||
public void setSamplesPerSymbolListener(Listener<Double> listener)
|
||||
{
|
||||
mSamplesPerSymbolListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive decoded QPSK symbols
|
||||
*/
|
||||
public void setComplexSymbolListener(Listener<Complex> listener)
|
||||
{
|
||||
mComplexSymbolListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive PLL error values
|
||||
*/
|
||||
public void setPLLErrorListener(Listener<Double> listener)
|
||||
{
|
||||
mPLLErrorListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive PLL error values
|
||||
*/
|
||||
public void setPLLFrequencyListener(Listener<Double> listener)
|
||||
{
|
||||
mPLLFrequencyListener = listener;
|
||||
}
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
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<Dibit>
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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<T>
|
||||
{
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
|
||||
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<TapGroup> 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<Complex> 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<Complex> 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<Complex>, 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<TapGroup> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
137
src/main/java/io/github/dsheirer/dsp/psk/PSKDemodulator.java
Normal file
137
src/main/java/io/github/dsheirer/dsp/psk/PSKDemodulator.java
Normal file
@@ -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 <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
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<T> implements ComplexSampleListener
|
||||
{
|
||||
private InterpolatingSampleBuffer mInterpolatingSampleBuffer;
|
||||
private IPhaseLockedLoop mPLL;
|
||||
private Complex mReceivedSample = new Complex(0, 0);
|
||||
private Listener<T> 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<T> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<ActionEvent>()
|
||||
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<ActionEvent>()
|
||||
{
|
||||
@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<ActionEvent>()
|
||||
{
|
||||
@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<ActionEvent>()
|
||||
{
|
||||
@Override
|
||||
public void handle(ActionEvent event)
|
||||
{
|
||||
getViewerDesktop().setDecoder(decoderType);
|
||||
setTitle(decoderType);
|
||||
}
|
||||
});
|
||||
|
||||
decoderMenu.getItems().add(decoderMenuItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<ComplexBuffer>
|
||||
public class PlaybackController extends HBox implements IFrameLocationListener, Listener<ReusableComplexBuffer>
|
||||
{
|
||||
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<ComplexBuffer> 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<ComplexBuffer>)this);
|
||||
((ComplexWaveSource)mControllableFileSource).setListener((Listener<ReusableComplexBuffer>)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<ComplexBuffer> listener)
|
||||
public void addListener(Listener<ReusableComplexBuffer> 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<ComplexBuffer> listener)
|
||||
public void removeListener(Listener<ReusableComplexBuffer> 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);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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<ComplexBuffer>
|
||||
public class ComplexSampleLineChart extends LineChart implements Listener<ReusableComplexBuffer>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ComplexSampleLineChart.class);
|
||||
|
||||
@@ -92,7 +92,7 @@ public class ComplexSampleLineChart extends LineChart implements Listener<Comple
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(ComplexBuffer complexBuffer)
|
||||
public void receive(ReusableComplexBuffer complexBuffer)
|
||||
{
|
||||
float[] samples = complexBuffer.getSamples();
|
||||
|
||||
@@ -110,6 +110,8 @@ public class ComplexSampleLineChart extends LineChart implements Listener<Comple
|
||||
}
|
||||
|
||||
updateChart();
|
||||
|
||||
complexBuffer.decrementUserCount();
|
||||
}
|
||||
|
||||
private void updateChart()
|
||||
|
||||
@@ -16,15 +16,15 @@
|
||||
package io.github.dsheirer.instrument.gui.viewer.decoder;
|
||||
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.complex.ComplexBuffer;
|
||||
import io.github.dsheirer.sample.complex.reusable.ReusableBufferBroadcaster;
|
||||
import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.text.Text;
|
||||
|
||||
public class DecoderPane extends VBox implements Listener<ComplexBuffer>
|
||||
public class DecoderPane extends VBox implements Listener<ReusableComplexBuffer>
|
||||
{
|
||||
private Broadcaster<ComplexBuffer> 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<ComplexBuffer>
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(ComplexBuffer complexBuffer)
|
||||
public void receive(ReusableComplexBuffer reusableComplexBuffer)
|
||||
{
|
||||
mComplexBufferBroadcaster.broadcast(complexBuffer);
|
||||
mComplexBufferBroadcaster.broadcast(reusableComplexBuffer);
|
||||
}
|
||||
|
||||
public void addListener(Listener<ComplexBuffer> listener)
|
||||
public void addListener(Listener<ReusableComplexBuffer> listener)
|
||||
{
|
||||
mComplexBufferBroadcaster.addListener(listener);
|
||||
}
|
||||
|
||||
public void removeListener(Listener<ComplexBuffer> listener)
|
||||
public void removeListener(Listener<ReusableComplexBuffer> listener)
|
||||
{
|
||||
mComplexBufferBroadcaster.removeListener(listener);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
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<Message>()
|
||||
{
|
||||
@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;
|
||||
}
|
||||
}
|
||||
@@ -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<ComplexBuffer> 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;
|
||||
}
|
||||
@@ -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)
|
||||
*/
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<ReusableComplexBuffer>
|
||||
{
|
||||
private double mSampleRate;
|
||||
private P25MessageProcessor mMessageProcessor;
|
||||
private AliasList mAliasList;
|
||||
private Listener<SourceEvent> 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<SourceEvent> 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<SourceEvent> getSourceEventListener()
|
||||
{
|
||||
return new Listener<SourceEvent>()
|
||||
{
|
||||
@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<ReusableComplexBuffer> 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()
|
||||
{
|
||||
|
||||
@@ -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<ComplexBuffer>
|
||||
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<Double,float[]> 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<FrequencyChangeEvent> listener)
|
||||
protected void process(SourceEvent sourceEvent)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeFrequencyChangeListener()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public Listener<FrequencyChangeEvent> getFrequencyChangeListener()
|
||||
{
|
||||
return new Listener<FrequencyChangeEvent>()
|
||||
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<ComplexBuffer> 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()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
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<Double> mPLLPhaseErrorListener;
|
||||
private Listener<Double> mPLLFrequencyListener;
|
||||
private Listener<Double> mSamplesPerSymbolListener;
|
||||
private Listener<Complex> mComplexSymbolListener;
|
||||
private Listener<ReusableComplexBuffer> mFilteredSymbolListener;
|
||||
private Listener<SymbolDecisionData> 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<Complex> listener)
|
||||
{
|
||||
mComplexSymbolListener = listener;
|
||||
((DQPSKDecisionDirectedDemodulatorInstrumented)mQPSKDemodulator).setComplexSymbolListener(listener);
|
||||
}
|
||||
|
||||
public void setPLLPhaseErrorListener(Listener<Double> listener)
|
||||
{
|
||||
mPLLPhaseErrorListener = listener;
|
||||
((DQPSKDecisionDirectedDemodulatorInstrumented)mQPSKDemodulator).setPLLErrorListener(listener);
|
||||
}
|
||||
|
||||
public void setPLLFrequencyListener(Listener<Double> listener)
|
||||
{
|
||||
mPLLFrequencyListener = listener;
|
||||
((DQPSKDecisionDirectedDemodulatorInstrumented)mQPSKDemodulator).setPLLFrequencyListener(listener);
|
||||
}
|
||||
|
||||
public void setFilteredBufferListener(Listener<ReusableComplexBuffer> listener)
|
||||
{
|
||||
mFilteredSymbolListener = listener;
|
||||
}
|
||||
|
||||
public void setSymbolDecisionDataListener(Listener<SymbolDecisionData> listener)
|
||||
{
|
||||
mSymbolDecisionDataListener = listener;
|
||||
((DQPSKDecisionDirectedDemodulatorInstrumented)mQPSKDemodulator).setSymbolDecisionDataListener(listener);
|
||||
}
|
||||
|
||||
public void setSamplesPerSymbolListener(Listener<Double> listener)
|
||||
{
|
||||
mSamplesPerSymbolListener = listener;
|
||||
((DQPSKDecisionDirectedDemodulatorInstrumented)mQPSKDemodulator).setSamplesPerSymbolListener(listener);
|
||||
}
|
||||
}
|
||||
@@ -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<Channel>
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private JComboBox<P25_LSMDecoder.Modulation> mComboModulation;
|
||||
private JComboBox<P25DecoderLSM.Modulation> mComboModulation;
|
||||
private JCheckBox mIgnoreDataCalls;
|
||||
private JLabel mTrafficChannelPoolSizeLabel;
|
||||
private JSlider mTrafficChannelPoolSize;
|
||||
@@ -57,9 +61,9 @@ public class P25DecoderEditor extends ValidatingEditor<Channel>
|
||||
{
|
||||
setLayout( new MigLayout( "insets 0 0 0 0,wrap 3", "[right][grow,fill][]", "" ) );
|
||||
|
||||
mComboModulation = new JComboBox<P25_LSMDecoder.Modulation>();
|
||||
mComboModulation.setModel( new DefaultComboBoxModel<P25_LSMDecoder.Modulation>(
|
||||
P25_LSMDecoder.Modulation.values() ) );
|
||||
mComboModulation = new JComboBox<P25DecoderLSM.Modulation>();
|
||||
mComboModulation.setModel( new DefaultComboBoxModel<P25DecoderLSM.Modulation>(
|
||||
P25DecoderLSM.Modulation.values() ) );
|
||||
mComboModulation.setEnabled( false );
|
||||
mComboModulation.addActionListener( new ActionListener()
|
||||
{
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
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<Double,float[]> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<Double> mPLLPhaseErrorListener;
|
||||
private Listener<Double> mPLLFrequencyListener;
|
||||
private Listener<Double> mSamplesPerSymbolListener;
|
||||
private Listener<Complex> mSymbolListener;
|
||||
private Listener<ComplexBuffer> mFilteredSymbolListener;
|
||||
private Listener<Complex> mComplexSymbolListener;
|
||||
private Listener<ReusableComplexBuffer> mFilteredSymbolListener;
|
||||
private Listener<SymbolDecisionData> 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<Complex> listener)
|
||||
public void setComplexSymbolListener(Listener<Complex> listener)
|
||||
{
|
||||
mSymbolListener = listener;
|
||||
((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setSymbolListener(listener);
|
||||
mComplexSymbolListener = listener;
|
||||
((DQPSKGardnerDemodulatorInstrumented)mQPSKDemodulator).setComplexSymbolListener(listener);
|
||||
}
|
||||
|
||||
public void setPLLPhaseErrorListener(Listener<Double> listener)
|
||||
{
|
||||
mPLLPhaseErrorListener = listener;
|
||||
((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setPLLErrorListener(listener);
|
||||
((DQPSKGardnerDemodulatorInstrumented)mQPSKDemodulator).setPLLErrorListener(listener);
|
||||
}
|
||||
|
||||
public void setPLLFrequencyListener(Listener<Double> listener)
|
||||
{
|
||||
mPLLFrequencyListener = listener;
|
||||
((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setPLLFrequencyListener(listener);
|
||||
((DQPSKGardnerDemodulatorInstrumented)mQPSKDemodulator).setPLLFrequencyListener(listener);
|
||||
}
|
||||
|
||||
public void setFilteredBufferListener(Listener<ComplexBuffer> listener)
|
||||
public void setFilteredBufferListener(Listener<ReusableComplexBuffer> listener)
|
||||
{
|
||||
mFilteredSymbolListener = listener;
|
||||
}
|
||||
@@ -98,12 +111,12 @@ public class P25_C4FMDecoderInstrumented extends P25_C4FMDecoder
|
||||
public void setSymbolDecisionDataListener(Listener<SymbolDecisionData> listener)
|
||||
{
|
||||
mSymbolDecisionDataListener = listener;
|
||||
((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setSymbolDecisionDataListener(listener);
|
||||
((DQPSKGardnerDemodulatorInstrumented)mQPSKDemodulator).setSymbolDecisionDataListener(listener);
|
||||
}
|
||||
|
||||
public void setSamplesPerSymbolListener(Listener<Double> listener)
|
||||
{
|
||||
mSamplesPerSymbolListener = listener;
|
||||
((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setSamplesPerSymbolListener(listener);
|
||||
((DQPSKGardnerDemodulatorInstrumented)mQPSKDemodulator).setSamplesPerSymbolListener(listener);
|
||||
}
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
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<TapGroup> 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<ReusableComplexBuffer> getReusableComplexBufferListener()
|
||||
{
|
||||
return mReusableBufferListener;
|
||||
}
|
||||
|
||||
public Modulation getModulation()
|
||||
{
|
||||
return Modulation.CQPSK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a list of instrumentation taps for monitoring internal processing
|
||||
*/
|
||||
@Override
|
||||
public List<TapGroup> 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<SourceEvent> getSourceEventListener()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSourceEventListener(Listener<SourceEvent> listener)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeSourceEventListener()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public class ReusableBufferListener implements Listener<ReusableComplexBuffer>
|
||||
{
|
||||
@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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -40,7 +40,7 @@ public interface IControllableFileSource
|
||||
|
||||
public long getFrameCount() throws IOException;
|
||||
|
||||
public int getSampleRate();
|
||||
public double getSampleRate();
|
||||
|
||||
public void setListener(IFrameLocationListener listener);
|
||||
|
||||
|
||||
@@ -77,11 +77,11 @@ public class TunerModel extends AbstractTableModel implements Listener<TunerEven
|
||||
* @param name of the tuner
|
||||
* @return named tuner or null
|
||||
*/
|
||||
public Tuner getTuner(String name)
|
||||
public Tuner getTuner(String name)
|
||||
{
|
||||
if(name != null)
|
||||
{
|
||||
for(Tuner tuner: mTuners)
|
||||
for(Tuner tuner : mTuners)
|
||||
{
|
||||
if(tuner.getName().equalsIgnoreCase(name))
|
||||
{
|
||||
@@ -93,13 +93,13 @@ public class TunerModel extends AbstractTableModel implements Listener<TunerEven
|
||||
return null;
|
||||
}
|
||||
|
||||
public void addTuners( List<Tuner> Tuners )
|
||||
{
|
||||
for( Tuner tuner: Tuners )
|
||||
{
|
||||
addTuner( tuner );
|
||||
}
|
||||
}
|
||||
public void addTuners(List<Tuner> Tuners)
|
||||
{
|
||||
for(Tuner tuner : Tuners)
|
||||
{
|
||||
addTuner(tuner);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the Tuner to this model
|
||||
@@ -284,17 +284,17 @@ public class TunerModel extends AbstractTableModel implements Listener<TunerEven
|
||||
*/
|
||||
public Source getSource(SourceConfigTuner config, int bandwidth)
|
||||
{
|
||||
TunerChannelSource retVal = null;
|
||||
|
||||
TunerChannel tunerChannel = config.getTunerChannel();
|
||||
|
||||
tunerChannel.setBandwidth( bandwidth );
|
||||
|
||||
Iterator<Tuner> it = mTuners.iterator();
|
||||
|
||||
Tuner tuner;
|
||||
TunerChannelSource retVal = null;
|
||||
|
||||
if(config.hasPreferredTuner())
|
||||
TunerChannel tunerChannel = config.getTunerChannel();
|
||||
|
||||
tunerChannel.setBandwidth(bandwidth);
|
||||
|
||||
Iterator<Tuner> it = mTuners.iterator();
|
||||
|
||||
Tuner tuner;
|
||||
|
||||
if(config.hasPreferredTuner())
|
||||
{
|
||||
tuner = getTuner(config.getPreferredTuner());
|
||||
|
||||
@@ -302,7 +302,7 @@ public class TunerModel extends AbstractTableModel implements Listener<TunerEven
|
||||
{
|
||||
try
|
||||
{
|
||||
retVal = tuner.getChannel( tunerChannel );
|
||||
retVal = tuner.getChannelSourceManager().getSource(tunerChannel);
|
||||
|
||||
if(retVal != null)
|
||||
{
|
||||
@@ -319,17 +319,17 @@ public class TunerModel extends AbstractTableModel implements Listener<TunerEven
|
||||
config.getPreferredTuner() + "] - searching for another tuner");
|
||||
}
|
||||
|
||||
while( it.hasNext() && retVal == null )
|
||||
{
|
||||
tuner = it.next();
|
||||
|
||||
try
|
||||
while(it.hasNext() && retVal == null)
|
||||
{
|
||||
tuner = it.next();
|
||||
|
||||
try
|
||||
{
|
||||
retVal = tuner.getChannelSourceManager().getSource(tunerChannel);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error( "error obtaining channel from tuner [" + tuner.getName() + "]", e );
|
||||
mLog.error("error obtaining channel from tuner [" + tuner.getName() + "]", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import io.github.dsheirer.gui.control.JFrequencyControl;
|
||||
import io.github.dsheirer.gui.editor.Editor;
|
||||
import io.github.dsheirer.source.ISourceEventProcessor;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import io.github.dsheirer.source.SourceException;
|
||||
import io.github.dsheirer.source.config.SourceConfigTuner;
|
||||
import io.github.dsheirer.source.config.SourceConfiguration;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
@@ -58,10 +59,10 @@ public class TunerSourceEditor extends Editor<Channel>
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<RealBuffer>,
|
||||
SettingChangeListener,
|
||||
SpectralDisplayAdjuster
|
||||
public class ChannelSpectrumPanel extends JPanel implements ChannelEventListener,
|
||||
Listener<RealBuffer>, 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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user