/*******************************************************************************
* 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 buffer.BooleanAveragingBuffer;
import dsp.symbol.SymbolEvent;
import dsp.symbol.SymbolEvent.Shift;
import instrument.Instrumentable;
import instrument.tap.Tap;
import instrument.tap.TapGroup;
import instrument.tap.stream.SymbolEventTap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.Listener;
import sample.real.RealBuffer;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.List;
/**
* 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>
{
private final static Logger mLog = LoggerFactory.getLogger(FSK2Decoder.class);
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);
}
}