Improved LTR FSK decoder for LTR, LTR-Net and Passport decoders. Added IIR DC blocking filter and used improved slicer from the FSK decoder.

Added instrumentation to Passport decoder and updated some of the instrumentation components.
This commit is contained in:
dennis.sheirer
2014-05-01 23:24:51 +00:00
parent d7115f2886
commit da1db8a804
25 changed files with 803 additions and 496 deletions

View File

@@ -1,137 +0,0 @@
/*******************************************************************************
* 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 dsp;
import sample.Listener;
import buffer.FloatCircularAveragingBuffer2;
/**
* Binary slicer tuned to operate on LTR type short burst signals in a heavy
* DC bias environment coupled with transmitter automatic frequency correction
* being applied as the transmission progresses.
*
* Uses a pair of float buffers that calculate the average of the samples
* contained in the buffer.
*
* The leading and trailing buffers are the same length as the sync pattern.
*
* The slicer uses the Leading buffer's average as the slicing reference for
* normal operation.
*
* Once a sync pattern is detected, the trailing buffer should have the sync
* pattern bauds and the leading buffer should have the first of the message
* bits. At this point, ie sync detect, we switch to using the average of both
* buffers as the slicing reference point.
*
* Once the reference counter counts down to indicate that there are sync length
* bauds remaining of the message, and those remaining bauds should be in the
* leading buffer, we swich to using the trailing buffer as the slicing
* reference point for the remainder of the message.
*
* As the reference counter decrements to 0, meaning the message should be
* clear of both buffers, we switch back to normal operation of using the
* leading buffer as the slicing reference.
*
* An external sync detector indicates a sync pattern match by using the
* syncDetected() method, which immediately switches the slicing average
* reference to both, and sets the counter to the expected message length.
*/
public class FloatToBinarySlicerLTR implements Listener<Float>,
SyncDetectListener
{
private enum SlicingReference
{
LEADING, TRAILING, BOTH;
}
private SlicingReference mSlicingReference = SlicingReference.LEADING;
private FloatCircularAveragingBuffer2 mLeadingBuffer;
private FloatCircularAveragingBuffer2 mTrailingBuffer;
private int mReferenceCounter = 0;
private int mSyncLength;
private int mMessageLength;
private Listener<Boolean> mListener;
public FloatToBinarySlicerLTR( int syncLength,
int messageLength,
int samplesPerBaud )
{
mSyncLength = syncLength * samplesPerBaud;
mMessageLength = messageLength * samplesPerBaud;
mLeadingBuffer = new FloatCircularAveragingBuffer2( mSyncLength );
mTrailingBuffer = new FloatCircularAveragingBuffer2( mSyncLength );
}
@Override
public void receive( Float newestSample )
{
float middleSample = mLeadingBuffer.get( newestSample );
mTrailingBuffer.get( middleSample );
//Dispatch the decision
if( mListener != null )
{
switch( mSlicingReference )
{
case LEADING:
mListener.receive( middleSample > mLeadingBuffer.average() );
break;
case BOTH:
mListener.receive( middleSample >
( ( mLeadingBuffer.average() +
mTrailingBuffer.average() ) / 2.0f ) );
break;
case TRAILING:
mListener.receive( middleSample > mTrailingBuffer.average() );
break;
}
mReferenceCounter--;
if( mReferenceCounter <= mSyncLength && mReferenceCounter > 0 )
{
mSlicingReference = SlicingReference.TRAILING;
}
else if( mReferenceCounter <= 0 )
{
mSlicingReference = SlicingReference.LEADING;
mReferenceCounter = 0;
}
}
}
@Override
public void syncDetected()
{
mSlicingReference = SlicingReference.BOTH;
mReferenceCounter = mMessageLength;
}
public void setListener( Listener<Boolean> listener )
{
mListener = listener;
}
public void clearListener()
{
mListener = null;
}
}

155
src/dsp/Slicer.java Normal file
View File

@@ -0,0 +1,155 @@
package dsp;
import instrument.tap.Tap;
import instrument.tap.stream.SymbolEventTap;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.List;
import sample.Listener;
import dsp.fsk.SymbolEvent;
import dsp.fsk.SymbolEvent.Shift;
/**
* Symbol slicer with auto-aligning baud timing
*/
public class Slicer implements Listener<Boolean>
{
public enum Output{ NORMAL, INVERTED };
private BitSet mBitSet = new BitSet();
private int mSymbolLength;
private int mDecisionThreshold;
private int mSampleCounter;
private SymbolEventTap mSymbolEventTap;
private boolean mNormalOutput = true;
private Listener<Boolean> mListener;
public Slicer( Output output, int samplesPerSymbol )
{
mNormalOutput = ( output == Output.NORMAL );
mSymbolLength = samplesPerSymbol;
/* Round up */
mDecisionThreshold = (int)( mSymbolLength / 2 ) + ( mSymbolLength % 2 );
}
public void receive( Boolean sample )
{
if( mSampleCounter >= 0 )
{
if( sample )
{
mBitSet.set( mSampleCounter );
}
else
{
mBitSet.clear( mSampleCounter );
}
}
mSampleCounter++;
if( mSampleCounter >= mSymbolLength )
{
boolean decision = mBitSet.cardinality() >= mDecisionThreshold;
send( decision );
/* Shift timing left if the left bit in the bitset is opposite
* the decision and the right bit is the same */
if( ( mBitSet.get( 0 ) ^ decision ) &&
( !( mBitSet.get( mSymbolLength - 1 ) ^ decision ) ) )
{
sendTapEvent( mBitSet, Shift.LEFT, decision );
reset();
mSampleCounter--;
}
/* Shift timing right if the left bit is the same as the
* decision and the right bit is opposite */
else if( ( !( mBitSet.get( 0 ) ^ decision ) ) &&
( mBitSet.get( mSymbolLength - 1 ) ^ decision ) )
{
sendTapEvent( mBitSet, Shift.RIGHT, decision );
/* Last bit from previous symbol to pre-fill next symbol */
boolean previousSoftBit = mBitSet.get( mSymbolLength - 1 );
reset();
if( previousSoftBit )
{
mBitSet.set( 0 );
}
mSampleCounter++;
}
/* No shift */
else
{
sendTapEvent( mBitSet, Shift.NONE, decision );
reset();
}
}
}
/**
* Sends the bit decision to the listener
*/
private void send( boolean decision )
{
if( mListener != null )
{
mListener.receive( mNormalOutput ? decision : !decision );
}
}
private void reset()
{
mBitSet.clear();
mSampleCounter = 0;
}
public void setListener( Listener<Boolean> listener)
{
mListener = listener;
}
public void removeListener( Listener<Boolean> listener )
{
mListener = null;
}
/**
* Sends instrumentation tap event to all registered listeners
*/
private void sendTapEvent( BitSet bitset, Shift shift, boolean decision )
{
if( mSymbolEventTap != null )
{
SymbolEvent event =
new SymbolEvent( bitset.get( 0, mSymbolLength ),
mSymbolLength,
decision,
shift );
mSymbolEventTap.receive( event );
}
}
public void addTap( SymbolEventTap tap )
{
mSymbolEventTap = tap;
}
public void removeTap()
{
mSymbolEventTap = null;
}
}

View File

@@ -17,84 +17,50 @@
******************************************************************************/
package dsp.filter;
import java.util.Arrays;
import java.util.BitSet;
import sample.Listener;
/**
* Averages (low pass filter) boolean values over the defined length
* Averages (low pass filter) boolean values over the defined size
*/
public class BooleanAveragingFilter implements Listener<Boolean>
{
private boolean[] mBuffer;
private BitSet mBuffer = new BitSet();
private int mBufferPointer;
private int mBufferLength;
private int mThreshold;
private Listener<Boolean> mListener;
public BooleanAveragingFilter( int length )
{
mBuffer = new boolean[ length ];
if( length % 2 == 0 )
mBufferLength = length;
/* Round up */
mThreshold = (int)( length / 2) + ( length % 2 );
}
public void receive( Boolean value )
{
if( value )
{
mThreshold = (int)( length /2 );
mBuffer.set( mBufferPointer );
}
else
{
mThreshold = (int)( ( length + 1 ) / 2 );
mBuffer.clear( mBufferPointer );
}
//Preload the array with false values
Arrays.fill( mBuffer, false );
}
/**
* Loads the newValue into this buffer and adjusts the buffer pointer
* to prepare for the next get/put cycle
*/
public void put( boolean newValue )
{
//Store the new value to the buffer
mBuffer[ mBufferPointer ] = newValue;
//Increment the buffer pointer
mBufferPointer++;
//Wrap the buffer pointer around to 0 when necessary
if( mBufferPointer >= mBuffer.length )
mBufferPointer++;
if( mBufferPointer >= mBufferLength )
{
mBufferPointer = 0;
}
}
/**
* Loads the newValue into the buffer, calculates the average
* and returns that average from this method
*
* This effectively performs low-pass filtering
*/
public void receive( Boolean newValue )
{
//Load the new value into the buffer
put( newValue );
int trueCount = 0;
for( int x = 0; x < mBuffer.length; x++ )
{
if( mBuffer[ x ] )
{
trueCount++;
}
}
/**
* If the number of true values in the buffer is
* more than half, send true, otherwise, false
*/
if( mListener != null )
{
mListener.receive( trueCount >= mThreshold );
mListener.receive( mBuffer.cardinality() >= mThreshold );
}
}
@@ -103,7 +69,7 @@ public class BooleanAveragingFilter implements Listener<Boolean>
mListener = listener;
}
public void clearListener()
public void removeListener( Listener<Boolean> listener )
{
mListener = null;
}

View File

@@ -0,0 +1,45 @@
package dsp.filter;
import sample.Listener;
public class DCRemovalFilter3 implements Listener<Float>
{
private float mAlpha;
private float mPreviousInput = 0.0f;
private float mPreviousOutput = 0.0f;
private Listener<Float> mListener;
/**
* IIR single-pole DC removal filter, as described by J M de Freitas in
* 29Jan2007 paper at:
*
* http://www.mathworks.com/matlabcentral/fileexchange/downloads/72134/download
*
* @param alpha 0.0 - 1.0 float - the closer alpha is to unity, the closer
* the cutoff frequency is to DC.
*/
public DCRemovalFilter3( float alpha )
{
mAlpha = alpha;
}
@Override
public void receive( Float currentInput )
{
float currentOutput = ( currentInput - mPreviousInput ) +
( mAlpha * mPreviousOutput );
if( mListener != null )
{
mListener.receive( currentOutput );
}
mPreviousInput = currentInput;
mPreviousOutput = currentOutput;
}
public void setListener( Listener<Float> listener )
{
mListener = listener;
}
}

View File

@@ -0,0 +1,103 @@
package dsp.filter;
import java.util.BitSet;
import sample.Listener;
public class LTRPulseShapingFilter implements Listener<Boolean>
{
private int[][] mIndexes = { { 0, 1, 2, 3, 4 },
{ 1, 2, 3, 4, 0 },
{ 2, 3, 4, 0, 1 },
{ 3, 4, 0, 1, 2 },
{ 4, 0, 1, 2, 3 } };
private BitSet mBuffer;
private int mBufferPointer = 0;
private Listener<Boolean> mListener;
/**
* Specialized pulse shaping filter that detects a weak 0 or 1 pulse that
* is characterized by two consecutive values, true or false, within a
* sequence of five boolean values where the remainder are the opposite
* value, and elongates that weak pulse to be three consecutive values,
* which is enough to cause the slicer to render a correct value.
*/
public LTRPulseShapingFilter()
{
mBuffer = new BitSet();
mBuffer.clear();
}
@Override
public void receive( Boolean current )
{
boolean previous = mBuffer.get( mBufferPointer );
if( current )
{
mBuffer.set( mBufferPointer );
}
else
{
mBuffer.clear( mBufferPointer );
}
mBufferPointer++;
if( mBufferPointer >= 5 )
{
mBufferPointer = 0;
}
/* Check for weak positive pulse*/
if( mBuffer.cardinality() == 2 )
{
if( mBuffer.get( mIndexes[ mBufferPointer ][ 1 ] ) &&
mBuffer.get( mIndexes[ mBufferPointer ][ 2 ] ) )
{
mBuffer.set( mIndexes[ mBufferPointer][ 3 ] );
}
}
/* Or, a weak negative pulse */
else if( mBuffer.cardinality() == 3 )
{
if( !mBuffer.get( mIndexes[ mBufferPointer][ 1 ] ) &&
!mBuffer.get( mIndexes[ mBufferPointer][ 2 ] ) )
{
mBuffer.clear( mIndexes[ mBufferPointer][ 3 ] );
}
}
if( mListener != null )
{
mListener.receive( previous );
}
}
public String getIndexSet( int pointer )
{
StringBuilder sb = new StringBuilder();
sb.append( "[" );
for( int x = 0; x < 5; x++ )
{
sb.append( mIndexes[ pointer ][ x ] );
sb.append( "," );
}
sb.append( "[" );
return sb.toString();
}
public void setListener( Listener<Boolean> listener )
{
mListener = listener;
}
public void removeListener( Listener<Boolean> listener )
{
mListener = null;
}
}

View File

@@ -35,7 +35,7 @@ public class SquaringFilter implements Listener<Float>
{
if( mListener != null )
{
mListener.receive( ( sample >= 0 ? true : false ) );
mListener.receive( sample >= 0 );
}
}

View File

@@ -26,7 +26,7 @@ import java.util.BitSet;
import java.util.List;
import sample.Listener;
import buffer.AveragingCircularBooleanBuffer;
import buffer.BooleanAveragingBuffer;
import dsp.fsk.SymbolEvent.Shift;
/**
@@ -60,8 +60,8 @@ public class FSK2Decoder implements Instrumentable, Listener<Float>
private ArrayList<SymbolEventTap> mTaps = new ArrayList<SymbolEventTap>();
private Listener<Boolean> mListener;
private AveragingCircularBooleanBuffer mDelayBuffer;
private AveragingCircularBooleanBuffer mLowPassFilter;
private BooleanAveragingBuffer mDelayBuffer;
private BooleanAveragingBuffer mLowPassFilter;
private Slicer mSlicer;
private boolean mNormalOutput;
private int mSamplesPerSymbol;
@@ -76,8 +76,8 @@ public class FSK2Decoder implements Instrumentable, Listener<Float>
mNormalOutput = ( output == Output.NORMAL );
mSymbolRate = symbolRate;
mDelayBuffer = new AveragingCircularBooleanBuffer( mSamplesPerSymbol );
mLowPassFilter = new AveragingCircularBooleanBuffer( mSamplesPerSymbol );
mDelayBuffer = new BooleanAveragingBuffer( mSamplesPerSymbol );
mLowPassFilter = new BooleanAveragingBuffer( mSamplesPerSymbol );
mSlicer = new Slicer( mSamplesPerSymbol );
}

View File

@@ -0,0 +1,227 @@
/*******************************************************************************
* 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 dsp.fsk;
import instrument.Instrumentable;
import instrument.tap.Tap;
import instrument.tap.stream.BinaryTap;
import instrument.tap.stream.FloatTap;
import instrument.tap.stream.SymbolEventTap;
import java.util.ArrayList;
import java.util.List;
import sample.Listener;
import dsp.Slicer;
import dsp.Slicer.Output;
import dsp.filter.DCRemovalFilter3;
import dsp.filter.Filters;
import dsp.filter.FloatHalfBandFilter;
import dsp.filter.FloatHalfBandNoDecimateFilter;
import dsp.filter.LTRPulseShapingFilter;
import dsp.filter.SquaringFilter;
public class LTRFSKDecoder implements Listener<Float>, Instrumentable
{
private FloatHalfBandFilter mHBFilter1;
private FloatHalfBandFilter mHBFilter2;
private FloatHalfBandFilter mHBFilter3;
private FloatHalfBandFilter mHBFilter4;
private FloatHalfBandFilter mHBFilter5;
private FloatHalfBandNoDecimateFilter mHBFilter6;
private DCRemovalFilter3 mDCFilter;
private SquaringFilter mSquaringFilter;
private LTRPulseShapingFilter mPulseShaper;
private Slicer mSlicer;
private List<Tap> mAvailableTaps;
private final String TAP_F1_F2 = "LTR FSK Demod Filter1 >< Filter2";
private final String TAP_F2_F3 = "LTR FSK Demod Filter2 >< Filter3";
private final String TAP_F3_F4 = "LTR FSK Demod Filter3 >< Filter4";
private final String TAP_F4_F5 = "LTR FSK Demod Filter4 >< Filter5";
private final String TAP_F5_F6 = "LTR FSK Demod Filter5 >< Filter6";
private final String TAP_F6_DT = "LTR FSK Demod Filter6 >< DC Filter";
private final String TAP_DT_SF1 = "LTR FSK Demod DC Filter >< Squaring Filter";
private final String TAP_SF1_PS = "LTR FSK Demod Squaring Filter >< Pulse Shaper";
private final String TAP_PS_SLICER = "LTR FSK Demod Pulse Shaper >< Slicer";
private final String TAP_SYMBOL_EVENT = "LTR FSK Demod Symbol Event";
/**
* Implements a Logic Trunked Radio sub-audible 300 baud FSK signaling
* decoder. Expects a 48000 sample rate input.
*/
public LTRFSKDecoder()
{
mHBFilter1 = new FloatHalfBandFilter(
Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO, 1.1002 );
mHBFilter2 = new FloatHalfBandFilter(
Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO, 1.1002 );
mHBFilter1.setListener( mHBFilter2 );
mHBFilter3 = new FloatHalfBandFilter(
Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO, 1.1002 );
mHBFilter2.setListener( mHBFilter3 );
mHBFilter4 = new FloatHalfBandFilter(
Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO, 1.1002 );
mHBFilter3.setListener( mHBFilter4 );
mHBFilter5 = new FloatHalfBandFilter(
Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO, 1.1002 );
mHBFilter4.setListener( mHBFilter5 );
mHBFilter6 = new FloatHalfBandNoDecimateFilter(
Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO, 1.1002 );
mHBFilter5.setListener( mHBFilter6 );
mDCFilter = new DCRemovalFilter3( 0.9946f );
mHBFilter6.setListener( mDCFilter );
mSquaringFilter = new SquaringFilter();
mDCFilter.setListener( mSquaringFilter );
mPulseShaper = new LTRPulseShapingFilter();
mSquaringFilter.setListener( mPulseShaper );
mSlicer = new Slicer( Output.NORMAL, 5 );
mPulseShaper.setListener( mSlicer );
}
@Override
public void receive( Float sample )
{
mHBFilter1.receive( sample );
}
public void addListener( Listener<Boolean> listener )
{
mSlicer.setListener( listener );
}
public void removeListener( Listener<Boolean> listener )
{
mSlicer.removeListener( listener );
}
@Override
public List<Tap> getTaps()
{
if( mAvailableTaps == null )
{
mAvailableTaps = new ArrayList<Tap>();
mAvailableTaps.add( new FloatTap( TAP_F1_F2, 0, 1.0f ) );
mAvailableTaps.add( new FloatTap( TAP_F2_F3, 8, 0.5f ) );
mAvailableTaps.add( new FloatTap( TAP_F3_F4, 12, 0.25f ) );
mAvailableTaps.add( new FloatTap( TAP_F4_F5, 14, 0.125f ) );
mAvailableTaps.add( new FloatTap( TAP_F5_F6, 15, 0.0625f ) );
mAvailableTaps.add( new FloatTap( TAP_F6_DT, 31, 0.0625f ) );
mAvailableTaps.add( new FloatTap( TAP_DT_SF1, 31, 0.0625f ) );
mAvailableTaps.add( new BinaryTap( TAP_SF1_PS, 33, 0.0625f ) );
mAvailableTaps.add( new BinaryTap( TAP_PS_SLICER, 33, 0.0625f ) );
mAvailableTaps.add( new SymbolEventTap( TAP_SYMBOL_EVENT, 7, 0.0125f ) );
}
return mAvailableTaps;
}
@Override
public void addTap( Tap tap )
{
switch( tap.getName() )
{
case TAP_F1_F2:
mHBFilter1.setListener( (FloatTap)tap );
((FloatTap)tap).setListener( mHBFilter2 );
break;
case TAP_F2_F3:
mHBFilter2.setListener( (FloatTap)tap );
((FloatTap)tap).setListener( mHBFilter3 );
break;
case TAP_F3_F4:
mHBFilter3.setListener( (FloatTap)tap );
((FloatTap)tap).setListener( mHBFilter4 );
break;
case TAP_F4_F5:
mHBFilter4.setListener( (FloatTap)tap );
((FloatTap)tap).setListener( mHBFilter5 );
break;
case TAP_F5_F6:
mHBFilter5.setListener( (FloatTap)tap );
((FloatTap)tap).setListener( mHBFilter6 );
break;
case TAP_F6_DT:
mHBFilter6.setListener( (FloatTap)tap );
((FloatTap)tap).setListener( mDCFilter );
break;
case TAP_DT_SF1:
mDCFilter.setListener( (FloatTap)tap );
((FloatTap)tap).setListener( mSquaringFilter );
break;
case TAP_SF1_PS:
mSquaringFilter.setListener( (BinaryTap)tap );
((BinaryTap)tap).setListener( mPulseShaper );
break;
case TAP_PS_SLICER:
mPulseShaper.setListener( (BinaryTap)tap );
((BinaryTap)tap).setListener( mSlicer );
break;
case TAP_SYMBOL_EVENT:
mSlicer.addTap( (SymbolEventTap)tap );
break;
}
}
@Override
public void removeTap( Tap tap )
{
switch( tap.getName() )
{
case TAP_F1_F2:
mHBFilter1.setListener( mHBFilter2 );
break;
case TAP_F2_F3:
mHBFilter2.setListener( mHBFilter3 );
break;
case TAP_F3_F4:
mHBFilter3.setListener( mHBFilter4 );
break;
case TAP_F4_F5:
mHBFilter4.setListener( mHBFilter5 );
break;
case TAP_F5_F6:
mHBFilter5.setListener( mHBFilter6 );
break;
case TAP_F6_DT:
mHBFilter6.setListener( mDCFilter );
break;
case TAP_DT_SF1:
mDCFilter.setListener( mSquaringFilter );
break;
case TAP_SF1_PS:
mSquaringFilter.setListener( mPulseShaper );
break;
case TAP_PS_SLICER:
mPulseShaper.setListener( mSlicer );
break;
case TAP_SYMBOL_EVENT:
mSlicer.removeTap();
break;
}
}
}