diff --git a/.classpath b/.classpath index abba7323..5ac3191b 100644 --- a/.classpath +++ b/.classpath @@ -24,5 +24,6 @@ + diff --git a/src/audio/Audio.java b/src/audio/Audio.java new file mode 100644 index 00000000..68b624f6 --- /dev/null +++ b/src/audio/Audio.java @@ -0,0 +1,20 @@ +package audio; + +import javax.sound.sampled.AudioFormat; + +public class Audio +{ + public static final AudioFormat MONO_48K_16_LE = + new AudioFormat( 48000, //SampleRate + 16, //Sample Size + 1, //Channels + true, //Signed + false ); //Little Endian + + public static final AudioFormat STEREO_48K_16_LE = + new AudioFormat( 48000, //SampleRate + 16, //Sample Size + 2, //Channels + true, //Signed + false ); //Little Endian +} diff --git a/src/audio/AudioOutputManager.java b/src/audio/AudioOutputManager.java index 8777fef6..b85d7134 100644 --- a/src/audio/AudioOutputManager.java +++ b/src/audio/AudioOutputManager.java @@ -1,12 +1,5 @@ package audio; -import javax.sound.sampled.AudioFormat; -import javax.sound.sampled.AudioSystem; -import javax.sound.sampled.DataLine; -import javax.sound.sampled.Line; -import javax.sound.sampled.Mixer; -import javax.sound.sampled.SourceDataLine; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -14,51 +7,19 @@ public class AudioOutputManager { private final static Logger mLog = LoggerFactory.getLogger( AudioOutputManager.class ); - + + /** + * Handles all audio produced by the decoding channels and routes audio + * packets to any combination of outputs based on any alias audio routing + * options specified by the user. + */ public AudioOutputManager() { } - + public static void main( String[] args ) { - Mixer.Info[] mi = AudioSystem.getMixerInfo(); - - for (Mixer.Info info : mi) - { - mLog.debug( "info: " + info ); - - Mixer m = AudioSystem.getMixer(info); - - mLog.debug( "mixer " + m ); - - Line.Info[] sl = m.getSourceLineInfo(); - - for (Line.Info info2 : sl) - { - mLog.debug( " info: " + info2 ); - - try - { - Line line = AudioSystem.getLine( info2 ); - - if (line instanceof SourceDataLine) - { - SourceDataLine source = (SourceDataLine) line; - - DataLine.Info i = (DataLine.Info) source.getLineInfo(); - - for (AudioFormat format : i.getFormats()) - { - mLog.debug( " format: " + format ); - } - } - } - catch( Exception e ) - { - mLog.error( "errors", e ); - } - } - } + } } diff --git a/src/audio/AudioPacket.java b/src/audio/AudioPacket.java new file mode 100644 index 00000000..faa232c1 --- /dev/null +++ b/src/audio/AudioPacket.java @@ -0,0 +1,24 @@ +package audio; + +import java.nio.ByteBuffer; + +public class AudioPacket +{ + private ByteBuffer mAudioBuffer; + private int mPriority; + + public AudioPacket() + { + } + + public void setAudioBuffer( ByteBuffer audioBuffer ) + { + mAudioBuffer = audioBuffer; + } + + public ByteBuffer getAudioBuffer() + { + return mAudioBuffer; + } + +} diff --git a/src/audio/IAudioPacketReceiver.java b/src/audio/IAudioPacketReceiver.java new file mode 100644 index 00000000..1bab1254 --- /dev/null +++ b/src/audio/IAudioPacketReceiver.java @@ -0,0 +1,6 @@ +package audio; + +public interface IAudioPacketReceiver +{ + +} diff --git a/src/audio/MonoAudioOutput.java b/src/audio/MonoAudioOutput.java new file mode 100644 index 00000000..f771a709 --- /dev/null +++ b/src/audio/MonoAudioOutput.java @@ -0,0 +1,12 @@ +package audio; + +public class MonoAudioOutput +{ + /** + * Buffered + */ + public MonoAudioOutput() + { + + } +} diff --git a/src/buffer/FloatCircularBuffer.java b/src/buffer/FloatCircularBuffer.java index c03ec75e..0fa3ad79 100644 --- a/src/buffer/FloatCircularBuffer.java +++ b/src/buffer/FloatCircularBuffer.java @@ -39,6 +39,11 @@ public class FloatCircularBuffer Arrays.fill( mBuffer, (float)0.0 ); } + + public float[] getBuffer() + { + return mBuffer; + } /** * Puts the new value into the buffer and returns the oldest buffer value diff --git a/src/decode/p25/P25Decoder.java b/src/decode/p25/P25Decoder.java index 7c738b51..a24928d8 100644 --- a/src/decode/p25/P25Decoder.java +++ b/src/decode/p25/P25Decoder.java @@ -43,10 +43,11 @@ import dsp.filter.ComplexFIRFilter; import dsp.filter.FilterFactory; import dsp.filter.FloatFIRFilter; import dsp.filter.Window.WindowType; -import dsp.gain.ComplexAutomaticGainControl; +import dsp.gain.ComplexFeedForwardGainControl; import dsp.gain.DirectGainControl; import dsp.nbfm.FMDiscriminator; import dsp.psk.CQPSKDemodulator; +import dsp.psk.QPSKStarSlicer; import dsp.symbol.FrameSync; public class P25Decoder extends Decoder @@ -74,21 +75,21 @@ public class P25Decoder extends Decoder private CQPSKDemodulator mCQPSKDemodulator; /* Gain */ - private ComplexAutomaticGainControl mAGC; + private ComplexFeedForwardGainControl mAGC; private DirectGainControl mDGC; /* Filters */ private ComplexFIRFilter mBasebandFilter; + private ComplexFIRFilter mRootRaisedCosineFilter; private FloatFIRFilter mAudioFilter; private C4FMSymbolFilter mSymbolFilter; /* Slicers */ private C4FMSlicer mC4FMSlicer; - private CQPSKSlicer mCQPSKSlicer; + private QPSKStarSlicer mCQPSKSlicer; /* Message framers and handlers */ private P25MessageFramer mNormalFramer; -// private P25MessageFramer mInvertedFramer; private P25MessageProcessor mMessageProcessor; /* Audio */ @@ -119,31 +120,36 @@ public class P25Decoder extends Decoder if( mSourceSampleType == SampleType.COMPLEX ) { mBasebandFilter = new ComplexFIRFilter( FilterFactory.getLowPass( - 48000, 7000, 7500, 60, WindowType.HAMMING, true ), 1.0 ); + 48000, 7250, 8000, 60, WindowType.HANNING, true ), 1.0 ); + this.addComplexListener( mBasebandFilter ); + if( modulation == Modulation.CQPSK ) { - this.addComplexListener( mBasebandFilter ); - - mAGC = new ComplexAutomaticGainControl(); + mAGC = new ComplexFeedForwardGainControl( 32 ); mBasebandFilter.setListener( mAGC ); - + + /* Root raised cosine filter using 34 symbol periods and 10 + * samples per symbol with a roll-off (alpha) value of 0.2. + * This should produce a filter with 341 coefficients. */ + mRootRaisedCosineFilter = new ComplexFIRFilter( FilterFactory + .getRootRaisedCosine( 10, 34, 0.2 ), 1.0 ); + mAGC.setListener( mRootRaisedCosineFilter ); + mCQPSKDemodulator = new CQPSKDemodulator(); - mAGC.setListener( mCQPSKDemodulator ); + mRootRaisedCosineFilter.setListener( mCQPSKDemodulator ); - mCQPSKSlicer = new CQPSKSlicer(); + mCQPSKSlicer = new QPSKStarSlicer(); mCQPSKDemodulator.setListener( mCQPSKSlicer ); mCQPSKSlicer.addListener( mNormalFramer ); } else /* C4FM */ { - this.addComplexListener( mBasebandFilter ); - mFMDemodulator = new FMDiscriminator( 1.0f ); mBasebandFilter.setListener( mFMDemodulator ); - /* Route output of the fm demod back to this channel so that we + /* Route output of the FM demod back to this channel so that we * can process the output as if it were coming from any other * real sample source */ mFMDemodulator.setListener( getRealReceiver() ); @@ -217,6 +223,7 @@ public class P25Decoder extends Decoder mAvailableTaps.add( new ComplexTap( INSTRUMENT_AGC_OUTPUT, 0, 1.0f ) ); mAvailableTaps.add( new QPSKTap( INSTRUMENT_QPSK_DEMODULATOR_OUTPUT, 0, 1.0f ) ); mAvailableTaps.add( new DibitTap( INSTRUMENT_CQPSK_SLICER_OUTPUT, 0, 0.1f ) ); + mAvailableTaps.addAll( mCQPSKDemodulator.getTaps() ); } } @@ -231,6 +238,11 @@ public class P25Decoder extends Decoder mSymbolFilter.addTap( tap ); } + if( mCQPSKDemodulator != null ) + { + mCQPSKDemodulator.addTap( tap ); + } + switch( tap.getName() ) { case INSTRUMENT_COMPLEX_INPUT: @@ -294,6 +306,11 @@ public class P25Decoder extends Decoder mSymbolFilter.removeTap( tap ); } + if( mCQPSKDemodulator != null ) + { + mCQPSKDemodulator.addTap( tap ); + } + switch( tap.getName() ) { case INSTRUMENT_COMPLEX_INPUT: @@ -376,8 +393,8 @@ public class P25Decoder extends Decoder public enum Modulation { - C4FM( "C4FM", "C4FM" ), - CQPSK( "LSM SIMULCAST", "LSM" ); + CQPSK( "LSM SIMULCAST", "LSM" ), + C4FM( "C4FM", "C4FM" ); private String mLabel; private String mShortLabel; @@ -403,5 +420,4 @@ public class P25Decoder extends Decoder return getLabel(); } }; - } diff --git a/src/decode/p25/P25Panel.java b/src/decode/p25/P25Panel.java index c4be6e3b..6bd67b6e 100644 --- a/src/decode/p25/P25Panel.java +++ b/src/decode/p25/P25Panel.java @@ -211,7 +211,7 @@ public class P25Panel extends ChannelStatePanel } repaint(); - } + } }); } diff --git a/src/decode/p25/audio/P25AudioOutput.java b/src/decode/p25/audio/P25AudioOutput.java index 874c138a..51033768 100644 --- a/src/decode/p25/audio/P25AudioOutput.java +++ b/src/decode/p25/audio/P25AudioOutput.java @@ -7,9 +7,6 @@ import java.util.concurrent.atomic.AtomicBoolean; import javax.sound.sampled.AudioInputStream; import javax.sound.sampled.AudioSystem; -import javax.sound.sampled.Control; -import javax.sound.sampled.FloatControl; -import javax.sound.sampled.FloatControl.Type; import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.SourceDataLine; @@ -72,7 +69,7 @@ public class P25AudioOutput implements IAudioOutput, IAudioTypeListener, * times a second. Each LDU message contains 9 frames and the * frame rate is 50, so we need to run at least 6 times a second */ mThreadPoolManager.scheduleFixedRate( ThreadType.SOURCE_SAMPLE_PROCESSING, - new IMBEFrameConverter(), 150, TimeUnit.MILLISECONDS ); + new IMBEFrameConverter(), 40, TimeUnit.MILLISECONDS ); } } @@ -152,16 +149,6 @@ public class P25AudioOutput implements IAudioOutput, IAudioTypeListener, mOutput.open( IMBEAudioFormat.PCM_SIGNED_8KHZ_16BITS, PROCESSED_AUDIO_FRAME_SIZE * 2 ); - Control[] controls = mOutput.getControls(); - - FloatControl gain = (FloatControl)mOutput - .getControl( FloatControl.Type.MASTER_GAIN ); - - if( gain != null ) - { - gain.setValue( 2.0f ); - } - mCanProcessAudio = true; } else @@ -169,20 +156,28 @@ public class P25AudioOutput implements IAudioOutput, IAudioTypeListener, mLog.debug( "Couldn't create output line" ); } } - catch ( LineUnavailableException e ) + catch ( LineUnavailableException lue ) { - mLog.error( "AudioOutput - couldn't open audio speakers " - + "for playback", e ); + mLog.error( "AudioOutput - couldn't open audio output line " + + "for playback", lue ); } + catch( IllegalArgumentException iae ) + { + mLog.debug( "Error fetching or opening audio output for 8kHz 16 bit audio", iae ); + } + catch( Exception e ) + { + mLog.error( "Error with audio output", e ); + } } else { - mLog.debug( "could not load IMBE audio converter library" ); + mLog.info( "could not load IMBE audio converter library" ); } } catch( IllegalArgumentException iae ) { - mLog.info( "could NOT find/load IMBE audio converter library" ); + mLog.error( "could NOT find/load IMBE audio converter library", iae ); } } diff --git a/src/decode/p25/message/ldu/LDU1Message.java b/src/decode/p25/message/ldu/LDU1Message.java index c1997626..b9a19f4c 100644 --- a/src/decode/p25/message/ldu/LDU1Message.java +++ b/src/decode/p25/message/ldu/LDU1Message.java @@ -1,5 +1,8 @@ package decode.p25.message.ldu; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import alias.AliasList; import bits.BinaryMessage; import decode.p25.reference.DataUnitID; @@ -12,6 +15,8 @@ import edac.ReedSolomon_63_47_17; public class LDU1Message extends LDUMessage { + private final static Logger mLog = LoggerFactory.getLogger( LDU1Message.class ); + public static final int ENCRYPTION_FLAG = 352; public static final int IMPLICIT_VENDOR_FLAG = 353; public static final int[] OPCODE = { 354,355,356,357,362,363 }; diff --git a/src/decode/p25/message/tsbk/motorola/TrafficChannelBaseStationIdentification.java b/src/decode/p25/message/tsbk/motorola/TrafficChannelBaseStationIdentification.java index 194d3572..ba83eea6 100644 --- a/src/decode/p25/message/tsbk/motorola/TrafficChannelBaseStationIdentification.java +++ b/src/decode/p25/message/tsbk/motorola/TrafficChannelBaseStationIdentification.java @@ -60,11 +60,6 @@ public class TrafficChannelBaseStationIdentification extends MotorolaTSBKMessage { int value = mMessage.getInt( field ); - if( value != 0 ) - { - return String.valueOf( (char)( value + 43 ) ); - } - - return null; + return String.valueOf( (char)( value + 43 ) ); } } diff --git a/src/dsp/filter/ComplexCICDecimate.java b/src/dsp/filter/ComplexCICDecimate.java index 8461d0e5..ae9f2c41 100644 --- a/src/dsp/filter/ComplexCICDecimate.java +++ b/src/dsp/filter/ComplexCICDecimate.java @@ -262,7 +262,6 @@ public class ComplexCICDecimate extends ComplexFilter mDecimation = decimation; mCleanupFilter = new ComplexFIRFilter( FilterFactory .getCICCleanupFilter( outputSampleRate, - 1, 12000, 60, WindowType.BLACKMAN ), 1.0d ); diff --git a/src/dsp/filter/ComplexHalfBandNoDecimateFilter.java b/src/dsp/filter/ComplexHalfBandNoDecimateFilter.java index 9b71a55f..aa459ae6 100644 --- a/src/dsp/filter/ComplexHalfBandNoDecimateFilter.java +++ b/src/dsp/filter/ComplexHalfBandNoDecimateFilter.java @@ -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 ); + } } diff --git a/src/dsp/filter/ComplexPrimeCICDecimate.java b/src/dsp/filter/ComplexPrimeCICDecimate.java index b57a2710..8bdd6bd5 100644 --- a/src/dsp/filter/ComplexPrimeCICDecimate.java +++ b/src/dsp/filter/ComplexPrimeCICDecimate.java @@ -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 listener ) { - mCleanupFilter.setListener( listener ); + mHalfBandFilter.setListener( listener ); } /** @@ -337,31 +367,12 @@ public class ComplexPrimeCICDecimate */ public void removeListener( Listener listener ) { - mCleanupFilter.setListener( null ); + mHalfBandFilter.setListener( null ); } public Listener 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(); } } } diff --git a/src/dsp/filter/FilterFactory.java b/src/dsp/filter/FilterFactory.java index edaf62b2..0b65b419 100644 --- a/src/dsp/filter/FilterFactory.java +++ b/src/dsp/filter/FilterFactory.java @@ -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 ) ); } + + } diff --git a/src/dsp/filter/interpolator/QPSKInterpolator.java b/src/dsp/filter/interpolator/QPSKInterpolator.java index a53326bf..6c096158 100644 --- a/src/dsp/filter/interpolator/QPSKInterpolator.java +++ b/src/dsp/filter/interpolator/QPSKInterpolator.java @@ -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 ); } } diff --git a/src/dsp/gain/ComplexFeedForwardGainControl.java b/src/dsp/gain/ComplexFeedForwardGainControl.java new file mode 100644 index 00000000..674b038e --- /dev/null +++ b/src/dsp/gain/ComplexFeedForwardGainControl.java @@ -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 + * + * 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, Provider +{ + 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 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 listener ) + { + mListener = listener; + } + + @Override + public void removeListener( Listener listener ) + { + mListener = null; + } +} diff --git a/src/dsp/psk/CQPSKDemodulator.java b/src/dsp/psk/CQPSKDemodulator.java index 05105c24..3eb4bd61 100644 --- a/src/dsp/psk/CQPSKDemodulator.java +++ b/src/dsp/psk/CQPSKDemodulator.java @@ -30,14 +30,20 @@ package dsp.psk; * along with this program. If not, see ******************************************************************************/ +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, +public class CQPSKDemodulator implements Instrumentable, + Listener, Provider { 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 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, 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, /** * Gardner symbol timing detector */ - public class GardnerDetector implements Listener + public class GardnerSymbolTiming implements Listener { 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, 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, 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, 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, { 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, 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, @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, * 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, /* 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, } /** - * 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, { mListener = null; } + + @Override + public List getTaps() + { + List taps = new ArrayList(); + + 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; + } + } } diff --git a/src/dsp/psk/EyeDiagramData.java b/src/dsp/psk/EyeDiagramData.java new file mode 100644 index 00000000..04c3ea73 --- /dev/null +++ b/src/dsp/psk/EyeDiagramData.java @@ -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; + } +} diff --git a/src/dsp/psk/QPSKPolarSlicer.java b/src/dsp/psk/QPSKPolarSlicer.java new file mode 100644 index 00000000..73537a30 --- /dev/null +++ b/src/dsp/psk/QPSKPolarSlicer.java @@ -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 +{ + private Broadcaster mBroadcaster = new Broadcaster(); + + /** + * 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 listener ) + { + mBroadcaster.addListener( listener ); + } + + public void removeListener( Listener 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; + } + } + } + +} diff --git a/src/decode/p25/CQPSKSlicer.java b/src/dsp/psk/QPSKStarSlicer.java similarity index 50% rename from src/decode/p25/CQPSKSlicer.java rename to src/dsp/psk/QPSKStarSlicer.java index 89662005..74778746 100644 --- a/src/decode/p25/CQPSKSlicer.java +++ b/src/dsp/psk/QPSKStarSlicer.java @@ -1,15 +1,23 @@ -package decode.p25; +package dsp.psk; import sample.Broadcaster; import sample.Listener; import sample.complex.ComplexSample; import dsp.symbol.Dibit; -public class CQPSKSlicer implements Listener +public class QPSKStarSlicer implements Listener { private Broadcaster mBroadcaster = new Broadcaster(); - - public CQPSKSlicer() + + /** + * Slices a ComplexSample representing a phase shifted symbol according to + * the following constellation pattern: + * + * 11 | 01 + * ---|--- + * 10 | 00 + */ + public QPSKStarSlicer() { } @@ -29,34 +37,22 @@ public class CQPSKSlicer implements Listener mBroadcaster.removeListener( listener ); } - /** - * Receives a complex sample and maps the value into one of four quadrants - * corresponding to the CQPSK LSM pattern. - */ @Override public void receive( ComplexSample sample ) { - if( sample.real() > 0 ) + mBroadcaster.broadcast( decide( sample ) ); + } + + public static Dibit decide( ComplexSample sample ) + { + if( sample.inphase() > 0 ) { - if( sample.imaginary() > 0 ) - { - mBroadcaster.broadcast( Dibit.D10_MINUS_1 ); - } - else - { - mBroadcaster.broadcast( Dibit.D11_MINUS_3 ); - } + return sample.quadrature() > 0 ? Dibit.D00_PLUS_1 : Dibit.D10_MINUS_1; } else { - if( sample.imaginary() > 0 ) - { - mBroadcaster.broadcast( Dibit.D00_PLUS_1 ); - } - else - { - mBroadcaster.broadcast( Dibit.D01_PLUS_3 ); - } + return sample.quadrature() > 0 ? Dibit.D01_PLUS_3 : Dibit.D11_MINUS_3; } } + } diff --git a/src/instrument/gui/AudioSourceFrame.java b/src/instrument/gui/AudioSourceFrame.java index ac9d9987..191e9c9d 100644 --- a/src/instrument/gui/AudioSourceFrame.java +++ b/src/instrument/gui/AudioSourceFrame.java @@ -83,8 +83,8 @@ public class AudioSourceFrame extends JInternalFrame implements PositionListener private void initGui() { setTitle( "Audio Source [" + mSource.getSampleType().toString() + "]" ); - setPreferredSize( new Dimension( 600, 150 ) ); - setSize( 600, 150 ); + setPreferredSize( new Dimension( 700, 150 ) ); + setSize( 700, 150 ); setResizable( true ); setClosable( true ); @@ -101,6 +101,7 @@ public class AudioSourceFrame extends JInternalFrame implements PositionListener panel.add( new JLabel( "Jump:" ) ); panel.add( new JumpToField( mSource ) ); panel.add( new NextSampleButton( mSource, ">", 1 ) ); + panel.add( new NextSampleButton( mSource, "> 10 >", 10 ) ); panel.add( new NextSampleButton( mSource, "> 100 >", 100 ) ); panel.add( new NextSampleButton( mSource, "> 1000 >", 1000 ) ); panel.add( new JLabel( "Posn:" ) ); diff --git a/src/instrument/gui/DecoderViewFrame.java b/src/instrument/gui/DecoderViewFrame.java index 343b71e5..5836f0bf 100644 --- a/src/instrument/gui/DecoderViewFrame.java +++ b/src/instrument/gui/DecoderViewFrame.java @@ -26,6 +26,8 @@ import instrument.tap.stream.ComplexTap; import instrument.tap.stream.ComplexTapViewPanel; import instrument.tap.stream.DibitTap; import instrument.tap.stream.DibitTapViewPanel; +import instrument.tap.stream.EyeDiagramDataTap; +import instrument.tap.stream.EyeDiagramDataTapPanel; import instrument.tap.stream.FloatTap; import instrument.tap.stream.FloatTapViewPanel; import instrument.tap.stream.QPSKTap; @@ -178,6 +180,9 @@ public class DecoderViewFrame extends JInternalFrame case STREAM_DIBIT: panel = new DibitTapViewPanel( (DibitTap)tap ); break; + case STREAM_EYE_DIAGRAM: + panel = new EyeDiagramDataTapPanel( (EyeDiagramDataTap)tap ) ; + break; case STREAM_FLOAT: panel = new FloatTapViewPanel( (FloatTap)tap ); break; diff --git a/src/instrument/tap/TapType.java b/src/instrument/tap/TapType.java index 64111ab3..dbb3f55d 100644 --- a/src/instrument/tap/TapType.java +++ b/src/instrument/tap/TapType.java @@ -23,6 +23,7 @@ public enum TapType STREAM_BINARY, STREAM_COMPLEX, STREAM_DIBIT, + STREAM_EYE_DIAGRAM, STREAM_FLOAT, STREAM_QPSK, STREAM_SYMBOL; diff --git a/src/instrument/tap/stream/EyeDiagramDataTap.java b/src/instrument/tap/stream/EyeDiagramDataTap.java new file mode 100644 index 00000000..3888aeb4 --- /dev/null +++ b/src/instrument/tap/stream/EyeDiagramDataTap.java @@ -0,0 +1,57 @@ +/******************************************************************************* + * 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 + ******************************************************************************/ +package instrument.tap.stream; + +import instrument.tap.TapListener; +import instrument.tap.TapType; +import sample.Listener; +import dsp.psk.EyeDiagramData; + +public class EyeDiagramDataTap extends StreamTap implements Listener +{ + private Listener mListener; + + public EyeDiagramDataTap( String name, int delay, float sampleRate ) + { + super( TapType.STREAM_EYE_DIAGRAM, name, delay, sampleRate ); + } + + @Override + public void receive( EyeDiagramData data ) + { + if( mListener != null ) + { + mListener.receive( data ); + } + + for( TapListener listener: mListeners ) + { + listener.receive( data ); + } + } + + public void setListener( Listener listener ) + { + mListener = listener; + } + + public void removeListener( Listener listener ) + { + mListener = null; + } +} diff --git a/src/instrument/tap/stream/EyeDiagramDataTapPanel.java b/src/instrument/tap/stream/EyeDiagramDataTapPanel.java new file mode 100644 index 00000000..784767cc --- /dev/null +++ b/src/instrument/tap/stream/EyeDiagramDataTapPanel.java @@ -0,0 +1,203 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package instrument.tap.stream; + +import instrument.gui.SampleModel; +import instrument.tap.TapViewPanel; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.geom.GeneralPath; +import java.awt.geom.Path2D; +import java.util.List; +import java.util.Observable; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import sample.complex.ComplexSample; +import dsp.psk.EyeDiagramData; + +public class EyeDiagramDataTapPanel extends TapViewPanel +{ + private static final long serialVersionUID = 1L; + private final static Logger mLog = + LoggerFactory.getLogger( EyeDiagramDataTapPanel.class ); + + private EyeDiagramDataTap mTap; + private List mSamples; + + public EyeDiagramDataTapPanel( EyeDiagramDataTap tap ) + { + super( new SampleModel(), tap.getName() ); + + mTap = tap; + mTap.addListener( getModel() ); + + getModel().setSampleCount( 15 ); + getModel().setDelay( tap.getDelay() ); + } + + @Override + public void update( Observable arg0, Object arg1 ) + { + mSamples = (List)getModel().getSamples(); + + repaint(); + } + + public void paintComponent( Graphics g ) + { + super.paintComponent( g ); + + Graphics2D g2 = (Graphics2D)g; + + + int min = ( getHeight() < getWidth() ? getHeight() : getWidth() ); + + + float halfWidth = getWidth() / 2.0f; + float halfHeight = getHeight() / 2.0f; + float xAxisIncrement = getWidth() / 19.0f / 4.0f; + float middle = 6.0f * xAxisIncrement; + + g2.setColor( Color.BLUE ); + g2.drawString( "Eye Diagram - I Leg", 5 , 15 ); + g2.drawString( "Eye Diagram - Q Leg", halfWidth , 15 ); + + + /* Vertical Line */ + g2.drawLine( getWidth() / 8, 5, getWidth() / 8, getHeight() - 5 ); + g2.drawLine( (int)( halfWidth + ( getWidth() / 8 ) ), 5, + (int)( halfWidth + ( getWidth() / 8 ) ), getHeight() - 5 ); + + g2.setColor( Color.LIGHT_GRAY ); + + g2.drawLine( (int)( getWidth() / 8 - xAxisIncrement ), 5, (int)( getWidth() / 8 - xAxisIncrement ), getHeight() - 5 ); + g2.drawLine( (int)( halfWidth + ( getWidth() / 8 ) - xAxisIncrement ), 5, + (int)( halfWidth + ( getWidth() / 8 ) - xAxisIncrement ), getHeight() - 5 ); + + g2.drawLine( (int)( getWidth() / 8 + xAxisIncrement ), 5, (int)( getWidth() / 8 + xAxisIncrement ), getHeight() - 5 ); + g2.drawLine( (int)( halfWidth + ( getWidth() / 8 ) + xAxisIncrement ), 5, + (int)( halfWidth + ( getWidth() / 8 ) + xAxisIncrement ), getHeight() - 5 ); + + /* Horizontal Line */ + g2.drawLine( 0, getHeight() / 2, getWidth() / 4, getHeight() / 2 ); + g2.drawLine( (int)halfWidth, getHeight() / 2, (int)(getWidth() * .75), + getHeight() / 2 ); + + if( mSamples != null && + mSamples.size() > 0 ) + { + + g2.setColor( Color.BLUE ); + + for( int z = 0; z < mSamples.size(); z++ ) + { + EyeDiagramData data = mSamples.get( z ); + + if( z < mSamples.size() * .75 ) + { + g2.setColor( Color.LIGHT_GRAY ); + } + else if( z == mSamples.size() - 1 ) + { + g2.setColor( Color.BLUE ); + } + else + { + g2.setColor( Color.GRAY ); + } + + ComplexSample[] samples = data.getSamples(); + + Path2D.Float iLine = new Path2D.Float( GeneralPath.WIND_EVEN_ODD, 20 ); + Path2D.Float qLine = new Path2D.Float( GeneralPath.WIND_EVEN_ODD, 20 ); + + float xAxis = middle - ( data.getMiddlePoint() * xAxisIncrement ); + + iLine.moveTo( xAxis, halfHeight ); + qLine.moveTo( xAxis + halfWidth, halfHeight ); + + for( int x = 0; x < samples.length; x++ ) + { + float i = samples[ x ].inphase(); + + if( i > 1.0 ) + { + i = 1.0f; + } + if( i < -1.0 ) + { + i = -1.0f; + } + + float iy = halfHeight + ( .85f * ( halfHeight * i ) ); + + iLine.lineTo( xAxis, iy ); + + float q = samples[ x ].quadrature(); + + if( q > 1.0 ) + { + q = 1.0f; + } + if( q < -1.0 ) + { + q = -1.0f; + } + + float qy = halfHeight + ( .85f * ( halfHeight * q ) ); + + qLine.lineTo( xAxis + halfWidth, qy ); + + xAxis += xAxisIncrement; + } + + g2.draw( iLine ); + g2.draw( qLine ); + } + } + + if( mSamples.size() > 0 ) + { + EyeDiagramData data = mSamples.get( mSamples.size() - 1 ); + + g2.setColor( Color.GREEN ); + + float xAxis = middle - ( data.getMiddlePoint() * xAxisIncrement ); + + Path2D.Float magnitude = new Path2D.Float( GeneralPath.WIND_EVEN_ODD, 20 ); + magnitude.moveTo( xAxis, halfHeight ); + + for( ComplexSample sample: data.getSamples() ) + { + magnitude.lineTo( xAxis, getHeight() - ( getHeight() * sample.magnitude() ) ); + + xAxis += xAxisIncrement; + } + + g2.setStroke( new BasicStroke( 3 ) ); + g2.draw( magnitude ); + } + + g2.dispose(); + } +} diff --git a/src/instrument/tap/stream/QPSKTapPanel.java b/src/instrument/tap/stream/QPSKTapPanel.java index e842e8a2..46d24fd1 100644 --- a/src/instrument/tap/stream/QPSKTapPanel.java +++ b/src/instrument/tap/stream/QPSKTapPanel.java @@ -50,7 +50,7 @@ public class QPSKTapPanel extends TapViewPanel mTap = tap; mTap.addListener( getModel() ); - mSampleCount = (int)( 100 * tap.getSampleRateRatio() ); + mSampleCount = (int)( 60 * tap.getSampleRateRatio() ); getModel().setSampleCount( mSampleCount ); getModel().setDelay( tap.getDelay() ); diff --git a/src/sample/adapter/ByteSampleAdapter.java b/src/sample/adapter/ByteSampleAdapter.java index 19fd622b..e0c7085f 100644 --- a/src/sample/adapter/ByteSampleAdapter.java +++ b/src/sample/adapter/ByteSampleAdapter.java @@ -31,10 +31,10 @@ public class ByteSampleAdapter extends SampleAdapter for( int x = 0; x < 256; x++ ) { - LOOKUP_VALUES[ x ] = (float)( x - 128 ); + LOOKUP_VALUES[ x ] = (float)( x - 127 ) / 128.0f; } - - LOOKUP_VALUES[ 128 ] = 0.0000001f; +// +// LOOKUP_VALUES[ 128 ] = 0.0000001f; } @Override diff --git a/src/sample/complex/ComplexSample.java b/src/sample/complex/ComplexSample.java index 9222ae89..de67df53 100644 --- a/src/sample/complex/ComplexSample.java +++ b/src/sample/complex/ComplexSample.java @@ -284,18 +284,42 @@ public class ComplexSample implements Serializable return new ComplexSample( (float)Math.cos( angle ), (float)Math.sin( angle ) ); } - + + /** + * Angle of this sample in radians + */ public float angle() { return (float)Math.atan2( y(), x() ); } + + /** + * Provides an approximate magnitude value for this sample. + */ + public float envelope() + { + float realAbsolute = realAbsolute(); + float imagAbsolute = imaginaryAbsolute(); + + if( realAbsolute > imagAbsolute ) + { + return realAbsolute + ( 0.4f * imagAbsolute ); + } + else + { + return imagAbsolute + ( 0.4f * realAbsolute ); + } + } public static void main( String[] args ) { - ComplexSample sample1 = new ComplexSample( .45f, .45f ); - ComplexSample sample2 = new ComplexSample( .25f, .25f ); - ComplexSample sample1_2 = ComplexSample.subtract( sample1, sample2 ); + ComplexSample s1 = new ComplexSample( 0.24414134f, -0.6026385f ); + ComplexSample s2 = new ComplexSample( -0.09531293f, -0.7052474f ); + + ComplexSample symbol = ComplexSample.multiply( s1, s2.conjugate() ); - mLog.debug( sample1_2.toString() ); + mLog.debug( "S1 Mag:" + s1.magnitude() + " S2 Mag:" + s2.magnitude() + " SymMag:" + symbol.magnitude() ); + mLog.debug( "S1:" + s1 + " S2:" + s2 + " Symbol:" + symbol ); + } } diff --git a/src/source/tuner/TunerChannelSource.java b/src/source/tuner/TunerChannelSource.java index 03019355..2f15516d 100644 --- a/src/source/tuner/TunerChannelSource.java +++ b/src/source/tuner/TunerChannelSource.java @@ -118,7 +118,8 @@ public class TunerChannelSource extends ComplexSource mBuffer.clear(); mBuffer = null; - mDecimationFilter.dispose(); +// mDecimationFilter.dispose(); + mDecimationFilter = null; mBufferAssembler.dispose(); @@ -183,7 +184,7 @@ public class TunerChannelSource extends ComplexSource mDecimationFilter = FilterFactory .getDecimationFilter( sampleRate, CHANNEL_RATE, - 2, //Order + 1, //Order CHANNEL_PASS_FREQUENCY, 60, //dB attenuation WindowType.HAMMING ); diff --git a/src/util/Oscillator.java b/src/util/Oscillator.java index 01721df1..e320f685 100644 --- a/src/util/Oscillator.java +++ b/src/util/Oscillator.java @@ -99,4 +99,14 @@ public class Oscillator rotate(); return mCurrentAngle.real(); } + + public static void main( String[] args ) + { + Oscillator o = new Oscillator( 1200, 2400 ); + + for( int x = 0; x < 20; x++ ) + { + System.out.println( o.nextComplex().toString() ); + } + } }