mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 08:37:25 -05:00
297 lines
7.6 KiB
Java
297 lines
7.6 KiB
Java
/*******************************************************************************
|
|
* 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.TapGroup;
|
|
import instrument.tap.stream.SymbolEventTap;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.BitSet;
|
|
import java.util.List;
|
|
|
|
import sample.Listener;
|
|
import sample.real.RealBuffer;
|
|
import buffer.BooleanAveragingBuffer;
|
|
import dsp.symbol.SymbolEvent;
|
|
import dsp.symbol.SymbolEvent.Shift;
|
|
|
|
/**
|
|
* Binary Frequency Shift Keying (FSK) decoder. Implements a BFSK correlation
|
|
* decoder. Provides normal or inverted decoded output. Requires the symbol
|
|
* rate to be an integer multiple of the sample rate.
|
|
*
|
|
* Implements a correlation decoder that correlates a modulated FSK signal upon
|
|
* against a one-baud delayed version of itself, converting all float samples to
|
|
* binary (0 or 1), and uses XOR to produce the correlation value. Low pass
|
|
* filtering smoothes the correlated output.
|
|
*
|
|
* Automatically aligns to symbol timing during symbol transitions (0 to 1, or
|
|
* 1 to 0) by inspecting the samples at the symbol edges and advancing or
|
|
* retarding symbol window to maintain continuous symbol alignment.
|
|
*
|
|
* Use a DC-removal filter prior to this decoder to ensure samples don't have
|
|
* a DC component.
|
|
*
|
|
* Implements instrumentable interface, so that slice events can be received
|
|
* externally to analyze decoder performance.
|
|
*/
|
|
public class FSK2Decoder implements Instrumentable, Listener<RealBuffer>
|
|
{
|
|
public enum Output{ NORMAL, INVERTED };
|
|
|
|
/* Instrumentation taps */
|
|
private static final String INSTRUMENT_DECISION =
|
|
"Tap Point: FSK2 Symbol Decision";
|
|
private List<TapGroup> mAvailableTaps;
|
|
private ArrayList<SymbolEventTap> mTaps = new ArrayList<SymbolEventTap>();
|
|
|
|
private Listener<Boolean> mListener;
|
|
private BooleanAveragingBuffer mDelayBuffer;
|
|
private BooleanAveragingBuffer mLowPassFilter;
|
|
private Slicer mSlicer;
|
|
private boolean mNormalOutput;
|
|
private int mSamplesPerSymbol;
|
|
private int mSymbolRate;
|
|
|
|
public FSK2Decoder( int sampleRate, int symbolRate, Output output )
|
|
{
|
|
/* Ensure we're using an integral symbolRate of the sampleRate */
|
|
assert( sampleRate % symbolRate == 0 );
|
|
|
|
mSamplesPerSymbol = (int)( sampleRate / symbolRate );
|
|
mNormalOutput = ( output == Output.NORMAL );
|
|
mSymbolRate = symbolRate;
|
|
|
|
mDelayBuffer = new BooleanAveragingBuffer( mSamplesPerSymbol );
|
|
mLowPassFilter = new BooleanAveragingBuffer( mSamplesPerSymbol );
|
|
mSlicer = new Slicer( mSamplesPerSymbol );
|
|
}
|
|
|
|
/**
|
|
* Disposes of all references to prepare for garbage collection
|
|
*/
|
|
public void dispose()
|
|
{
|
|
mListener = null;
|
|
}
|
|
|
|
/**
|
|
* Primary sample input
|
|
*/
|
|
@Override
|
|
public void receive( RealBuffer buffer )
|
|
{
|
|
for( float sample: buffer.getSamples() )
|
|
{
|
|
/* Square the sample. Greater than zero is a 1 (true) and less than
|
|
* zero is a 0 (false) */
|
|
boolean bitSample = ( sample >= 0.0f );
|
|
|
|
/* Feed the delay buffer and fetch the one-baud delayed sample */
|
|
boolean delayedBitSample = mDelayBuffer.get( bitSample );
|
|
|
|
/* Correlation: xor current bit with delayed bit */
|
|
boolean softBit = bitSample ^ delayedBitSample;
|
|
|
|
/* Low pass filter to smooth the correlated values */
|
|
boolean filteredSoftBit = mLowPassFilter.getAverage( softBit );
|
|
|
|
/* Send the filtered correlated bit to the slicer */
|
|
mSlicer.receive( filteredSoftBit );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Registers a listener to receive the decoded FSK bits
|
|
*/
|
|
public void setListener( Listener<Boolean> listener )
|
|
{
|
|
mListener = listener;
|
|
}
|
|
|
|
/**
|
|
* Removes the listener
|
|
*/
|
|
public void removeListener( Listener<Boolean> listener )
|
|
{
|
|
mListener = null;
|
|
}
|
|
|
|
/**
|
|
* Symbol slicer with auto-aligning baud timing
|
|
*/
|
|
public class Slicer
|
|
{
|
|
private BitSet mBitSet = new BitSet();
|
|
private int mSymbolLength;
|
|
private int mDecisionThreshold;
|
|
private int mSampleCounter;
|
|
|
|
public Slicer( int samplesPerSymbol )
|
|
{
|
|
mSymbolLength = samplesPerSymbol;
|
|
|
|
mDecisionThreshold = (int)( mSymbolLength / 2 );
|
|
|
|
/* Adjust for an odd number of samples per baud */
|
|
if( mSymbolLength % 2 == 1 )
|
|
{
|
|
mDecisionThreshold++;
|
|
}
|
|
}
|
|
|
|
public void receive( boolean softBit )
|
|
{
|
|
if( mSampleCounter >= 0 )
|
|
{
|
|
if( softBit )
|
|
{
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Sends instrumentation tap event to all registered listeners
|
|
*/
|
|
private void sendTapEvent( BitSet bitset, Shift shift, boolean decision )
|
|
{
|
|
for( SymbolEventTap tap: mTaps )
|
|
{
|
|
SymbolEvent event =
|
|
new SymbolEvent( bitset.get( 0, mSymbolLength ),
|
|
mSymbolLength,
|
|
decision,
|
|
shift );
|
|
|
|
tap.receive( event );
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get instrumentation taps
|
|
*/
|
|
@Override
|
|
public List<TapGroup> getTapGroups()
|
|
{
|
|
if( mAvailableTaps == null )
|
|
{
|
|
mAvailableTaps = new ArrayList<TapGroup>();
|
|
|
|
TapGroup tapGroup = new TapGroup( "FSK2 Decoder" );
|
|
tapGroup.add( new SymbolEventTap( INSTRUMENT_DECISION, 0, .025f ) );
|
|
|
|
mAvailableTaps.add( tapGroup );
|
|
}
|
|
|
|
return mAvailableTaps;
|
|
}
|
|
|
|
/**
|
|
* Add instrumentation tap
|
|
*/
|
|
@Override
|
|
public void registerTap( Tap tap )
|
|
{
|
|
if( tap instanceof SymbolEventTap && !mTaps.contains( tap ) )
|
|
{
|
|
mTaps.add( (SymbolEventTap)tap );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove instrumentation tap
|
|
*/
|
|
@Override
|
|
public void unregisterTap( Tap tap )
|
|
{
|
|
mTaps.remove( tap );
|
|
}
|
|
}
|