/*******************************************************************************
* 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
******************************************************************************/
package dsp.fsk;
import instrument.Instrumentable;
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 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
{
public enum Output{ NORMAL, INVERTED };
/* Instrumentation taps */
private static final String INSTRUMENT_DECISION =
"Tap Point: FSK2 Symbol Decision";
private ArrayList mAvailableTaps;
private ArrayList mTaps = new ArrayList();
private Listener 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 listener )
{
mListener = listener;
}
/**
* Removes the listener
*/
public void removeListener( Listener 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 getTaps()
{
if( mAvailableTaps == null )
{
mAvailableTaps = new ArrayList();
mAvailableTaps.add(
new SymbolEventTap( INSTRUMENT_DECISION, 0, .025f ) );
}
return mAvailableTaps;
}
/**
* Add instrumentation tap
*/
@Override
public void addTap( Tap tap )
{
if( tap instanceof SymbolEventTap && !mTaps.contains( tap ) )
{
mTaps.add( (SymbolEventTap)tap );
}
}
/**
* Remove instrumentation tap
*/
@Override
public void removeTap( Tap tap )
{
mTaps.remove( tap );
}
}