diff --git a/src/decode/am/AMDecoder.java b/src/decode/am/AMDecoder.java index cc9ca1d2..15ac40c3 100644 --- a/src/decode/am/AMDecoder.java +++ b/src/decode/am/AMDecoder.java @@ -1,10 +1,13 @@ package decode.am; -import sample.simplex.SimplexSampleListener; +import sample.real.RealSampleListener; import source.Source.SampleType; import decode.Decoder; import decode.DecoderType; +import dsp.agc.ComplexAutomaticGainControl; +import dsp.agc.RealAutomaticGainControl; import dsp.am.AMDemodulator; +import dsp.filter.ComplexFIRFilter; import dsp.filter.DCRemovalFilter2; import dsp.filter.FilterFactory; import dsp.filter.FloatFIRFilter; @@ -18,8 +21,14 @@ public class AMDecoder extends Decoder */ private static final double DC_REMOVAL_RATIO = 0.003; - private AMDemodulator mDemodulator; - private DCRemovalFilter2 mDCRemovalFilter; + private ComplexFIRFilter mIQFilter; + private ComplexAutomaticGainControl mBasebandAGC = + new ComplexAutomaticGainControl(); + private AMDemodulator mDemodulator = new AMDemodulator(); + private DCRemovalFilter2 mDCRemovalFilter = + new DCRemovalFilter2( DC_REMOVAL_RATIO ); + private RealAutomaticGainControl mAudioAGC = new RealAutomaticGainControl(); + private FloatFIRFilter mLowPassFilter; public AMDecoder( SampleType sampleType ) @@ -33,6 +42,9 @@ public class AMDecoder extends Decoder */ if( mSourceSampleType == SampleType.COMPLEX ) { + mIQFilter = new ComplexFIRFilter( + FilterFactory.getLowPass( 48000, 6500, 73, WindowType.HAMMING ), + 1.0 ); /** * The Decoder super class is both a Complex listener and a float * listener. So, we add the demod to listen to the incoming @@ -40,15 +52,15 @@ public class AMDecoder extends Decoder * back to this class, so we can receive the demodulated output * to process */ - mDemodulator = new AMDemodulator(); + this.addComplexListener( mIQFilter ); + mIQFilter.setListener( mBasebandAGC ); + mBasebandAGC.setListener( mDemodulator ); - this.addComplexListener( mDemodulator ); /** * Remove the DC component that is present when we're mistuned */ - mDCRemovalFilter = new DCRemovalFilter2( DC_REMOVAL_RATIO ); - mDemodulator.addListener( mDCRemovalFilter ); + mDemodulator.setListener( mDCRemovalFilter ); /** * Route the demodulated, filtered samples back to this class to send @@ -56,12 +68,12 @@ public class AMDecoder extends Decoder */ mLowPassFilter = new FloatFIRFilter( - FilterFactory.getLowPass( 48000, 3000, 31, - WindowType.COSINE ), 100.0 ); + FilterFactory.getLowPass( 48000, 3000, 31, WindowType.COSINE ), 1.0 ); mDCRemovalFilter.setListener( mLowPassFilter ); - mLowPassFilter.setListener( this.getSimplexReceiver() ); + mLowPassFilter.setListener( mAudioAGC ); + mAudioAGC.setListener( this.getRealReceiver() ); } } @@ -72,8 +84,9 @@ public class AMDecoder extends Decoder } @Override - public void addUnfilteredSimplexSampleListener( SimplexSampleListener listener ) + public void addUnfilteredRealSampleListener( RealSampleListener listener ) { - mDemodulator.addListener( listener ); + throw new IllegalArgumentException( "cannot add real sample " + + "listener to AM demodulator - not implemented" ); } } diff --git a/src/dsp/agc/RealAutomaticGainControl.java b/src/dsp/agc/RealAutomaticGainControl.java new file mode 100644 index 00000000..4279499e --- /dev/null +++ b/src/dsp/agc/RealAutomaticGainControl.java @@ -0,0 +1,197 @@ +package dsp.agc; + +import java.util.concurrent.atomic.AtomicBoolean; + +import sample.complex.ComplexSample; +import sample.real.RealSampleListener; +import sample.real.RealSampleProvider; +import buffer.DoubleCircularBuffer; +import buffer.FloatCircularBuffer; + +/** + * Automatic Gain Control (AGC) for complex samples at 48000 sample rate. + */ +public class RealAutomaticGainControl implements RealSampleListener, + RealSampleProvider +{ + private static final double SAMPLE_RATE = 48000; + + /* Signal delay line - time delay in seconds */ +// private static final double DELAY_TIME_CONSTANT = 0.015; + private static final double DELAY_TIME_CONSTANT = 0.01; + + /* Peak detector window - time delay in seconds */ + private static final double WINDOW_TIME_CONSTANT = 0.018; + + /* Attack time constants in seconds */ + private static final double ATTACK_RISE_TIME_CONSTANT = 0.002; + private static final double ATTACK_FALL_TIME_CONSTANT = 0.005; + + private static final double ATTACK_RISE_ALPHA = 1.0 - + Math.exp( -1.0 / SAMPLE_RATE * ATTACK_RISE_TIME_CONSTANT ); + + private static final double ATTACK_FALL_ALPHA = 1.0 - + Math.exp( -1.0 / SAMPLE_RATE * ATTACK_FALL_TIME_CONSTANT ); + + /* AGC decay value in milliseconds (20 to 5000) */ + private static final double DECAY = 200; + + /* Ratio between rise and fall times of decay time constants - adjust for + * best action with SSB */ + private static final double DECAY_RISEFALL_RATIO = 0.3; + + private static final double DECAY_RISE_ALPHA = 1.0 - + Math.exp( -1.0 / ( SAMPLE_RATE * DECAY * .001 * DECAY_RISEFALL_RATIO ) ); + + private static final double DECAY_FALL_ALPHA = 1.0 - + Math.exp( -1.0 / ( SAMPLE_RATE * DECAY * .001 ) ); + + /* Hang timer release decay time constant in seconds */ + private static final double RELEASE_TIME_CONSTANT = 0.05; + + /* Specifies the AGC Knee in dB if AGC is active (nominal range -160 to 0 dB) */ + private static final double THRESHOLD = -100; + + /* Limit output to about 3db of maximum */ + private static final double AGC_OUT_SCALE = 0.7; + + /* Keep max input and output the same */ + private static final double MAX_AMPLITUDE = 32767.0; //1.0; + private static final double MAX_MANUAL_AMPLITUDE = 32767.0; //1.0; + + /* Specifies AGC manual gain in dB if AGC is not active ( 0 to 100 dB) */ + private static final double MANUAL_GAIN = 0.0; + + private static final double MANUAL_AGC_GAIN = MAX_MANUAL_AMPLITUDE * + Math.pow( 10.0, MANUAL_GAIN / 20.0 ); + + /* Specifies dB reduction in output at knee from max output level (0 - 10dB) */ + private static final double SLOPE_FACTOR = 2.0; + + private static final double KNEE = THRESHOLD / 20.0; + + private static final double GAIN_SLOPE = SLOPE_FACTOR / 100.0; + + private static final double FIXED_GAIN = AGC_OUT_SCALE * + Math.pow( 10.0, KNEE * ( GAIN_SLOPE - 1.0 ) ); + + /* Constant for calc log() so that a value of 0 magnitude = -8 */ + private static final double MIN_CONSTANT = 3.2767E-4; + + private AtomicBoolean mAGCEnabled = new AtomicBoolean( true ); + + private double mPeakMagnitude = 0.0; + private double mAttackAverage = 0.0; + private double mDecayAverage = 0.0; + + private FloatCircularBuffer mDelayBuffer = + new FloatCircularBuffer( (int)( SAMPLE_RATE * DELAY_TIME_CONSTANT ) ); + private DoubleCircularBuffer mMagnitudeBuffer = + new DoubleCircularBuffer( (int)( SAMPLE_RATE * WINDOW_TIME_CONSTANT ) ); + + private RealSampleListener mListener; + + public RealAutomaticGainControl() + { + } + + @Override + public void receive( float currentSample ) + { + float delayedSample = mDelayBuffer.get( currentSample ); + + double gain = MANUAL_AGC_GAIN; + + if( mAGCEnabled.get() ) + { + double currentMagnitude = Math.log10( Math.abs( currentSample ) + + MIN_CONSTANT ) - Math.log10( MAX_AMPLITUDE ); + + double delayedMagnitude = mMagnitudeBuffer.get( currentMagnitude ); + + if( currentMagnitude > mPeakMagnitude ) + { + /* Use current magnitude as peak if it's larger */ + mPeakMagnitude = currentMagnitude; + } + else if( delayedMagnitude == mPeakMagnitude ) + { + /* If delayed magnitude is the current peak, then find a new peak */ + mPeakMagnitude = mMagnitudeBuffer.max(); + } + + /* Exponential decay mode */ + if( mPeakMagnitude > mAttackAverage ) + { + mAttackAverage = ( ( 1.0 - ATTACK_RISE_ALPHA ) * mAttackAverage ) + + ( ATTACK_RISE_ALPHA * mPeakMagnitude ); + } + else + { + mAttackAverage = ( ( 1.0 - ATTACK_FALL_ALPHA ) * mAttackAverage ) + + ( ATTACK_FALL_ALPHA * mPeakMagnitude ); + } + + if( mPeakMagnitude > mDecayAverage ) + { + mDecayAverage = ( ( 1.0 - DECAY_RISE_ALPHA ) * mDecayAverage ) + + ( DECAY_RISE_ALPHA * mPeakMagnitude ); + } + else + { + mDecayAverage = ( ( 1.0 - DECAY_FALL_ALPHA ) * mDecayAverage ) + + ( DECAY_RISE_ALPHA * mPeakMagnitude ); + } + + double magnitude = ( mAttackAverage > mDecayAverage ) ? + mAttackAverage : mDecayAverage; + + if( magnitude < KNEE ) + { + gain = FIXED_GAIN; + } + else + { + gain = AGC_OUT_SCALE * Math.pow( 10.0, + magnitude * ( GAIN_SLOPE - 1.0 ) ); + } + + } + + delayedSample = (float)( delayedSample * gain ); + + if( mListener != null ) + { + mListener.receive( delayedSample ); + } + } + + /** + * Enables or disables Automatic Gain Control (AGC). + */ + public void setAGCEnabled( boolean enabled ) + { + mAGCEnabled.set( enabled ); + } + + /** + * Indicates if AGC is enabled + * @return true=AGC, false=MANUAL GAIN + */ + public boolean isAGCEnabled() + { + return mAGCEnabled.get(); + } + + @Override + public void setListener( RealSampleListener listener ) + { + mListener = listener; + } + + @Override + public void removeListener( RealSampleListener listener ) + { + mListener = null; + } +} diff --git a/src/dsp/am/AMDemodulator.java b/src/dsp/am/AMDemodulator.java index 8be4d8c6..2978f494 100644 --- a/src/dsp/am/AMDemodulator.java +++ b/src/dsp/am/AMDemodulator.java @@ -2,62 +2,44 @@ package dsp.am; import sample.Listener; import sample.complex.ComplexSample; -import sample.simplex.SimplexSampleBroadcaster; -import sample.simplex.SimplexSampleListener; -import dsp.filter.ComplexFIRFilter; -import dsp.filter.FilterFactory; -import dsp.filter.Window.WindowType; +import sample.real.RealSampleListener; +import sample.real.RealSampleProvider; -public class AMDemodulator implements Listener +public class AMDemodulator implements Listener, RealSampleProvider { - private SimplexSampleBroadcaster mBroadcaster = new SimplexSampleBroadcaster(); - private ComplexFIRFilter mIQFilter; + private RealSampleListener mListener; public AMDemodulator() { - this( FilterFactory.getLowPass( 48000, 3000, 73, WindowType.HAMMING ), - 1000.0 ); - } - - public AMDemodulator( double[] iqFilter, double iqGain ) - { - mIQFilter = new ComplexFIRFilter( iqFilter, iqGain ); - - mIQFilter.setListener( new Demodulator() ); } @Override public void receive( ComplexSample sample ) { - mIQFilter.receive( sample ); + float demodulated = (float)Math.sqrt( ( sample.real() * + sample.real() ) + + ( sample.imaginery() * + sample.imaginery() ) ); + + if( mListener != null ) + { + mListener.receive( demodulated * 500.0f ); + } } /** * Adds a listener to receive demodulated samples */ - public void addListener( SimplexSampleListener listener ) + public void setListener( RealSampleListener listener ) { - mBroadcaster.addListener( listener ); + mListener = listener; } /** * Removes a listener from receiving demodulated samples */ - public void removeListener( SimplexSampleListener listener ) + public void removeListener( RealSampleListener listener ) { - mBroadcaster.removeListener( listener ); - } - - public class Demodulator implements Listener - { - @Override - public void receive( ComplexSample sample ) - { - float demodulated = (float)Math.abs( - Math.sqrt( ( sample.real() * sample.real() ) + - ( sample.imaginery() * sample.imaginery() ) ) ); - - mBroadcaster.broadcast( demodulated * 30.0f ); - } + mListener = null; } }