/******************************************************************************* * 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 ); } }