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() );
+ }
+ }
}