mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-15 01:57:39 -05:00
AM Decoder final commit. Includes automatic gain control on baseband input and audio output.
This commit is contained in:
@@ -1,10 +1,13 @@
|
|||||||
package decode.am;
|
package decode.am;
|
||||||
|
|
||||||
import sample.simplex.SimplexSampleListener;
|
import sample.real.RealSampleListener;
|
||||||
import source.Source.SampleType;
|
import source.Source.SampleType;
|
||||||
import decode.Decoder;
|
import decode.Decoder;
|
||||||
import decode.DecoderType;
|
import decode.DecoderType;
|
||||||
|
import dsp.agc.ComplexAutomaticGainControl;
|
||||||
|
import dsp.agc.RealAutomaticGainControl;
|
||||||
import dsp.am.AMDemodulator;
|
import dsp.am.AMDemodulator;
|
||||||
|
import dsp.filter.ComplexFIRFilter;
|
||||||
import dsp.filter.DCRemovalFilter2;
|
import dsp.filter.DCRemovalFilter2;
|
||||||
import dsp.filter.FilterFactory;
|
import dsp.filter.FilterFactory;
|
||||||
import dsp.filter.FloatFIRFilter;
|
import dsp.filter.FloatFIRFilter;
|
||||||
@@ -18,8 +21,14 @@ public class AMDecoder extends Decoder
|
|||||||
*/
|
*/
|
||||||
private static final double DC_REMOVAL_RATIO = 0.003;
|
private static final double DC_REMOVAL_RATIO = 0.003;
|
||||||
|
|
||||||
private AMDemodulator mDemodulator;
|
private ComplexFIRFilter mIQFilter;
|
||||||
private DCRemovalFilter2 mDCRemovalFilter;
|
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;
|
private FloatFIRFilter mLowPassFilter;
|
||||||
|
|
||||||
public AMDecoder( SampleType sampleType )
|
public AMDecoder( SampleType sampleType )
|
||||||
@@ -33,6 +42,9 @@ public class AMDecoder extends Decoder
|
|||||||
*/
|
*/
|
||||||
if( mSourceSampleType == SampleType.COMPLEX )
|
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
|
* The Decoder super class is both a Complex listener and a float
|
||||||
* listener. So, we add the demod to listen to the incoming
|
* 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
|
* back to this class, so we can receive the demodulated output
|
||||||
* to process
|
* 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
|
* Remove the DC component that is present when we're mistuned
|
||||||
*/
|
*/
|
||||||
mDCRemovalFilter = new DCRemovalFilter2( DC_REMOVAL_RATIO );
|
mDemodulator.setListener( mDCRemovalFilter );
|
||||||
mDemodulator.addListener( mDCRemovalFilter );
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Route the demodulated, filtered samples back to this class to send
|
* Route the demodulated, filtered samples back to this class to send
|
||||||
@@ -56,12 +68,12 @@ public class AMDecoder extends Decoder
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
mLowPassFilter = new FloatFIRFilter(
|
mLowPassFilter = new FloatFIRFilter(
|
||||||
FilterFactory.getLowPass( 48000, 3000, 31,
|
FilterFactory.getLowPass( 48000, 3000, 31, WindowType.COSINE ), 1.0 );
|
||||||
WindowType.COSINE ), 100.0 );
|
|
||||||
|
|
||||||
mDCRemovalFilter.setListener( mLowPassFilter );
|
mDCRemovalFilter.setListener( mLowPassFilter );
|
||||||
|
|
||||||
mLowPassFilter.setListener( this.getSimplexReceiver() );
|
mLowPassFilter.setListener( mAudioAGC );
|
||||||
|
mAudioAGC.setListener( this.getRealReceiver() );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,8 +84,9 @@ public class AMDecoder extends Decoder
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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" );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
197
src/dsp/agc/RealAutomaticGainControl.java
Normal file
197
src/dsp/agc/RealAutomaticGainControl.java
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,62 +2,44 @@ package dsp.am;
|
|||||||
|
|
||||||
import sample.Listener;
|
import sample.Listener;
|
||||||
import sample.complex.ComplexSample;
|
import sample.complex.ComplexSample;
|
||||||
import sample.simplex.SimplexSampleBroadcaster;
|
import sample.real.RealSampleListener;
|
||||||
import sample.simplex.SimplexSampleListener;
|
import sample.real.RealSampleProvider;
|
||||||
import dsp.filter.ComplexFIRFilter;
|
|
||||||
import dsp.filter.FilterFactory;
|
|
||||||
import dsp.filter.Window.WindowType;
|
|
||||||
|
|
||||||
public class AMDemodulator implements Listener<ComplexSample>
|
public class AMDemodulator implements Listener<ComplexSample>, RealSampleProvider
|
||||||
{
|
{
|
||||||
private SimplexSampleBroadcaster mBroadcaster = new SimplexSampleBroadcaster();
|
private RealSampleListener mListener;
|
||||||
private ComplexFIRFilter mIQFilter;
|
|
||||||
|
|
||||||
public AMDemodulator()
|
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
|
@Override
|
||||||
public void receive( ComplexSample sample )
|
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
|
* 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
|
* Removes a listener from receiving demodulated samples
|
||||||
*/
|
*/
|
||||||
public void removeListener( SimplexSampleListener listener )
|
public void removeListener( RealSampleListener listener )
|
||||||
{
|
{
|
||||||
mBroadcaster.removeListener( listener );
|
mListener = null;
|
||||||
}
|
|
||||||
|
|
||||||
public class Demodulator implements Listener<ComplexSample>
|
|
||||||
{
|
|
||||||
@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 );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user