/******************************************************************************* * sdr-trunk * Copyright (C) 2014-2018 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 io.github.dsheirer.dsp.afsk; import io.github.dsheirer.bits.IBinarySymbolProcessor; import io.github.dsheirer.buffer.FloatAveragingBuffer; import io.github.dsheirer.dsp.filter.design.FilterDesignException; import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification; import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter2; import io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesigner; import io.github.dsheirer.dsp.filter.resample.RealResampler; import io.github.dsheirer.dsp.mixer.IOscillator; import io.github.dsheirer.dsp.mixer.Oscillator; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.buffer.ReusableBuffer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Audio Frequency Shift Keying (AFSK) 1200-baud correlation decoder for decoding 8 kHz FM-demodulated audio samples. * * Note: internally the incoming 8 kHz sample rate is resampled to 7200 Hz to be an integral of the 1200 Hz baud rate * which equates to six samples per symbol. Each symbol is correlated over an eight sample period with correlation * values averaged over a seven correlation value period. * * Provides normal or inverted decoded output. */ public class AFSK1200Decoder implements Listener { private final static Logger mLog = LoggerFactory.getLogger(AFSK1200Decoder.class); public enum Output { NORMAL, INVERTED } public static final double SAMPLE_RATE = 7200.0d; public static final int SAMPLES_PER_SYMBOL = 6; //SAMPLE_RATE / 1200 baud; //Correlation period of 8 samples works well and should align nicely with SIMD intrinsic vector sizes public static final int CORRELATION_PERIOD = SAMPLES_PER_SYMBOL + 2; public static final int AVERAGING_PERIOD = SAMPLES_PER_SYMBOL + 1; public static final double MARK = 1200.0; public static final double SPACE = 1800.0; public static final float TIMING_ERROR_GAIN = 1.0f / 3.0f; //Timing error adjustments over 3 symbol periods private static float[] sBandPassFilterCoefficients; static { FIRFilterSpecification specification = FIRFilterSpecification.bandPassBuilder() .sampleRate(8000) .stopFrequency1(1000) .passFrequencyBegin(1100) .passFrequencyEnd(1900) .stopFrequency2(2000) .stopRipple(0.000001) .passRipple(0.00001) .build(); try { RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification); if(designer.isValid()) { sBandPassFilterCoefficients = designer.getImpulseResponse(); } } catch(FilterDesignException fde) { mLog.error("Filter design error", fde); } } private Correlator mCorrelator1200 = new Correlator(SAMPLE_RATE, MARK, AVERAGING_PERIOD, CORRELATION_PERIOD); private Correlator mCorrelator1800 = new Correlator(SAMPLE_RATE, SPACE, AVERAGING_PERIOD, CORRELATION_PERIOD); private float[] mCorrelationValues1200; private float[] mCorrelationValues1800; protected boolean mNormalOutput; protected float mSymbolTimingGain = TIMING_ERROR_GAIN; protected AFSKSampleBuffer mSampleBuffer; protected AFSKTimingErrorDetector mTimingErrorDetector = new AFSKTimingErrorDetector(SAMPLES_PER_SYMBOL); private RealFIRFilter2 mBandPassFilter = new RealFIRFilter2(sBandPassFilterCoefficients); protected IBinarySymbolProcessor mBinarySymbolProcessor; private boolean mSampleDecision; //Resample to an integral of the baud rate 1200 baud * 6 samples per symbol = 7200.0 Hertz private RealResampler mResampler = new RealResampler(8000.0, SAMPLE_RATE, 2000, 1); /** * Constructs a decoder using the provided arguments. * * @param sampleBuffer for storing incoming samples and calculating symbols * @param detector for timing error alignment * @param output NORMAL: 1200Hz = Mark(1) and 1800Hz = Space(0), or INVERTED (vice-versa) */ public AFSK1200Decoder(AFSKSampleBuffer sampleBuffer, AFSKTimingErrorDetector detector, Output output) { mTimingErrorDetector = detector; mSampleBuffer = sampleBuffer; mSampleBuffer.setTimingGain(mSymbolTimingGain); mResampler.setListener(new Decoder()); mNormalOutput = (output == Output.NORMAL); } /** * Constructs a decoder using the provided arguments. * * @param output NORMAL: 1200Hz = Mark(1) and 1800Hz = Space(0), or INVERTED (vice-versa) */ public AFSK1200Decoder(Output output) { this(new AFSKSampleBuffer(SAMPLES_PER_SYMBOL, TIMING_ERROR_GAIN), new AFSKTimingErrorDetector(SAMPLES_PER_SYMBOL), output); } /** * Disposes of all references to prepare for garbage collection */ public void dispose() { } /** * Processes the buffer samples by converting all samples to boolean values reflecting if the sample value is * greater than zero (or not). Average symbol timing offset is calculated for the full buffer and the offset is * adjusted and then each of the symbols are decoded using a simple majority decision. * * @param buffer containing 8.0 kHz unfiltered FM demodulated audio samples with sub-audible LTR signalling. */ @Override public void receive(ReusableBuffer buffer) { //TODO: remove the passband filter ... the correlators should be able to deal with the noise and the filters may be //TODO: introducing noise anyway // ReusableBuffer bandPassFiltered = mBandPassFilter.filter(buffer); // mResampler.resample(bandPassFiltered); mResampler.resample(buffer); } protected void dispatch(boolean symbol) { if(mBinarySymbolProcessor != null) { mBinarySymbolProcessor.receive(mNormalOutput ? symbol : !symbol); } } /** * Registers a listener to receive decoded LTR symbols. * * @param binarySymbolProcessor to receive symbols. */ public void setSymbolProcessor(IBinarySymbolProcessor binarySymbolProcessor) { mBinarySymbolProcessor = binarySymbolProcessor; } /** * Removes the symbol listener from receiving decoded LTR symbols. */ public void removeListener() { mBinarySymbolProcessor = null; } public class Decoder implements Listener { @Override public void receive(ReusableBuffer buffer) { //Calculate correlation values against each 1200/1800 reference signal mCorrelationValues1200 = mCorrelator1200.process(buffer); mCorrelationValues1800 = mCorrelator1800.process(buffer); buffer.decrementUserCount(); for(int x = 0; x < mCorrelationValues1200.length; x++) { //1200 = Mark (1) and 1800 = Space (0) mSampleDecision = mCorrelationValues1200[x] > mCorrelationValues1800[x]; mSampleBuffer.receive(mSampleDecision); mTimingErrorDetector.receive(mSampleDecision); if(mSampleBuffer.hasSymbol()) { dispatch(mSampleBuffer.getSymbol()); mSampleBuffer.resetAndAdjust(mTimingErrorDetector.getError()); } } } } /** * Generates a correlation value for each FM demodulated sample against samples generated for an established * reference frequency, correlated and averaged over separate periods relative to the samples per symbol period. * * The sample rate of the correlator should be an integral value of the target frequency for optimal performance, * This correlator only works across integral sample periods and does not provide intra-sample interpolation. * * Correlation period (ie number of samples) should be relative to the samples per symbol. You may have to * experiment with various values of (samplesPerSymbol +/- 1, 2 or 3) to obtain the optimal decoder performance. * * Averaging period defines the number of correlation values to average before producing the final correlation * value for each sample. */ public class Correlator { private FloatAveragingBuffer mAveragingBuffer; private float[] mReferenceSamples; private float[] mDemodulatedSamples; private float[] mCorrelationValues; private float mCorrelationAccumulator; /** * Constructs a correlator instance, * * @param sampleRate of the incoming sample stream. Note: this should be an integral of the symbol rate. * @param frequency of the mark or space symbol to test for correlation * @param averagingPeriod is the number of correlation values to average each period * @param correlationPeriod is the number of samples to correlate each period */ public Correlator(double sampleRate, double frequency, int averagingPeriod, int correlationPeriod) { mAveragingBuffer = new FloatAveragingBuffer(averagingPeriod); IOscillator referenceSignalGenerator = new Oscillator(frequency, sampleRate); mReferenceSamples = referenceSignalGenerator.generateReal(correlationPeriod); mDemodulatedSamples = new float[correlationPeriod]; } /** * Processes each sample in the incoming sample buffer against a generated reference sample set for one symbol * period to derive a correlation value that is in-turn averaged over one symbol period. * * @param reusableBuffer containing FM demodulated samples * @return a reusable array of correlation values for each sample */ public float[] process(ReusableBuffer reusableBuffer) { if(mCorrelationValues == null || mCorrelationValues.length != reusableBuffer.getSampleCount()) { mCorrelationValues = new float[reusableBuffer.getSampleCount()]; } float[] samples = reusableBuffer.getSamples(); int y; for(int x = 0; x < samples.length; x++) { //Note: we're using array copy and dot product structures that the JRE can promote to SIMD intrinsics //when the host processor supports SIMD instructions System.arraycopy(mDemodulatedSamples, 1, mDemodulatedSamples, 0, mDemodulatedSamples.length - 1); mDemodulatedSamples[mDemodulatedSamples.length - 1] = samples[x]; mCorrelationAccumulator = 0.0f; for(y = 0; y < mDemodulatedSamples.length; y++) { mCorrelationAccumulator += mDemodulatedSamples[y] * mReferenceSamples[y]; } //Add the absolute value of correlation accumulator value to the averaging buffer and store the current //average as the correlation value for this sample. We use absolute value because we don't care if the //signal is out of phase with the reference samples mCorrelationValues[x] = mAveragingBuffer.get(Math.abs(mCorrelationAccumulator)); } return mCorrelationValues; } } }