mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 16:47:31 -05:00
P25 - work in progress to improve CQPSK LSM audio output. Audio output is still choppy but greatly improved over the previous test release.
This commit is contained in:
@@ -24,5 +24,6 @@
|
||||
<classpathentry kind="lib" path="imports/logback-classic-1.1.2.jar"/>
|
||||
<classpathentry kind="lib" path="imports/logback-core-1.1.2.jar"/>
|
||||
<classpathentry kind="lib" path="imports/commons-io-2.4.jar"/>
|
||||
<classpathentry kind="lib" path="/jmbe/library/jmbe-0.1.1.jar"/>
|
||||
<classpathentry kind="output" path="bin"/>
|
||||
</classpath>
|
||||
|
||||
20
src/audio/Audio.java
Normal file
20
src/audio/Audio.java
Normal file
@@ -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
|
||||
}
|
||||
@@ -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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
24
src/audio/AudioPacket.java
Normal file
24
src/audio/AudioPacket.java
Normal file
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
6
src/audio/IAudioPacketReceiver.java
Normal file
6
src/audio/IAudioPacketReceiver.java
Normal file
@@ -0,0 +1,6 @@
|
||||
package audio;
|
||||
|
||||
public interface IAudioPacketReceiver
|
||||
{
|
||||
|
||||
}
|
||||
12
src/audio/MonoAudioOutput.java
Normal file
12
src/audio/MonoAudioOutput.java
Normal file
@@ -0,0 +1,12 @@
|
||||
package audio;
|
||||
|
||||
public class MonoAudioOutput
|
||||
{
|
||||
/**
|
||||
* Buffered
|
||||
*/
|
||||
public MonoAudioOutput()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -211,7 +211,7 @@ public class P25Panel extends ChannelStatePanel
|
||||
}
|
||||
|
||||
repaint();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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 );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 ) );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,7 +262,6 @@ public class ComplexCICDecimate extends ComplexFilter
|
||||
mDecimation = decimation;
|
||||
mCleanupFilter = new ComplexFIRFilter( FilterFactory
|
||||
.getCICCleanupFilter( outputSampleRate,
|
||||
1,
|
||||
12000,
|
||||
60,
|
||||
WindowType.BLACKMAN ), 1.0d );
|
||||
|
||||
@@ -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 );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import sample.Listener;
|
||||
import sample.complex.ComplexBuffer;
|
||||
import sample.complex.ComplexSample;
|
||||
import sample.complex.ComplexSampleListener;
|
||||
import sample.decimator.ComplexDecimator;
|
||||
@@ -72,15 +71,14 @@ public class ComplexPrimeCICDecimate
|
||||
else
|
||||
{
|
||||
/* Wire the current stage to the previous stage */
|
||||
mDecimatingStages.get( x - 1 ).addListener( stage );
|
||||
mDecimatingStages.get( x - 1 ).setListener( stage );
|
||||
}
|
||||
}
|
||||
|
||||
mOutput = new Output( 48000, order, decimation, passFrequency,
|
||||
attenuation, windowType );
|
||||
mOutput = new Output( 48000, passFrequency, attenuation, windowType );
|
||||
|
||||
mDecimatingStages.get( mDecimatingStages.size() - 1 )
|
||||
.addListener( mOutput );
|
||||
.setListener( mOutput );
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
@@ -170,7 +168,16 @@ public class ComplexPrimeCICDecimate
|
||||
{
|
||||
for( int x = 0; x < order; x++ )
|
||||
{
|
||||
Stage stage = new Stage( size );
|
||||
Stage stage;
|
||||
|
||||
if( size == 2 )
|
||||
{
|
||||
stage = new TwoStage();
|
||||
}
|
||||
else
|
||||
{
|
||||
stage = new Stage( size );
|
||||
}
|
||||
|
||||
mStages.add( stage );
|
||||
|
||||
@@ -180,13 +187,13 @@ public class ComplexPrimeCICDecimate
|
||||
}
|
||||
else
|
||||
{
|
||||
mStages.get( x - 1 ).addListener( stage );
|
||||
mStages.get( x - 1 ).setListener( stage );
|
||||
}
|
||||
}
|
||||
|
||||
mDecimator = new ComplexDecimator( size );
|
||||
|
||||
mStages.get( mStages.size() - 1 ).addListener( mDecimator );
|
||||
mStages.get( mStages.size() - 1 ).setListener( mDecimator );
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
@@ -209,29 +216,35 @@ public class ComplexPrimeCICDecimate
|
||||
mFirstStage.receive( i, q );
|
||||
}
|
||||
|
||||
public void addListener( ComplexSampleListener listener )
|
||||
public void setListener( ComplexSampleListener listener )
|
||||
{
|
||||
mDecimator.setListener( listener );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Single non-decimating CIC stage component. Uses a circular buffer
|
||||
* internally to implement the stage so that stage size has essentially
|
||||
* no impact on the performance of the stage
|
||||
* Single non-decimating CIC stage component. Uses a circular buffer and a
|
||||
* running average internally to implement the stage so that stage size has
|
||||
* essentially no impact on the computational requirements of the stage
|
||||
*/
|
||||
public class Stage implements ComplexSampleListener
|
||||
{
|
||||
private ComplexSampleListener mListener;
|
||||
protected ComplexSampleListener mListener;
|
||||
|
||||
private float[] mISamples;
|
||||
private float[] mQSamples;
|
||||
|
||||
private float mISum;
|
||||
private float mQSum;
|
||||
protected float mISum;
|
||||
protected float mQSum;
|
||||
|
||||
private int mSamplePointer = 0;
|
||||
private int mSize;
|
||||
|
||||
protected float mGain;
|
||||
|
||||
public Stage()
|
||||
{
|
||||
}
|
||||
|
||||
public Stage( int size )
|
||||
{
|
||||
@@ -239,6 +252,8 @@ public class ComplexPrimeCICDecimate
|
||||
|
||||
mISamples = new float[ mSize ];
|
||||
mQSamples = new float[ mSize ];
|
||||
|
||||
mGain = 1.0f / (float)size;
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
@@ -265,15 +280,44 @@ public class ComplexPrimeCICDecimate
|
||||
|
||||
if( mListener != null )
|
||||
{
|
||||
mListener.receive( mISum, mQSum );
|
||||
mListener.receive( ( mISum * mGain ), ( mQSum * mGain ) );
|
||||
}
|
||||
}
|
||||
|
||||
public void addListener( ComplexSampleListener listener )
|
||||
public void setListener( ComplexSampleListener listener )
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Size 2 stage that removes the unnecessary circular buffer management for
|
||||
* a two-stage.
|
||||
*/
|
||||
public class TwoStage extends Stage
|
||||
{
|
||||
public TwoStage()
|
||||
{
|
||||
super();
|
||||
|
||||
mGain = 0.5f;
|
||||
}
|
||||
|
||||
public void receive( float i, float q )
|
||||
{
|
||||
float iSum = mISum + i;
|
||||
float qSum = mQSum + q;
|
||||
|
||||
mISum = i;
|
||||
mQSum = q;
|
||||
|
||||
if( mListener != null )
|
||||
{
|
||||
mListener.receive( ( iSum * mGain ), ( qSum * mGain ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Output adapter - applies gain correction, applies cleanup filter, casts
|
||||
@@ -282,24 +326,21 @@ public class ComplexPrimeCICDecimate
|
||||
*/
|
||||
public class Output implements ComplexSampleListener
|
||||
{
|
||||
private int mDecimation;
|
||||
private int mOrder;
|
||||
private double mGain = 1.0d;
|
||||
private ComplexFilter mCleanupFilter;
|
||||
private ComplexHalfBandNoDecimateFilter mHalfBandFilter =
|
||||
new ComplexHalfBandNoDecimateFilter(
|
||||
Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO, 0.4 );
|
||||
|
||||
public Output( int outputSampleRate, int order, int decimation,
|
||||
int passFrequency, int attenuation, WindowType windowType )
|
||||
public Output( int outputSampleRate, int passFrequency, int attenuation,
|
||||
WindowType windowType )
|
||||
{
|
||||
mOrder = order;
|
||||
mDecimation = decimation;
|
||||
mCleanupFilter = new ComplexFIRFilter( FilterFactory
|
||||
.getCICCleanupFilter( outputSampleRate,
|
||||
mOrder,
|
||||
passFrequency,
|
||||
attenuation,
|
||||
windowType ), 1.0d );
|
||||
windowType ), 0.4d );
|
||||
|
||||
setGain();
|
||||
mCleanupFilter.setListener( mHalfBandFilter );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -310,26 +351,15 @@ public class ComplexPrimeCICDecimate
|
||||
@Override
|
||||
public void receive( float left, float right )
|
||||
{
|
||||
mCleanupFilter.receive(
|
||||
new ComplexSample( (float)( left * mGain ),
|
||||
(float)( right * mGain ) ) );
|
||||
mCleanupFilter.receive( new ComplexSample( left, right ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a new decimation rate
|
||||
*/
|
||||
public void setDecimationRate( int rate )
|
||||
{
|
||||
mDecimation = rate;
|
||||
setGain();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener to receive output samples
|
||||
*/
|
||||
public void setListener( Listener<ComplexSample> listener )
|
||||
{
|
||||
mCleanupFilter.setListener( listener );
|
||||
mHalfBandFilter.setListener( listener );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -337,31 +367,12 @@ public class ComplexPrimeCICDecimate
|
||||
*/
|
||||
public void removeListener( Listener<ComplexSample> listener )
|
||||
{
|
||||
mCleanupFilter.setListener( null );
|
||||
mHalfBandFilter.setListener( null );
|
||||
}
|
||||
|
||||
public Listener<ComplexSample> getListener()
|
||||
{
|
||||
return mCleanupFilter.getListener();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts the gain correction factor for output samples.
|
||||
*
|
||||
* The CIC decimation filter induces significant gain as a factor of
|
||||
* delay, decimation rate and stage count. So, we apply a gain reduction
|
||||
* to offset the added gain estimate:
|
||||
*
|
||||
* gain = ( decimation * delay ) to power of stageCount
|
||||
*
|
||||
* Delay is ONE for the Comb used in this filter.
|
||||
*/
|
||||
private void setGain()
|
||||
{
|
||||
/* decimation is NOT multiplied by delay since delay is 1 */
|
||||
double gain = Math.pow( (double)mDecimation, (double)mOrder );
|
||||
|
||||
mGain = 1.0d / gain;
|
||||
return mHalfBandFilter.getListener();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ) );
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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 );
|
||||
}
|
||||
}
|
||||
|
||||
124
src/dsp/gain/ComplexFeedForwardGainControl.java
Normal file
124
src/dsp/gain/ComplexFeedForwardGainControl.java
Normal file
@@ -0,0 +1,124 @@
|
||||
package dsp.gain;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import sample.Listener;
|
||||
import sample.Provider;
|
||||
import sample.complex.ComplexSample;
|
||||
import buffer.FloatCircularBuffer;
|
||||
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2015 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
* This class is modeled using concepts detailed in the GNURadio
|
||||
* implementation of feed forward AGC class located at:
|
||||
* https://github.com/gnuradio/gnuradio/blob/master/gr-analog/lib/
|
||||
* feedforward_agc_cc_impl.cc
|
||||
*
|
||||
******************************************************************************/
|
||||
public class ComplexFeedForwardGainControl
|
||||
implements Listener<ComplexSample>, Provider<ComplexSample>
|
||||
{
|
||||
private final static Logger mLog =
|
||||
LoggerFactory.getLogger( ComplexFeedForwardGainControl.class );
|
||||
|
||||
public static final float OBJECTIVE_ENVELOPE = 1.0f;
|
||||
public static final float MINIMUM_ENVELOPE = 0.0001f;
|
||||
|
||||
private Listener<ComplexSample> mListener;
|
||||
|
||||
private FloatCircularBuffer mEnvelopeHistory;
|
||||
|
||||
private float mMaxEnvelope = 0.0f;
|
||||
private float mGain = 1.0f;
|
||||
|
||||
/**
|
||||
* Dynamic gain control for incoming sample stream to amplify or attenuate
|
||||
* all samples toward an objective unity)gain, using the maximum envelope
|
||||
* value detected in the stream history window.
|
||||
*
|
||||
* Uses the specified damping factor to limit gain swings. Damping factor
|
||||
* is applied against the delta between current gain value and a recalculated
|
||||
* gain value to limit how quickly the gain value will increase or decrease.
|
||||
*
|
||||
* @param window - history size to use in detecting maximum envelope value
|
||||
* @param damping - a value between 0 < damping <= 1.0;
|
||||
*/
|
||||
public ComplexFeedForwardGainControl( int window )
|
||||
{
|
||||
mEnvelopeHistory = new FloatCircularBuffer( window );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( ComplexSample sample )
|
||||
{
|
||||
float envelope = sample.envelope();
|
||||
|
||||
if( envelope > mMaxEnvelope )
|
||||
{
|
||||
mMaxEnvelope = envelope;
|
||||
|
||||
adjustGain();
|
||||
}
|
||||
|
||||
/* Replace oldest envelope value with current envelope value */
|
||||
float oldestEnvelope = mEnvelopeHistory.get( envelope );
|
||||
|
||||
/* If the oldest envelope value was the max envelope value, then we
|
||||
* have to rediscover the max value from the envelope history */
|
||||
if( mMaxEnvelope == oldestEnvelope && mMaxEnvelope != envelope )
|
||||
{
|
||||
mMaxEnvelope = MINIMUM_ENVELOPE;
|
||||
|
||||
for( float value: mEnvelopeHistory.getBuffer() )
|
||||
{
|
||||
if( value > mMaxEnvelope )
|
||||
{
|
||||
mMaxEnvelope = value;
|
||||
}
|
||||
}
|
||||
|
||||
adjustGain();
|
||||
}
|
||||
|
||||
/* Apply current gain value to the sample and send to the listener */
|
||||
if( mListener != null )
|
||||
{
|
||||
sample.multiply( mGain );
|
||||
|
||||
mListener.receive( sample );
|
||||
}
|
||||
}
|
||||
|
||||
private void adjustGain()
|
||||
{
|
||||
mGain = OBJECTIVE_ENVELOPE / mMaxEnvelope;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setListener( Listener<ComplexSample> listener )
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeListener( Listener<ComplexSample> listener )
|
||||
{
|
||||
mListener = null;
|
||||
}
|
||||
}
|
||||
@@ -30,14 +30,20 @@ package dsp.psk;
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
|
||||
import instrument.Instrumentable;
|
||||
import instrument.tap.Tap;
|
||||
import instrument.tap.stream.EyeDiagramDataTap;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import sample.Listener;
|
||||
import sample.Provider;
|
||||
import sample.complex.ComplexSample;
|
||||
import source.tuner.FrequencyChangeEvent;
|
||||
import source.tuner.FrequencyChangeEvent.Attribute;
|
||||
import source.tuner.FrequencyChangeListener;
|
||||
import buffer.FloatAveragingBuffer;
|
||||
import dsp.filter.interpolator.QPSKInterpolator;
|
||||
@@ -50,18 +56,30 @@ import dsp.filter.interpolator.QPSKInterpolator;
|
||||
* Sample Rate: 48000
|
||||
* Symbol Rate: 4800
|
||||
*/
|
||||
public class CQPSKDemodulator implements Listener<ComplexSample>,
|
||||
public class CQPSKDemodulator implements Instrumentable,
|
||||
Listener<ComplexSample>,
|
||||
Provider<ComplexSample>
|
||||
{
|
||||
private final static Logger mLog =
|
||||
LoggerFactory.getLogger( CQPSKDemodulator.class );
|
||||
|
||||
/* 45 degree rotation angle */
|
||||
public static final float THETA = (float)( Math.PI / 4.0d );
|
||||
|
||||
/* 45 degree point */
|
||||
public static final ComplexSample POINT_45_DEGREES =
|
||||
new ComplexSample( (float)Math.sin( THETA ), (float)Math.cos( THETA ) );
|
||||
|
||||
private DecimalFormat mDecimalFormat = new DecimalFormat( "0.000000" );
|
||||
|
||||
private Listener<ComplexSample> mListener;
|
||||
|
||||
private GardnerDetector mGardnerDetector = new GardnerDetector();
|
||||
private GardnerSymbolTiming mGardnerDetector = new GardnerSymbolTiming();
|
||||
|
||||
private FrequencyChangeListener mFrequencyChangeListener;
|
||||
|
||||
private EyeDiagramDataTap mEyeDiagramDataTap;
|
||||
|
||||
public CQPSKDemodulator()
|
||||
{
|
||||
}
|
||||
@@ -89,6 +107,22 @@ public class CQPSKDemodulator implements Listener<ComplexSample>,
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constrains timing error to +/- the maximum value and corrects any
|
||||
* floating point invalid numbers
|
||||
*/
|
||||
private float normalize( float error, float maximum )
|
||||
{
|
||||
if( Float.isNaN( error ) )
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
return clip( error, maximum );
|
||||
}
|
||||
}
|
||||
|
||||
public void addListener( FrequencyChangeListener listener )
|
||||
{
|
||||
mFrequencyChangeListener = listener;
|
||||
@@ -161,7 +195,7 @@ public class CQPSKDemodulator implements Listener<ComplexSample>,
|
||||
/**
|
||||
* Gardner symbol timing detector
|
||||
*/
|
||||
public class GardnerDetector implements Listener<ComplexSample>
|
||||
public class GardnerSymbolTiming implements Listener<ComplexSample>
|
||||
{
|
||||
public static final int HALF_SAMPLES_PER_SYMBOL = 5;
|
||||
public static final int SAMPLES_PER_SYMBOL = 10;
|
||||
@@ -172,10 +206,10 @@ public class CQPSKDemodulator implements Listener<ComplexSample>,
|
||||
private int mDelayLinePointer = 0;
|
||||
|
||||
/* Sampling point */
|
||||
private float mMu = 10.5f;
|
||||
private float mGainMu = 0.05f;
|
||||
private float mMu = 10.0f;
|
||||
private float mGainMu = 0.25f;
|
||||
|
||||
/* samples per symbol */
|
||||
/* Samples per symbol */
|
||||
private float mOmega = 10.0f;
|
||||
private float mGainOmega = 0.1f * mGainMu * mGainMu;
|
||||
private float mOmegaRel = 0.005f;
|
||||
@@ -183,11 +217,16 @@ public class CQPSKDemodulator implements Listener<ComplexSample>,
|
||||
|
||||
private CostasLoop mCostasLoop = new CostasLoop();
|
||||
|
||||
private QPSKInterpolator mInterpolator = new QPSKInterpolator();
|
||||
private QPSKInterpolator mInterpolator = new QPSKInterpolator( 1.0f );
|
||||
|
||||
private ComplexSample mPreviousSample = new ComplexSample( 0.0f, 0.0f );
|
||||
private ComplexSample mPreviousMiddleSample = new ComplexSample( 0.0f, 0.0f );
|
||||
private ComplexSample mPreviousSymbol = new ComplexSample( 0.0f, 0.0f );
|
||||
|
||||
public GardnerDetector()
|
||||
/**
|
||||
* Provides symbol sampling timing control
|
||||
*/
|
||||
public GardnerSymbolTiming()
|
||||
{
|
||||
for( int x = 0; x < ( 2 * TWICE_SAMPLES_PER_SYMBOL ); x++ )
|
||||
{
|
||||
@@ -203,84 +242,93 @@ public class CQPSKDemodulator implements Listener<ComplexSample>,
|
||||
|
||||
mCostasLoop.increment();
|
||||
|
||||
/* Mix incoming sample with costas loop to remove any rotation */
|
||||
ComplexSample unrotatedSample = ComplexSample
|
||||
/* Mix incoming sample with costas loop to remove any rotation
|
||||
* that is present from a mis-tuned carrier frequency */
|
||||
ComplexSample derotatedSample = ComplexSample
|
||||
.multiply( mCostasLoop.getCurrentVector(), sample );
|
||||
|
||||
/* Fill up the delay line for the interpolator */
|
||||
mDelayLine[ mDelayLinePointer ] = unrotatedSample;
|
||||
mDelayLine[ mDelayLinePointer + TWICE_SAMPLES_PER_SYMBOL ] = unrotatedSample;
|
||||
|
||||
/* Keep the delay line pointer in bounds */
|
||||
/* Fill up the delay line to use with the interpolator */
|
||||
mDelayLine[ mDelayLinePointer ] = derotatedSample;
|
||||
mDelayLine[ mDelayLinePointer + TWICE_SAMPLES_PER_SYMBOL ] = derotatedSample;
|
||||
|
||||
/* Imcrement pointer and keep pointer in bounds */
|
||||
mDelayLinePointer = ( mDelayLinePointer + 1 ) % TWICE_SAMPLES_PER_SYMBOL;
|
||||
|
||||
/* Calculate the symbol once we've stored enough samples */
|
||||
if( mMu <= 1.0f )
|
||||
{
|
||||
float half_omega = mOmega / 2.0f;
|
||||
int half_sps = (int)Math.floor( half_omega );
|
||||
float half_mu = mMu + half_omega - (float)half_sps;
|
||||
|
||||
if( half_mu > 1.0 )
|
||||
{
|
||||
half_mu -= 1.0;
|
||||
half_sps += 1;
|
||||
}
|
||||
|
||||
/* Calculate interpolated middle sample and current sample */
|
||||
ComplexSample middleSample = mInterpolator
|
||||
.filter( mDelayLine, mDelayLinePointer, mMu );
|
||||
|
||||
middleSample.normalize();
|
||||
|
||||
ComplexSample currentSample = mInterpolator
|
||||
.filter( mDelayLine, mDelayLinePointer +
|
||||
HALF_SAMPLES_PER_SYMBOL, mMu );
|
||||
.filter( mDelayLine, mDelayLinePointer + half_sps, half_mu );
|
||||
|
||||
/* Multiply current and previous samples to get symbols to use
|
||||
* for gardner error feedback */
|
||||
ComplexSample middleSymbol = ComplexSample.multiply(
|
||||
middleSample, mPreviousMiddleSample.conjugate() );
|
||||
|
||||
currentSample.normalize();
|
||||
ComplexSample currentSymbol = ComplexSample.multiply(
|
||||
currentSample, mPreviousSample.conjugate() );
|
||||
|
||||
/* Set gain to unity */
|
||||
middleSymbol.normalize();
|
||||
currentSymbol.normalize();
|
||||
|
||||
// if( mEyeDiagramDataTap != null )
|
||||
// {
|
||||
// mEyeDiagramDataTap.receive(
|
||||
// new EyeDiagramData( Arrays.copyOfRange( mDelayLine,
|
||||
// mDelayLinePointer, mDelayLinePointer + 20 ),
|
||||
// mMu, (float)half_sps + half_mu ) );
|
||||
// }
|
||||
|
||||
/* Gardner timing error calculations */
|
||||
float errorReal = ( mPreviousSample.real() -
|
||||
currentSample.real() ) * middleSample.real();
|
||||
float errorInphase = ( mPreviousSymbol.inphase() -
|
||||
currentSymbol.inphase() ) * middleSymbol.inphase();
|
||||
|
||||
float errorImaginary = ( mPreviousSample.imaginary() -
|
||||
currentSample.imaginary() ) * middleSample.imaginary();
|
||||
float errorQuadrature = ( mPreviousSymbol.quadrature() -
|
||||
currentSymbol.quadrature() ) * middleSymbol.quadrature();
|
||||
|
||||
float gardnerError = normalize( errorReal + errorImaginary );
|
||||
float gardnerError = normalize( errorInphase + errorQuadrature, 1.0f );
|
||||
|
||||
/* mOmega is samples per symbol and is constrained to floating
|
||||
* between +/- .005 of the nominal 10.0 samples per symbol */
|
||||
mOmega = mOmega + mGainOmega * gardnerError;
|
||||
mOmega = mOmegaMid + clip( mOmega - mOmegaMid, mOmegaRel );
|
||||
|
||||
mMu += mOmega + mGainMu * gardnerError;
|
||||
|
||||
/* Calculate symbol as delta between previous and current samples */
|
||||
ComplexSample symbol = ComplexSample.multiply(
|
||||
currentSample, mPreviousSample.conjugate() );
|
||||
/* Adjust sample timing based on error of current sample */
|
||||
mMu += mOmega + ( mGainMu * gardnerError );
|
||||
|
||||
/* Store current samples/symbols to use for the next period */
|
||||
mPreviousSample = currentSample;
|
||||
mPreviousMiddleSample = middleSample;
|
||||
mPreviousSymbol = currentSymbol;
|
||||
|
||||
/* Update costas loop phase error from error in current symbol */
|
||||
mCostasLoop.receive( symbol );
|
||||
/* Update costas loop using phase error present in current
|
||||
* symbol. The symbol is rotated from star orientation to polar
|
||||
* orientation to simplify error calculation */
|
||||
mCostasLoop.receive(
|
||||
ComplexSample.multiply( currentSymbol, POINT_45_DEGREES ) );
|
||||
|
||||
/* Dispatch the symbol to the registered listener */
|
||||
/* Dispatch the differentiated symbol to the registered listener */
|
||||
if( mListener != null )
|
||||
{
|
||||
mListener.receive( symbol );
|
||||
mListener.receive( currentSymbol );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constrains timing error to +/- 1
|
||||
*/
|
||||
private float normalize( float error )
|
||||
{
|
||||
if( Float.isNaN( error ) )
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
if( error < -1.0 )
|
||||
{
|
||||
return -1.0f;
|
||||
}
|
||||
|
||||
if( error > 1.0 )
|
||||
{
|
||||
return 1.0f;
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -298,17 +346,15 @@ public class CQPSKDemodulator implements Listener<ComplexSample>,
|
||||
{
|
||||
public static final double TWO_PI = 2.0 * Math.PI;
|
||||
|
||||
/* 45 degree rotation angle */
|
||||
public static final float THETA = (float)( Math.PI / 4.0d );
|
||||
|
||||
private static final float MAXIMUM_FREQUENCY = ( 2.0f * (float)Math.PI *
|
||||
1200.0f ) / 48000.0f;
|
||||
private static final float MAXIMUM_FREQUENCY =
|
||||
( 1200.0f * (float)TWO_PI ) / 48000.0f;
|
||||
|
||||
/* http://www.trondeau.com/blog/2011/8/13/control-loop-gain-values.html */
|
||||
private float mDamping = (float)Math.sqrt( 2.0 ) / 2.0f;
|
||||
|
||||
/* Use denominator between 100 and 200 to adjust control level */
|
||||
private float mLoopBandwidth = (float)( ( 2.0d * Math.PI ) / 100.0d );
|
||||
/* Use denominator between 100 (most) and 400 (least) to adjust costas
|
||||
* loop control level */
|
||||
private float mLoopBandwidth = (float)( TWO_PI / 400.0d );
|
||||
|
||||
private float mAlphaGain = ( 4.0f * mDamping * mLoopBandwidth ) /
|
||||
( 1.0f + ( 2.0f * mDamping * mLoopBandwidth ) +
|
||||
@@ -323,11 +369,11 @@ public class CQPSKDemodulator implements Listener<ComplexSample>,
|
||||
private float mLoopFrequency = 0.0f;
|
||||
|
||||
private FrequencyControl mFrequencyControl = new FrequencyControl();
|
||||
|
||||
|
||||
public CostasLoop()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Increments the phase of the loop
|
||||
*/
|
||||
@@ -363,9 +409,6 @@ public class CQPSKDemodulator implements Listener<ComplexSample>,
|
||||
@Override
|
||||
public void receive( ComplexSample sample )
|
||||
{
|
||||
/* Normalize magnitude of incoming signal to 1.0 */
|
||||
sample.normalize();
|
||||
|
||||
/* Calculate phase error */
|
||||
float phaseError = getPhaseError( sample );
|
||||
|
||||
@@ -377,7 +420,7 @@ public class CQPSKDemodulator implements Listener<ComplexSample>,
|
||||
* Updates the costas loop frequency and phase to adjust for the phase
|
||||
* error value
|
||||
*
|
||||
* @param phase_error - (+)= late and (-)= early
|
||||
* @param phase_error - (-)= late and (+)= early
|
||||
*/
|
||||
private void adjust( float phase_error )
|
||||
{
|
||||
@@ -387,10 +430,13 @@ public class CQPSKDemodulator implements Listener<ComplexSample>,
|
||||
/* Maintain phase between +/- 2 * PI */
|
||||
unwrapPhase();
|
||||
|
||||
/* Limit frequency to +/- maximum frequency */
|
||||
/* Limit frequency to +/- maximum loop frequency */
|
||||
limitFrequency();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Constrains the frequency within the bounds of +/- loop frequency
|
||||
*/
|
||||
private void limitFrequency()
|
||||
{
|
||||
/* Check for and issue tuner offset correction */
|
||||
@@ -408,46 +454,38 @@ public class CQPSKDemodulator implements Listener<ComplexSample>,
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the phase error as the difference between the real and
|
||||
* imaginary values of the sample. When balanced, both absolute values
|
||||
* should be equal to ~ 0.707. The difference between the real and
|
||||
* imaginary values is used as the error signal. Mapping the sample
|
||||
* vector to the correct quadrant allows us to change the sign of the
|
||||
* error to correctly reflect early (-) or late (+) phase timing.
|
||||
*
|
||||
* Expects a normalized (to unit circle) sample value.
|
||||
*
|
||||
* The feedback error signal is small when the sample's phase is closely
|
||||
* aligned with a perfect constellation and grows larger as the sample's
|
||||
* phase diverges from alignment with a perfect constellation.
|
||||
* Detects rotational error present when the costas loop is not aligned
|
||||
* with the carrier frequency. Provides error feedback to adjust
|
||||
* mixer frequency.
|
||||
*/
|
||||
public float getPhaseError( ComplexSample sample )
|
||||
{
|
||||
float error = Math.abs( Math.abs( sample.imaginary() ) -
|
||||
Math.abs( sample.real() ) );
|
||||
|
||||
if( sample.realAbsolute() > sample.imaginaryAbsolute() )
|
||||
{
|
||||
if( sample.imaginary() > 0 )
|
||||
{
|
||||
return sample.real() > 0 ? -error : error;
|
||||
}
|
||||
else
|
||||
{
|
||||
return sample.real() > 0 ? error : -error;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if( sample.real() > 0 )
|
||||
{
|
||||
return sample.real() > 0 ? error : -error;
|
||||
}
|
||||
else
|
||||
{
|
||||
return sample.real() > 0 ? -error : error;
|
||||
}
|
||||
}
|
||||
float phase_error = 0;
|
||||
|
||||
if( Math.abs( sample.real() ) > Math.abs( sample.imaginary() ) )
|
||||
{
|
||||
if( sample.real() > 0 )
|
||||
{
|
||||
phase_error = -sample.imaginary();
|
||||
}
|
||||
else
|
||||
{
|
||||
phase_error = sample.imaginary();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if( sample.imaginary() > 0 )
|
||||
{
|
||||
phase_error = sample.real();
|
||||
}
|
||||
else
|
||||
{
|
||||
phase_error = -sample.real();
|
||||
}
|
||||
}
|
||||
|
||||
return phase_error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -462,4 +500,32 @@ public class CQPSKDemodulator implements Listener<ComplexSample>,
|
||||
{
|
||||
mListener = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Tap> getTaps()
|
||||
{
|
||||
List<Tap> taps = new ArrayList<Tap>();
|
||||
|
||||
taps.add( new EyeDiagramDataTap( "Eye Diagram", 0, 4800 ) );
|
||||
|
||||
return taps;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addTap( Tap tap )
|
||||
{
|
||||
if( tap instanceof EyeDiagramDataTap )
|
||||
{
|
||||
mEyeDiagramDataTap = (EyeDiagramDataTap)tap;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeTap( Tap tap )
|
||||
{
|
||||
if( tap instanceof EyeDiagramDataTap )
|
||||
{
|
||||
mEyeDiagramDataTap = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
33
src/dsp/psk/EyeDiagramData.java
Normal file
33
src/dsp/psk/EyeDiagramData.java
Normal file
@@ -0,0 +1,33 @@
|
||||
package dsp.psk;
|
||||
|
||||
import sample.complex.ComplexSample;
|
||||
|
||||
public class EyeDiagramData
|
||||
{
|
||||
private ComplexSample[] mSamples;
|
||||
private float mSamplePoint;
|
||||
private float mMiddlePoint;
|
||||
|
||||
public EyeDiagramData( ComplexSample[] samples, float samplePoint, float middlePoint )
|
||||
{
|
||||
mSamples = samples;
|
||||
|
||||
mSamplePoint = samplePoint;
|
||||
mMiddlePoint = middlePoint;
|
||||
}
|
||||
|
||||
public ComplexSample[] getSamples()
|
||||
{
|
||||
return mSamples;
|
||||
}
|
||||
|
||||
public float getSamplePoint()
|
||||
{
|
||||
return mSamplePoint;
|
||||
}
|
||||
|
||||
public float getMiddlePoint()
|
||||
{
|
||||
return mMiddlePoint;
|
||||
}
|
||||
}
|
||||
75
src/dsp/psk/QPSKPolarSlicer.java
Normal file
75
src/dsp/psk/QPSKPolarSlicer.java
Normal file
@@ -0,0 +1,75 @@
|
||||
package dsp.psk;
|
||||
|
||||
import sample.Broadcaster;
|
||||
import sample.Listener;
|
||||
import sample.complex.ComplexSample;
|
||||
import dsp.symbol.Dibit;
|
||||
|
||||
public class QPSKPolarSlicer implements Listener<ComplexSample>
|
||||
{
|
||||
private Broadcaster<Dibit> mBroadcaster = new Broadcaster<Dibit>();
|
||||
|
||||
/**
|
||||
* Slices a ComplexSample representing a phase shifted symbol according to
|
||||
* the following constellation pattern:
|
||||
*
|
||||
* \ 00 /
|
||||
* \ /
|
||||
* 11 \/ 10
|
||||
* /\
|
||||
* / \
|
||||
* / 11 \
|
||||
*/
|
||||
public QPSKPolarSlicer()
|
||||
{
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mBroadcaster.dispose();
|
||||
mBroadcaster = null;
|
||||
}
|
||||
|
||||
public void addListener( Listener<Dibit> listener )
|
||||
{
|
||||
mBroadcaster.addListener( listener );
|
||||
}
|
||||
|
||||
public void removeListener( Listener<Dibit> listener )
|
||||
{
|
||||
mBroadcaster.removeListener( listener );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( ComplexSample sample )
|
||||
{
|
||||
mBroadcaster.broadcast( decide( sample ) );
|
||||
}
|
||||
|
||||
public static Dibit decide( ComplexSample sample )
|
||||
{
|
||||
if( sample.inPhaseAbsolute() > sample.quadratureAbsolute() )
|
||||
{
|
||||
if( sample.inphase() > 0 )
|
||||
{
|
||||
return Dibit.D10_MINUS_1;
|
||||
}
|
||||
else
|
||||
{
|
||||
return Dibit.D01_PLUS_3;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if( sample.quadrature() > 0 )
|
||||
{
|
||||
return Dibit.D00_PLUS_1;
|
||||
}
|
||||
else
|
||||
{
|
||||
return Dibit.D11_MINUS_3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<ComplexSample>
|
||||
public class QPSKStarSlicer implements Listener<ComplexSample>
|
||||
{
|
||||
private Broadcaster<Dibit> mBroadcaster = new Broadcaster<Dibit>();
|
||||
|
||||
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<ComplexSample>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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:" ) );
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -23,6 +23,7 @@ public enum TapType
|
||||
STREAM_BINARY,
|
||||
STREAM_COMPLEX,
|
||||
STREAM_DIBIT,
|
||||
STREAM_EYE_DIAGRAM,
|
||||
STREAM_FLOAT,
|
||||
STREAM_QPSK,
|
||||
STREAM_SYMBOL;
|
||||
|
||||
57
src/instrument/tap/stream/EyeDiagramDataTap.java
Normal file
57
src/instrument/tap/stream/EyeDiagramDataTap.java
Normal file
@@ -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 <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
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<EyeDiagramData>
|
||||
{
|
||||
private Listener<EyeDiagramData> 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<EyeDiagramData> listener )
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
public void removeListener( Listener<EyeDiagramData> listener )
|
||||
{
|
||||
mListener = null;
|
||||
}
|
||||
}
|
||||
203
src/instrument/tap/stream/EyeDiagramDataTapPanel.java
Normal file
203
src/instrument/tap/stream/EyeDiagramDataTapPanel.java
Normal file
@@ -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 <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
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<EyeDiagramData> mSamples;
|
||||
|
||||
public EyeDiagramDataTapPanel( EyeDiagramDataTap tap )
|
||||
{
|
||||
super( new SampleModel<EyeDiagramData>(), tap.getName() );
|
||||
|
||||
mTap = tap;
|
||||
mTap.addListener( getModel() );
|
||||
|
||||
getModel().setSampleCount( 15 );
|
||||
getModel().setDelay( tap.getDelay() );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update( Observable arg0, Object arg1 )
|
||||
{
|
||||
mSamples = (List<EyeDiagramData>)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();
|
||||
}
|
||||
}
|
||||
@@ -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() );
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 );
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 );
|
||||
|
||||
@@ -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() );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user