P25 - work in progress to improve CQPSK LSM audio output. Audio output is still choppy but greatly improved over the previous test release.

This commit is contained in:
dennis.sheirer
2015-04-25 23:21:20 +00:00
parent 505f2ca474
commit c12123a81b
32 changed files with 1009 additions and 338 deletions

View File

@@ -262,7 +262,6 @@ public class ComplexCICDecimate extends ComplexFilter
mDecimation = decimation;
mCleanupFilter = new ComplexFIRFilter( FilterFactory
.getCICCleanupFilter( outputSampleRate,
1,
12000,
60,
WindowType.BLACKMAN ), 1.0d );

View File

@@ -177,4 +177,18 @@ public class ComplexHalfBandNoDecimateFilter extends ComplexFilter
}
}
}
public static void main( String[] args )
{
double[] c = Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO.mCoefficients;
double sum = 0;
for( int x = 0; x < c.length; x++ )
{
sum += c[ x ];
}
System.out.println( sum );
}
}

View File

@@ -7,7 +7,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.Listener;
import sample.complex.ComplexBuffer;
import sample.complex.ComplexSample;
import sample.complex.ComplexSampleListener;
import sample.decimator.ComplexDecimator;
@@ -72,15 +71,14 @@ public class ComplexPrimeCICDecimate
else
{
/* Wire the current stage to the previous stage */
mDecimatingStages.get( x - 1 ).addListener( stage );
mDecimatingStages.get( x - 1 ).setListener( stage );
}
}
mOutput = new Output( 48000, order, decimation, passFrequency,
attenuation, windowType );
mOutput = new Output( 48000, passFrequency, attenuation, windowType );
mDecimatingStages.get( mDecimatingStages.size() - 1 )
.addListener( mOutput );
.setListener( mOutput );
}
public void dispose()
@@ -170,7 +168,16 @@ public class ComplexPrimeCICDecimate
{
for( int x = 0; x < order; x++ )
{
Stage stage = new Stage( size );
Stage stage;
if( size == 2 )
{
stage = new TwoStage();
}
else
{
stage = new Stage( size );
}
mStages.add( stage );
@@ -180,13 +187,13 @@ public class ComplexPrimeCICDecimate
}
else
{
mStages.get( x - 1 ).addListener( stage );
mStages.get( x - 1 ).setListener( stage );
}
}
mDecimator = new ComplexDecimator( size );
mStages.get( mStages.size() - 1 ).addListener( mDecimator );
mStages.get( mStages.size() - 1 ).setListener( mDecimator );
}
public void dispose()
@@ -209,29 +216,35 @@ public class ComplexPrimeCICDecimate
mFirstStage.receive( i, q );
}
public void addListener( ComplexSampleListener listener )
public void setListener( ComplexSampleListener listener )
{
mDecimator.setListener( listener );
}
}
/**
* Single non-decimating CIC stage component. Uses a circular buffer
* internally to implement the stage so that stage size has essentially
* no impact on the performance of the stage
* Single non-decimating CIC stage component. Uses a circular buffer and a
* running average internally to implement the stage so that stage size has
* essentially no impact on the computational requirements of the stage
*/
public class Stage implements ComplexSampleListener
{
private ComplexSampleListener mListener;
protected ComplexSampleListener mListener;
private float[] mISamples;
private float[] mQSamples;
private float mISum;
private float mQSum;
protected float mISum;
protected float mQSum;
private int mSamplePointer = 0;
private int mSize;
protected float mGain;
public Stage()
{
}
public Stage( int size )
{
@@ -239,6 +252,8 @@ public class ComplexPrimeCICDecimate
mISamples = new float[ mSize ];
mQSamples = new float[ mSize ];
mGain = 1.0f / (float)size;
}
public void dispose()
@@ -265,15 +280,44 @@ public class ComplexPrimeCICDecimate
if( mListener != null )
{
mListener.receive( mISum, mQSum );
mListener.receive( ( mISum * mGain ), ( mQSum * mGain ) );
}
}
public void addListener( ComplexSampleListener listener )
public void setListener( ComplexSampleListener listener )
{
mListener = listener;
}
}
/**
* Size 2 stage that removes the unnecessary circular buffer management for
* a two-stage.
*/
public class TwoStage extends Stage
{
public TwoStage()
{
super();
mGain = 0.5f;
}
public void receive( float i, float q )
{
float iSum = mISum + i;
float qSum = mQSum + q;
mISum = i;
mQSum = q;
if( mListener != null )
{
mListener.receive( ( iSum * mGain ), ( qSum * mGain ) );
}
}
}
/**
* Output adapter - applies gain correction, applies cleanup filter, casts
@@ -282,24 +326,21 @@ public class ComplexPrimeCICDecimate
*/
public class Output implements ComplexSampleListener
{
private int mDecimation;
private int mOrder;
private double mGain = 1.0d;
private ComplexFilter mCleanupFilter;
private ComplexHalfBandNoDecimateFilter mHalfBandFilter =
new ComplexHalfBandNoDecimateFilter(
Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO, 0.4 );
public Output( int outputSampleRate, int order, int decimation,
int passFrequency, int attenuation, WindowType windowType )
public Output( int outputSampleRate, int passFrequency, int attenuation,
WindowType windowType )
{
mOrder = order;
mDecimation = decimation;
mCleanupFilter = new ComplexFIRFilter( FilterFactory
.getCICCleanupFilter( outputSampleRate,
mOrder,
passFrequency,
attenuation,
windowType ), 1.0d );
windowType ), 0.4d );
setGain();
mCleanupFilter.setListener( mHalfBandFilter );
}
/**
@@ -310,26 +351,15 @@ public class ComplexPrimeCICDecimate
@Override
public void receive( float left, float right )
{
mCleanupFilter.receive(
new ComplexSample( (float)( left * mGain ),
(float)( right * mGain ) ) );
mCleanupFilter.receive( new ComplexSample( left, right ) );
}
/**
* Sets a new decimation rate
*/
public void setDecimationRate( int rate )
{
mDecimation = rate;
setGain();
}
/**
* Adds a listener to receive output samples
*/
public void setListener( Listener<ComplexSample> listener )
{
mCleanupFilter.setListener( listener );
mHalfBandFilter.setListener( listener );
}
/**
@@ -337,31 +367,12 @@ public class ComplexPrimeCICDecimate
*/
public void removeListener( Listener<ComplexSample> listener )
{
mCleanupFilter.setListener( null );
mHalfBandFilter.setListener( null );
}
public Listener<ComplexSample> getListener()
{
return mCleanupFilter.getListener();
}
/**
* Adjusts the gain correction factor for output samples.
*
* The CIC decimation filter induces significant gain as a factor of
* delay, decimation rate and stage count. So, we apply a gain reduction
* to offset the added gain estimate:
*
* gain = ( decimation * delay ) to power of stageCount
*
* Delay is ONE for the Comb used in this filter.
*/
private void setGain()
{
/* decimation is NOT multiplied by delay since delay is 1 */
double gain = Math.pow( (double)mDecimation, (double)mOrder );
mGain = 1.0d / gain;
return mHalfBandFilter.getListener();
}
}
}

View File

@@ -34,8 +34,8 @@ import dsp.filter.Window.WindowType;
public class FilterFactory
{
// private final static Logger mLog =
// LoggerFactory.getLogger( FilterFactory.class );
private final static Logger mLog =
LoggerFactory.getLogger( FilterFactory.class );
/**
* Generates coefficients for a unity-gain, windowed low-pass filter
@@ -575,7 +575,6 @@ public class FilterFactory
* @return
*/
public static double[] getCICCleanupFilter( int outputSampleRate,
int order,
int passFrequency,
int attenuation,
WindowType window )
@@ -590,7 +589,7 @@ public class FilterFactory
}
double[] frequencyResponse =
getCICResponseArray( outputSampleRate, passFrequency, taps, order );
getCICResponseArray( outputSampleRate, passFrequency, taps );
//Apply Inverse DFT against frequency response unity values, leaving the
//IDFT bin results in the frequency response array
@@ -615,13 +614,14 @@ public class FilterFactory
//Apply the window against the coefficients
coefficients = Window.apply( window, coefficients );
normalize( coefficients );
return coefficients;
}
public static double[] getCICResponseArray( int sampleRate,
int frequency,
int length,
int order )
int length )
{
double[] cicArray = new double[ length * 2 ];
@@ -630,15 +630,15 @@ public class FilterFactory
cicArray[ 0 ] = 1.0d;
double unityResponse = Math.pow( Math.sin( 1.0d / (double)length ) /
( 1.0d / (double)length ), order );
double unityResponse = Math.sin( 1.0d / (double)length ) /
( 1.0d / (double)length );
for( int x = 1; x <= binCount; x++ )
{
/* Calculate unity response plus amplification for droop */
double compensated = 1.0d + ( unityResponse -
Math.pow( ( Math.sin( (double)x / (double)length ) /
( (double)x / (double)length ) ), order ) );
( Math.sin( (double)x / (double)length ) /
( (double)x / (double)length ) ) );
cicArray[ x ] = compensated;
cicArray[ length - x ] = compensated;
@@ -648,38 +648,40 @@ public class FilterFactory
}
/**
* Creates root raised cosine filter coefficients.
* Creates root raised cosine filter coefficients with a tap count equal
* to the symbols x samplesPerSymbol + 1.
*
* Symbol count should be an even integer.
*
* Ported to java from gnuradio/filter/firdes.cc
*
* @param gain - gain value to apply
* @param sampleRate - sample rate in hertz
* @param symbolRate - symbol rate in hertz
* For 40db attenuation, calculate the number of symbols based on the
* following formula:
*
* Symbols = -44 * alpha + 33
*
* @param samplesPerSymbol - number of samples per symbol
* @param symbols - number of symbols - must be even
* @param alpha - roll-off factor
* @param tapCount - odd number of taps, or will be increased to make odd
*
* @return - filter coefficients
*/
public static double[] getRootRaisedCosine( double gain,
double sampleRate,
double symbolRate,
double alpha,
int tapCount )
public static double[] getRootRaisedCosine( int samplesPerSymbol,
int symbols,
double alpha )
{
/* Ensure odd number of taps */
int taps = tapCount | 1;
int taps = samplesPerSymbol * symbols + 1;
double scale = 0;
double samplesPerSymbol = sampleRate / symbolRate;
double[] coefficients = new double[ taps ];
for( int x = 0; x < taps; x++ )
{
double index = x - ( taps / 2 );
double x1 = Math.PI * index / samplesPerSymbol;
double x2 = 4.0 * alpha * index / samplesPerSymbol;
double x1 = Math.PI * index / (double)samplesPerSymbol;
double x2 = 4.0 * alpha * index / (double)samplesPerSymbol;
double x3 = x2 * x2 - 1;
double numerator, denominator;
@@ -690,7 +692,7 @@ public class FilterFactory
{
numerator = Math.cos( ( 1.0 + alpha ) * x1 ) +
Math.sin( ( 1.0 - alpha ) * x1 ) /
( 4.0 * alpha * index / samplesPerSymbol );
( 4.0 * alpha * index / (double)samplesPerSymbol );
}
else
{
@@ -712,12 +714,13 @@ public class FilterFactory
x2 = ( 1.0 + alpha ) * x1;
numerator = ( Math.sin( x2 ) * ( 1.0 + alpha ) * Math.PI -
Math.cos( x3 ) * ( ( 1.0 - alpha ) * Math.PI * samplesPerSymbol ) /
( 4 * alpha * index ) +
Math.sin( x3 ) * samplesPerSymbol * samplesPerSymbol /
( 4.0 * alpha * index * index ) );
Math.cos( x3 ) * ( ( 1.0 - alpha ) * Math.PI *
(double)samplesPerSymbol ) / ( 4 * alpha * index ) +
Math.sin( x3 ) * (double)samplesPerSymbol *
(double)samplesPerSymbol / ( 4.0 * alpha * index * index ) );
denominator = -32.0 * Math.PI * alpha * alpha * index / samplesPerSymbol;
denominator = -32.0 * Math.PI * alpha * alpha * index /
(double)samplesPerSymbol;
}
coefficients[ x ] = 4.0 * alpha * numerator / denominator;
@@ -730,16 +733,18 @@ public class FilterFactory
*/
for( int x = 0; x < taps; x++ )
{
coefficients[ x ] = coefficients[ x ] * gain / scale;
coefficients[ x ] = coefficients[ x ] / scale;
}
return coefficients;
}
public static void main( String[] args )
{
double[] coefficients = getCICCleanupFilter( 48000, 2, 60, 24000, WindowType.BLACKMAN );
double[] c = FilterFactory.getRootRaisedCosine( 10, 4, 0.2 );
System.out.println( Arrays.toString( coefficients ) );
mLog.debug( "C:" + Arrays.toString( c ) );
}
}

View File

@@ -11,6 +11,12 @@ public class QPSKInterpolator extends Interpolator
{
private final static Logger mLog = LoggerFactory.getLogger( QPSKInterpolator.class );
private float mGain;
public QPSKInterpolator( 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
@@ -50,6 +56,6 @@ public class QPSKInterpolator extends Interpolator
imaginaryAccumulator += ( TAPS[ index ][ 1 ] * samples[ offset + 6 ].imaginary() );
imaginaryAccumulator += ( TAPS[ index ][ 0 ] * samples[ offset + 7 ].imaginary() );
return new ComplexSample( realAccumulator, imaginaryAccumulator );
return new ComplexSample( realAccumulator * mGain, imaginaryAccumulator * mGain );
}
}

View File

@@ -0,0 +1,124 @@
package dsp.gain;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.Listener;
import sample.Provider;
import sample.complex.ComplexSample;
import buffer.FloatCircularBuffer;
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2015 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
* This class is modeled using concepts detailed in the GNURadio
* implementation of feed forward AGC class located at:
* https://github.com/gnuradio/gnuradio/blob/master/gr-analog/lib/
* feedforward_agc_cc_impl.cc
*
******************************************************************************/
public class ComplexFeedForwardGainControl
implements Listener<ComplexSample>, Provider<ComplexSample>
{
private final static Logger mLog =
LoggerFactory.getLogger( ComplexFeedForwardGainControl.class );
public static final float OBJECTIVE_ENVELOPE = 1.0f;
public static final float MINIMUM_ENVELOPE = 0.0001f;
private Listener<ComplexSample> mListener;
private FloatCircularBuffer mEnvelopeHistory;
private float mMaxEnvelope = 0.0f;
private float mGain = 1.0f;
/**
* Dynamic gain control for incoming sample stream to amplify or attenuate
* all samples toward an objective unity)gain, using the maximum envelope
* value detected in the stream history window.
*
* Uses the specified damping factor to limit gain swings. Damping factor
* is applied against the delta between current gain value and a recalculated
* gain value to limit how quickly the gain value will increase or decrease.
*
* @param window - history size to use in detecting maximum envelope value
* @param damping - a value between 0 < damping <= 1.0;
*/
public ComplexFeedForwardGainControl( int window )
{
mEnvelopeHistory = new FloatCircularBuffer( window );
}
@Override
public void receive( ComplexSample sample )
{
float envelope = sample.envelope();
if( envelope > mMaxEnvelope )
{
mMaxEnvelope = envelope;
adjustGain();
}
/* Replace oldest envelope value with current envelope value */
float oldestEnvelope = mEnvelopeHistory.get( envelope );
/* If the oldest envelope value was the max envelope value, then we
* have to rediscover the max value from the envelope history */
if( mMaxEnvelope == oldestEnvelope && mMaxEnvelope != envelope )
{
mMaxEnvelope = MINIMUM_ENVELOPE;
for( float value: mEnvelopeHistory.getBuffer() )
{
if( value > mMaxEnvelope )
{
mMaxEnvelope = value;
}
}
adjustGain();
}
/* Apply current gain value to the sample and send to the listener */
if( mListener != null )
{
sample.multiply( mGain );
mListener.receive( sample );
}
}
private void adjustGain()
{
mGain = OBJECTIVE_ENVELOPE / mMaxEnvelope;
}
@Override
public void setListener( Listener<ComplexSample> listener )
{
mListener = listener;
}
@Override
public void removeListener( Listener<ComplexSample> listener )
{
mListener = null;
}
}

View File

@@ -30,14 +30,20 @@ package dsp.psk;
* along with this program. If not, see <http://www.gnu.org/licenses/>
******************************************************************************/
import instrument.Instrumentable;
import instrument.tap.Tap;
import instrument.tap.stream.EyeDiagramDataTap;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.Listener;
import sample.Provider;
import sample.complex.ComplexSample;
import source.tuner.FrequencyChangeEvent;
import source.tuner.FrequencyChangeEvent.Attribute;
import source.tuner.FrequencyChangeListener;
import buffer.FloatAveragingBuffer;
import dsp.filter.interpolator.QPSKInterpolator;
@@ -50,18 +56,30 @@ import dsp.filter.interpolator.QPSKInterpolator;
* Sample Rate: 48000
* Symbol Rate: 4800
*/
public class CQPSKDemodulator implements Listener<ComplexSample>,
public class CQPSKDemodulator implements Instrumentable,
Listener<ComplexSample>,
Provider<ComplexSample>
{
private final static Logger mLog =
LoggerFactory.getLogger( CQPSKDemodulator.class );
/* 45 degree rotation angle */
public static final float THETA = (float)( Math.PI / 4.0d );
/* 45 degree point */
public static final ComplexSample POINT_45_DEGREES =
new ComplexSample( (float)Math.sin( THETA ), (float)Math.cos( THETA ) );
private DecimalFormat mDecimalFormat = new DecimalFormat( "0.000000" );
private Listener<ComplexSample> mListener;
private GardnerDetector mGardnerDetector = new GardnerDetector();
private GardnerSymbolTiming mGardnerDetector = new GardnerSymbolTiming();
private FrequencyChangeListener mFrequencyChangeListener;
private EyeDiagramDataTap mEyeDiagramDataTap;
public CQPSKDemodulator()
{
}
@@ -89,6 +107,22 @@ public class CQPSKDemodulator implements Listener<ComplexSample>,
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( FrequencyChangeListener listener )
{
mFrequencyChangeListener = listener;
@@ -161,7 +195,7 @@ public class CQPSKDemodulator implements Listener<ComplexSample>,
/**
* Gardner symbol timing detector
*/
public class GardnerDetector implements Listener<ComplexSample>
public class GardnerSymbolTiming implements Listener<ComplexSample>
{
public static final int HALF_SAMPLES_PER_SYMBOL = 5;
public static final int SAMPLES_PER_SYMBOL = 10;
@@ -172,10 +206,10 @@ public class CQPSKDemodulator implements Listener<ComplexSample>,
private int mDelayLinePointer = 0;
/* Sampling point */
private float mMu = 10.5f;
private float mGainMu = 0.05f;
private float mMu = 10.0f;
private float mGainMu = 0.25f;
/* samples per symbol */
/* Samples per symbol */
private float mOmega = 10.0f;
private float mGainOmega = 0.1f * mGainMu * mGainMu;
private float mOmegaRel = 0.005f;
@@ -183,11 +217,16 @@ public class CQPSKDemodulator implements Listener<ComplexSample>,
private CostasLoop mCostasLoop = new CostasLoop();
private QPSKInterpolator mInterpolator = new QPSKInterpolator();
private QPSKInterpolator mInterpolator = new QPSKInterpolator( 1.0f );
private ComplexSample mPreviousSample = new ComplexSample( 0.0f, 0.0f );
private ComplexSample mPreviousMiddleSample = new ComplexSample( 0.0f, 0.0f );
private ComplexSample mPreviousSymbol = new ComplexSample( 0.0f, 0.0f );
public GardnerDetector()
/**
* Provides symbol sampling timing control
*/
public GardnerSymbolTiming()
{
for( int x = 0; x < ( 2 * TWICE_SAMPLES_PER_SYMBOL ); x++ )
{
@@ -203,84 +242,93 @@ public class CQPSKDemodulator implements Listener<ComplexSample>,
mCostasLoop.increment();
/* Mix incoming sample with costas loop to remove any rotation */
ComplexSample unrotatedSample = ComplexSample
/* Mix incoming sample with costas loop to remove any rotation
* that is present from a mis-tuned carrier frequency */
ComplexSample derotatedSample = ComplexSample
.multiply( mCostasLoop.getCurrentVector(), sample );
/* Fill up the delay line for the interpolator */
mDelayLine[ mDelayLinePointer ] = unrotatedSample;
mDelayLine[ mDelayLinePointer + TWICE_SAMPLES_PER_SYMBOL ] = unrotatedSample;
/* Keep the delay line pointer in bounds */
/* Fill up the delay line to use with the interpolator */
mDelayLine[ mDelayLinePointer ] = derotatedSample;
mDelayLine[ mDelayLinePointer + TWICE_SAMPLES_PER_SYMBOL ] = derotatedSample;
/* Imcrement 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 */
ComplexSample middleSample = mInterpolator
.filter( mDelayLine, mDelayLinePointer, mMu );
middleSample.normalize();
ComplexSample currentSample = mInterpolator
.filter( mDelayLine, mDelayLinePointer +
HALF_SAMPLES_PER_SYMBOL, mMu );
.filter( mDelayLine, mDelayLinePointer + half_sps, half_mu );
/* Multiply current and previous samples to get symbols to use
* for gardner error feedback */
ComplexSample middleSymbol = ComplexSample.multiply(
middleSample, mPreviousMiddleSample.conjugate() );
currentSample.normalize();
ComplexSample currentSymbol = ComplexSample.multiply(
currentSample, mPreviousSample.conjugate() );
/* Set gain to unity */
middleSymbol.normalize();
currentSymbol.normalize();
// if( mEyeDiagramDataTap != null )
// {
// mEyeDiagramDataTap.receive(
// new EyeDiagramData( Arrays.copyOfRange( mDelayLine,
// mDelayLinePointer, mDelayLinePointer + 20 ),
// mMu, (float)half_sps + half_mu ) );
// }
/* Gardner timing error calculations */
float errorReal = ( mPreviousSample.real() -
currentSample.real() ) * middleSample.real();
float errorInphase = ( mPreviousSymbol.inphase() -
currentSymbol.inphase() ) * middleSymbol.inphase();
float errorImaginary = ( mPreviousSample.imaginary() -
currentSample.imaginary() ) * middleSample.imaginary();
float errorQuadrature = ( mPreviousSymbol.quadrature() -
currentSymbol.quadrature() ) * middleSymbol.quadrature();
float gardnerError = normalize( errorReal + errorImaginary );
float gardnerError = normalize( errorInphase + errorQuadrature, 1.0f );
/* 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 );
mMu += mOmega + mGainMu * gardnerError;
/* Calculate symbol as delta between previous and current samples */
ComplexSample symbol = ComplexSample.multiply(
currentSample, mPreviousSample.conjugate() );
/* Adjust sample timing based on error of current sample */
mMu += mOmega + ( mGainMu * gardnerError );
/* Store current samples/symbols to use for the next period */
mPreviousSample = currentSample;
mPreviousMiddleSample = middleSample;
mPreviousSymbol = currentSymbol;
/* Update costas loop phase error from error in current symbol */
mCostasLoop.receive( symbol );
/* 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(
ComplexSample.multiply( currentSymbol, POINT_45_DEGREES ) );
/* Dispatch the symbol to the registered listener */
/* Dispatch the differentiated symbol to the registered listener */
if( mListener != null )
{
mListener.receive( symbol );
mListener.receive( currentSymbol );
}
}
}
/**
* Constrains timing error to +/- 1
*/
private float normalize( float error )
{
if( Float.isNaN( error ) )
{
return 0.0f;
}
if( error < -1.0 )
{
return -1.0f;
}
if( error > 1.0 )
{
return 1.0f;
}
return error;
}
}
/**
@@ -298,17 +346,15 @@ public class CQPSKDemodulator implements Listener<ComplexSample>,
{
public static final double TWO_PI = 2.0 * Math.PI;
/* 45 degree rotation angle */
public static final float THETA = (float)( Math.PI / 4.0d );
private static final float MAXIMUM_FREQUENCY = ( 2.0f * (float)Math.PI *
1200.0f ) / 48000.0f;
private static final float MAXIMUM_FREQUENCY =
( 1200.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 and 200 to adjust control level */
private float mLoopBandwidth = (float)( ( 2.0d * Math.PI ) / 100.0d );
/* 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 ) +
@@ -323,11 +369,11 @@ public class CQPSKDemodulator implements Listener<ComplexSample>,
private float mLoopFrequency = 0.0f;
private FrequencyControl mFrequencyControl = new FrequencyControl();
public CostasLoop()
{
}
/**
* Increments the phase of the loop
*/
@@ -363,9 +409,6 @@ public class CQPSKDemodulator implements Listener<ComplexSample>,
@Override
public void receive( ComplexSample sample )
{
/* Normalize magnitude of incoming signal to 1.0 */
sample.normalize();
/* Calculate phase error */
float phaseError = getPhaseError( sample );
@@ -377,7 +420,7 @@ public class CQPSKDemodulator implements Listener<ComplexSample>,
* Updates the costas loop frequency and phase to adjust for the phase
* error value
*
* @param phase_error - (+)= late and (-)= early
* @param phase_error - (-)= late and (+)= early
*/
private void adjust( float phase_error )
{
@@ -387,10 +430,13 @@ public class CQPSKDemodulator implements Listener<ComplexSample>,
/* Maintain phase between +/- 2 * PI */
unwrapPhase();
/* Limit frequency to +/- maximum frequency */
/* 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 */
@@ -408,46 +454,38 @@ public class CQPSKDemodulator implements Listener<ComplexSample>,
}
/**
* Calculates the phase error as the difference between the real and
* imaginary values of the sample. When balanced, both absolute values
* should be equal to ~ 0.707. The difference between the real and
* imaginary values is used as the error signal. Mapping the sample
* vector to the correct quadrant allows us to change the sign of the
* error to correctly reflect early (-) or late (+) phase timing.
*
* Expects a normalized (to unit circle) sample value.
*
* The feedback error signal is small when the sample's phase is closely
* aligned with a perfect constellation and grows larger as the sample's
* phase diverges from alignment with a perfect constellation.
* 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( ComplexSample sample )
{
float error = Math.abs( Math.abs( sample.imaginary() ) -
Math.abs( sample.real() ) );
if( sample.realAbsolute() > sample.imaginaryAbsolute() )
{
if( sample.imaginary() > 0 )
{
return sample.real() > 0 ? -error : error;
}
else
{
return sample.real() > 0 ? error : -error;
}
}
else
{
if( sample.real() > 0 )
{
return sample.real() > 0 ? error : -error;
}
else
{
return sample.real() > 0 ? -error : error;
}
}
float phase_error = 0;
if( Math.abs( sample.real() ) > Math.abs( sample.imaginary() ) )
{
if( sample.real() > 0 )
{
phase_error = -sample.imaginary();
}
else
{
phase_error = sample.imaginary();
}
}
else
{
if( sample.imaginary() > 0 )
{
phase_error = sample.real();
}
else
{
phase_error = -sample.real();
}
}
return phase_error;
}
}
@@ -462,4 +500,32 @@ public class CQPSKDemodulator implements Listener<ComplexSample>,
{
mListener = null;
}
@Override
public List<Tap> getTaps()
{
List<Tap> taps = new ArrayList<Tap>();
taps.add( new EyeDiagramDataTap( "Eye Diagram", 0, 4800 ) );
return taps;
}
@Override
public void addTap( Tap tap )
{
if( tap instanceof EyeDiagramDataTap )
{
mEyeDiagramDataTap = (EyeDiagramDataTap)tap;
}
}
@Override
public void removeTap( Tap tap )
{
if( tap instanceof EyeDiagramDataTap )
{
mEyeDiagramDataTap = null;
}
}
}

View File

@@ -0,0 +1,33 @@
package dsp.psk;
import sample.complex.ComplexSample;
public class EyeDiagramData
{
private ComplexSample[] mSamples;
private float mSamplePoint;
private float mMiddlePoint;
public EyeDiagramData( ComplexSample[] samples, float samplePoint, float middlePoint )
{
mSamples = samples;
mSamplePoint = samplePoint;
mMiddlePoint = middlePoint;
}
public ComplexSample[] getSamples()
{
return mSamples;
}
public float getSamplePoint()
{
return mSamplePoint;
}
public float getMiddlePoint()
{
return mMiddlePoint;
}
}

View File

@@ -0,0 +1,75 @@
package dsp.psk;
import sample.Broadcaster;
import sample.Listener;
import sample.complex.ComplexSample;
import dsp.symbol.Dibit;
public class QPSKPolarSlicer implements Listener<ComplexSample>
{
private Broadcaster<Dibit> mBroadcaster = new Broadcaster<Dibit>();
/**
* Slices a ComplexSample representing a phase shifted symbol according to
* the following constellation pattern:
*
* \ 00 /
* \ /
* 11 \/ 10
* /\
* / \
* / 11 \
*/
public QPSKPolarSlicer()
{
}
public void dispose()
{
mBroadcaster.dispose();
mBroadcaster = null;
}
public void addListener( Listener<Dibit> listener )
{
mBroadcaster.addListener( listener );
}
public void removeListener( Listener<Dibit> listener )
{
mBroadcaster.removeListener( listener );
}
@Override
public void receive( ComplexSample sample )
{
mBroadcaster.broadcast( decide( sample ) );
}
public static Dibit decide( ComplexSample sample )
{
if( sample.inPhaseAbsolute() > sample.quadratureAbsolute() )
{
if( sample.inphase() > 0 )
{
return Dibit.D10_MINUS_1;
}
else
{
return Dibit.D01_PLUS_3;
}
}
else
{
if( sample.quadrature() > 0 )
{
return Dibit.D00_PLUS_1;
}
else
{
return Dibit.D11_MINUS_3;
}
}
}
}

View File

@@ -0,0 +1,58 @@
package dsp.psk;
import sample.Broadcaster;
import sample.Listener;
import sample.complex.ComplexSample;
import dsp.symbol.Dibit;
public class QPSKStarSlicer implements Listener<ComplexSample>
{
private Broadcaster<Dibit> mBroadcaster = new Broadcaster<Dibit>();
/**
* Slices a ComplexSample representing a phase shifted symbol according to
* the following constellation pattern:
*
* 11 | 01
* ---|---
* 10 | 00
*/
public QPSKStarSlicer()
{
}
public void dispose()
{
mBroadcaster.dispose();
mBroadcaster = null;
}
public void addListener( Listener<Dibit> listener )
{
mBroadcaster.addListener( listener );
}
public void removeListener( Listener<Dibit> listener )
{
mBroadcaster.removeListener( listener );
}
@Override
public void receive( ComplexSample sample )
{
mBroadcaster.broadcast( decide( sample ) );
}
public static Dibit decide( ComplexSample sample )
{
if( sample.inphase() > 0 )
{
return sample.quadrature() > 0 ? Dibit.D00_PLUS_1 : Dibit.D10_MINUS_1;
}
else
{
return sample.quadrature() > 0 ? Dibit.D01_PLUS_3 : Dibit.D11_MINUS_3;
}
}
}