mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 08:37:25 -05:00
330 WIP - changing to 45 degree rotation before demodulation.
This commit is contained in:
@@ -1,17 +1,17 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 Dennis Sheirer
|
||||
*
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
@@ -23,96 +23,98 @@ import java.util.Arrays;
|
||||
* Circular sample buffer - allocates a buffer and stores samples in a circular
|
||||
* fashion, overwriting older samples with newly arrived samples. Initially
|
||||
* fills buffer with 0-valued samples
|
||||
*
|
||||
* Can be used as a delay-type buffer, to delay samples by the 'size' amount
|
||||
* @author denny
|
||||
*
|
||||
* Can be used as a delay-type buffer, to delay samples by the 'size' amount
|
||||
*
|
||||
* @author denny
|
||||
*/
|
||||
public class FloatCircularBuffer
|
||||
{
|
||||
float[] mBuffer;
|
||||
int mBufferPointer = 0;
|
||||
|
||||
public FloatCircularBuffer( int size )
|
||||
{
|
||||
mBuffer = new float[ size ];
|
||||
|
||||
Arrays.fill( mBuffer, (float)0.0 );
|
||||
}
|
||||
|
||||
public int getSize()
|
||||
{
|
||||
return mBuffer.length;
|
||||
}
|
||||
|
||||
public float[] getBuffer()
|
||||
{
|
||||
return mBuffer;
|
||||
}
|
||||
float[] mBuffer;
|
||||
int mBufferPointer = 0;
|
||||
|
||||
/**
|
||||
* Puts the new value into the buffer and returns the oldest buffer value
|
||||
* that it replaced
|
||||
* @param newValue
|
||||
* @return
|
||||
*/
|
||||
public float get( float newValue )
|
||||
{
|
||||
float oldestSample = mBuffer[ mBufferPointer ];
|
||||
/**
|
||||
* Creates a circular double buffer of the specified size and all entries filled with the specified initial value.
|
||||
*
|
||||
* @param size of the buffer
|
||||
* @param initialFillValue to fill the buffer entries
|
||||
*/
|
||||
public FloatCircularBuffer(int size, float initialFillValue)
|
||||
{
|
||||
mBuffer = new float[size];
|
||||
Arrays.fill(mBuffer, initialFillValue);
|
||||
}
|
||||
|
||||
mBuffer[ mBufferPointer ] = newValue;
|
||||
|
||||
mBufferPointer++;
|
||||
|
||||
if( mBufferPointer >= mBuffer.length )
|
||||
{
|
||||
mBufferPointer = 0;
|
||||
}
|
||||
|
||||
return oldestSample;
|
||||
}
|
||||
/**
|
||||
* Creates a circular double buffer of the specified size and all entries filled with an initial value of zero.
|
||||
*
|
||||
* @param size of the buffer
|
||||
*/
|
||||
public FloatCircularBuffer(int size)
|
||||
{
|
||||
this(size, 0.0f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to put a new value in the buffer, while discarding
|
||||
* the old value
|
||||
*/
|
||||
public void put( float newValue )
|
||||
{
|
||||
get( newValue );
|
||||
}
|
||||
public float[] getBuffer()
|
||||
{
|
||||
return mBuffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of arraySize, loaded with sampleCount of the
|
||||
* oldest samples, loaded into the array starting at index 0.
|
||||
*
|
||||
* This is useful for fetching an array filled half way with samples to
|
||||
* use in FFT processing.
|
||||
* @param arraySize - size of the array (must be larger than sampleCount)
|
||||
* @param sampleCount - number of the oldest samples in the buffer to load
|
||||
* @return
|
||||
*/
|
||||
public float[] get( int arraySize, int sampleCount )
|
||||
{
|
||||
float[] samples = new float[ arraySize ];
|
||||
/**
|
||||
* Puts the new value into the buffer and returns the oldest buffer value that it replaced
|
||||
*
|
||||
* @param newValue to add to the buffer
|
||||
* @return oldest value that was overwritten by the new value
|
||||
*/
|
||||
public float get(float newValue)
|
||||
{
|
||||
float oldestSample = mBuffer[mBufferPointer];
|
||||
|
||||
if( arraySize > sampleCount )
|
||||
{
|
||||
int tempCounter = mBufferPointer;
|
||||
mBuffer[mBufferPointer] = newValue;
|
||||
|
||||
for( int x = 0; x < sampleCount; x++ )
|
||||
{
|
||||
samples[ x ] = mBuffer[ tempCounter ];
|
||||
|
||||
tempCounter++;
|
||||
|
||||
if( tempCounter > mBuffer.length )
|
||||
{
|
||||
tempCounter = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return samples;
|
||||
}
|
||||
|
||||
mBufferPointer++;
|
||||
|
||||
if(mBufferPointer >= mBuffer.length)
|
||||
{
|
||||
mBufferPointer = 0;
|
||||
}
|
||||
|
||||
return oldestSample;
|
||||
}
|
||||
|
||||
public float[] getAll()
|
||||
{
|
||||
int valuePointer = 0;
|
||||
float[] values = new float[mBuffer.length];
|
||||
int bufferPointer = mBufferPointer;
|
||||
|
||||
while(valuePointer < values.length)
|
||||
{
|
||||
values[valuePointer++] = mBuffer[bufferPointer++];
|
||||
|
||||
if(bufferPointer >= mBuffer.length)
|
||||
{
|
||||
bufferPointer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Places the new value into the buffer, overwriting the oldest value
|
||||
*
|
||||
* @param newValue to add to the buffer
|
||||
*/
|
||||
public void put(float newValue)
|
||||
{
|
||||
mBuffer[mBufferPointer] = newValue;
|
||||
|
||||
mBufferPointer++;
|
||||
|
||||
if(mBufferPointer >= mBuffer.length)
|
||||
{
|
||||
mBufferPointer = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,54 +5,86 @@ import org.apache.commons.lang3.Validate;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class RealInterpolator extends Interpolator
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger( RealInterpolator.class );
|
||||
private final static Logger mLog = LoggerFactory.getLogger(RealInterpolator.class);
|
||||
|
||||
private float mGain;
|
||||
|
||||
public RealInterpolator( float gain )
|
||||
{
|
||||
mGain = gain;
|
||||
}
|
||||
/**
|
||||
* Calculates an interpolated value from the 8 samples beginning at the
|
||||
* offset index. Uses the mu value to determine which of 128 filter kernels
|
||||
* to use in order to closely approximate the ideal frequency response for
|
||||
* the value of mu, ranging from 0.0 to 1.0, indicating the interpolated
|
||||
* position in the sample set ranging from offset to offset + 7.
|
||||
*
|
||||
* @param samples - sample array of length at least offset + 7
|
||||
*
|
||||
* @param mu - interpolated sample position between 0 and 1.0
|
||||
*
|
||||
* @return - interpolated sample value
|
||||
*/
|
||||
public float filter( float[] samples, int offset, float mu )
|
||||
{
|
||||
/* Ensure we have enough samples in the array */
|
||||
Validate.isTrue(samples.length >= offset + 7);
|
||||
private float mGain;
|
||||
|
||||
/* Identify the filter bank that corresponds to mu */
|
||||
int index = (int)( NSTEPS * mu );
|
||||
|
||||
float accumulator = ( TAPS[ index ][ 7 ] * samples[ offset ] );
|
||||
accumulator += ( TAPS[ index ][ 6 ] * samples[ offset + 1 ] );
|
||||
accumulator += ( TAPS[ index ][ 5 ] * samples[ offset + 2 ] );
|
||||
accumulator += ( TAPS[ index ][ 4 ] * samples[ offset + 3 ] );
|
||||
accumulator += ( TAPS[ index ][ 3 ] * samples[ offset + 4 ] );
|
||||
accumulator += ( TAPS[ index ][ 2 ] * samples[ offset + 5 ] );
|
||||
accumulator += ( TAPS[ index ][ 1 ] * samples[ offset + 6 ] );
|
||||
accumulator += ( TAPS[ index ][ 0 ] * samples[ offset + 7 ] );
|
||||
|
||||
return accumulator * mGain;
|
||||
}
|
||||
|
||||
public Complex filter( float[] iSamples, float[] qSamples, int offset, float mu )
|
||||
{
|
||||
float i = filter( iSamples, offset, mu );
|
||||
float q = filter( qSamples, offset, mu );
|
||||
|
||||
return new Complex( i, q );
|
||||
}
|
||||
/**
|
||||
* Provides an interpolated sample point along an eight-sample waveform representation using 128 filters to
|
||||
* provide approximate interpolation with a resolution of 1/128th of a sample.
|
||||
*
|
||||
* @param gain to apply to the interpolated sample
|
||||
*/
|
||||
public RealInterpolator(float gain)
|
||||
{
|
||||
mGain = gain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates an interpolated value from eight samples that start at the offset into the sample array. The
|
||||
* interpolated sample will fall within the middle of the eight sample array, between indexes offset+3 and
|
||||
* offset+4. The mu argument is translated into an index position between 0 and 128, where a mu of 0.0 will be
|
||||
* converted to index zero and will be equal to the sample at index offset+3 and a mu of 1.0 will be equal to
|
||||
* the sample at offset+4. All mu values between 0.0 and 1.0 will be converted to a 1 - 127 index and will
|
||||
* produce an approximated value from among 127 interpolated sample points between indexes offset+3 and offset+4.
|
||||
*
|
||||
* @param samples - sample array of length at least offset + 7
|
||||
* @param mu - interpolated sample position between 0.0 and 1.0
|
||||
* @return - interpolated sample value
|
||||
*/
|
||||
public float filter(float[] samples, int offset, float mu)
|
||||
{
|
||||
/* Ensure we have enough samples in the array */
|
||||
Validate.isTrue(samples.length >= offset + 7);
|
||||
|
||||
/* Identify the filter bank that corresponds to mu */
|
||||
int index = (int)(NSTEPS * mu);
|
||||
|
||||
float accumulator = (TAPS[index][7] * samples[offset]);
|
||||
accumulator += (TAPS[index][6] * samples[offset + 1]);
|
||||
accumulator += (TAPS[index][5] * samples[offset + 2]);
|
||||
accumulator += (TAPS[index][4] * samples[offset + 3]);
|
||||
accumulator += (TAPS[index][3] * samples[offset + 4]);
|
||||
accumulator += (TAPS[index][2] * samples[offset + 5]);
|
||||
accumulator += (TAPS[index][1] * samples[offset + 6]);
|
||||
accumulator += (TAPS[index][0] * samples[offset + 7]);
|
||||
|
||||
return accumulator * mGain;
|
||||
}
|
||||
|
||||
public Complex filter(float[] iSamples, float[] qSamples, int offset, float mu)
|
||||
{
|
||||
float i = filter(iSamples, offset, mu);
|
||||
float q = filter(qSamples, offset, mu);
|
||||
|
||||
return new Complex(i, q);
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
RealInterpolator interpolator = new RealInterpolator(1.0f);
|
||||
DecimalFormat decimalFormat = new DecimalFormat("0.0000");
|
||||
|
||||
double TWO_PI = Math.PI * 2.0;
|
||||
|
||||
float[] samples = new float[16];
|
||||
|
||||
for(int x = 0; x < 16; x++)
|
||||
{
|
||||
samples[x] = (float)Math.sin(TWO_PI * (double)x / 8.0);
|
||||
}
|
||||
|
||||
mLog.debug("Samples: " + Arrays.toString(samples));
|
||||
|
||||
for(float x = 0.0f; x <= 1.01f; x += 0.1f)
|
||||
{
|
||||
mLog.debug(decimalFormat.format(x) + ": " + decimalFormat.format(interpolator.filter(samples, 1, x)));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,11 +20,20 @@ import io.github.dsheirer.sample.complex.Complex;
|
||||
public class DQPSKSymbolPhaseErrorCalculator extends QPSKSymbolPhaseErrorCalculator
|
||||
{
|
||||
//Negative 45 degrees rotation to offset for differential additive 45 degrees of rotation
|
||||
public static final Complex DIFFERENTIAL_OFFSET = Complex.fromAngle(Math.PI / -4.0d);
|
||||
public static final Complex DIFFERENTIAL_OFFSET = Complex.fromAngle(Math.PI / 4.0d);
|
||||
|
||||
@Override
|
||||
public void adjust(Complex symbol)
|
||||
{
|
||||
symbol.multiply(DIFFERENTIAL_OFFSET);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getPhaseError(Complex symbol)
|
||||
{
|
||||
Complex copy = symbol.copy();
|
||||
|
||||
copy.multiply(DIFFERENTIAL_OFFSET);
|
||||
return super.getPhaseError(copy);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,33 +25,34 @@ public class InterpolatingSymbolBuffer
|
||||
|
||||
private float[] mDelayLineInphase;
|
||||
private float[] mDelayLineQuadrature;
|
||||
private float mMu; //Sample Counter
|
||||
private float mGainMu = 0.05f; //Sample Counter Gain
|
||||
private float mOmega;
|
||||
private float mGainOmega = 0.1f * mGainMu * mGainMu;
|
||||
private float mOmegaRel = 0.005f;
|
||||
private float mOmegaMid;
|
||||
private float mSamplingPoint; //Sample Counter
|
||||
private float mSampleCounterGain = 0.05f; //Sample Counter Gain
|
||||
private float mSamplesPerSymbol;
|
||||
private float mDetectedSamplesPerSymbol;
|
||||
private float mDetectedSamplesPerSymbolGain = 0.1f * mSampleCounterGain * mSampleCounterGain;
|
||||
private float mMaxDeviation = 0.005f;
|
||||
|
||||
private int mDelayLinePointer = 0;
|
||||
private float mSamplesPerSymbol;
|
||||
private int mTwiceSamplesPerSymbol;
|
||||
private RealInterpolator mInterpolator = new RealInterpolator(1.0f);
|
||||
private SymbolDecisionData2 mSymbolDecisionData2;
|
||||
|
||||
public InterpolatingSymbolBuffer(float samplesPerSymbol)
|
||||
{
|
||||
mSamplesPerSymbol = samplesPerSymbol;
|
||||
mMu = samplesPerSymbol;
|
||||
mSamplingPoint = samplesPerSymbol;
|
||||
mTwiceSamplesPerSymbol = (int)Math.floor(2.0 * samplesPerSymbol);
|
||||
mDelayLineInphase = new float[2 * mTwiceSamplesPerSymbol];
|
||||
mDelayLineQuadrature = new float[2 * mTwiceSamplesPerSymbol];
|
||||
mOmega = samplesPerSymbol;
|
||||
mOmegaMid = samplesPerSymbol;
|
||||
mDetectedSamplesPerSymbol = samplesPerSymbol;
|
||||
mSamplesPerSymbol = samplesPerSymbol;
|
||||
|
||||
mSymbolDecisionData2 = new SymbolDecisionData2((int)samplesPerSymbol);
|
||||
}
|
||||
|
||||
public void receive(Complex currentSample)
|
||||
{
|
||||
mMu--;
|
||||
mSymbolDecisionData2.receive(currentSample);
|
||||
mSamplingPoint--;
|
||||
|
||||
//Fill up the delay line to use with the interpolator
|
||||
mDelayLineInphase[mDelayLinePointer] = currentSample.inphase();
|
||||
@@ -62,6 +63,7 @@ public class InterpolatingSymbolBuffer
|
||||
//Increment pointer and keep pointer in bounds
|
||||
mDelayLinePointer++;
|
||||
mDelayLinePointer = mDelayLinePointer % mTwiceSamplesPerSymbol;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,7 +72,7 @@ public class InterpolatingSymbolBuffer
|
||||
*/
|
||||
public void increaseSampleCounter(float samplesToAdd)
|
||||
{
|
||||
mMu += samplesToAdd;
|
||||
mSamplingPoint += samplesToAdd;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,13 +83,13 @@ public class InterpolatingSymbolBuffer
|
||||
*/
|
||||
public void resetAndAdjust(float symbolTimingError)
|
||||
{
|
||||
//mOmega is samples per symbol and is constrained to floating between +/- .005 of the nominal samples per
|
||||
//mDetectedSamplesPerSymbol is samples per symbol and is constrained to floating between +/- .005 of the nominal samples per
|
||||
//symbol
|
||||
mOmega = mOmega + mGainOmega * -symbolTimingError;
|
||||
mOmega = mOmegaMid + clip(mOmega - mOmegaMid, mOmegaRel);
|
||||
mDetectedSamplesPerSymbol = mDetectedSamplesPerSymbol + mDetectedSamplesPerSymbolGain * symbolTimingError;
|
||||
mDetectedSamplesPerSymbol = mSamplesPerSymbol + clip(mDetectedSamplesPerSymbol - mSamplesPerSymbol, mMaxDeviation);
|
||||
|
||||
//Add another symbol's worth of samples to the counter and adjust timing based on gardner error
|
||||
increaseSampleCounter((mOmega + (mGainMu * -symbolTimingError)));
|
||||
increaseSampleCounter((mDetectedSamplesPerSymbol + (mSampleCounterGain * symbolTimingError)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -95,7 +97,7 @@ public class InterpolatingSymbolBuffer
|
||||
*/
|
||||
public boolean hasSymbol()
|
||||
{
|
||||
return mMu < 1.0f;
|
||||
return mSamplingPoint < 1.0f;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,7 +107,7 @@ public class InterpolatingSymbolBuffer
|
||||
public Complex getMiddleSample()
|
||||
{
|
||||
/* Calculate interpolated middle sample and current sample */
|
||||
mMiddleSample.setValues(getInphase(mMu), getQuadrature(mMu));
|
||||
mMiddleSample.setValues(getInphase(mSamplingPoint), getQuadrature(mSamplingPoint));
|
||||
|
||||
return mMiddleSample;
|
||||
}
|
||||
@@ -116,17 +118,18 @@ public class InterpolatingSymbolBuffer
|
||||
*/
|
||||
public Complex getCurrentSample()
|
||||
{
|
||||
float half_omega = mOmega / 2.0f;
|
||||
int half_sps = (int)Math.floor(half_omega);
|
||||
float half_mu = mMu + half_omega - half_sps;
|
||||
float halfDetectedSamplesPerSymbol = mDetectedSamplesPerSymbol / 2.0f;
|
||||
int halfSamplingPointOffset = (int)Math.floor(halfDetectedSamplesPerSymbol);
|
||||
float halfSamplingPoint = mSamplingPoint + halfDetectedSamplesPerSymbol - halfSamplingPointOffset;
|
||||
|
||||
if(half_mu > 1.0)
|
||||
if(halfSamplingPoint > 1.0)
|
||||
{
|
||||
half_mu -= 1.0;
|
||||
half_sps += 1;
|
||||
halfSamplingPoint -= 1.0;
|
||||
halfSamplingPointOffset += 1;
|
||||
}
|
||||
|
||||
mCurrentSample.setValues(getInphase(half_mu, half_sps), getQuadrature(half_mu, half_sps));
|
||||
mCurrentSample.setValues(getInphase(halfSamplingPointOffset, halfSamplingPoint),
|
||||
getQuadrature(halfSamplingPointOffset, halfSamplingPoint));
|
||||
|
||||
return mCurrentSample;
|
||||
}
|
||||
@@ -136,11 +139,10 @@ public class InterpolatingSymbolBuffer
|
||||
* be used to support an external eye-diagram chart.
|
||||
* @return symbol decision data.
|
||||
*/
|
||||
public SymbolDecisionData getSymbolDecisionData()
|
||||
public SymbolDecisionData2 getSymbolDecisionData()
|
||||
{
|
||||
//TODO: I think that delay line pointer needs to have +4 added to it since the filter is 8 taps long
|
||||
return new SymbolDecisionData(mDelayLineInphase, mDelayLineQuadrature,
|
||||
mSamplesPerSymbol, mDelayLinePointer, mMu);
|
||||
mSymbolDecisionData2.setSamplingPoint(mSamplingPoint);
|
||||
return mSymbolDecisionData2;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -155,12 +157,13 @@ public class InterpolatingSymbolBuffer
|
||||
|
||||
/**
|
||||
* Returns the interpolated inphase value for the specified offset
|
||||
* @param delayLineOffset offset into the inphase delay line buffer
|
||||
* @param interpolation into the buffer to calculate the interpolated sample
|
||||
* @return inphase value of the interpolated sample
|
||||
*/
|
||||
public float getInphase(float interpolation, int indexOffet)
|
||||
public float getInphase(int delayLineOffset, float interpolation)
|
||||
{
|
||||
return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer + indexOffet, interpolation);
|
||||
return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer + delayLineOffset, interpolation);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -175,12 +178,13 @@ public class InterpolatingSymbolBuffer
|
||||
|
||||
/**
|
||||
* Returns the interpolated quadrature value for the specified offset
|
||||
* @param delayLineOffset offset into the quadrature delay line buffer
|
||||
* @param interpolation into the buffer to calculate the interpolated sample
|
||||
* @return quadrature value of the interpolated sample
|
||||
*/
|
||||
public float getQuadrature(float interpolation, int indexOffset)
|
||||
public float getQuadrature(int delayLineOffset, float interpolation)
|
||||
{
|
||||
return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer + indexOffset, interpolation);
|
||||
return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer + delayLineOffset, interpolation);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -34,7 +34,7 @@ public class QPSKDemodulator implements ComplexSampleListener
|
||||
private Listener<Double> mPLLErrorListener;
|
||||
private Listener<Double> mPLLFrequencyListener;
|
||||
private Listener<Dibit> mDibitListener;
|
||||
private Listener<SymbolDecisionData> mSymbolDecisionDataListener;
|
||||
private Listener<SymbolDecisionData2> mSymbolDecisionDataListener;
|
||||
|
||||
private Complex mPreviousSample = new Complex(0, 0);
|
||||
private Complex mPreviousMiddleSample = new Complex(0, 0);
|
||||
@@ -47,6 +47,7 @@ public class QPSKDemodulator implements ComplexSampleListener
|
||||
private float mSymbolTimingError;
|
||||
private double mSampleRate;
|
||||
|
||||
|
||||
/**
|
||||
* Decoder for Quaternary Phase Shift Keying (QPSK) and Differential QPSK (DQPSK). This decoder uses both a
|
||||
* phase-locked loop and a gardner symbol timing error detector to automatically align to the incoming carrier
|
||||
@@ -129,6 +130,16 @@ public class QPSKDemodulator implements ComplexSampleListener
|
||||
mMiddleSymbol.normalize();
|
||||
mCurrentSymbol.normalize();
|
||||
|
||||
//Send to an external constellation symbol listener when registered
|
||||
if(mSymbolListener != null)
|
||||
{
|
||||
mSymbolListener.receive(mCurrentSymbol);
|
||||
}
|
||||
|
||||
//We rotate the symbol from star pattern to polar pattern to facilitate phase error calculation
|
||||
// mSymbolPhaseErrorCalculator.adjust(mCurrentSymbol);
|
||||
// mSymbolPhaseErrorCalculator.adjust(mMiddleSymbol);
|
||||
|
||||
//Symbol timing error calculations
|
||||
mSymbolTimingError = mGardnerDetector.getError(mPreviousSymbol, mMiddleSymbol, mCurrentSymbol);
|
||||
mInterpolatingSymbolBuffer.resetAndAdjust(mSymbolTimingError);
|
||||
@@ -138,20 +149,12 @@ public class QPSKDemodulator implements ComplexSampleListener
|
||||
mPreviousMiddleSample.setValues(mMiddleSample);
|
||||
mPreviousSymbol.setValues(mCurrentSymbol);
|
||||
|
||||
//Send to an external constellation symbol listener when registered
|
||||
if(mSymbolListener != null)
|
||||
{
|
||||
mSymbolListener.receive(mCurrentSymbol);
|
||||
}
|
||||
|
||||
//Adjust the current symbol as necessary (e.g. for differential encoding)
|
||||
mSymbolPhaseErrorCalculator.adjust(mCurrentSymbol);
|
||||
|
||||
//Calculate the phase error of the current symbol relative to the expected constellation and provide
|
||||
//feedback to the PLL
|
||||
mPhaseError = mSymbolPhaseErrorCalculator.getPhaseError(mCurrentSymbol);
|
||||
|
||||
mPhaseError = GardnerDetector.clip(mPhaseError, 0.2f);
|
||||
mPhaseError = GardnerDetector.clip(mPhaseError, 0.15f);
|
||||
mPLL.adjust(mPhaseError);
|
||||
|
||||
if(mPLLErrorListener != null)
|
||||
@@ -210,7 +213,7 @@ public class QPSKDemodulator implements ComplexSampleListener
|
||||
mPLLFrequencyListener = listener;
|
||||
}
|
||||
|
||||
public void setSymbolDecisionDataListener(Listener<SymbolDecisionData> listener)
|
||||
public void setSymbolDecisionDataListener(Listener<SymbolDecisionData2> listener)
|
||||
{
|
||||
mSymbolDecisionDataListener = listener;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
/*******************************************************************************
|
||||
* 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.buffer.ComplexCircularBuffer;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
|
||||
public class SymbolDecisionData2
|
||||
{
|
||||
private ComplexCircularBuffer mBuffer;
|
||||
private float mSamplingPoint;
|
||||
|
||||
public SymbolDecisionData2(int samplesPerSymbol)
|
||||
{
|
||||
mBuffer = new ComplexCircularBuffer(2 * samplesPerSymbol);
|
||||
}
|
||||
|
||||
public void receive(float inphase, float quadrature)
|
||||
{
|
||||
mBuffer.put(new Complex(inphase, quadrature));
|
||||
}
|
||||
|
||||
public void receive(Complex complex)
|
||||
{
|
||||
mBuffer.put(complex.copy());
|
||||
}
|
||||
|
||||
public void setSamplingPoint(float samplingPoint)
|
||||
{
|
||||
mSamplingPoint = samplingPoint;
|
||||
}
|
||||
|
||||
public float getSamplingPoint()
|
||||
{
|
||||
return mSamplingPoint;
|
||||
}
|
||||
|
||||
public Complex[] getDemodulated()
|
||||
{
|
||||
Complex[] samples = mBuffer.getAll();
|
||||
|
||||
int length = samples.length / 2;
|
||||
|
||||
Complex[] demodulated = new Complex[length];
|
||||
|
||||
for(int x = 0; x < samples.length / 2; x++)
|
||||
{
|
||||
Complex copy = samples[length + x].copy();
|
||||
copy.multiply(samples[x].conjugate());
|
||||
demodulated[x] = copy;
|
||||
}
|
||||
|
||||
return demodulated;
|
||||
}
|
||||
}
|
||||
@@ -192,7 +192,12 @@ public class CostasLoop implements IPhaseLockedLoop
|
||||
/* Limit frequency to +/- maximum loop frequency */
|
||||
limitFrequency();
|
||||
|
||||
mErrorBuffer.put(phaseError);
|
||||
// checkLoopBandwidth();
|
||||
}
|
||||
|
||||
private void checkLoopBandwidth()
|
||||
{
|
||||
mErrorBuffer.put(mLoopFrequency);
|
||||
|
||||
double standardDeviation = mErrorBuffer.standardDeviation();
|
||||
|
||||
|
||||
@@ -22,9 +22,9 @@ public enum Tracking
|
||||
// FINE(150.0, 0.0001, 0.001),
|
||||
// LOCKED(200.0, 0.0, 0.0001);
|
||||
SEARCHING(100.0, 0.4, Math.PI),
|
||||
COARSE(150.0, 0.2, 0.4),
|
||||
FINE(175.0, 0.1, 0.2),
|
||||
LOCKED(200.0, 0.0, 0.1);
|
||||
COARSE(200.0, 0.2, 0.4),
|
||||
FINE(400.0, 0.1, 0.2),
|
||||
LOCKED(600.0, 0.0, 0.1);
|
||||
|
||||
private double mLoopBandwidth;
|
||||
private double mThresholdMinimum;
|
||||
|
||||
@@ -37,14 +37,15 @@ public class ComplexSampleLineChart extends LineChart implements Listener<Comple
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ComplexSampleLineChart.class);
|
||||
|
||||
private ComplexCircularBuffer mComplexCircularBuffer;
|
||||
private ComplexCircularBuffer mDemodulationCircularBuffer;
|
||||
private ComplexGain mComplexGain = new ComplexGain(500.0f);
|
||||
private ObservableList<XYChart.Data<Integer,Float>> mISamples = FXCollections.observableArrayList();
|
||||
private ObservableList<XYChart.Data<Integer,Float>> mQSamples = FXCollections.observableArrayList();
|
||||
private IntegerProperty mLengthProperty = new SimpleIntegerProperty(40);
|
||||
|
||||
public ComplexSampleLineChart(int length)
|
||||
public ComplexSampleLineChart(int length, int samplesPerSymbol)
|
||||
{
|
||||
super(new NumberAxis("Sample Number", 1, length, 2),
|
||||
super(new NumberAxis("Demodulated Samples", 0, length, 10),
|
||||
new NumberAxis("Value", -1.0, 1.0, 0.25));
|
||||
|
||||
LineChart.Series<Integer,Float> iSampleSeries = new LineChart.Series<>("Inphase", mISamples);
|
||||
@@ -53,10 +54,15 @@ public class ComplexSampleLineChart extends LineChart implements Listener<Comple
|
||||
FXCollections.observableArrayList(iSampleSeries, qSampleSeries);
|
||||
|
||||
setData(observableList);
|
||||
init(length);
|
||||
init(length, samplesPerSymbol);
|
||||
}
|
||||
|
||||
private void init(int length)
|
||||
public void setSamplesPerSymbol(int samplesPerSymbol)
|
||||
{
|
||||
mDemodulationCircularBuffer = new ComplexCircularBuffer(samplesPerSymbol);
|
||||
}
|
||||
|
||||
private void init(int length, int samplesPerSymbol)
|
||||
{
|
||||
mLengthProperty.addListener(new ChangeListener<Number>()
|
||||
{
|
||||
@@ -67,6 +73,7 @@ public class ComplexSampleLineChart extends LineChart implements Listener<Comple
|
||||
}
|
||||
});
|
||||
mComplexCircularBuffer = new ComplexCircularBuffer(length);
|
||||
mDemodulationCircularBuffer = new ComplexCircularBuffer(samplesPerSymbol);
|
||||
length().setValue(length);
|
||||
|
||||
for(int x = 1; x <= length; x++)
|
||||
@@ -89,14 +96,16 @@ public class ComplexSampleLineChart extends LineChart implements Listener<Comple
|
||||
{
|
||||
float[] samples = complexBuffer.getSamples();
|
||||
|
||||
Complex sample;
|
||||
Complex sample = new Complex(0,0);
|
||||
|
||||
for(int x = 0; x < samples.length; x += 2)
|
||||
{
|
||||
sample = new Complex(samples[x], samples[x + 1]);
|
||||
Complex copy = sample.copy();
|
||||
mComplexGain.apply(copy);
|
||||
mComplexCircularBuffer.put(copy);
|
||||
sample.setValues(samples[x], samples[x + 1]);
|
||||
mComplexGain.apply(sample);
|
||||
|
||||
Complex previous = mDemodulationCircularBuffer.get(sample.copy());
|
||||
sample.multiply(previous.conjugate());
|
||||
mComplexCircularBuffer.put(sample.copy());
|
||||
}
|
||||
|
||||
updateChart();
|
||||
|
||||
@@ -15,8 +15,7 @@
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.instrument.gui.viewer.chart;
|
||||
|
||||
import io.github.dsheirer.dsp.fm.FMDemodulator;
|
||||
import io.github.dsheirer.dsp.psk.SymbolDecisionData;
|
||||
import io.github.dsheirer.dsp.psk.SymbolDecisionData2;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
import javafx.collections.FXCollections;
|
||||
@@ -26,7 +25,7 @@ import javafx.scene.chart.NumberAxis;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class EyeDiagramChart extends LineChart implements Listener<SymbolDecisionData>
|
||||
public class EyeDiagramChart extends LineChart implements Listener<SymbolDecisionData2>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(EyeDiagramChart.class);
|
||||
|
||||
@@ -38,9 +37,9 @@ public class EyeDiagramChart extends LineChart implements Listener<SymbolDecisio
|
||||
public EyeDiagramChart(int seriesCount)
|
||||
{
|
||||
super(new NumberAxis("Symbol Timing", 1.0, 10.0, 1.0),
|
||||
new NumberAxis("Value", -1.0, 1.0, 0.25));
|
||||
new NumberAxis("Value", -1.25, 1.25, 0.25));
|
||||
|
||||
mSeriesCount = seriesCount;
|
||||
mSeriesCount = seriesCount * 2;
|
||||
init();
|
||||
}
|
||||
|
||||
@@ -82,37 +81,41 @@ public class EyeDiagramChart extends LineChart implements Listener<SymbolDecisio
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(SymbolDecisionData symbolDecisionData)
|
||||
public void receive(SymbolDecisionData2 symbolDecisionData)
|
||||
{
|
||||
checkChartLength((int)symbolDecisionData.getSamplesPerSymbol());
|
||||
int length = (int)symbolDecisionData.getSamplesPerSymbol() + 1;
|
||||
Complex[] demodulated = symbolDecisionData.getDemodulated();
|
||||
|
||||
int length = demodulated.length;
|
||||
|
||||
checkChartLength(length);
|
||||
|
||||
|
||||
ObservableList<Data<Double,Double>> series = getSeries(mSeriesPointer++, length + 1);
|
||||
ObservableList<Data<Double,Double>> iSeries = getSeries(mSeriesPointer++, length + 1);
|
||||
|
||||
if(mSeriesPointer >= mSeriesCount)
|
||||
{
|
||||
mSeriesPointer = 0;
|
||||
}
|
||||
|
||||
int index = symbolDecisionData.getSampleIndex();
|
||||
ObservableList<Data<Double,Double>> qSeries = getSeries(mSeriesPointer++, length + 1);
|
||||
|
||||
Complex previous = new Complex(symbolDecisionData.getInphaseSamples()[index],
|
||||
symbolDecisionData.getQuadratureSamples()[index]);
|
||||
|
||||
for(int x = 1; x <= length; x++)
|
||||
if(mSeriesPointer >= mSeriesCount)
|
||||
{
|
||||
Complex current = new Complex(symbolDecisionData.getInphaseSamples()[index + x],
|
||||
symbolDecisionData.getQuadratureSamples()[index + x]);
|
||||
mSeriesPointer = 0;
|
||||
}
|
||||
|
||||
double demodulated = FMDemodulator.demodulate(previous, current);
|
||||
demodulated *= 6;
|
||||
for(int x = 0; x < length; x++)
|
||||
{
|
||||
demodulated[x].normalize();
|
||||
Data<Double,Double> iDataPoint = iSeries.get(x);
|
||||
iDataPoint.setXValue((double)x + 1 - symbolDecisionData.getSamplingPoint());
|
||||
double iValue = demodulated[x].inphase();
|
||||
iDataPoint.setYValue(iValue);
|
||||
|
||||
Data<Double,Double> dataPoint = series.get(x);
|
||||
dataPoint.setXValue((double)x - symbolDecisionData.getSampleIndexOffset());
|
||||
dataPoint.setYValue(demodulated);
|
||||
|
||||
previous = current;
|
||||
Data<Double,Double> qDataPoint = qSeries.get(x);
|
||||
qDataPoint.setXValue((double)x + 1 - symbolDecisionData.getSamplingPoint());
|
||||
double qValue = demodulated[x].quadrature();
|
||||
qDataPoint.setYValue(qValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ public class SymbolChart extends ScatterChart implements Listener<Complex>
|
||||
}
|
||||
|
||||
ObservableList<XYChart.Series> data = FXCollections.observableArrayList();
|
||||
data.add(new ScatterChart.Series("Current", mCurrentPoints));
|
||||
data.add(new ScatterChart.Series("Constellation", mCurrentPoints));
|
||||
// data.add(new ScatterChart.Series("Previous", mPreviousPoints));
|
||||
// data.add(new ScatterChart.Series("Oldest", mOldestPoints));
|
||||
|
||||
|
||||
@@ -56,6 +56,8 @@ public class P25Phase1Pane extends DecoderPane
|
||||
getDecoder().setDEBUGPLLFrequencyListener(getPLLFrequencyLineChart());
|
||||
getDecoder().setDEBUGSymbolDecisionDataListener(getEyeDiagramChart());
|
||||
|
||||
HBox.setHgrow(getSampleChartBox(), Priority.ALWAYS);
|
||||
HBox.setHgrow(getDecoderChartBox(), Priority.ALWAYS);
|
||||
getChildren().addAll(getSampleChartBox(), getDecoderChartBox());
|
||||
}
|
||||
|
||||
@@ -65,6 +67,7 @@ public class P25Phase1Pane extends DecoderPane
|
||||
mLog.debug("Configuring for sample rate: " + sampleRate);
|
||||
|
||||
mDecoder.setSampleRate(sampleRate);
|
||||
getSampleLineChart().setSamplesPerSymbol((int)(sampleRate / 4800));
|
||||
}
|
||||
|
||||
private P25_C4FMDecoder2 getDecoder()
|
||||
@@ -87,6 +90,7 @@ public class P25Phase1Pane extends DecoderPane
|
||||
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);
|
||||
@@ -104,6 +108,7 @@ public class P25Phase1Pane extends DecoderPane
|
||||
if(mSampleChartBox == null)
|
||||
{
|
||||
mSampleChartBox = new HBox();
|
||||
mSampleChartBox.setMaxHeight(Double.MAX_VALUE);
|
||||
getSampleLineChart().setMaxWidth(Double.MAX_VALUE);
|
||||
getEyeDiagramChart().setMaxWidth(Double.MAX_VALUE);
|
||||
HBox.setHgrow(getSampleLineChart(), Priority.ALWAYS);
|
||||
@@ -118,7 +123,7 @@ public class P25Phase1Pane extends DecoderPane
|
||||
{
|
||||
if(mSampleLineChart == null)
|
||||
{
|
||||
mSampleLineChart = new ComplexSampleLineChart(40);
|
||||
mSampleLineChart = new ComplexSampleLineChart(100, 10);
|
||||
}
|
||||
|
||||
return mSampleLineChart;
|
||||
|
||||
@@ -26,7 +26,7 @@ import io.github.dsheirer.dsp.psk.IQPSKSymbolDecoder;
|
||||
import io.github.dsheirer.dsp.psk.ISymbolPhaseErrorCalculator;
|
||||
import io.github.dsheirer.dsp.psk.QPSKDemodulator;
|
||||
import io.github.dsheirer.dsp.psk.QPSKSymbolDecoder;
|
||||
import io.github.dsheirer.dsp.psk.SymbolDecisionData;
|
||||
import io.github.dsheirer.dsp.psk.SymbolDecisionData2;
|
||||
import io.github.dsheirer.dsp.psk.pll.CostasLoop;
|
||||
import io.github.dsheirer.dsp.psk.pll.Tracking;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
@@ -64,7 +64,7 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen
|
||||
private Listener<Complex> mDEBUGSymbolListener;
|
||||
private Listener<Double> mDEBUGPLLPhaseErrorListener;
|
||||
private Listener<Double> mDEBUGPLLFrequencyListener;
|
||||
private Listener<SymbolDecisionData> mDEBUGSymbolDecisionDataListener;
|
||||
private Listener<SymbolDecisionData2> mDEBUGSymbolDecisionDataListener;
|
||||
|
||||
public P25_C4FMDecoder2(AliasList aliasList)
|
||||
{
|
||||
@@ -121,7 +121,7 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen
|
||||
mQPSKDemodulator.setPLLFrequencyListener(listener);
|
||||
}
|
||||
|
||||
public void setDEBUGSymbolDecisionDataListener(Listener<SymbolDecisionData> listener)
|
||||
public void setDEBUGSymbolDecisionDataListener(Listener<SymbolDecisionData2> listener)
|
||||
{
|
||||
mDEBUGSymbolDecisionDataListener = listener;
|
||||
mQPSKDemodulator.setSymbolDecisionDataListener(listener);
|
||||
@@ -131,9 +131,9 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen
|
||||
public void receive(ComplexBuffer complexBuffer)
|
||||
{
|
||||
ComplexBuffer basebandFiltered = mBasebandFilter.filter(complexBuffer);
|
||||
// ComplexBuffer symbolFiltered = mBasebandFilter.filter(basebandFiltered);
|
||||
ComplexBuffer symbolFiltered = mSymbolFilter.filter(basebandFiltered);
|
||||
// ComplexBuffer amplified = mAGC.filter(symbolFiltered);
|
||||
mQPSKDemodulator.receive(basebandFiltered);
|
||||
mQPSKDemodulator.receive(symbolFiltered);
|
||||
// mQPSKDemodulator.receive(complexBuffer);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user