diff --git a/src/main/java/io/github/dsheirer/buffer/DoubleCircularBuffer.java b/src/main/java/io/github/dsheirer/buffer/DoubleCircularBuffer.java index 0c9172fe..af5bd86d 100644 --- a/src/main/java/io/github/dsheirer/buffer/DoubleCircularBuffer.java +++ b/src/main/java/io/github/dsheirer/buffer/DoubleCircularBuffer.java @@ -1,80 +1,168 @@ /******************************************************************************* * 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 io.github.dsheirer.buffer; +import org.apache.commons.math3.stat.descriptive.moment.Mean; +import org.apache.commons.math3.stat.descriptive.moment.Variance; +import org.apache.commons.math3.stat.descriptive.rank.Max; +import org.apache.commons.math3.stat.descriptive.rank.Min; + import java.util.Arrays; /** - * Circular sample buffer - allocates a buffer and stores samples in a circular - * fashion, overwriting older samples with newly arrived samples. Initially - * fills buffer with 0-valued samples - * - * Can be used as a delay-type buffer, to delay samples by the 'size' amount + * Circular sample buffer - allocates a buffer and stores samples in a circular fashion, overwriting older samples with + * newly arrived samples. Initially fills buffer with 0-valued (default) samples or with a specified value. */ public class DoubleCircularBuffer { - double[] mBuffer; - int mBufferPointer = 0; - - public DoubleCircularBuffer( int size ) - { - mBuffer = new double[ size ]; - - Arrays.fill( mBuffer, 0.0 ); - } + private Variance mVariance; + private Max mMax; + private Mean mMean; + private Min mMin; + private double[] mBuffer; + private int mBufferPointer = 0; - /** - * Puts the new value into the buffer and returns the oldest buffer value - * that it replaced - * @param newValue - * @return - */ - public double get( double newValue ) - { - double oldestSample = mBuffer[ mBufferPointer ]; + /** + * Creates a circular double buffer of the specified size and all entries filled with the specified initial value. + * @param size of the buffer + * @param initialFillValue to fill the buffer entries + */ + public DoubleCircularBuffer(int size, double initialFillValue) + { + mBuffer = new double[size]; + Arrays.fill(mBuffer, initialFillValue); + } - mBuffer[ mBufferPointer ] = newValue; - - mBufferPointer++; - - if( mBufferPointer >= mBuffer.length ) - { - mBufferPointer = 0; - } - - return oldestSample; - } + /** + * Creates a circular double buffer of the specified size and all entries filled with an initial value of zero. + * @param size of the buffer + */ + public DoubleCircularBuffer(int size) + { + this(size, 0.0); + } - /** - * Returns the maximum from the values currently in the buffer - */ - public double max() - { - double max = 0.0d; - - for( int x = 0; x < mBuffer.length; x++ ) - { - if( mBuffer[ x ] > max ) - { - max = mBuffer[ x ]; - } - } - - return max; - } + /** + * Puts the new value into the buffer and returns the oldest buffer value that it replaced + * + * @param newValue to add to the buffer + * @return oldest value that was overwritten by the new value + */ + public double get(double newValue) + { + double oldestSample = mBuffer[mBufferPointer]; + + mBuffer[mBufferPointer] = newValue; + + mBufferPointer++; + + if(mBufferPointer >= mBuffer.length) + { + mBufferPointer = 0; + } + + return oldestSample; + } + + public double[] getAll() + { + int valuePointer = 0; + double[] values = new double[mBuffer.length]; + int bufferPointer = mBufferPointer; + + while(valuePointer < values.length) + { + values[valuePointer++] = mBuffer[bufferPointer++]; + + if(bufferPointer >= mBuffer.length) + { + bufferPointer = 0; + } + } + + return values; + } + + /** + * Places the new value into the buffer, overwriting the oldest value + * @param newValue to add to the buffer + */ + public void put(double newValue) + { + mBuffer[mBufferPointer] = newValue; + + mBufferPointer++; + + if(mBufferPointer >= mBuffer.length) + { + mBufferPointer = 0; + } + } + + /** + * Returns the maximum from the values currently in the buffer + */ + public double max() + { + if(mMax == null) + { + mMax = new Max(); + } + + return mMax.evaluate(mBuffer, 0, mBuffer.length); + } + + /** + * Returns the minimum from the values currently in the buffer + */ + public double min() + { + if(mMin == null) + { + mMin = new Min(); + } + + return mMin.evaluate(mBuffer, 0, mBuffer.length); + } + + /** + * Calculates the variance of the values contained in this buffer + */ + public double variance() + { + if(mVariance == null) + { + mVariance = new Variance(); + } + + return mVariance.evaluate(mBuffer); + } + + /** + * Calculates the average/mean of the values contained in this buffer + */ + public double mean() + { + if(mMean == null) + { + mMean = new Mean(); + } + + return mMean.evaluate(mBuffer, 0, mBuffer.length); + } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java index e2e7d9ca..c129a26d 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java @@ -39,15 +39,24 @@ public class FilterViewer extends Application */ private float[] getFilter() { +// FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() +// .sampleRate(48000) +// .gridDensity(16) +// .passBandCutoff(2500) +// .passBandAmplitude(1.0) +// .passBandRipple(0.01) +// .stopBandStart(4000) +// .stopBandAmplitude(0.0) +// .stopBandRipple(0.008) +// .build(); FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() - .sampleRate(48000) - .gridDensity(16) - .passBandCutoff(2500) + .sampleRate((int)48000.0) + .passBandCutoff(6000) .passBandAmplitude(1.0) - .passBandRipple(0.01) - .stopBandStart(4000) + .passBandRipple(0.1) .stopBandAmplitude(0.0) - .stopBandRipple(0.008) + .stopBandStart(6250) + .stopBandRipple(0.01) .build(); // FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder() diff --git a/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java b/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java index 8190b9a1..6ac9a164 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java @@ -1,54 +1,90 @@ +/******************************************************************************* + * 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.filter.fir.complex; import io.github.dsheirer.dsp.filter.fir.FIRFilter; import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter; import io.github.dsheirer.sample.complex.Complex; +import io.github.dsheirer.sample.complex.ComplexBuffer; public class ComplexFIRFilter extends FIRFilter { - private RealFIRFilter mIFilter; - private RealFIRFilter mQFilter; - - /** - * Complex FIR Filter for processing complex sample pairs. Wraps two real - * FIR filters for processing each of the inphase and quadrature samples. - * - * @param coefficients - filter taps - * @param gain - gain to apply to filtered outputs - use 1.0f for no gain - */ - public ComplexFIRFilter( float[] coefficients, float gain ) - { - mIFilter = new RealFIRFilter( coefficients, gain ); - mQFilter = new RealFIRFilter( coefficients, gain ); - } - - public float[] getCoefficients() - { - return mIFilter.getCoefficients(); - } - - public float filterInphase( float sample ) - { - return mIFilter.filter( sample ); - } - - public float filterQuadrature( float sample ) - { - return mQFilter.filter( sample ); - } - - public Complex filter( Complex sample ) - { - float i = filterInphase( sample.inphase() ); - float q = filterQuadrature( sample.quadrature() ); - - return new Complex( i, q ); - } - - @Override - public void dispose() - { - mIFilter.dispose(); - mQFilter.dispose(); - } + private RealFIRFilter mIFilter; + private RealFIRFilter mQFilter; + + /** + * Complex FIR Filter for processing complex sample pairs. Wraps two real + * FIR filters for processing each of the inphase and quadrature samples. + * + * @param coefficients - filter taps + * @param gain - gain to apply to filtered outputs - use 1.0f for no gain + */ + public ComplexFIRFilter(float[] coefficients, float gain) + { + mIFilter = new RealFIRFilter(coefficients, gain); + mQFilter = new RealFIRFilter(coefficients, gain); + } + + public float[] getCoefficients() + { + return mIFilter.getCoefficients(); + } + + public float filterInphase(float sample) + { + return mIFilter.filter(sample); + } + + public float filterQuadrature(float sample) + { + return mQFilter.filter(sample); + } + + public Complex filter(Complex sample) + { + float i = filterInphase(sample.inphase()); + float q = filterQuadrature(sample.quadrature()); + + return new Complex(i, q); + } + + /** + * Filters the complex samples from the buffer and returns a new complex buffer with the filtered output + * + * @param buffer with complex samples to filter + * @return new buffer containing filtered complex samples + */ + public ComplexBuffer filter(ComplexBuffer buffer) + { + float[] samples = buffer.getSamples(); + float[] filtered = new float[samples.length]; + + for(int x = 0; x < samples.length; x += 2) + { + filtered[x] = filterInphase(samples[x]); + filtered[x + 1] = filterQuadrature(samples[x + 1]); + } + + return new ComplexBuffer(filtered); + } + + @Override + public void dispose() + { + mIFilter.dispose(); + mQFilter.dispose(); + } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter_CB_CB.java b/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter_CB_CB.java index 33cd4862..d864d2e2 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter_CB_CB.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter_CB_CB.java @@ -3,8 +3,7 @@ package io.github.dsheirer.dsp.filter.fir.complex; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.complex.ComplexBuffer; -public class ComplexFIRFilter_CB_CB extends ComplexFIRFilter - implements Listener +public class ComplexFIRFilter_CB_CB extends ComplexFIRFilter implements Listener { private Listener mListener; diff --git a/src/main/java/io/github/dsheirer/dsp/gain/ComplexFeedForwardGainControl.java b/src/main/java/io/github/dsheirer/dsp/gain/ComplexFeedForwardGainControl.java index c939b952..215d7b46 100644 --- a/src/main/java/io/github/dsheirer/dsp/gain/ComplexFeedForwardGainControl.java +++ b/src/main/java/io/github/dsheirer/dsp/gain/ComplexFeedForwardGainControl.java @@ -1,112 +1,125 @@ +/******************************************************************************* + * 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.gain; import io.github.dsheirer.buffer.FloatCircularBuffer; import io.github.dsheirer.sample.complex.Complex; +import io.github.dsheirer.sample.complex.ComplexBuffer; import io.github.dsheirer.sample.complex.ComplexSampleListener; -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2015 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 - * - * This class is modeled using concepts detailed in the GNURadio - * implementation of feed forward AGC class located at: - * https://github.com/gnuradio/gnuradio/blob/master/gr-analog/lib/ - * feedforward_agc_cc_impl.cc - * - ******************************************************************************/ public class ComplexFeedForwardGainControl implements ComplexSampleListener { - public static final float OBJECTIVE_ENVELOPE = 1.0f; - public static final float MINIMUM_ENVELOPE = 0.0001f; + public static final float OBJECTIVE_ENVELOPE = 1.0f; + public static final float MINIMUM_ENVELOPE = 0.0001f; - private ComplexSampleListener mListener; + private ComplexSampleListener mListener; - private FloatCircularBuffer mEnvelopeHistory; - - private float mMaxEnvelope = 0.0f; - private float mGain = 1.0f; - - /** - * Dynamic gain control for incoming sample stream to amplify or attenuate - * all samples toward an objective unity)gain, using the maximum envelope - * value detected in the stream history window. - * - * Uses the specified damping factor to limit gain swings. Damping factor - * is applied against the delta between current gain value and a recalculated - * gain value to limit how quickly the gain value will increase or decrease. - * - * @param window - history size to use in detecting maximum envelope value - * @param damping - a value between 0 < damping <= 1.0; - */ - public ComplexFeedForwardGainControl( int window ) - { - mEnvelopeHistory = new FloatCircularBuffer( window ); - } - - public void dispose() - { - mListener = null; - } + private FloatCircularBuffer mEnvelopeHistory; - @Override - public void receive( float inphase, float quadrature ) - { - float envelope = Complex.envelope( inphase, quadrature ); + private float mMaxEnvelope = 0.0f; + private float mGain = 1.0f; - if( envelope > mMaxEnvelope ) - { - mMaxEnvelope = envelope; - - adjustGain(); - } + /** + * Dynamic gain control for incoming sample stream to amplify or attenuate + * all samples toward an objective unity)gain, using the maximum envelope + * value detected in the stream history window. + * + * Uses the specified damping factor to limit gain swings. Damping factor + * is applied against the delta between current gain value and a recalculated + * gain value to limit how quickly the gain value will increase or decrease. + * + * @param window - history size to use in detecting maximum envelope value + */ + public ComplexFeedForwardGainControl(int window) + { + mEnvelopeHistory = new FloatCircularBuffer(window); + } - /* Replace oldest envelope value with current envelope value */ - float oldestEnvelope = mEnvelopeHistory.get( envelope ); + public void dispose() + { + mListener = null; + } - /* If the oldest envelope value was the max envelope value, then we - * have to rediscover the max value from the envelope history */ - if( mMaxEnvelope == oldestEnvelope && mMaxEnvelope != envelope ) - { - mMaxEnvelope = MINIMUM_ENVELOPE; - - for( float value: mEnvelopeHistory.getBuffer() ) - { - if( value > mMaxEnvelope ) - { - mMaxEnvelope = value; - } - } - - adjustGain(); - } - - /* Apply current gain value to the sample and send to the listener */ - if( mListener != null ) - { - mListener.receive( inphase *= mGain, quadrature *= mGain ); - } - } - - private void adjustGain() - { - mGain = OBJECTIVE_ENVELOPE / mMaxEnvelope; - } + @Override + public void receive(float inphase, float quadrature) + { + process(inphase, quadrature); - public void setListener( ComplexSampleListener listener ) - { - mListener = listener; - } + /* Apply current gain value to the sample and send to the listener */ + if(mListener != null) + { + mListener.receive(inphase *= mGain, quadrature *= mGain); + } + } + + private void process(float inphase, float quadrature) + { + float envelope = Complex.envelope(inphase, quadrature); + + if(envelope > mMaxEnvelope) + { + mMaxEnvelope = envelope; + + adjustGain(); + } + + /* Replace oldest envelope value with current envelope value */ + float oldestEnvelope = mEnvelopeHistory.get(envelope); + + /* If the oldest envelope value was the max envelope value, then we + * have to rediscover the max value from the envelope history */ + if(mMaxEnvelope == oldestEnvelope && mMaxEnvelope != envelope) + { + mMaxEnvelope = MINIMUM_ENVELOPE; + + for(float value : mEnvelopeHistory.getBuffer()) + { + if(value > mMaxEnvelope) + { + mMaxEnvelope = value; + } + } + + adjustGain(); + } + } + + private void adjustGain() + { + mGain = OBJECTIVE_ENVELOPE / mMaxEnvelope; + } + + public void setListener(ComplexSampleListener listener) + { + mListener = listener; + } + + public ComplexBuffer filter(ComplexBuffer complexBuffer) + { + float[] samples = complexBuffer.getSamples(); + float[] processed = new float[samples.length]; + + for(int x = 0; x < samples.length; x += 2) + { + process(samples[x], samples[x + 1]); + + processed[x] = samples[x] * mGain; + processed[x + 1] = samples[x + 1] * mGain; + } + + return new ComplexBuffer(processed); + } } diff --git a/src/main/java/io/github/dsheirer/dsp/mixer/Oscillator.java b/src/main/java/io/github/dsheirer/dsp/mixer/Oscillator.java index 50511208..9825dfbb 100644 --- a/src/main/java/io/github/dsheirer/dsp/mixer/Oscillator.java +++ b/src/main/java/io/github/dsheirer/dsp/mixer/Oscillator.java @@ -51,8 +51,7 @@ public class Oscillator */ private void update() { - float anglePerSample = - (float)( 2.0d * Math.PI * mFrequency / mSampleRate ); + float anglePerSample = (float)( 2.0d * Math.PI * mFrequency / mSampleRate ); mAnglePerSample = Complex.fromAngle( anglePerSample ); } diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolPhaseErrorCalculator.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolPhaseErrorCalculator.java new file mode 100644 index 00000000..c299d122 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolPhaseErrorCalculator.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * 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.psk; + +import io.github.dsheirer.sample.complex.Complex; + +public class DQPSKSymbolPhaseErrorCalculator extends QPSKSymbolPhaseErrorCalculator +{ + //Negative 45 degrees rotation to offset for differential additive 45 degrees of rotation + public static final Complex DIFFERENTIAL_OFFSET = Complex.fromAngle(Math.PI / -4.0d); + + @Override + public void adjust(Complex symbol) + { + symbol.multiply(DIFFERENTIAL_OFFSET); + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/psk/GardnerDetector.java b/src/main/java/io/github/dsheirer/dsp/psk/GardnerDetector.java new file mode 100644 index 00000000..023b3e87 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/GardnerDetector.java @@ -0,0 +1,82 @@ +/******************************************************************************* + * 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.psk; + +import io.github.dsheirer.sample.complex.Complex; + +public class GardnerDetector +{ + private float mErrorInphase; + private float mErrorQuadrature; + private float mError; + + /** + * Gardner detector for recovering symbol timing error. + */ + public GardnerDetector() + { + } + + + /** + * Calculates the symbol timing error by evaluating the previous, middle and current symbol instants. + * + * @param previous symbol + * @param middle symbol (ie midway between the previous and current) + * @param current symbol + * @return symbol timing error + */ + public float getError(Complex previous, Complex middle, Complex current) + { + mErrorInphase = (previous.inphase() - current.inphase()) * middle.inphase(); + mErrorQuadrature = (previous.quadrature() - current.quadrature()) * middle.quadrature(); + mError = normalize(mErrorInphase + mErrorQuadrature, 1.0f); + + return mError; + } + + /** + * Constrains timing error to +/- the maximum value and corrects any floating point invalid numbers + */ + public static float normalize(float error, float maximum) + { + if(Float.isNaN(error)) + { + return 0.0f; + } + else + { + return clip(error, maximum); + } + } + + /** + * Constrains value to the range of ( -maximum <> maximum ) + */ + public static float clip(float value, float maximum) + { + if(value > maximum) + { + return maximum; + } + else if(value < -maximum) + { + return -maximum; + } + + return value; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/psk/IQPSKSymbolDecoder.java b/src/main/java/io/github/dsheirer/dsp/psk/IQPSKSymbolDecoder.java new file mode 100644 index 00000000..0befad75 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/IQPSKSymbolDecoder.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * 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.psk; + +import io.github.dsheirer.dsp.symbol.Dibit; +import io.github.dsheirer.sample.complex.Complex; + +public interface IQPSKSymbolDecoder +{ + /** + * Decodes the symbol and converts it into a Dibit + * @param symbol to decode + * @return decoded dibit + */ + Dibit decode(Complex symbol); +} diff --git a/src/main/java/io/github/dsheirer/dsp/psk/ISymbolPhaseErrorCalculator.java b/src/main/java/io/github/dsheirer/dsp/psk/ISymbolPhaseErrorCalculator.java new file mode 100644 index 00000000..5b5df5a9 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/ISymbolPhaseErrorCalculator.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * 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.psk; + +import io.github.dsheirer.sample.complex.Complex; + +public interface ISymbolPhaseErrorCalculator +{ + /** + * Adjusts the calculated symbol as necessary before calculating any phase error. This might be useful for + * differential encoding to remove a 45 degree rotational spin. + * @param symbol to adjust + */ + void adjust(Complex symbol); + + /** + * Calculates the phase error of the symbol relative to an alignment for the constellation + * @param symbol to calculate error + * @return error value + */ + float getPhaseError(Complex symbol); +} diff --git a/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer.java b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer.java new file mode 100644 index 00000000..c67a2b9a --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer.java @@ -0,0 +1,187 @@ +/******************************************************************************* + * 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.psk; + +import io.github.dsheirer.dsp.filter.interpolator.RealInterpolator; +import io.github.dsheirer.sample.complex.Complex; + +public class InterpolatingSymbolBuffer +{ + private Complex mMiddleSample = new Complex(0,0); + private Complex mCurrentSample = new Complex(0,0); + + private float[] mDelayLineInphase; + private float[] mDelayLineQuadrature; + private float mMu; //Sample Counter + private float mGainMu = 0.05f; //Sample Counter Gain + private float mOmega; + private float mGainOmega = 0.1f * mGainMu * mGainMu; + private float mOmegaRel = 0.005f; + private float mOmegaMid; + + private int mDelayLinePointer = 0; + private int mTwiceSamplesPerSymbol; + private RealInterpolator mInterpolator = new RealInterpolator(1.0f); + + public InterpolatingSymbolBuffer(float samplesPerSymbol) + { + mMu = samplesPerSymbol; + mTwiceSamplesPerSymbol = (int)Math.floor(2.0 * samplesPerSymbol); + mDelayLineInphase = new float[2 * mTwiceSamplesPerSymbol]; + mDelayLineQuadrature = new float[2 * mTwiceSamplesPerSymbol]; + mOmega = samplesPerSymbol; + mOmegaMid = samplesPerSymbol; + + } + + public void receive(Complex currentSample) + { + mMu--; + + //Fill up the delay line to use with the interpolator + mDelayLineInphase[mDelayLinePointer] = currentSample.inphase(); + mDelayLineInphase[mDelayLinePointer + mTwiceSamplesPerSymbol] = currentSample.inphase(); + mDelayLineQuadrature[mDelayLinePointer] = currentSample.quadrature(); + mDelayLineQuadrature[mDelayLinePointer + mTwiceSamplesPerSymbol] = currentSample.quadrature(); + + //Increment pointer and keep pointer in bounds + mDelayLinePointer = (mDelayLinePointer + 1) % mTwiceSamplesPerSymbol; + } + + /** + * Updates the internal sample counter with additional samples to add to the symbol sample counter. + * @param samplesToAdd to the sample counter + */ + public void increaseSampleCounter(float samplesToAdd) + { + mMu += samplesToAdd; + } + + /** + * Adjusts samples per symbol and symbol timing counters and increments the sample counter to collect another + * symbol. + * + * @param symbolTimingError from a symbol timing error detector + */ + public void resetAndAdjust(float symbolTimingError) + { + //mOmega is samples per symbol and is constrained to floating between +/- .005 of the nominal samples per + //symbol + mOmega = mOmega + mGainOmega * symbolTimingError; + mOmega = mOmegaMid + clip(mOmega - mOmegaMid, mOmegaRel); + + //Add another symbol's worth of samples to the counter and adjust timing based on gardner error + increaseSampleCounter((mOmega + (mGainMu * symbolTimingError))); + } + + /** + * Indicates if this buffer has accumulated a enough samples to represent a full symbol + */ + public boolean hasSymbol() + { + return mMu < 1.0f; + } + + /** + * Interpolated middle sample for the symbol. Note: this method should only be invoked after testing for a + * complete symbol with the hasSymbol() method. + */ + public Complex getMiddleSample() + { + /* Calculate interpolated middle sample and current sample */ + mMiddleSample.setValues(getInphase(mMu), getQuadrature(mMu)); + + return mMiddleSample; + } + + /** + * Interpolated current sample for the symbol. This sample represents the ideal symbol sampling point. + * Note: this method should only be invoked after testing for a complete symbol with the hasSymbol() method. + */ + public Complex getCurrentSample() + { + float half_omega = mOmega / 2.0f; + int half_sps = (int)Math.floor(half_omega); + float half_mu = mMu + half_omega - half_sps; + + if(half_mu > 1.0) + { + half_mu -= 1.0; + half_sps += 1; + } + + mCurrentSample.setValues(getInphase(half_mu, half_sps), getQuadrature(half_mu, half_sps)); + + return mCurrentSample; + } + + /** + * Returns the interpolated inphase value for the specified offset + * @param interpolation into the buffer to calculate the interpolated sample + * @return inphase value of the interpolated sample + */ + public float getInphase(float interpolation) + { + return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer, interpolation); + } + + /** + * Returns the interpolated inphase value for the specified offset + * @param interpolation into the buffer to calculate the interpolated sample + * @return inphase value of the interpolated sample + */ + public float getInphase(float interpolation, int indexOffet) + { + return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer + indexOffet, interpolation); + } + + /** + * Returns the interpolated quadrature value for the specified offset + * @param interpolation into the buffer to calculate the interpolated sample + * @return quadrature value of the interpolated sample + */ + public float getQuadrature(float interpolation) + { + return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer, interpolation); + } + + /** + * Returns the interpolated quadrature value for the specified offset + * @param interpolation into the buffer to calculate the interpolated sample + * @return quadrature value of the interpolated sample + */ + public float getQuadrature(float interpolation, int indexOffset) + { + return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer + indexOffset, interpolation); + } + + /** + * Constrains value to the range of ( -maximum <> maximum ) + */ + public static float clip(float value, float maximum) + { + if(value > maximum) + { + return maximum; + } + else if(value < -maximum) + { + return -maximum; + } + + return value; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/psk/LSMConstellationDecoder.java b/src/main/java/io/github/dsheirer/dsp/psk/LSMConstellationDecoder.java new file mode 100644 index 00000000..7c500838 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/LSMConstellationDecoder.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * 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.psk; + +import io.github.dsheirer.dsp.symbol.Dibit; +import io.github.dsheirer.sample.complex.Complex; + +public class LSMConstellationDecoder implements IQPSKSymbolDecoder +{ + @Override + public Dibit decode(Complex symbol) + { + return null; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/psk/LSMDemodulator.java b/src/main/java/io/github/dsheirer/dsp/psk/LSMDemodulator.java index 030ef0d8..aba3a907 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/LSMDemodulator.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/LSMDemodulator.java @@ -2,36 +2,37 @@ package io.github.dsheirer.dsp.psk; /******************************************************************************* * SDR Trunk - * Copyright (C) 2014, 2015 Dennis Sheirer - * + * Copyright (C) 2014 - 2018 Dennis Sheirer + * * Gardner detector, Costas loop and Interpolator derived from: - * + * * OP25 - gardner_costas_cc_impl.cc * Copyright 2010, 2011, 2012, 2013 KA1RBI (gardner_costas_cc_impl.cc) - * + * * OP25 - scope.py * Copyright 2008-2011 Steve Glass * Copyright 2011, 2012, 2013 KA1RBI - * + * * GNURadio - control_loop and mpsk_receiver_cc * Copyright 2011,2013 Free Software Foundation, Inc. - * + * * 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 ******************************************************************************/ import io.github.dsheirer.buffer.FloatAveragingBuffer; import io.github.dsheirer.dsp.filter.interpolator.RealInterpolator; +import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop; import io.github.dsheirer.dsp.symbol.Dibit; import io.github.dsheirer.instrument.Instrumentable; import io.github.dsheirer.instrument.tap.Tap; @@ -48,134 +49,139 @@ import java.util.Arrays; import java.util.List; /** - * Implements a LSM (Pi/4) demodulator using a Gardner Detector to determine - * optimal sampling timing and a Costas Loop as a phase locked loop synchronized + * Implements a LSM (Pi/4) demodulator using a Gardner Detector to determine + * optimal sampling timing and a Costas Loop as a phase locked loop synchronized * with the incoming signal carrier frequency. - * + * * Sample Rate: 48000 * Symbol Rate: 4800 */ public class LSMDemodulator implements Instrumentable, ComplexSampleListener { - private List mAvailableTaps; - - /* 45 degree rotation angle */ - public static final float THETA = (float)( Math.PI / 4.0d ); + private List mAvailableTaps; - /* 45 degree point */ - public static final Complex POINT_45_DEGREES = - new Complex( (float)Math.sin( THETA ), (float)Math.cos( THETA ) ); - - private Listener mSymbolListener; - - private GardnerSymbolTiming mGardnerDetector = new GardnerSymbolTiming(); - - private CostasLoop mCostasLoop = new CostasLoop(); - - private IFrequencyChangeListener mFrequencyChangeListener; - - private EyeDiagramDataTap mEyeDiagramDataTap; - - public LSMDemodulator() - { - } - - public void dispose() - { - mSymbolListener = null; - mFrequencyChangeListener = null; - } - - public void setSymbolListener( Listener listener ) - { - mSymbolListener = listener; - } - - public void removeSymbolListener() - { - mSymbolListener = null; - } - - @Override - public void receive( float inphase, float quadrature ) - { - mGardnerDetector.receive( inphase, quadrature ); - } - - /** - * Applies a phase correction value to the costas loop to correct when a - * phase lock error is detected in the binary output stream. - * - * @param correction - value in radians - */ - public void correctPhaseError( double correction ) - { - mCostasLoop.correctPhaseError( correction ); - } - - /** - * Constrains value to the range of ( -maximum <> maximum ) - */ - public static float clip( float value, float maximum ) - { - if( value > maximum ) - { - return maximum; - } - else if( value < -maximum ) - { - return -maximum; - } - - return value; - } - - /** - * Constrains timing error to +/- the maximum value and corrects any - * floating point invalid numbers - */ - private float normalize( float error, float maximum ) - { - if( Float.isNaN( error ) ) - { - return 0.0f; - } - else - { - return clip( error, maximum ); - } - } - - public void addListener( IFrequencyChangeListener listener ) - { - mFrequencyChangeListener = listener; - } - - public void removeListener( IFrequencyChangeListener listener ) - { - mFrequencyChangeListener = null; - } + /* 45 degree rotation angle */ + public static final float THETA = (float)(Math.PI / 4.0d); - /** - * Processes the current loop frequency of the costas loop and broadcasts - * tuner frequency offset adjustments 10 times a second as needed to keep - * the frequency offset below half of the maximum frequency - */ - public class FrequencyControl - { - /* Set the trigger threshold at half of the maximum costas loop control - * frequency */ - private static final float CORRECTION_THRESHOLD_FREQUENCY = - ( 2.0f * (float)Math.PI * 600.0f ) / 48000.0f; + /* 45 degree point */ + public static final Complex POINT_45_DEGREES = new Complex((float)Math.sin(THETA), (float)Math.cos(THETA)); - private FloatAveragingBuffer mBuffer = new FloatAveragingBuffer( 40 ); - - private long mFrequencyError = 0; - - private int mCounter = 0; - - public void receive( float frequency ) - { + private Listener mSymbolListener; + + private GardnerSymbolTiming mGardnerDetector = new GardnerSymbolTiming(); + + private CostasLoop mCostasLoop = new CostasLoop(); + private QPSKSymbolDecoder mQPSKSymbolDecoder = new QPSKSymbolDecoder(); + + private IFrequencyChangeListener mFrequencyChangeListener; + + private EyeDiagramDataTap mEyeDiagramDataTap; + + public LSMDemodulator() + { + } + + public CostasLoop getCostasLoop() + { + return mCostasLoop; + } + + public void dispose() + { + mSymbolListener = null; + mFrequencyChangeListener = null; + } + + public void setSymbolListener(Listener listener) + { + mSymbolListener = listener; + } + + public void removeSymbolListener() + { + mSymbolListener = null; + } + + @Override + public void receive(float inphase, float quadrature) + { + mGardnerDetector.receive(inphase, quadrature); + } + + /** + * Applies a phase correction value to the costas loop to correct when a + * phase lock error is detected in the binary output stream. + * + * @param correction - value in radians + */ + public void correctPhaseError(double correction) + { + mCostasLoop.correctPhaseError(correction); + } + + /** + * Constrains value to the range of ( -maximum <> maximum ) + */ + public static float clip(float value, float maximum) + { + if(value > maximum) + { + return maximum; + } + else if(value < -maximum) + { + return -maximum; + } + + return value; + } + + /** + * Constrains timing error to +/- the maximum value and corrects any + * floating point invalid numbers + */ + private float normalize(float error, float maximum) + { + if(Float.isNaN(error)) + { + return 0.0f; + } + else + { + return clip(error, maximum); + } + } + + public void addListener(IFrequencyChangeListener listener) + { + mFrequencyChangeListener = listener; + } + + public void removeListener(IFrequencyChangeListener listener) + { + mFrequencyChangeListener = null; + } + + /** + * Processes the current loop frequency of the costas loop and broadcasts + * tuner frequency offset adjustments 10 times a second as needed to keep + * the frequency offset below half of the maximum frequency + */ + public class FrequencyControl + { + /* Set the trigger threshold at half of the maximum costas loop control + * frequency */ + private static final float CORRECTION_THRESHOLD_FREQUENCY = + (2.0f * (float)Math.PI * 600.0f) / 48000.0f; + + private FloatAveragingBuffer mBuffer = new FloatAveragingBuffer(40); + + private long mFrequencyError = 0; + + private int mCounter = 0; + + public void receive(float frequency) + { // mCounter++; // // if( mCounter >= 480 ) @@ -212,377 +218,395 @@ public class LSMDemodulator implements Instrumentable, ComplexSampleListener // { // mBuffer.get( frequency ); // } - } - } + } + } - /** - * Gardner symbol timing detector - */ - public class GardnerSymbolTiming implements ComplexSampleListener - { - public static final int HALF_SAMPLES_PER_SYMBOL = 5; - public static final int SAMPLES_PER_SYMBOL = 10; - public static final int TWICE_SAMPLES_PER_SYMBOL = 20; + /** + * Gardner symbol timing detector + */ + public class GardnerSymbolTiming implements ComplexSampleListener + { + public static final int HALF_SAMPLES_PER_SYMBOL = 5; + public static final int SAMPLES_PER_SYMBOL = 10; + public static final int TWICE_SAMPLES_PER_SYMBOL = 20; - private float[] mDelayLineInphase = new float[ 2 * TWICE_SAMPLES_PER_SYMBOL ]; - private float[] mDelayLineQuadrature = new float[ 2 * TWICE_SAMPLES_PER_SYMBOL ]; + private float[] mDelayLineInphase = new float[2 * TWICE_SAMPLES_PER_SYMBOL]; + private float[] mDelayLineQuadrature = new float[2 * TWICE_SAMPLES_PER_SYMBOL]; - private int mDelayLinePointer = 0; + private int mDelayLinePointer = 0; - /* Sampling point */ - private float mMu = 10.0f; - private float mGainMu = 0.05f; - - /* Samples per symbol */ - private float mOmega = 10.0f; - private float mGainOmega = 0.1f * mGainMu * mGainMu; - private float mOmegaRel = 0.005f; - private float mOmegaMid = 10.0f; + /* Sampling point */ + private float mMu = 10.0f; + private float mGainMu = 0.05f; - private RealInterpolator mInterpolator = new RealInterpolator( 1.0f ); - - private Complex mPreviousSample = new Complex( 0, 0 ); - private Complex mPreviousMiddleSample = new Complex( 0, 0 ); - private Complex mPreviousSymbol = new Complex( 0, 0 ); - - /** - * Provides symbol sampling timing control - */ - public GardnerSymbolTiming() - { - } + /* Samples per symbol */ + private float mOmega = 10.0f; + private float mGainOmega = 0.1f * mGainMu * mGainMu; + private float mOmegaRel = 0.005f; + private float mOmegaMid = 10.0f; - @Override - public void receive( float inphase, float quadrature ) - { - /* Count down samples per symbol until we calculate the symbol */ - mMu--; + private RealInterpolator mInterpolator = new RealInterpolator(1.0f); - mCostasLoop.increment(); - - /* Mix incoming sample with costas loop to remove any rotation - * that is present from a mis-tuned carrier frequency */ - - Complex costas = mCostasLoop.getCurrentVector(); - - float derotatedInphase = Complex.multiplyInphase( inphase, quadrature, costas.inphase(), costas.quadrature() ); - float derotatedQuadrature = Complex.multiplyQuadrature( inphase, quadrature, costas.inphase(), costas.quadrature() ); - - /* Fill up the delay line to use with the interpolator */ - mDelayLineInphase[ mDelayLinePointer ] = derotatedInphase; - mDelayLineInphase[ mDelayLinePointer + TWICE_SAMPLES_PER_SYMBOL ] = derotatedInphase; + private Complex mPreviousSample = new Complex(0, 0); + private Complex mPreviousMiddleSample = new Complex(0, 0); + private Complex mPreviousSymbol = new Complex(0, 0); - mDelayLineQuadrature[ mDelayLinePointer ] = derotatedQuadrature; - mDelayLineQuadrature[ mDelayLinePointer + TWICE_SAMPLES_PER_SYMBOL ] = derotatedQuadrature; + /** + * Provides symbol sampling timing control + */ + public GardnerSymbolTiming() + { + } - /* Increment pointer and keep pointer in bounds */ - mDelayLinePointer = ( mDelayLinePointer + 1 ) % TWICE_SAMPLES_PER_SYMBOL; - - /* Calculate the symbol once we've stored enough samples */ - if( mMu <= 1.0f ) - { - float half_omega = mOmega / 2.0f; - int half_sps = (int)Math.floor( half_omega ); - float half_mu = mMu + half_omega - (float)half_sps; - - if( half_mu > 1.0 ) - { - half_mu -= 1.0; - half_sps += 1; - } + @Override + public void receive(float inphase, float quadrature) + { + /* Count down samples per symbol until we calculate the symbol */ + mMu--; - /* Calculate interpolated middle sample and current sample */ - float middleSampleInphase = mInterpolator.filter( - mDelayLineInphase, mDelayLinePointer, mMu ); - float middleSampleQuadrature = mInterpolator.filter( - mDelayLineQuadrature, mDelayLinePointer, mMu ); + mCostasLoop.increment(); - int index = mDelayLinePointer + half_sps; - - float currentSampleInphase = mInterpolator.filter( - mDelayLineInphase, index, half_mu ); - float currentSampleQuadrature = mInterpolator.filter( - mDelayLineQuadrature, index, half_mu ); + /* Mix incoming sample with costas loop to remove any rotation + * that is present from a mis-tuned carrier frequency */ - /* Multiply current sample and conjugate (negated quadrature) of - * previous sample to get symbols to use for gardner error feedback */ - Complex middleSymbol = Complex.multiply( middleSampleInphase, - middleSampleQuadrature, mPreviousMiddleSample.conjugate() ); - Complex currentSymbol = Complex.multiply( currentSampleInphase, - currentSampleQuadrature, mPreviousSample.conjugate() ); + Complex costas = mCostasLoop.getCurrentVector(); - /* Set gain to unity */ - middleSymbol.normalize(); - currentSymbol.normalize(); + float derotatedInphase = Complex.multiplyInphase(inphase, quadrature, costas.inphase(), costas.quadrature()); + float derotatedQuadrature = Complex.multiplyQuadrature(inphase, quadrature, costas.inphase(), costas.quadrature()); - /* Gardner timing error calculations */ - float errorInphase = ( mPreviousSymbol.inphase() - - currentSymbol.inphase() ) * middleSymbol.inphase(); + /* Fill up the delay line to use with the interpolator */ + mDelayLineInphase[mDelayLinePointer] = derotatedInphase; + mDelayLineInphase[mDelayLinePointer + TWICE_SAMPLES_PER_SYMBOL] = derotatedInphase; - float errorQuadrature = ( mPreviousSymbol.quadrature() - - currentSymbol.quadrature() ) * middleSymbol.quadrature(); + mDelayLineQuadrature[mDelayLinePointer] = derotatedQuadrature; + mDelayLineQuadrature[mDelayLinePointer + TWICE_SAMPLES_PER_SYMBOL] = derotatedQuadrature; - float gardnerError = normalize( errorInphase + errorQuadrature, 1.0f ); - - if( mEyeDiagramDataTap != null ) - { - mEyeDiagramDataTap.receive( - new EyeDiagramData( - Arrays.copyOfRange( mDelayLineInphase, 0, 20 ), - Arrays.copyOfRange( mDelayLineQuadrature, 0, 20 ), - mMu, (float)half_sps + half_mu, gardnerError ) ); - } - - /* mOmega is samples per symbol and is constrained to floating - * between +/- .005 of the nominal 10.0 samples per symbol */ - mOmega = mOmega + mGainOmega * gardnerError; - mOmega = mOmegaMid + clip( mOmega - mOmegaMid, mOmegaRel ); + /* Increment pointer and keep pointer in bounds */ + mDelayLinePointer = (mDelayLinePointer + 1) % TWICE_SAMPLES_PER_SYMBOL; - /* Adjust sample timing based on error of current sample */ - mMu += mOmega + ( mGainMu * gardnerError ); + /* Calculate the symbol once we've stored enough samples */ + if(mMu <= 1.0f) + { + float half_omega = mOmega / 2.0f; + int half_sps = (int)Math.floor(half_omega); + float half_mu = mMu + half_omega - (float)half_sps; - /* Store current samples/symbols to use for the next period */ - mPreviousSample.setValues( currentSampleInphase, currentSampleQuadrature ); - mPreviousMiddleSample.setValues( middleSampleInphase, middleSampleQuadrature ); - mPreviousSymbol = currentSymbol; + if(half_mu > 1.0) + { + half_mu -= 1.0; + half_sps += 1; + } - /* Update costas loop using phase error present in current - * symbol. The symbol is rotated from star orientation to polar - * orientation to simplify error calculation */ - mCostasLoop.receive( currentSymbol ); + /* Calculate interpolated middle sample and current sample */ + float middleSampleInphase = mInterpolator.filter( + mDelayLineInphase, mDelayLinePointer, mMu); + float middleSampleQuadrature = mInterpolator.filter( + mDelayLineQuadrature, mDelayLinePointer, mMu); + + int index = mDelayLinePointer + half_sps; + + float currentSampleInphase = mInterpolator.filter( + mDelayLineInphase, index, half_mu); + float currentSampleQuadrature = mInterpolator.filter( + mDelayLineQuadrature, index, half_mu); + + /* Multiply current sample and conjugate (negated quadrature) of + * previous sample to get symbols to use for gardner error feedback */ + Complex middleSymbol = Complex.multiply(middleSampleInphase, + middleSampleQuadrature, mPreviousMiddleSample.conjugate()); + Complex currentSymbol = Complex.multiply(currentSampleInphase, + currentSampleQuadrature, mPreviousSample.conjugate()); + + /* Set gain to unity */ + middleSymbol.normalize(); + currentSymbol.normalize(); + + /* Gardner timing error calculations */ + float errorInphase = (mPreviousSymbol.inphase() - + currentSymbol.inphase()) * middleSymbol.inphase(); + + float errorQuadrature = (mPreviousSymbol.quadrature() - + currentSymbol.quadrature()) * middleSymbol.quadrature(); + + float gardnerError = normalize(errorInphase + errorQuadrature, 1.0f); + + if(mEyeDiagramDataTap != null) + { + mEyeDiagramDataTap.receive( + new EyeDiagramData( + Arrays.copyOfRange(mDelayLineInphase, 0, 20), + Arrays.copyOfRange(mDelayLineQuadrature, 0, 20), + mMu, (float)half_sps + half_mu, gardnerError)); + } + + /* mOmega is samples per symbol and is constrained to floating + * between +/- .005 of the nominal 10.0 samples per symbol */ + mOmega = mOmega + mGainOmega * gardnerError; + mOmega = mOmegaMid + clip(mOmega - mOmegaMid, mOmegaRel); + + /* Adjust sample timing based on error of current sample */ + mMu += mOmega + (mGainMu * gardnerError); + + /* Store current samples/symbols to use for the next period */ + mPreviousSample.setValues(currentSampleInphase, currentSampleQuadrature); + mPreviousMiddleSample.setValues(middleSampleInphase, middleSampleQuadrature); + mPreviousSymbol = currentSymbol; + + /* Update costas loop using phase error present in current + * symbol. The symbol is rotated from star orientation to polar + * orientation to simplify error calculation */ + mCostasLoop.receive(currentSymbol); // mCostasLoop.receive( // ComplexSample.multiply( currentSymbol, POINT_45_DEGREES ) ); - - /* Dispatch the differentiated symbol to the registered listener */ - if( mSymbolListener != null ) - { - mSymbolListener.receive( currentSymbol ); - } - } - } - } - - /** - * Costas Loop - phase locked loop synchronized to the frequency offset of - * the incoming signal to enable the signal to be mixed down to zero offset - * for proper synchronization. The mFrequency value indicates the detected - * frequency offset. We attempt to keep that value close to zero by issuing - * frequency adjustments to the tuner channel source. - * - * Most of the costas loop code was ported from gnuradio/control_loop and - * gnuradio/mpsk_receiver_cc using initialization values from KA1RBI's - * OP25/cqpsk.py - */ - public class CostasLoop implements Listener - { - public static final double TWO_PI = 2.0 * Math.PI; - - private static final float MAXIMUM_FREQUENCY = - ( 2400.0f * (float)TWO_PI ) / 48000.0f; - - /* http://www.trondeau.com/blog/2011/8/13/control-loop-gain-values.html */ - private float mDamping = (float)Math.sqrt( 2.0 ) / 2.0f; - - /* Use denominator between 100 (most) and 400 (least) to adjust costas - * loop control level */ - private float mLoopBandwidth = (float)( TWO_PI / 400.0d ); - - private float mAlphaGain = ( 4.0f * mDamping * mLoopBandwidth ) / - ( 1.0f + ( 2.0f * mDamping * mLoopBandwidth ) + - ( mLoopBandwidth * mLoopBandwidth ) ); - private float mBetaGain = ( 4.0f * mLoopBandwidth * mLoopBandwidth ) / - ( 1.0f + ( 2.0f * mDamping * mLoopBandwidth ) + - ( mLoopBandwidth * mLoopBandwidth ) ); + /* Dispatch the differentiated symbol to the registered listener */ + if(mSymbolListener != null) + { + mSymbolListener.receive(currentSymbol); + } + } + } + } - private float mLoopPhase = 0.0f; - - private float mLoopFrequency = 0.0f; - - private FrequencyControl mFrequencyControl = new FrequencyControl(); - - public CostasLoop() - { - } - - /** - * Applies a phase correction value to the current phasor. Use this - * method to apply correction when a +/- 90, or 180 degree phase error - * is detected in the binary output stream. - * - * If the supplied correction value places the loop frequency outside - * of the max frequency, then the frequency will be corrected 360 - * degrees in the opposite direction to maintain within the max - * frequency bounds. - * - * @param correction - correction value in radians - */ - public void correctPhaseError( double correction ) - { - mLoopFrequency += correction; - - if( mLoopFrequency > MAXIMUM_FREQUENCY ) - { - mLoopFrequency -= 2.0d * MAXIMUM_FREQUENCY; - } - - if( mLoopFrequency < -MAXIMUM_FREQUENCY ) - { - mLoopFrequency += 2.0d * MAXIMUM_FREQUENCY; - } - } - - /** - * Increments the phase of the loop - */ - public void increment() - { - mLoopPhase += mLoopFrequency; + /** + * Costas Loop - phase locked loop synchronized to the frequency offset of + * the incoming signal to enable the signal to be mixed down to zero offset + * for proper synchronization. The mFrequency value indicates the detected + * frequency offset. We attempt to keep that value close to zero by issuing + * frequency adjustments to the tuner channel source. + * + * Most of the costas loop code was ported from gnuradio/control_loop and + * gnuradio/mpsk_receiver_cc using initialization values from KA1RBI's + * OP25/cqpsk.py + */ + public class CostasLoop implements Listener, IPhaseLockedLoop + { + public static final double TWO_PI = 2.0 * Math.PI; - /* Keep the loop phase in bounds */ - unwrapPhase(); - } + private static final float MAXIMUM_FREQUENCY = + (2400.0f * (float)TWO_PI) / 48000.0f; - private void unwrapPhase() - { - while( mLoopPhase > TWO_PI ) - { - mLoopPhase -= TWO_PI; - } - - while( mLoopPhase < -TWO_PI ) - { - mLoopPhase += TWO_PI; - } - } - - /** - * Current vector of the loop rotated by THETA degrees. - */ - public Complex getCurrentVector() - { + /* http://www.trondeau.com/blog/2011/8/13/control-loop-gain-values.html */ + private float mDamping = (float)Math.sqrt(2.0) / 2.0f; + + /* Use denominator between 100 (most) and 400 (least) to adjust costas + * loop control level */ + private float mLoopBandwidth = (float)(TWO_PI / 400.0d); + + private float mAlphaGain = (4.0f * mDamping * mLoopBandwidth) / + (1.0f + (2.0f * mDamping * mLoopBandwidth) + + (mLoopBandwidth * mLoopBandwidth)); + + private float mBetaGain = (4.0f * mLoopBandwidth * mLoopBandwidth) / + (1.0f + (2.0f * mDamping * mLoopBandwidth) + + (mLoopBandwidth * mLoopBandwidth)); + + private float mLoopPhase = 0.0f; + + private float mLoopFrequency = 0.0f; + + private FrequencyControl mFrequencyControl = new FrequencyControl(); + + public CostasLoop() + { + } + + /** + * Applies a phase correction value to the current phasor. Use this + * method to apply correction when a +/- 90, or 180 degree phase error + * is detected in the binary output stream. + * + * If the supplied correction value places the loop frequency outside + * of the max frequency, then the frequency will be corrected 360 + * degrees in the opposite direction to maintain within the max + * frequency bounds. + * + * @param correction - correction value in radians + */ + public void correctPhaseError(double correction) + { + mLoopFrequency += correction; + + if(mLoopFrequency > MAXIMUM_FREQUENCY) + { + mLoopFrequency -= 2.0d * MAXIMUM_FREQUENCY; + } + + if(mLoopFrequency < -MAXIMUM_FREQUENCY) + { + mLoopFrequency += 2.0d * MAXIMUM_FREQUENCY; + } + } + + /** + * Increments the phase of the loop + */ + public void increment() + { + mLoopPhase += mLoopFrequency; + + /* Keep the loop phase in bounds */ + unwrapPhase(); + } + + @Override + public void adjust(double error) + { + adjust(error); + } + + @Override + public void correctInversion(double correction) + { + correctPhaseError(correction); + } + + private void unwrapPhase() + { + while(mLoopPhase > TWO_PI) + { + mLoopPhase -= TWO_PI; + } + + while(mLoopPhase < -TWO_PI) + { + mLoopPhase += TWO_PI; + } + } + + /** + * Current vector of the loop rotated by THETA degrees. + */ + public Complex getCurrentVector() + { // return ComplexSample.fromAngle( mLoopPhase + THETA ); - return Complex.fromAngle( mLoopPhase ); - } - - @Override - public void receive( Complex complex ) - { - /* Calculate phase error */ - float phaseError = getPhaseError( complex ); - - Dibit d = QPSKPolarSlicer.decide( complex ); + return Complex.fromAngle(mLoopPhase); + } + + @Override + public Complex incrementAndGetCurrentVector() + { + return null; + } + + @Override + public void receive(Complex complex) + { + /* Calculate phase error */ + float phaseError = getPhaseError(complex); + + Dibit d = mQPSKSymbolDecoder.decode(complex); // mLog.debug( "Dibit: " + d.name() + " Error: " + phaseError ); - /* Adjust for phase error */ - adjust( phaseError ); - } + /* Adjust for phase error */ + adjust(phaseError); + } - /** - * Updates the costas loop frequency and phase to adjust for the phase - * error value - * - * @param phase_error - (-)= late and (+)= early - */ - private void adjust( float phase_error ) - { - mLoopFrequency += mBetaGain * phase_error; - mLoopPhase += mLoopFrequency + mAlphaGain * phase_error; + /** + * Updates the costas loop frequency and phase to adjust for the phase + * error value + * + * @param phase_error - (-)= late and (+)= early + */ + private void adjust(float phase_error) + { + mLoopFrequency += mBetaGain * phase_error; + mLoopPhase += mLoopFrequency + mAlphaGain * phase_error; - /* Maintain phase between +/- 2 * PI */ - unwrapPhase(); + /* Maintain phase between +/- 2 * PI */ + unwrapPhase(); - /* Limit frequency to +/- maximum loop frequency */ - limitFrequency(); - } + /* Limit frequency to +/- maximum loop frequency */ + limitFrequency(); + } - /** - * Constrains the frequency within the bounds of +/- loop frequency - */ - private void limitFrequency() - { - /* Check for and issue tuner offset correction */ - mFrequencyControl.receive( mLoopFrequency ); - - if( mLoopFrequency > MAXIMUM_FREQUENCY ) - { - mLoopFrequency = MAXIMUM_FREQUENCY; - } - - if( mLoopFrequency < -MAXIMUM_FREQUENCY ) - { - mLoopFrequency = -MAXIMUM_FREQUENCY; - } - } + /** + * Constrains the frequency within the bounds of +/- loop frequency + */ + private void limitFrequency() + { + /* Check for and issue tuner offset correction */ + mFrequencyControl.receive(mLoopFrequency); - /** - * Detects rotational error present when the costas loop is not aligned - * with the carrier frequency. Provides error feedback to adjust - * mixer frequency. - */ - public float getPhaseError( Complex complex ) - { - float phase_error = 0; - - if( Math.abs( complex.inphase() ) > Math.abs( complex.quadrature() ) ) - { - if( complex.inphase() > 0 ) - { - phase_error = -complex.quadrature(); - } - else - { - phase_error = complex.quadrature(); - } - } - else - { - if( complex.quadrature() > 0 ) - { - phase_error = complex.inphase(); - } - else - { - phase_error = -complex.inphase(); - } - } - - return phase_error; - } - } + if(mLoopFrequency > MAXIMUM_FREQUENCY) + { + mLoopFrequency = MAXIMUM_FREQUENCY; + } - @Override - public List getTapGroups() - { - if( mAvailableTaps == null ) - { - mAvailableTaps = new ArrayList<>(); - - TapGroup group = new TapGroup( "LSM Demodulator" ); - - group.add( new EyeDiagramDataTap( "Eye Diagram", 0, 4800 ) ); + if(mLoopFrequency < -MAXIMUM_FREQUENCY) + { + mLoopFrequency = -MAXIMUM_FREQUENCY; + } + } - mAvailableTaps.add( group ); - } - - return mAvailableTaps; - } + /** + * Detects rotational error present when the costas loop is not aligned + * with the carrier frequency. Provides error feedback to adjust + * mixer frequency. + */ + public float getPhaseError(Complex complex) + { + float phase_error = 0; - @Override - public void registerTap( Tap tap ) - { - if( tap instanceof EyeDiagramDataTap ) - { - mEyeDiagramDataTap = (EyeDiagramDataTap)tap; - } - } + if(Math.abs(complex.inphase()) > Math.abs(complex.quadrature())) + { + if(complex.inphase() > 0) + { + phase_error = -complex.quadrature(); + } + else + { + phase_error = complex.quadrature(); + } + } + else + { + if(complex.quadrature() > 0) + { + phase_error = complex.inphase(); + } + else + { + phase_error = -complex.inphase(); + } + } - @Override - public void unregisterTap( Tap tap ) - { - if( tap instanceof EyeDiagramDataTap ) - { - mEyeDiagramDataTap = null; - } - } + return phase_error; + } + } + + @Override + public List getTapGroups() + { + if(mAvailableTaps == null) + { + mAvailableTaps = new ArrayList<>(); + + TapGroup group = new TapGroup("LSM Demodulator"); + + group.add(new EyeDiagramDataTap("Eye Diagram", 0, 4800)); + + mAvailableTaps.add(group); + } + + return mAvailableTaps; + } + + @Override + public void registerTap(Tap tap) + { + if(tap instanceof EyeDiagramDataTap) + { + mEyeDiagramDataTap = (EyeDiagramDataTap)tap; + } + } + + @Override + public void unregisterTap(Tap tap) + { + if(tap instanceof EyeDiagramDataTap) + { + mEyeDiagramDataTap = null; + } + } } diff --git a/src/main/java/io/github/dsheirer/dsp/psk/QPSKDemodulator.java b/src/main/java/io/github/dsheirer/dsp/psk/QPSKDemodulator.java new file mode 100644 index 00000000..91d1ce76 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/QPSKDemodulator.java @@ -0,0 +1,182 @@ +/******************************************************************************* + * 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.psk; + +import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop; +import io.github.dsheirer.dsp.symbol.Dibit; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.complex.Complex; +import io.github.dsheirer.sample.complex.ComplexBuffer; +import io.github.dsheirer.sample.complex.ComplexSampleListener; + +public class QPSKDemodulator implements ComplexSampleListener +{ + private InterpolatingSymbolBuffer mInterpolatingSymbolBuffer; + private IPhaseLockedLoop mPLL; + private ISymbolPhaseErrorCalculator mSymbolPhaseErrorCalculator; + private IQPSKSymbolDecoder mQPSKSymbolDecoder; + private GardnerDetector mGardnerDetector = new GardnerDetector(); + private Listener mSymbolListener; + private Listener mPLLErrorListener; + private Listener mDibitListener; + + private Complex mPreviousSample = new Complex(0, 0); + private Complex mPreviousMiddleSample = new Complex(0, 0); + private Complex mCurrentSample = new Complex(0, 0); + private Complex mMiddleSample = new Complex(0, 0); + private Complex mPreviousSymbol = new Complex(0, 0); + private Complex mMiddleSymbol = new Complex(0, 0); + private Complex mCurrentSymbol = new Complex(0, 0); + private float mPhaseError; + private float mSymbolTimingError; + + /** + * Decoder for Quaternary Phase Shift Keying (QPSK) and Differential QPSK (DQPSK). This decoder uses both a + * phase-locked loop and a gardner symbol timing error detector to automatically align to the incoming carrier + * frequency and to adjust for any changes in symbol timing. + * + * @param phaseLockedLoop for tracking carrier frequency error + * @param symbolPhaseErrorCalculator to calculate symbol phase errors and optionally adjust for differential encoding + * @param symbolDecoder to decode demodulated symbols into dibits + * @param sampleRate of the incoming complex sample stream + * @param symbolRate of the decoded QPSK symbols. + */ + public QPSKDemodulator(IPhaseLockedLoop phaseLockedLoop, ISymbolPhaseErrorCalculator symbolPhaseErrorCalculator, + IQPSKSymbolDecoder symbolDecoder, double sampleRate, double symbolRate) + { + if(sampleRate < (symbolRate * 2)) + { + throw new IllegalArgumentException("Sample rate [" + sampleRate + + "] must be at least 2 x symbol rate [" + symbolRate + "]"); + } + + mPLL = phaseLockedLoop; + mSymbolPhaseErrorCalculator = symbolPhaseErrorCalculator; + mQPSKSymbolDecoder = symbolDecoder; + + float samplesPerSymbol = (float)(sampleRate / symbolRate); + mInterpolatingSymbolBuffer = new InterpolatingSymbolBuffer(samplesPerSymbol); + } + + /** + * Submits a buffer containing complex samples for decoding + * @param complexBuffer with complex samples + */ + public void receive(ComplexBuffer complexBuffer) + { + float[] samples = complexBuffer.getSamples(); + + for(int x = 0; x < samples.length; x += 2) + { + receive(samples[x], samples[x + 1]); + } + } + + /** + * Submits a complex sample for decoding. + * @param inphase value for the sample + * @param quadrature value for the sample + */ + @Override + public void receive(float inphase, float quadrature) + { + //Update current sample with values + mCurrentSample.setValues(inphase, quadrature); + + //Mix current sample with costas loop to remove any rotation that is present from a mis-tuned carrier frequency + mCurrentSample.multiply(mPLL.incrementAndGetCurrentVector()); + + mInterpolatingSymbolBuffer.receive(mCurrentSample); + + //Calculate the symbol once we've stored enough samples + if(mInterpolatingSymbolBuffer.hasSymbol()) + { + //Get middle and current samples from the interpolating buffer + mMiddleSample = mInterpolatingSymbolBuffer.getMiddleSample(); + mCurrentSample = mInterpolatingSymbolBuffer.getCurrentSample(); + + //Calculate middle and current symbols as the delta between the previous and current samples + mMiddleSymbol.setInphase(Complex.multiplyInphase(mMiddleSample.inphase(), mMiddleSample.quadrature(), mPreviousMiddleSample.inphase(), -mPreviousMiddleSample.quadrature())); + mMiddleSymbol.setQuadrature(Complex.multiplyQuadrature(mMiddleSample.inphase(), mMiddleSample.quadrature(), mPreviousMiddleSample.inphase(), -mPreviousMiddleSample.quadrature())); + mCurrentSymbol.setInphase(Complex.multiplyInphase(mCurrentSample.inphase(), mCurrentSample.quadrature(), mPreviousSample.inphase(), -mPreviousSample.quadrature())); + mCurrentSymbol.setQuadrature(Complex.multiplyQuadrature(mCurrentSample.inphase(), mCurrentSample.quadrature(), mPreviousSample.inphase(), -mPreviousSample.quadrature())); + + //Set gain to unity before we calculate the error value + mMiddleSymbol.normalize(); + mCurrentSymbol.normalize(); + + //Symbol timing error calculations + mSymbolTimingError = mGardnerDetector.getError(mPreviousSymbol, mMiddleSymbol, mCurrentSymbol); + mInterpolatingSymbolBuffer.resetAndAdjust(mSymbolTimingError); + + //Store current samples/symbols to use for the next period + mPreviousSample.setValues(mCurrentSample); + mPreviousMiddleSample.setValues(mMiddleSample); + mPreviousSymbol.setValues(mCurrentSymbol); + + //Adjust the current symbol as necessary (e.g. for differential encoding) + mSymbolPhaseErrorCalculator.adjust(mCurrentSymbol); + + //Calculate the phase error of the current symbol relative to the expected constellation and provide + //feedback to the PLL + mPhaseError = mSymbolPhaseErrorCalculator.getPhaseError(mCurrentSymbol); + mPLL.adjust(mPhaseError); + + if(mPLLErrorListener != null) + { + mPLLErrorListener.receive((double)mPhaseError); + } + + //Decode the dibit from the symbol and send to the listener + if(mDibitListener != null) + { + mDibitListener.receive(mQPSKSymbolDecoder.decode(mCurrentSymbol)); + } + + //Send to an external constellation symbol listener when registered + if(mSymbolListener != null) + { + mSymbolListener.receive(mCurrentSymbol); + } + + //TODO: Assemble dibits here and broadcast + } + } + + /** + * Registers the listener to receive decoded QPSK symbols + */ + public void setSymbolListener(Listener listener) + { + mSymbolListener = listener; + } + + /** + * Registers the listener to receive decoded QPSK dibits + */ + public void setDibitListener(Listener listener) + { + mDibitListener = listener; + } + + /** + * Registers the listener to receive PLL error values + */ + public void setPLLErrorListener(Listener listener) + { + mPLLErrorListener = listener; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/psk/QPSKPolarSlicer.java b/src/main/java/io/github/dsheirer/dsp/psk/QPSKSymbolDecoder.java similarity index 59% rename from src/main/java/io/github/dsheirer/dsp/psk/QPSKPolarSlicer.java rename to src/main/java/io/github/dsheirer/dsp/psk/QPSKSymbolDecoder.java index 3ff0c708..42a97b6c 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/QPSKPolarSlicer.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/QPSKSymbolDecoder.java @@ -1,3 +1,18 @@ +/******************************************************************************* + * 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.psk; import io.github.dsheirer.dsp.symbol.Dibit; @@ -5,24 +20,7 @@ import io.github.dsheirer.sample.Broadcaster; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.complex.Complex; -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014,2015 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 - ******************************************************************************/ -public class QPSKPolarSlicer implements Listener +public class QPSKSymbolDecoder implements Listener, IQPSKSymbolDecoder { private Broadcaster mBroadcaster = new Broadcaster(); @@ -37,7 +35,7 @@ public class QPSKPolarSlicer implements Listener * / \ * / 11 \ */ - public QPSKPolarSlicer() + public QPSKSymbolDecoder() { } @@ -60,10 +58,10 @@ public class QPSKPolarSlicer implements Listener @Override public void receive( Complex complex ) { - mBroadcaster.broadcast( decide( complex ) ); + mBroadcaster.broadcast( decode( complex ) ); } - public static Dibit decide( Complex complex ) + public Dibit decode(Complex complex ) { if( Math.abs( complex.inphase() ) > Math.abs( complex.quadrature() ) ) { diff --git a/src/main/java/io/github/dsheirer/dsp/psk/QPSKSymbolPhaseErrorCalculator.java b/src/main/java/io/github/dsheirer/dsp/psk/QPSKSymbolPhaseErrorCalculator.java new file mode 100644 index 00000000..d7c94323 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/QPSKSymbolPhaseErrorCalculator.java @@ -0,0 +1,63 @@ +/******************************************************************************* + * 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.psk; + +import io.github.dsheirer.sample.complex.Complex; + +/** + * Calculates the phase error for a QPSK polar-orientation constellation + */ +public class QPSKSymbolPhaseErrorCalculator implements ISymbolPhaseErrorCalculator +{ + private float mPhaseError; + + @Override + public void adjust(Complex symbol) + { + //No-op - since there is no differential, we don't have to adjust + } + + @Override + public float getPhaseError(Complex symbol) + { + mPhaseError = 0.0f; + + if(Math.abs(symbol.inphase()) > Math.abs(symbol.quadrature())) + { + if(symbol.inphase() > 0) + { + mPhaseError = -symbol.quadrature(); + } + else + { + mPhaseError = symbol.quadrature(); + } + } + else + { + if(symbol.quadrature() > 0) + { + mPhaseError = symbol.inphase(); + } + else + { + mPhaseError = -symbol.inphase(); + } + } + + return mPhaseError; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/psk/SymbolInversion.java b/src/main/java/io/github/dsheirer/dsp/psk/SymbolInversion.java new file mode 100644 index 00000000..0b7431c9 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/SymbolInversion.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * 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.psk; + +public enum SymbolInversion +{ + DEGREES_90_PLUS(Math.PI / 2.0), + DEGREES_90_MINUS(Math.PI / -2.0), + DEGREES_180(Math.PI); + + private double mAngleRadians; + + SymbolInversion(double angleRadians) + { + mAngleRadians = angleRadians; + } + + public double getError() + { + return mAngleRadians; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/psk/pll/CostasLoop.java b/src/main/java/io/github/dsheirer/dsp/psk/pll/CostasLoop.java new file mode 100644 index 00000000..71d07392 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/pll/CostasLoop.java @@ -0,0 +1,337 @@ +/******************************************************************************* + * 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.psk.pll; + +import io.github.dsheirer.buffer.DoubleCircularBuffer; +import io.github.dsheirer.dsp.mixer.Oscillator; +import io.github.dsheirer.sample.complex.Complex; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.text.DecimalFormat; + +/** + * Costas Loop - phase locked loop designed to automatically synchronize to the incoming carrier frequency in order to + * zeroize any frequency offset inherent in the signal due to either mis-tuning or carrier frequency drift. + * + * Costas loop structure was modeled from KA1RBI's OP25/cqpsk.py + * + * Loop bandwidth and alpha/beta gain formulas derived from: + * http://www.trondeau.com/blog/2011/8/13/control-loop-gain-values.html + */ +public class CostasLoop implements IPhaseLockedLoop +{ + private final static Logger mLog = LoggerFactory.getLogger(CostasLoop.class); + public static final double TWO_PI = 2.0 * Math.PI; + private DoubleCircularBuffer mErrorBuffer = new DoubleCircularBuffer(10); + private double mDamping = Math.sqrt(2.0) / 2.0; + private double mAlphaGain; + private double mBetaGain; + private double mLoopPhase = 0.0; + private double mLoopFrequency = 0.0; + private double mMaximumLoopFrequency; + private Complex mCurrentVector = new Complex(0, 0); + + private Tracking mTracking = Tracking.SEARCHING; + private ITrackingStateListener mTrackingStateListener; + private boolean mAutomaticTrackingEnabled = true; + + /** + * Costas Loop for tracking and correcting frequency error in a received carrier signal. + * + * @param sampleRate of the incoming samples + * @param symbolRate of the digital signal + */ + public CostasLoop(double sampleRate, double symbolRate) + { + mMaximumLoopFrequency = TWO_PI * (symbolRate / 2.0) / sampleRate; + updateLoopBandwidth(); + } + + /** + * Corrects the current phase tracker when an inverted output is detected. The inversion will take the form of + * +/- 90 degrees or +/- 180 degrees, although the latter correction is equal regardless of the sign. + * + * If the supplied correction value places the loop frequency outside of the max frequency, then the frequency will + * be corrected 360 degrees in the opposite direction to maintain within the max frequency bounds. + * + * @param correction as measured in radians + */ + public void correctInversion(double correction) + { + mLoopFrequency += correction; + + if(mLoopFrequency > mMaximumLoopFrequency) + { + mLoopFrequency -= 2.0 * mMaximumLoopFrequency; + } + + if(mLoopFrequency < -mMaximumLoopFrequency) + { + mLoopFrequency += 2.0 * mMaximumLoopFrequency; + } + } + + /** + * Updates the loop bandwidth and alpha/beta gains according to the current loop synchronization state. + */ + private void updateLoopBandwidth() + { + double bandwidth = TWO_PI / mTracking.getLoopBandwidth(); + + mAlphaGain = (4.0 * mDamping * bandwidth) / (1.0 + (2.0 * mDamping * bandwidth) + (bandwidth * bandwidth)); + mBetaGain = (4.0 * bandwidth * bandwidth) / (1.0 + (2.0 * mDamping * bandwidth) + (bandwidth * bandwidth)); + } + + /** + * Sets the tracking state for this costas loop. The tracking state affects the aggressiveness of the alpha/beta + * gain values in synchronizing with the signal carrier. + * + * @param tracking + */ + public void setTracking(Tracking tracking) + { + mTracking = tracking; + updateLoopBandwidth(); + + mLog.debug("Tracking: " + tracking.name()); + + if(mTrackingStateListener != null) + { + mTrackingStateListener.trackingStateChanged(tracking); + } + } + + /** + * Sets the listener to receive notifications when the tracking state changes + */ + public void setTrackingStateListener(ITrackingStateListener listener) + { + mTrackingStateListener = listener; + } + + /** + * Enables or disables automatic tracking state updates. When enabled, the loop will automatically increase or + * decrease the loop alpha/beta gain values based on the variance of the error tracking values submitted to the + * loop control. As the variance decreases, so will the gain values, and vice-versa. Setting this to disabled + * will force the tracker to retain the gain values associated with either the default or user-specified tracking + * value. + * + * @param enabled true (default) to enable automatic gain tracking + */ + public void setAutomaticTracking(boolean enabled) + { + mAutomaticTrackingEnabled = enabled; + } + + /** + * Increments the phase of the loop for each sample received at the sample rate. + */ + public void increment() + { + mLoopPhase += mLoopFrequency; + + /* Keep the loop phase in bounds */ + normalizePhase(); + } + + /** + * Current vector of the loop. Note: this value is updated for the current angle in radians each time this method + * is invoked. + */ + public Complex getCurrentVector() + { + mCurrentVector.setAngle(mLoopPhase); + return mCurrentVector; + } + + @Override + public Complex incrementAndGetCurrentVector() + { + increment(); + return getCurrentVector(); + } + + public double getLoopFrequency() + { + return mLoopFrequency; + } + + public double getMaximumLoopFrequency() + { + return mMaximumLoopFrequency; + } + + /** + * Updates the costas loop frequency and phase to adjust for the phase + * error value + * + * @param phaseError - (-)= late and (+)= early + */ + public void adjust(double phaseError) + { + mLoopFrequency += (mBetaGain * phaseError); + mLoopPhase += mLoopFrequency + (mAlphaGain * phaseError); + + /* Maintain phase between +/- 2 * PI */ + normalizePhase(); + + /* Limit frequency to +/- maximum loop frequency */ + limitFrequency(); + + mErrorBuffer.put(phaseError); + + double variance = mErrorBuffer.variance(); + + switch(mTracking) + { + case SEARCHING: + if(Tracking.COARSE.meetsThreshold(variance)) + { + setTracking(Tracking.COARSE); + } + break; + case COARSE: + if(Tracking.COARSE.meetsThreshold(variance)) + { + if(Tracking.FINE.meetsThreshold(variance)) + { + setTracking(Tracking.FINE); + } + } + else + { + setTracking(Tracking.SEARCHING); + } + break; + case FINE: + if(Tracking.FINE.meetsThreshold(variance)) + { + if(Tracking.LOCKED.meetsThreshold(variance)) + { + setTracking(Tracking.LOCKED); + } + } + else + { + setTracking(Tracking.COARSE); + } + break; + case LOCKED: + if(!Tracking.LOCKED.meetsThreshold(variance)) + { + setTracking(Tracking.FINE); + } + break; + } + } + + public double getErrorMean() + { + return mErrorBuffer.mean(); + } + + public double getErrorVariance() + { + return mErrorBuffer.variance(); + } + + /** + * Normalizes the phase tracker to maintain within +/- 2 Pi + */ + private void normalizePhase() + { + while(mLoopPhase > TWO_PI) + { + mLoopPhase -= TWO_PI; + } + + while(mLoopPhase < -TWO_PI) + { + mLoopPhase += TWO_PI; + } + } + + /** + * Constrains the frequency within the bounds of +/- loop frequency + */ + private void limitFrequency() + { + if(mLoopFrequency > mMaximumLoopFrequency) + { + mLoopFrequency = mMaximumLoopFrequency; + } + + if(mLoopFrequency < -mMaximumLoopFrequency) + { + mLoopFrequency = -mMaximumLoopFrequency; + } + } + + /** + * Tracking state listener interface for receiving updates when the tracking state of the costas loop changes + */ + public interface ITrackingStateListener + { + void trackingStateChanged(Tracking tracking); + } + + public static void main(String[] arguments) + { + DecimalFormat decimalFormat = new DecimalFormat("0.000000"); + double sampleRate = 25000.0; + int errorFrequency = 411; + double loopBandwidth = 50.0; + + Oscillator oscillator = new Oscillator(errorFrequency, (int)sampleRate); + CostasLoop costasLoop = new CostasLoop(sampleRate, 4800.0); + costasLoop.setTrackingStateListener(new ITrackingStateListener() + { + @Override + public void trackingStateChanged(Tracking tracking) + { + mLog.debug("Tracking State Changed: " + tracking.name()); + } + }); + + for(int x = 0; x < 1000; x++) + { + Complex complex = oscillator.getComplex(); + + oscillator.rotate(); + costasLoop.increment(); + + if(x % 5 == 0) + { + complex.multiply(costasLoop.getCurrentVector().conjugate()); + + double phaseError = complex.angle(); + costasLoop.adjust(phaseError); + + double loopFrequency = costasLoop.getLoopFrequency() * sampleRate / TWO_PI; + double maxFrequency = costasLoop.getMaximumLoopFrequency() * sampleRate / TWO_PI; + + mLog.debug(x + " Frequency: " + errorFrequency + + " Loop:" + decimalFormat.format(loopFrequency) + + " Max:" + maxFrequency + + " Detected Error:" + decimalFormat.format(phaseError) + + " Avg:" + decimalFormat.format(costasLoop.getErrorMean()) + + " Var:" + decimalFormat.format(costasLoop.getErrorVariance())); + } + } + } + +} diff --git a/src/main/java/io/github/dsheirer/dsp/psk/pll/IPhaseLockedLoop.java b/src/main/java/io/github/dsheirer/dsp/psk/pll/IPhaseLockedLoop.java new file mode 100644 index 00000000..1f4f2a52 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/pll/IPhaseLockedLoop.java @@ -0,0 +1,56 @@ +/******************************************************************************* + * 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.psk.pll; + +import io.github.dsheirer.sample.complex.Complex; + +/** + * Phase-Locked Loop (PLL) interface + */ +public interface IPhaseLockedLoop +{ + /** + * Increments the PLL by one sample period + */ + void increment(); + + /** + * Applies an error value to the PLL. + * + * @param error value in the range of -PI <> +PI + */ + void adjust(double error); + + /** + * Adjusts the current phase of the loop to account for +/-90 or 180 degree inverted phase locks when detected + * @param correction to apply to the phase of the loop + */ + void correctInversion(double correction); + + /** + * Current vector of the PLL to use in de-spinning the incoming samples. + * @return complex vector + */ + Complex getCurrentVector(); + + /** + * Increments the phase rotation and gets the current vector of the PLL to use in de-spinning the incoming samples. + * @return complex vector + */ + Complex incrementAndGetCurrentVector(); + + +} diff --git a/src/main/java/io/github/dsheirer/dsp/psk/pll/Tracking.java b/src/main/java/io/github/dsheirer/dsp/psk/pll/Tracking.java new file mode 100644 index 00000000..5cabc7ab --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/pll/Tracking.java @@ -0,0 +1,48 @@ +/******************************************************************************* + * 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.psk.pll; + +public enum Tracking +{ + SEARCHING(50.0, Math.PI), + COARSE(100.0, 0.01), + FINE(150.0, 0.001), + LOCKED(200.0, 0.0001); + + private double mLoopBandwidth; + private double mVarianceThreshold; + + Tracking(double loopBandwidth, double varianceThreshold) + { + mLoopBandwidth = loopBandwidth; + mVarianceThreshold = varianceThreshold; + } + + public double getLoopBandwidth() + { + return mLoopBandwidth; + } + + public double getVarianceThreshold() + { + return mVarianceThreshold; + } + + public boolean meetsThreshold(double variance) + { + return variance < getVarianceThreshold(); + } +} diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/DemodulatorViewerFX.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/DemodulatorViewerFX.java index d46e5463..0046606f 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/DemodulatorViewerFX.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/DemodulatorViewerFX.java @@ -15,6 +15,8 @@ ******************************************************************************/ package io.github.dsheirer.instrument.gui.viewer; +import io.github.dsheirer.instrument.gui.viewer.decoder.DecoderPaneFactory; +import io.github.dsheirer.module.decode.DecoderType; import javafx.application.Application; import javafx.application.Platform; import javafx.event.ActionEvent; @@ -44,7 +46,7 @@ public class DemodulatorViewerFX extends Application public void start(Stage primaryStage) throws Exception { mStage = primaryStage; - mStage.setTitle("Demodulator Viewer FX"); + setTitle(null); BorderPane borderPane = new BorderPane(); borderPane.setTop(getMenuBar()); @@ -56,6 +58,18 @@ public class DemodulatorViewerFX extends Application mStage.show(); } + private void setTitle(DecoderType decoderType) + { + if(decoderType != null) + { + mStage.setTitle("Demodulator Viewer FX - " + decoderType.getDisplayString()); + } + else + { + mStage.setTitle("Demodulator Viewer FX"); + } + } + private ViewerDesktop getViewerDesktop() { if(mViewerDesktop == null) @@ -72,9 +86,9 @@ public class DemodulatorViewerFX extends Application { mMenuBar = new MenuBar(); - Menu menuFile = new Menu("File"); - MenuItem menuItemOpen = new MenuItem("Open ..."); - menuItemOpen.setOnAction(new EventHandler() + Menu fileMenu = new Menu("File"); + MenuItem openMenuItem = new MenuItem("Open ..."); + openMenuItem.setOnAction(new EventHandler() { @Override public void handle(ActionEvent event) @@ -87,8 +101,8 @@ public class DemodulatorViewerFX extends Application } }); - MenuItem menuItemClose = new MenuItem("Close"); - menuItemClose.setOnAction(new EventHandler() + MenuItem closeMenuItem = new MenuItem("Close"); + closeMenuItem.setOnAction(new EventHandler() { @Override public void handle(ActionEvent event) @@ -97,8 +111,8 @@ public class DemodulatorViewerFX extends Application } }); - MenuItem menuItemExit = new MenuItem("Exit"); - menuItemExit.setOnAction(new EventHandler() + MenuItem exitMenuItem = new MenuItem("Exit"); + exitMenuItem.setOnAction(new EventHandler() { @Override public void handle(ActionEvent event) @@ -107,10 +121,34 @@ public class DemodulatorViewerFX extends Application } }); - menuFile.getItems().addAll(menuItemOpen, getRecentFilesMenu(), menuItemClose, new SeparatorMenuItem(), - menuItemExit); + fileMenu.getItems().addAll(openMenuItem, getRecentFilesMenu(), closeMenuItem, new SeparatorMenuItem(), + exitMenuItem); - mMenuBar.getMenus().addAll(menuFile); + mMenuBar.getMenus().add(fileMenu); + + Menu decoderMenu = new Menu("Decoder"); + + for(DecoderType decoderType: DecoderType.values()) + { + if(DecoderPaneFactory.isSupported(decoderType)) + { + MenuItem decoderMenuItem = new MenuItem(decoderType.getShortDisplayString()); + + decoderMenuItem.setOnAction(new EventHandler() + { + @Override + public void handle(ActionEvent event) + { + getViewerDesktop().setDecoder(decoderType); + setTitle(decoderType); + } + }); + + decoderMenu.getItems().add(decoderMenuItem); + } + } + + mMenuBar.getMenus().add(decoderMenu); } return mMenuBar; diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/PlaybackController.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/PlaybackController.java index 94a958f2..646f41cf 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/PlaybackController.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/PlaybackController.java @@ -15,6 +15,7 @@ ******************************************************************************/ package io.github.dsheirer.instrument.gui.viewer; +import io.github.dsheirer.instrument.gui.viewer.decoder.DecoderPane; import io.github.dsheirer.sample.Broadcaster; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.complex.ComplexBuffer; @@ -52,6 +53,7 @@ public class PlaybackController extends HBox implements IFrameLocationListener, private HBox mControlsBox; private Label mFileLabel; private EventHandler mPlayEventHandler; + private DecoderPane mSampleRateListener; private IControllableFileSource mControllableFileSource; private Broadcaster mComplexBufferBroadcaster = new Broadcaster<>(); @@ -65,6 +67,16 @@ public class PlaybackController extends HBox implements IFrameLocationListener, setControlsEnabled(false); } + public void setSampleRateListener(DecoderPane decoderPane) + { + mSampleRateListener = decoderPane; + + if(mSampleRateListener != null && mControllableFileSource != null) + { + mSampleRateListener.setSampleRate(mControllableFileSource.getSampleRate()); + } + } + public void load(File file) { mLog.debug("loading:" + file.getAbsolutePath()); @@ -76,6 +88,11 @@ public class PlaybackController extends HBox implements IFrameLocationListener, mControllableFileSource.setListener(this); ((ComplexWaveSource)mControllableFileSource).setListener((Listener)this); mControllableFileSource.open(); + + if(mSampleRateListener != null) + { + mSampleRateListener.setSampleRate(mControllableFileSource.getSampleRate()); + } } catch(UnsupportedAudioFileException e) { diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/ViewerDesktop.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/ViewerDesktop.java index 38e84acc..bdd01f5d 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/ViewerDesktop.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/ViewerDesktop.java @@ -15,25 +15,22 @@ ******************************************************************************/ package io.github.dsheirer.instrument.gui.viewer; +import io.github.dsheirer.instrument.gui.viewer.decoder.DecoderPane; +import io.github.dsheirer.instrument.gui.viewer.decoder.DecoderPaneFactory; +import io.github.dsheirer.module.decode.DecoderType; import javafx.scene.layout.BorderPane; -import javafx.scene.layout.HBox; -import javafx.scene.layout.VBox; import java.io.File; public class ViewerDesktop extends BorderPane { private PlaybackController mPlaybackController; - private VBox mChartBox; - private ComplexSampleLineChart mSampleLineChart; - private ComplexPhaseLineChart mPhaseLineChart; + private DecoderPane mDecoderPane = DecoderPaneFactory.getDefaultPane(); public ViewerDesktop() { setBottom(getPlaybackController()); - getPlaybackController().addListener(getSampleLineChart()); - getPlaybackController().addListener(getPhaseLineChart()); - setCenter(getChartBox()); + setCenter(getDecoderPane()); } public void load(File file) @@ -42,6 +39,31 @@ public class ViewerDesktop extends BorderPane getPlaybackController().load(file); } + public void setDecoder(DecoderType decoderType) + { + getPlaybackController().removeListener(getDecoderPane()); + getChildren().remove(getDecoderPane()); + + mDecoderPane = DecoderPaneFactory.getDecoderPane(decoderType); + setCenter(getDecoderPane()); + getPlaybackController().addListener(getDecoderPane()); + + getPlaybackController().setSampleRateListener(getDecoderPane()); + } + + /** + * Decoder pane + */ + public DecoderPane getDecoderPane() + { + if(mDecoderPane == null) + { + mDecoderPane = DecoderPaneFactory.getDefaultPane(); + } + + return mDecoderPane; + } + public void close() { getPlaybackController().close(); @@ -56,35 +78,4 @@ public class ViewerDesktop extends BorderPane return mPlaybackController; } - - private VBox getChartBox() - { - if(mChartBox == null) - { - mChartBox = new VBox(); - mChartBox.getChildren().addAll(getSampleLineChart(), getPhaseLineChart()); - } - - return mChartBox; - } - - private ComplexSampleLineChart getSampleLineChart() - { - if(mSampleLineChart == null) - { - mSampleLineChart = new ComplexSampleLineChart(40); - } - - return mSampleLineChart; - } - - private ComplexPhaseLineChart getPhaseLineChart() - { - if(mPhaseLineChart == null) - { - mPhaseLineChart = new ComplexPhaseLineChart(50); - } - - return mPhaseLineChart; - } } diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/ComplexPhaseLineChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexPhaseLineChart.java similarity index 94% rename from src/main/java/io/github/dsheirer/instrument/gui/viewer/ComplexPhaseLineChart.java rename to src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexPhaseLineChart.java index 9ec6d77f..68d86efb 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/ComplexPhaseLineChart.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexPhaseLineChart.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.gui.viewer; +package io.github.dsheirer.instrument.gui.viewer.chart; import io.github.dsheirer.buffer.ComplexCircularBuffer; import io.github.dsheirer.dsp.gain.ComplexGain; @@ -28,7 +28,6 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.scene.chart.LineChart; import javafx.scene.chart.NumberAxis; -import org.apache.commons.math3.complex.ComplexUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,7 +35,7 @@ public class ComplexPhaseLineChart extends LineChart implements Listener> mPhaseValues = FXCollections.observableArrayList(); @@ -44,8 +43,8 @@ public class ComplexPhaseLineChart extends LineChart implements Listener phaseSeries = new Series<>("Phase", mPhaseValues); ObservableList> observableList = FXCollections.observableArrayList(phaseSeries); diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/ComplexSampleLineChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexSampleLineChart.java similarity index 96% rename from src/main/java/io/github/dsheirer/instrument/gui/viewer/ComplexSampleLineChart.java rename to src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexSampleLineChart.java index 7044bdac..13f49d7d 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/ComplexSampleLineChart.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexSampleLineChart.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.gui.viewer; +package io.github.dsheirer.instrument.gui.viewer.chart; import io.github.dsheirer.buffer.ComplexCircularBuffer; import io.github.dsheirer.dsp.gain.ComplexGain; @@ -44,8 +44,8 @@ public class ComplexSampleLineChart extends LineChart implements Listener iSampleSeries = new LineChart.Series<>("Inphase", mISamples); LineChart.Series qSampleSeries = new LineChart.Series<>("Quadrature", mQSamples); diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/PhaseLineChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/PhaseLineChart.java new file mode 100644 index 00000000..3857026d --- /dev/null +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/PhaseLineChart.java @@ -0,0 +1,73 @@ +/******************************************************************************* + * 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.instrument.gui.viewer.chart; + +import io.github.dsheirer.buffer.DoubleCircularBuffer; +import io.github.dsheirer.sample.Listener; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.chart.LineChart; +import javafx.scene.chart.NumberAxis; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class PhaseLineChart extends LineChart implements Listener +{ + private final static Logger mLog = LoggerFactory.getLogger(PhaseLineChart.class); + + private DoubleCircularBuffer mDoubleCircularBuffer; + private ObservableList> mPhaseValues = FXCollections.observableArrayList(); + + public PhaseLineChart(int length) + { + super(new NumberAxis("Time", 1, length, 2), + new NumberAxis("Phase", -Math.PI, Math.PI, Math.PI / 2.0)); + + Series phaseSeries = new Series<>("Phase", mPhaseValues); + ObservableList> observableList = FXCollections.observableArrayList(phaseSeries); + + setData(observableList); + init(length); + } + + private void init(int length) + { + mDoubleCircularBuffer = new DoubleCircularBuffer(length); + + for(int x = 1; x <= length; x++) + { + mPhaseValues.add(new Data(x, 0.0)); + } + } + + @Override + public void receive(Double phase) + { + mDoubleCircularBuffer.put(phase); + + updateChart(); + } + + private void updateChart() + { + double[] values = mDoubleCircularBuffer.getAll(); + + for(int x = 0; x < values.length; x++) + { + mPhaseValues.get(x).setYValue(values[x]); + } + } +} diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/SymbolChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/SymbolChart.java new file mode 100644 index 00000000..5d67bb7d --- /dev/null +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/SymbolChart.java @@ -0,0 +1,90 @@ +/******************************************************************************* + * 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.instrument.gui.viewer.chart; + +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.complex.Complex; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.chart.NumberAxis; +import javafx.scene.chart.ScatterChart; +import javafx.scene.chart.XYChart; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SymbolChart extends ScatterChart implements Listener +{ + private final static Logger mLog = LoggerFactory.getLogger(SymbolChart.class); + + private ObservableList mCurrentPoints = FXCollections.observableArrayList(); + private ObservableList mPreviousPoints = FXCollections.observableArrayList(); + private ObservableList mOldestPoints = FXCollections.observableArrayList(); + private int mDataPointer; + private int mHistory; + + /** + * Symbol chart for plotting a series of constellation symbols over time. Maintains three history sets for + * current, previous, and oldest symbols. + * + * @param history size for each of the history sets + */ + public SymbolChart(int history) + { + super(new NumberAxis("I", -1.0, 1.0, 0.2), + new NumberAxis("Q", -1.0, 1.0, 0.2)); + + mHistory = history; + + for(int x = 0; x < mHistory; x++) + { + mCurrentPoints.add(new XYChart.Data<>(0.0, 0.0)); + mPreviousPoints.add(new XYChart.Data<>(0.0, 0.0)); + mOldestPoints.add(new XYChart.Data<>(0.0, 0.0)); + } + + ObservableList data = FXCollections.observableArrayList(); + data.add(new ScatterChart.Series("Current", mCurrentPoints)); + data.add(new ScatterChart.Series("Previous", mPreviousPoints)); + data.add(new ScatterChart.Series("Oldest", mOldestPoints)); + + this.setData(data); + } + + @Override + public void receive(Complex complex) + { + //Transfer previous to oldest + XYChart.Data oldest = mOldestPoints.get(mDataPointer); + XYChart.Data previous = mPreviousPoints.get(mDataPointer); + XYChart.Data current = mCurrentPoints.get(mDataPointer); + + oldest.setXValue(previous.getXValue()); + oldest.setYValue(previous.getYValue()); + + previous.setXValue(current.getXValue()); + previous.setYValue(current.getYValue()); + + current.setXValue(complex.inphase()); + current.setYValue(complex.quadrature()); + + mDataPointer++; + + if(mDataPointer >= mHistory) + { + mDataPointer = 0; + } + } +} diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPane.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPane.java new file mode 100644 index 00000000..70c39c2e --- /dev/null +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPane.java @@ -0,0 +1,62 @@ +/******************************************************************************* + * 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.instrument.gui.viewer.decoder; + +import io.github.dsheirer.module.decode.DecoderType; +import io.github.dsheirer.sample.Broadcaster; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.complex.ComplexBuffer; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; + +public class DecoderPane extends VBox implements Listener +{ + private Broadcaster mComplexBufferBroadcaster = new Broadcaster<>(); + private DecoderType mDecoderType; + + public DecoderPane(DecoderType decoderType) + { + mDecoderType = decoderType; + } + + /** + * Package private - used by factory class to create an empty pane + */ + DecoderPane() + { + getChildren().add(new Text("Please select a decoder from the menu")); + } + + @Override + public void receive(ComplexBuffer complexBuffer) + { + mComplexBufferBroadcaster.broadcast(complexBuffer); + } + + public void addListener(Listener listener) + { + mComplexBufferBroadcaster.addListener(listener); + } + + public void removeListener(Listener listener) + { + mComplexBufferBroadcaster.removeListener(listener); + } + + public void setSampleRate(double sampleRate) + { + } +} diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPaneFactory.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPaneFactory.java new file mode 100644 index 00000000..b069204e --- /dev/null +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPaneFactory.java @@ -0,0 +1,55 @@ +/******************************************************************************* + * 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.instrument.gui.viewer.decoder; + +import io.github.dsheirer.module.decode.DecoderType; + +import java.util.EnumSet; + +public class DecoderPaneFactory +{ + private static final EnumSet SUPPORTED_DECODER_TYPES = EnumSet.of(DecoderType.P25_PHASE1); + + /** + * Creates a decoder pane for the decoder type + */ + public static DecoderPane getDecoderPane(DecoderType decoderType) + { + switch(decoderType) + { + case P25_PHASE1: + return new P25Phase1Pane(); + } + + return getDefaultPane(); + } + + /** + * Creates an empty decoder pane + */ + public static DecoderPane getDefaultPane() + { + return new DecoderPane(); + } + + /** + * Indicates if the decoder type is supported by this factory + */ + public static boolean isSupported(DecoderType decoderType) + { + return SUPPORTED_DECODER_TYPES.contains(decoderType); + } +} diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1Pane.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1Pane.java new file mode 100644 index 00000000..62253028 --- /dev/null +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1Pane.java @@ -0,0 +1,141 @@ +/******************************************************************************* + * 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.instrument.gui.viewer.decoder; + +import io.github.dsheirer.instrument.gui.viewer.chart.ComplexPhaseLineChart; +import io.github.dsheirer.instrument.gui.viewer.chart.ComplexSampleLineChart; +import io.github.dsheirer.instrument.gui.viewer.chart.PhaseLineChart; +import io.github.dsheirer.instrument.gui.viewer.chart.SymbolChart; +import io.github.dsheirer.module.decode.DecoderType; +import io.github.dsheirer.module.decode.p25.P25_C4FMDecoder2; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class P25Phase1Pane extends DecoderPane +{ + private final static Logger mLog = LoggerFactory.getLogger(P25Phase1Pane.class); + + private HBox mSampleChartBox; + private ComplexSampleLineChart mSampleLineChart; + private ComplexPhaseLineChart mPhaseLineChart; + private HBox mDecoderChartBox; + private SymbolChart mSymbolChart; + private PhaseLineChart mPLLPhaseErrorLineChart; + private P25_C4FMDecoder2 mDecoder = new P25_C4FMDecoder2(null); + + public P25Phase1Pane() + { + super(DecoderType.P25_PHASE1); + init(); + } + + private void init() + { + addListener(getSampleLineChart()); + addListener(getPhaseLineChart()); + addListener(getDecoder()); + + getDecoder().setSymbolListener(getSymbolChart()); + getDecoder().setPLLPhaseErrorListener(getPLLPhaseErrorLineChart()); + + getChildren().addAll(getSampleChartBox(), getDecoderChartBox()); + } + + @Override + public void setSampleRate(double sampleRate) + { + mLog.debug("Configuring for sample rate: " + sampleRate); + + mDecoder.setSampleRate(sampleRate); + } + + private P25_C4FMDecoder2 getDecoder() + { + return mDecoder; + } + + private SymbolChart getSymbolChart() + { + if(mSymbolChart == null) + { + mSymbolChart = new SymbolChart(20); + } + + return mSymbolChart; + } + + private HBox getDecoderChartBox() + { + if(mDecoderChartBox == null) + { + mDecoderChartBox = new HBox(); + getSymbolChart().setMaxWidth(Double.MAX_VALUE); + getPLLPhaseErrorLineChart().setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(getSymbolChart(), Priority.ALWAYS); + HBox.setHgrow(getPLLPhaseErrorLineChart(), Priority.ALWAYS); + mDecoderChartBox.getChildren().addAll(getSymbolChart(), getPLLPhaseErrorLineChart()); + } + + return mDecoderChartBox; + } + + private HBox getSampleChartBox() + { + if(mSampleChartBox == null) + { + mSampleChartBox = new HBox(); + getSampleLineChart().setMaxWidth(Double.MAX_VALUE); + getPhaseLineChart().setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(getSampleLineChart(), Priority.ALWAYS); + HBox.setHgrow(getPhaseLineChart(), Priority.ALWAYS); + mSampleChartBox.getChildren().addAll(getSampleLineChart(), getPhaseLineChart()); + } + + return mSampleChartBox; + } + + private ComplexSampleLineChart getSampleLineChart() + { + if(mSampleLineChart == null) + { + mSampleLineChart = new ComplexSampleLineChart(40); + } + + return mSampleLineChart; + } + + private ComplexPhaseLineChart getPhaseLineChart() + { + if(mPhaseLineChart == null) + { + mPhaseLineChart = new ComplexPhaseLineChart(50); + } + + return mPhaseLineChart; + } + + private PhaseLineChart getPLLPhaseErrorLineChart() + { + if(mPLLPhaseErrorLineChart == null) + { + mPLLPhaseErrorLineChart = new PhaseLineChart(80); + } + + return mPLLPhaseErrorLineChart; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/Decoder.java b/src/main/java/io/github/dsheirer/module/decode/Decoder.java index 9efeb0e7..f8f4953b 100644 --- a/src/main/java/io/github/dsheirer/module/decode/Decoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/Decoder.java @@ -1,17 +1,17 @@ /******************************************************************************* * SDR Trunk * Copyright (C) 2014-2016 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 ******************************************************************************/ @@ -22,39 +22,39 @@ import io.github.dsheirer.message.Message; import io.github.dsheirer.module.Module; import io.github.dsheirer.sample.Listener; -public abstract class Decoder extends Module - implements IMessageProvider, Listener +public abstract class Decoder extends Module implements IMessageProvider { - /* This has to be a broadcaster in order for references to persist */ - protected Listener mMessageListener; + /* This has to be a broadcaster in order for references to persist */ + private Listener mMessageDistributor = new MessageDistributor(); + protected Listener mMessageListener; - /** - * Decoder - parent class for all decoders, demodulators and components. - */ - public Decoder() - { - } + /** + * Decoder - parent class for all decoders, demodulators and components. + */ + public Decoder() + { + } - @Override - public void dispose() - { - mMessageListener = null; - } + @Override + public void dispose() + { + mMessageListener = null; + } - /** - * Identifies the decoder type (ie protocol) - */ - public abstract DecoderType getDecoderType(); + /** + * Identifies the decoder type (ie protocol) + */ + public abstract DecoderType getDecoderType(); /** * Adds a listener for receiving decoded messages from this decoder */ @Override - public void setMessageListener( Listener listener ) + public void setMessageListener(Listener listener) { - mMessageListener = listener; - } + mMessageListener = listener; + } /** * Removes the listener from receiving decoded messages from all attached @@ -63,23 +63,29 @@ public abstract class Decoder extends Module @Override public void removeMessageListener() { - mMessageListener = null; + mMessageListener = null; } /** - * Broadcasts the message to the registered message listener + * Message listener for receiving the output from this decoder */ - public void broadcast( Message message ) + protected Listener getMessageListener() { - if( mMessageListener != null ) - { - mMessageListener.receive( message ); - } + return mMessageDistributor; } - @Override - public void receive( Message message ) - { - broadcast( message ); - } + /** + * Distributes/forwards messages from sub-class decoder implementations to the registered message listener. + */ + public class MessageDistributor implements Listener + { + @Override + public void receive(Message message) + { + if(mMessageListener != null) + { + mMessageListener.receive(message); + } + } + } } diff --git a/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2Decoder.java b/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2Decoder.java index 072d28f4..629d5f5f 100644 --- a/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2Decoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2Decoder.java @@ -90,7 +90,7 @@ public class Fleetsync2Decoder extends Decoder implements IFilteredRealBufferLis mMessageProcessor = new Fleetsync2MessageProcessor( aliasList ); mMessageFramer.addMessageListener( mMessageProcessor ); - mMessageProcessor.setMessageListener( this ); + mMessageProcessor.setMessageListener( getMessageListener() ); } public void dispose() diff --git a/src/main/java/io/github/dsheirer/module/decode/lj1200/LJ1200Decoder.java b/src/main/java/io/github/dsheirer/module/decode/lj1200/LJ1200Decoder.java index fb19a8d2..a1ed1899 100644 --- a/src/main/java/io/github/dsheirer/module/decode/lj1200/LJ1200Decoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/lj1200/LJ1200Decoder.java @@ -100,7 +100,7 @@ public class LJ1200Decoder extends Decoder implements IFilteredRealBufferListene mMessageProcessor = new LJ1200MessageProcessor( aliasList ); mTowerMessageFramer.addMessageListener( mMessageProcessor ); mTransponderMessageFramer.addMessageListener( mMessageProcessor ); - mMessageProcessor.setMessageListener( this ); + mMessageProcessor.setMessageListener( getMessageListener() ); } @Override diff --git a/src/main/java/io/github/dsheirer/module/decode/ltrnet/LTRNetDecoder.java b/src/main/java/io/github/dsheirer/module/decode/ltrnet/LTRNetDecoder.java index 08041b72..f8e234b9 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ltrnet/LTRNetDecoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/ltrnet/LTRNetDecoder.java @@ -74,7 +74,7 @@ public class LTRNetDecoder extends Decoder mLTRMessageFramer.addMessageListener( mLTRMessageProcessor ); - mLTRMessageProcessor.setMessageListener( this ); + mLTRMessageProcessor.setMessageListener( getMessageListener() ); } @Override diff --git a/src/main/java/io/github/dsheirer/module/decode/ltrstandard/LTRStandardDecoder.java b/src/main/java/io/github/dsheirer/module/decode/ltrstandard/LTRStandardDecoder.java index 0a96486b..6836aabf 100644 --- a/src/main/java/io/github/dsheirer/module/decode/ltrstandard/LTRStandardDecoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/ltrstandard/LTRStandardDecoder.java @@ -64,7 +64,7 @@ public class LTRStandardDecoder extends Decoder implements IUnFilteredRealBuffer mLTRMessageProcessor = new LTRStandardMessageProcessor( direction, aliasList ); mLTRMessageFramer.addMessageListener( mLTRMessageProcessor ); - mLTRMessageProcessor.setMessageListener( this ); + mLTRMessageProcessor.setMessageListener( getMessageListener() ); } @Override diff --git a/src/main/java/io/github/dsheirer/module/decode/mdc1200/MDCDecoder.java b/src/main/java/io/github/dsheirer/module/decode/mdc1200/MDCDecoder.java index beaf51da..85cb9657 100644 --- a/src/main/java/io/github/dsheirer/module/decode/mdc1200/MDCDecoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/mdc1200/MDCDecoder.java @@ -102,7 +102,7 @@ public class MDCDecoder extends Decoder implements IFilteredRealBufferListener, /* Message Processor */ mMessageProcessor = new MDCMessageProcessor( aliasList ); mMessageFramer.addMessageListener( mMessageProcessor ); - mMessageProcessor.addMessageListener( this ); + mMessageProcessor.addMessageListener(getMessageListener()); } public void dispose() diff --git a/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327Decoder.java b/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327Decoder.java index d78d3670..ecd67c24 100644 --- a/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327Decoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327Decoder.java @@ -148,7 +148,7 @@ public class MPT1327Decoder extends Decoder implements IFilteredRealBufferListen /* Fully decoded and framed messages processor */ mMessageProcessor = new MPT1327MessageProcessor(aliasList); - mMessageProcessor.setMessageListener(this); + mMessageProcessor.setMessageListener(getMessageListener()); mControlMessageFramer.addMessageListener(mMessageProcessor); mTrafficMessageFramer.addMessageListener(mMessageProcessor); } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25Decoder.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25Decoder.java index 38e3fdfc..7851230d 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25Decoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25Decoder.java @@ -1,70 +1,91 @@ +/******************************************************************************* + * 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.module.decode.p25; import io.github.dsheirer.alias.AliasList; -import io.github.dsheirer.instrument.Instrumentable; import io.github.dsheirer.module.decode.Decoder; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeListener; import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeProvider; -public abstract class P25Decoder extends Decoder implements IFrequencyChangeListener, IFrequencyChangeProvider, Instrumentable +public abstract class P25Decoder extends Decoder implements IFrequencyChangeListener, IFrequencyChangeProvider { - private P25MessageProcessor mMessageProcessor; - - public P25Decoder( AliasList aliasList ) - { - mMessageProcessor = new P25MessageProcessor( aliasList ); - mMessageProcessor.setMessageListener( this ); - } - - public void dispose() - { - super.dispose(); - - mMessageProcessor.dispose(); - mMessageProcessor = null; - } + private P25MessageProcessor mMessageProcessor; + private AliasList mAliasList; - @Override - public DecoderType getDecoderType() + public P25Decoder(AliasList aliasList) { - return DecoderType.P25_PHASE1; + mAliasList = aliasList; + mMessageProcessor = new P25MessageProcessor(mAliasList); + mMessageProcessor.setMessageListener(getMessageListener()); } - protected P25MessageProcessor getMessageProcessor() - { - return mMessageProcessor; - } - - public abstract Modulation getModulation(); - - public enum Modulation - { - CQPSK( "Simulcast (LSM)", "LSM" ), - C4FM( "Normal (C4FM)", "C4FM" ); - - private String mLabel; - private String mShortLabel; - - private Modulation( String label, String shortLabel ) - { - mLabel = label; - mShortLabel = shortLabel; - } - - public String getLabel() - { - return mLabel; - } - - public String getShortLabel() - { - return mShortLabel; - } - - public String toString() - { - return getLabel(); - } - } + protected AliasList getAliasList() + { + return mAliasList; + } + + public void dispose() + { + super.dispose(); + + mMessageProcessor.dispose(); + mMessageProcessor = null; + } + + @Override + public DecoderType getDecoderType() + { + return DecoderType.P25_PHASE1; + } + + protected P25MessageProcessor getMessageProcessor() + { + return mMessageProcessor; + } + + public abstract Modulation getModulation(); + + public enum Modulation + { + CQPSK("Simulcast (LSM)", "LSM"), + C4FM("Normal (C4FM)", "C4FM"); + + private String mLabel; + private String mShortLabel; + + private Modulation(String label, String shortLabel) + { + mLabel = label; + mShortLabel = shortLabel; + } + + public String getLabel() + { + return mLabel; + } + + public String getShortLabel() + { + return mShortLabel; + } + + public String toString() + { + return getLabel(); + } + } } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25MessageFramer.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25MessageFramer.java index 8a0d4397..54047262 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25MessageFramer.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25MessageFramer.java @@ -7,7 +7,7 @@ import io.github.dsheirer.bits.ISyncDetectListener; import io.github.dsheirer.bits.MultiSyncPatternMatcher; import io.github.dsheirer.bits.SoftSyncDetector; import io.github.dsheirer.bits.SyncDetector; -import io.github.dsheirer.dsp.psk.LSMDemodulator; +import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop; import io.github.dsheirer.dsp.symbol.Dibit; import io.github.dsheirer.dsp.symbol.FrameSync; import io.github.dsheirer.edac.BCH_63_16_11; @@ -39,619 +39,619 @@ import java.util.ArrayList; public class P25MessageFramer implements Listener { - private final static Logger mLog = - LoggerFactory.getLogger( P25MessageFramer.class ); + private final static Logger mLog = + LoggerFactory.getLogger(P25MessageFramer.class); - /* Determines the threshold for sync pattern soft matching */ - private static final int SYNC_MATCH_THRESHOLD = 2; - private static final int SYNC_IN_CALL_THRESHOLD = 4; + /* Determines the threshold for sync pattern soft matching */ + private static final int SYNC_MATCH_THRESHOLD = 2; + private static final int SYNC_IN_CALL_THRESHOLD = 4; - /* Costas Loop phase lock error correction values. A phase lock error of - * 90 degrees requires a correction of 1/4 of the symbol rate (1200Hz). An - * error of 180 degrees requires a correction of 1/2 of the symbol rate */ - public static final double SYMBOL_FREQUENCY = 2.0d * Math.PI * 4800.0d / 48000.0d; - public static final double PHASE_CORRECTION_90_DEGREES = SYMBOL_FREQUENCY / 4.0d; - public static final double PHASE_CORRECTION_180_DEGREES = SYMBOL_FREQUENCY / 2.0d; - public static final int TSBK_BEGIN = 64; - public static final int TSBK_CRC_START = 144; - public static final int TSBK_END = 260; - public static final int TSBK_DECODED_END = 160; + /* Costas Loop phase lock error correction values. A phase lock error of + * 90 degrees requires a correction of 1/4 of the symbol rate (1200Hz). An + * error of 180 degrees requires a correction of 1/2 of the symbol rate */ + public static final double SYMBOL_FREQUENCY = 2.0d * Math.PI * 4800.0d / 48000.0d; + public static final double PHASE_CORRECTION_90_DEGREES = SYMBOL_FREQUENCY / 4.0d; + public static final double PHASE_CORRECTION_180_DEGREES = SYMBOL_FREQUENCY / 2.0d; + public static final int TSBK_BEGIN = 64; + public static final int TSBK_CRC_START = 144; + public static final int TSBK_END = 260; + public static final int TSBK_DECODED_END = 160; - public static final int PDU0_BEGIN = 64; - public static final int PDU0_CRC_BEGIN = 144; - public static final int PDU0_END = 260; - public static final int PDU1_BEGIN = 160; - public static final int PDU1_END = 356; - public static final int PDU2_BEGIN = 256; - public static final int PDU2_END = 452; - public static final int PDU3_BEGIN = 352; - public static final int PDU3_END = 548; - public static final int PDU3_DECODED_END = 448; - - private SoftSyncDetector mPrimarySyncDetector = new SoftSyncDetector( - FrameSync.P25_PHASE1_NORMAL.getSync(), SYNC_MATCH_THRESHOLD ); + public static final int PDU0_BEGIN = 64; + public static final int PDU0_CRC_BEGIN = 144; + public static final int PDU0_END = 260; + public static final int PDU1_BEGIN = 160; + public static final int PDU1_END = 356; + public static final int PDU2_BEGIN = 256; + public static final int PDU2_END = 452; + public static final int PDU3_BEGIN = 352; + public static final int PDU3_END = 548; + public static final int PDU3_DECODED_END = 448; - private MultiSyncPatternMatcher mMatcher = new MultiSyncPatternMatcher( 48 ); - private ArrayList mAssemblers = - new ArrayList(); + private SoftSyncDetector mPrimarySyncDetector = new SoftSyncDetector( + FrameSync.P25_PHASE1_NORMAL.getSync(), SYNC_MATCH_THRESHOLD); - private Listener mListener; - private AliasList mAliasList; + private MultiSyncPatternMatcher mMatcher = new MultiSyncPatternMatcher(48); + private ArrayList mAssemblers = + new ArrayList(); - private Trellis_1_2_Rate mHalfRate = new Trellis_1_2_Rate(); - private Trellis_3_4_Rate mThreeQuarterRate = new Trellis_3_4_Rate(); - private BCH_63_16_11 mNIDDecoder = new BCH_63_16_11(); - - /** - * Constructs a P25 message framer to receive a stream of symbols and - * detect the sync pattern then capture the following stream of symbols up - * to the message length, and then broadcast that bit buffer to the registered - * listener. - */ - public P25MessageFramer( AliasList aliasList ) - { - mAliasList = aliasList; - - mPrimarySyncDetector.setListener( new ISyncDetectListener() - { - @Override - public void syncDetected() - { - for( P25MessageAssembler assembler: mAssemblers ) - { - if( !assembler.isActive() ) - { - assembler.setActive( true ); - break; - } - } - } - } ); - - mMatcher.add( mPrimarySyncDetector ); + private Listener mListener; + private AliasList mAliasList; - /** - * We use two message assemblers to catch any sync detections, so that - * if we have a false trigger then we still have a chance to catch the - * valid message. If we have two false triggers before the arrival of - * the actual message sync, then the third or subsequent real or false - * sync pattern will produce a debug message indicating no assemblers - * are available.. - */ - mAssemblers.add( new P25MessageAssembler() ); - mAssemblers.add( new P25MessageAssembler() ); - } - - public P25MessageFramer( AliasList aliasList, LSMDemodulator demodulator ) - { - this( aliasList ); + private Trellis_1_2_Rate mHalfRate = new Trellis_1_2_Rate(); + private Trellis_3_4_Rate mThreeQuarterRate = new Trellis_3_4_Rate(); + private BCH_63_16_11 mNIDDecoder = new BCH_63_16_11(); - if( demodulator != null ) - { - /* For CQPSK, we include 3 additional sync detectors to watch for and - * correct +/-90 and 180 degree costas loop phase lock errors */ - mMatcher.add( new CostasPhaseErrorDetector( FrameSync.P25_PHASE1_ERROR_90_CCW, - demodulator, PHASE_CORRECTION_90_DEGREES ) ); - - mMatcher.add( new CostasPhaseErrorDetector( FrameSync.P25_PHASE1_ERROR_90_CW, - demodulator, -PHASE_CORRECTION_90_DEGREES ) ); - - mMatcher.add( new CostasPhaseErrorDetector( FrameSync.P25_PHASE1_ERROR_180, - demodulator, PHASE_CORRECTION_180_DEGREES ) ); - } - } - - public void dispose() - { - for( P25MessageAssembler a: mAssemblers ) - { - a.dispose(); - } - - mAssemblers.clear(); - - mListener = null; - mAliasList = null; - - mMatcher.dispose(); - mMatcher = null; - - mPrimarySyncDetector.dispose(); - mPrimarySyncDetector = null; - - } - - private void dispatch( Message message ) - { - if( mListener != null ) - { - mListener.receive( message ); - } - } - - @Override - public void receive( Dibit symbol ) + /** + * Constructs a P25 message framer to receive a stream of symbols and + * detect the sync pattern then capture the following stream of symbols up + * to the message length, and then broadcast that bit buffer to the registered + * listener. + */ + public P25MessageFramer(AliasList aliasList) { - for( P25MessageAssembler assembler: mAssemblers ) - { - if( assembler.isActive() ) - { - assembler.receive( symbol ); - - if( assembler.complete() ) - { - assembler.reset(); - } - } - } - - mMatcher.receive( symbol.getBit1(), symbol.getBit2() ); + mAliasList = aliasList; + + mPrimarySyncDetector.setListener(new ISyncDetectListener() + { + @Override + public void syncDetected() + { + for(P25MessageAssembler assembler : mAssemblers) + { + if(!assembler.isActive()) + { + assembler.setActive(true); + break; + } + } + } + }); + + mMatcher.add(mPrimarySyncDetector); + + /** + * We use two message assemblers to catch any sync detections, so that + * if we have a false trigger then we still have a chance to catch the + * valid message. If we have two false triggers before the arrival of + * the actual message sync, then the third or subsequent real or false + * sync pattern will produce a debug message indicating no assemblers + * are available.. + */ + mAssemblers.add(new P25MessageAssembler()); + mAssemblers.add(new P25MessageAssembler()); } - public void setListener( Listener listener ) + public P25MessageFramer(AliasList aliasList, IPhaseLockedLoop phaseLockedLoop) { - mListener = listener; + this(aliasList); + + if(phaseLockedLoop != null) + { + //Add additional sync pattern detectors to detect when we get 90/180 degree out of phase sync pattern + //detections so that we can apply correction to the phase locked loop + mMatcher.add(new PLLPhaseInversionDetector(FrameSync.P25_PHASE1_ERROR_90_CCW, + phaseLockedLoop, PHASE_CORRECTION_90_DEGREES)); + + mMatcher.add(new PLLPhaseInversionDetector(FrameSync.P25_PHASE1_ERROR_90_CW, + phaseLockedLoop, -PHASE_CORRECTION_90_DEGREES)); + + mMatcher.add(new PLLPhaseInversionDetector(FrameSync.P25_PHASE1_ERROR_180, + phaseLockedLoop, PHASE_CORRECTION_180_DEGREES)); + } } - public void removeListener( Listener listener ) + public void dispose() { - mListener = null; + for(P25MessageAssembler a : mAssemblers) + { + a.dispose(); + } + + mAssemblers.clear(); + + mListener = null; + mAliasList = null; + + mMatcher.dispose(); + mMatcher = null; + + mPrimarySyncDetector.dispose(); + mPrimarySyncDetector = null; + } - + + private void dispatch(Message message) + { + if(mListener != null) + { + mListener.receive(message); + } + } + + @Override + public void receive(Dibit symbol) + { + for(P25MessageAssembler assembler : mAssemblers) + { + if(assembler.isActive()) + { + assembler.receive(symbol); + + if(assembler.complete()) + { + assembler.reset(); + } + } + } + + mMatcher.receive(symbol.getBit1(), symbol.getBit2()); + } + + public void setListener(Listener listener) + { + mListener = listener; + } + + public void removeListener(Listener listener) + { + mListener = null; + } + private class P25MessageAssembler { - /* Starting position of the status symbol counter is 24 symbols to - * account for the 48-bit sync pattern which is not included in message */ - private int mStatusSymbolPointer = 24; - private BinaryMessage mMessage; + /* Starting position of the status symbol counter is 24 symbols to + * account for the 48-bit sync pattern which is not included in message */ + private int mStatusSymbolPointer = 24; + private BinaryMessage mMessage; private int mMessageLength; private boolean mComplete = false; private boolean mActive = false; private DataUnitID mDUID = DataUnitID.NID; - + public P25MessageAssembler() { - mMessageLength = mDUID.getMessageLength(); - mMessage = new BinaryMessage( mMessageLength ); - reset(); + mMessageLength = mDUID.getMessageLength(); + mMessage = new BinaryMessage(mMessageLength); + reset(); } - - public void receive( Dibit dibit ) + + public void receive(Dibit dibit) { - if( mActive ) - { - if( mStatusSymbolPointer == 35 ) - { - mStatusSymbolPointer = 0; - } - else - { - mStatusSymbolPointer++; + if(mActive) + { + if(mStatusSymbolPointer == 35) + { + mStatusSymbolPointer = 0; + } + else + { + mStatusSymbolPointer++; try { - mMessage.add( dibit.getBit1() ); - mMessage.add( dibit.getBit2() ); + mMessage.add(dibit.getBit1()); + mMessage.add(dibit.getBit2()); } - catch( BitSetFullException e ) + catch(BitSetFullException e) { - mComplete = true; + mComplete = true; } - + /* Check the message for complete */ - if( mMessage.isFull() ) + if(mMessage.isFull()) { - checkComplete(); + checkComplete(); } - } - } + } + } } public void reset() { - mDUID = DataUnitID.NID; - mMessage.setSize( mDUID.getMessageLength() ); - mMessage.clear(); - mStatusSymbolPointer = 24; + mDUID = DataUnitID.NID; + mMessage.setSize(mDUID.getMessageLength()); + mMessage.clear(); + mStatusSymbolPointer = 24; mComplete = false; mActive = false; } - - public void setActive( boolean active ) + + public void setActive(boolean active) { - mActive = active; + mActive = active; } - private void setDUID( DataUnitID id ) + private void setDUID(DataUnitID id) { - mDUID = id; - mMessageLength = id.getMessageLength(); - mMessage.setSize( mMessageLength ); + mDUID = id; + mMessageLength = id.getMessageLength(); + mMessage.setSize(mMessageLength); } private void checkComplete() { - switch( mDUID ) - { - case NID: - mMessage = mNIDDecoder.correctNID( mMessage ); - - if( mMessage.getCRC() != CRC.FAILED_CRC ) - { - int value = mMessage.getInt( P25Message.DUID ); - - DataUnitID duid = DataUnitID.fromValue( value ); - - if( duid != DataUnitID.UNKN ) - { - setDUID( duid ); - } - else - { - mComplete = true; - dispatch( new P25Message( mMessage.copy(), mDUID, mAliasList ) ); - } - } - else - { - mComplete = true; - } - break; - case HDU: - mComplete = true; - dispatch( new HDUMessage( mMessage.copy(), mDUID, mAliasList ) ); - - /* We're in a call now, lower the sync match threshold */ - mPrimarySyncDetector.setThreshold( SYNC_IN_CALL_THRESHOLD ); - break; - case LDU1: - mComplete = true; - - LDU1Message ldu1 = new LDU1Message( mMessage.copy(), - mDUID, mAliasList ); + switch(mDUID) + { + case NID: + mMessage = mNIDDecoder.correctNID(mMessage); - /* Convert the LDU1 message into a link control LDU1 message */ - dispatch( LDULCMessageFactory.getMessage( ldu1 ) ); + if(mMessage.getCRC() != CRC.FAILED_CRC) + { + int value = mMessage.getInt(P25Message.DUID); + + DataUnitID duid = DataUnitID.fromValue(value); + + if(duid != DataUnitID.UNKN) + { + setDUID(duid); + } + else + { + mComplete = true; + dispatch(new P25Message(mMessage.copy(), mDUID, mAliasList)); + } + } + else + { + mComplete = true; + } + break; + case HDU: + mComplete = true; + dispatch(new HDUMessage(mMessage.copy(), mDUID, mAliasList)); /* We're in a call now, lower the sync match threshold */ - mPrimarySyncDetector.setThreshold( SYNC_IN_CALL_THRESHOLD ); - break; - case LDU2: - mComplete = true; - dispatch( new LDU2Message( mMessage.copy(), mDUID, mAliasList ) ); + mPrimarySyncDetector.setThreshold(SYNC_IN_CALL_THRESHOLD); + break; + case LDU1: + mComplete = true; + + LDU1Message ldu1 = new LDU1Message(mMessage.copy(), + mDUID, mAliasList); + + /* Convert the LDU1 message into a link control LDU1 message */ + dispatch(LDULCMessageFactory.getMessage(ldu1)); /* We're in a call now, lower the sync match threshold */ - mPrimarySyncDetector.setThreshold( SYNC_IN_CALL_THRESHOLD ); - break; - case PDU0: + mPrimarySyncDetector.setThreshold(SYNC_IN_CALL_THRESHOLD); + break; + case LDU2: + mComplete = true; + dispatch(new LDU2Message(mMessage.copy(), mDUID, mAliasList)); - /* Remove interleaving */ - P25Interleave.deinterleaveData( mMessage, PDU0_BEGIN, PDU0_END ); - - /* Remove trellis encoding - abort processing if we have an - * unsuccessful decode due to excessive errors */ - if( mHalfRate.decode( mMessage, PDU0_BEGIN, PDU0_END ) ) - { - mMessage = CRCP25.correctCCITT80( mMessage, - PDU0_BEGIN, PDU0_CRC_BEGIN ); - + /* We're in a call now, lower the sync match threshold */ + mPrimarySyncDetector.setThreshold(SYNC_IN_CALL_THRESHOLD); + break; + case PDU0: + + /* Remove interleaving */ + P25Interleave.deinterleaveData(mMessage, PDU0_BEGIN, PDU0_END); + + /* Remove trellis encoding - abort processing if we have an + * unsuccessful decode due to excessive errors */ + if(mHalfRate.decode(mMessage, PDU0_BEGIN, PDU0_END)) + { + mMessage = CRCP25.correctCCITT80(mMessage, + PDU0_BEGIN, PDU0_CRC_BEGIN); + + + if(mMessage.getCRC() != CRC.FAILED_CRC) + { + boolean confirmed = mMessage.get( + PDUMessage.CONFIRMATION_REQUIRED_INDICATOR); + + if(confirmed) + { + setDUID(DataUnitID.PDUC); + } + else + { + setDUID(DataUnitID.PDU1); + } + + mMessage.setPointer(PDU1_BEGIN); + } + else + { + mComplete = true; + } + } + else + { + mComplete = true; + } - if( mMessage.getCRC() != CRC.FAILED_CRC ) - { - boolean confirmed = mMessage.get( - PDUMessage.CONFIRMATION_REQUIRED_INDICATOR ); - - if( confirmed ) - { - setDUID( DataUnitID.PDUC ); - } - else - { - setDUID( DataUnitID.PDU1 ); - } - - mMessage.setPointer( PDU1_BEGIN ); - } - else - { - mComplete = true; - } - } - else - { - mComplete = true; - } - /* Set sync match threshold to normal */ - mPrimarySyncDetector.setThreshold( SYNC_MATCH_THRESHOLD ); - - break; - case PDU1: - /* Remove interleaving */ - P25Interleave.deinterleaveData( mMessage, PDU1_BEGIN, PDU1_END ); - - /* Remove trellis encoding - abort processing if we have an - * unsuccessful decode due to excessive errors */ - if( mHalfRate.decode( mMessage, PDU1_BEGIN, PDU1_END ) ) - { - if( mMessage.getInt( PDUMessage.BLOCKS_TO_FOLLOW ) == 1 ) - { - mMessage.setSize( PDU2_BEGIN ); - - PDUMessage pduMessage1 = PDUMessageFactory.getMessage( - mMessage.copy(), DataUnitID.PDU1, mAliasList ); + mPrimarySyncDetector.setThreshold(SYNC_MATCH_THRESHOLD); - dispatch( pduMessage1 ); - - mComplete = true; - } - else - { - setDUID( DataUnitID.PDU2 ); - - mMessage.setPointer( PDU2_BEGIN ); - } - } - else - { - mComplete = true; - } - break; - case PDU2: - /* Remove interleaving */ - P25Interleave.deinterleaveData( mMessage, PDU2_BEGIN, PDU2_END ); - - /* Remove trellis encoding - abort processing if we have an - * unsuccessful decode due to excessive errors */ - if( mHalfRate.decode( mMessage, PDU2_BEGIN, PDU2_END ) ) - { - if( mMessage.getInt( PDUMessage.BLOCKS_TO_FOLLOW ) == 2 ) - { - mMessage.setSize( PDU3_BEGIN ); - - PDUMessage pduMessage2 = PDUMessageFactory.getMessage( - mMessage.copy(), DataUnitID.PDU2, mAliasList ); + break; + case PDU1: + /* Remove interleaving */ + P25Interleave.deinterleaveData(mMessage, PDU1_BEGIN, PDU1_END); - dispatch( pduMessage2 ); - - mComplete = true; - } - else - { - setDUID( DataUnitID.PDU3 ); - - mMessage.setPointer( PDU3_BEGIN ); - } - } - else - { - mComplete = true; - } + /* Remove trellis encoding - abort processing if we have an + * unsuccessful decode due to excessive errors */ + if(mHalfRate.decode(mMessage, PDU1_BEGIN, PDU1_END)) + { + if(mMessage.getInt(PDUMessage.BLOCKS_TO_FOLLOW) == 1) + { + mMessage.setSize(PDU2_BEGIN); - /* Set sync match threshold to normal */ - mPrimarySyncDetector.setThreshold( SYNC_MATCH_THRESHOLD ); - break; - case PDU3: - /* Remove interleaving */ - P25Interleave.deinterleaveData( mMessage, PDU3_BEGIN, PDU3_END ); - - /* Remove trellis encoding - abort processing if we have an - * unsuccessful decode due to excessive errors */ - if( mHalfRate.decode( mMessage, PDU3_BEGIN, PDU3_END ) ) - { - mMessage.setSize( PDU3_DECODED_END ); - - PDUMessage pduMessage3 = PDUMessageFactory.getMessage(mMessage.copy(), DataUnitID.PDU3, mAliasList ); + PDUMessage pduMessage1 = PDUMessageFactory.getMessage( + mMessage.copy(), DataUnitID.PDU1, mAliasList); - dispatch( pduMessage3 ); - } - - mComplete = true; - break; - case PDUC: - /* De-interleave the latest block*/ - P25Interleave.deinterleaveData( mMessage, - mMessage.size() - 196, mMessage.size() ); + dispatch(pduMessage1); - /* Decode 3/4 rate convolutional encoding from latest block */ - if( mThreeQuarterRate.decode( mMessage, - mMessage.size() - 196, mMessage.size() ) ) - { - /* Resize the message and adjust the message pointer - * to account for removing 48 + 4 parity bits */ - mMessage.setSize( mMessage.size() - 52 ); - mMessage.adjustPointer( -52 ); - - int blocks = mMessage.getInt( PDUMessage.BLOCKS_TO_FOLLOW ); - - int current = ( mMessage.size() - 160 ) / 144; - - if( current < blocks ) - { - mMessage.setSize( mMessage.size() + 196 ); - mMessageLength = mMessage.size(); - } - else - { - PDUConfirmedMessage pducm = - new PDUConfirmedMessage( mMessage.copy(), - mAliasList ); + mComplete = true; + } + else + { + setDUID(DataUnitID.PDU2); - /* Translate into correct subclass */ - dispatch( PDUConfirmedMessageFactory.getMessage( pducm ) ); - - mComplete = true; - } - break; - } - else - { - PDUConfirmedMessage pducm = - new PDUConfirmedMessage( mMessage.copy(), - mAliasList ); + mMessage.setPointer(PDU2_BEGIN); + } + } + else + { + mComplete = true; + } + break; + case PDU2: + /* Remove interleaving */ + P25Interleave.deinterleaveData(mMessage, PDU2_BEGIN, PDU2_END); + + /* Remove trellis encoding - abort processing if we have an + * unsuccessful decode due to excessive errors */ + if(mHalfRate.decode(mMessage, PDU2_BEGIN, PDU2_END)) + { + if(mMessage.getInt(PDUMessage.BLOCKS_TO_FOLLOW) == 2) + { + mMessage.setSize(PDU3_BEGIN); + + PDUMessage pduMessage2 = PDUMessageFactory.getMessage( + mMessage.copy(), DataUnitID.PDU2, mAliasList); + + dispatch(pduMessage2); + + mComplete = true; + } + else + { + setDUID(DataUnitID.PDU3); + + mMessage.setPointer(PDU3_BEGIN); + } + } + else + { + mComplete = true; + } - /* Translate into correct subclass */ - dispatch( PDUConfirmedMessageFactory.getMessage( pducm ) ); - - mComplete = true; - } - /* Set sync match threshold to normal */ - mPrimarySyncDetector.setThreshold( SYNC_MATCH_THRESHOLD ); - break; - case TDU: - dispatch( new TDUMessage( mMessage.copy(), mDUID, mAliasList ) ); - mComplete = true; + mPrimarySyncDetector.setThreshold(SYNC_MATCH_THRESHOLD); + break; + case PDU3: + /* Remove interleaving */ + P25Interleave.deinterleaveData(mMessage, PDU3_BEGIN, PDU3_END); - /* Set sync match threshold to normal */ - mPrimarySyncDetector.setThreshold( SYNC_MATCH_THRESHOLD ); - break; - case TDULC: - TDULinkControlMessage tdulc = new TDULinkControlMessage( - mMessage.copy(), mDUID, mAliasList ); + /* Remove trellis encoding - abort processing if we have an + * unsuccessful decode due to excessive errors */ + if(mHalfRate.decode(mMessage, PDU3_BEGIN, PDU3_END)) + { + mMessage.setSize(PDU3_DECODED_END); - /* Convert to an appropriate link control message */ - tdulc = TDULCMessageFactory.getMessage( tdulc ); + PDUMessage pduMessage3 = PDUMessageFactory.getMessage(mMessage.copy(), DataUnitID.PDU3, mAliasList); - dispatch( tdulc ); - mComplete = true; + dispatch(pduMessage3); + } - /* Set sync match threshold to normal */ - mPrimarySyncDetector.setThreshold( SYNC_MATCH_THRESHOLD ); - break; - case TSBK1: - /* Remove interleaving */ - P25Interleave.deinterleaveData( mMessage, TSBK_BEGIN, TSBK_END ); - - /* Remove trellis encoding - abort processing if we have an - * unsuccessful decode due to excessive errors */ - - if( mHalfRate.decode( mMessage, TSBK_BEGIN, TSBK_END ) ) - { - mMessage = CRCP25.correctCCITT80( mMessage, - TSBK_BEGIN, TSBK_CRC_START ); + mComplete = true; + break; + case PDUC: + /* De-interleave the latest block*/ + P25Interleave.deinterleaveData(mMessage, + mMessage.size() - 196, mMessage.size()); - if( mMessage.getCRC() != CRC.FAILED_CRC ) - { - BinaryMessage tsbkBuffer1 = mMessage.copy(); - tsbkBuffer1.setSize( TSBK_DECODED_END ); - - TSBKMessage tsbkMessage1 = TSBKMessageFactory.getMessage( - tsbkBuffer1, DataUnitID.TSBK1, mAliasList ); + /* Decode 3/4 rate convolutional encoding from latest block */ + if(mThreeQuarterRate.decode(mMessage, + mMessage.size() - 196, mMessage.size())) + { + /* Resize the message and adjust the message pointer + * to account for removing 48 + 4 parity bits */ + mMessage.setSize(mMessage.size() - 52); + mMessage.adjustPointer(-52); - if( tsbkMessage1.isLastBlock() ) - { - mComplete = true; - } - else - { - setDUID( DataUnitID.TSBK2 ); - mMessage.setPointer( TSBK_BEGIN ); - } - - dispatch( tsbkMessage1 ); - } - else - { - mComplete = true; - } - } - else - { - mComplete = true; - } + int blocks = mMessage.getInt(PDUMessage.BLOCKS_TO_FOLLOW); - /* Set sync match threshold to normal */ - mPrimarySyncDetector.setThreshold( SYNC_MATCH_THRESHOLD ); - break; - case TSBK2: - /* Remove interleaving */ - P25Interleave.deinterleaveData( mMessage, TSBK_BEGIN, TSBK_END ); + int current = (mMessage.size() - 160) / 144; - /* Remove trellis encoding - abort processing if we have an - * unsuccessful decode due to excessive errors */ - if( mHalfRate.decode( mMessage, TSBK_BEGIN, TSBK_END ) ) - { - mMessage = CRCP25.correctCCITT80( mMessage, - TSBK_BEGIN, TSBK_CRC_START ); - - if( mMessage.getCRC() != CRC.FAILED_CRC ) - { - BinaryMessage tsbkBuffer2 = mMessage.copy(); - tsbkBuffer2.setSize( TSBK_DECODED_END ); - - TSBKMessage tsbkMessage2 = TSBKMessageFactory.getMessage( - tsbkBuffer2, DataUnitID.TSBK2, mAliasList ); + if(current < blocks) + { + mMessage.setSize(mMessage.size() + 196); + mMessageLength = mMessage.size(); + } + else + { + PDUConfirmedMessage pducm = + new PDUConfirmedMessage(mMessage.copy(), + mAliasList); - if( tsbkMessage2.isLastBlock() ) - { - mComplete = true; - } - else - { - setDUID( DataUnitID.TSBK3 ); - mMessage.setPointer( TSBK_BEGIN ); - } - - dispatch( tsbkMessage2 ); - } - else - { - mComplete = true; - } - } - else - { - mComplete = true; - } - break; - case TSBK3: - /* Remove interleaving */ - P25Interleave.deinterleaveData( mMessage, TSBK_BEGIN, TSBK_END ); - - /* Remove trellis encoding - abort processing if we have an - * unsuccessful decode due to excessive errors */ - if( mHalfRate.decode( mMessage, TSBK_BEGIN, TSBK_END ) ) - { - mMessage = CRCP25.correctCCITT80( mMessage, - TSBK_BEGIN, TSBK_CRC_START ); - - if( mMessage.getCRC() != CRC.FAILED_CRC ) - { - BinaryMessage tsbkBuffer3 = mMessage.copy(); - tsbkBuffer3.setSize( TSBK_DECODED_END ); - - TSBKMessage tsbkMessage3 = TSBKMessageFactory.getMessage( - tsbkBuffer3, DataUnitID.TSBK3, mAliasList ); + /* Translate into correct subclass */ + dispatch(PDUConfirmedMessageFactory.getMessage(pducm)); - dispatch( tsbkMessage3 ); - } - } + mComplete = true; + } + break; + } + else + { + PDUConfirmedMessage pducm = + new PDUConfirmedMessage(mMessage.copy(), + mAliasList); - mComplete = true; - break; - case VSELP1: - mComplete = true; - dispatch( new VSELP1Message( mMessage.copy(), mDUID, mAliasList ) ); - break; - case VSELP2: - mComplete = true; - dispatch( new VSELP2Message( mMessage.copy(), mDUID, mAliasList ) ); - break; - case UNKN: - mComplete = true; - dispatch( new P25Message( mMessage.copy(), mDUID, mAliasList ) ); - break; - default: - mComplete = true; - break; - } + /* Translate into correct subclass */ + dispatch(PDUConfirmedMessageFactory.getMessage(pducm)); + + mComplete = true; + } + + /* Set sync match threshold to normal */ + mPrimarySyncDetector.setThreshold(SYNC_MATCH_THRESHOLD); + break; + case TDU: + dispatch(new TDUMessage(mMessage.copy(), mDUID, mAliasList)); + mComplete = true; + + /* Set sync match threshold to normal */ + mPrimarySyncDetector.setThreshold(SYNC_MATCH_THRESHOLD); + break; + case TDULC: + TDULinkControlMessage tdulc = new TDULinkControlMessage( + mMessage.copy(), mDUID, mAliasList); + + /* Convert to an appropriate link control message */ + tdulc = TDULCMessageFactory.getMessage(tdulc); + + dispatch(tdulc); + mComplete = true; + + /* Set sync match threshold to normal */ + mPrimarySyncDetector.setThreshold(SYNC_MATCH_THRESHOLD); + break; + case TSBK1: + /* Remove interleaving */ + P25Interleave.deinterleaveData(mMessage, TSBK_BEGIN, TSBK_END); + + /* Remove trellis encoding - abort processing if we have an + * unsuccessful decode due to excessive errors */ + + if(mHalfRate.decode(mMessage, TSBK_BEGIN, TSBK_END)) + { + mMessage = CRCP25.correctCCITT80(mMessage, + TSBK_BEGIN, TSBK_CRC_START); + + if(mMessage.getCRC() != CRC.FAILED_CRC) + { + BinaryMessage tsbkBuffer1 = mMessage.copy(); + tsbkBuffer1.setSize(TSBK_DECODED_END); + + TSBKMessage tsbkMessage1 = TSBKMessageFactory.getMessage( + tsbkBuffer1, DataUnitID.TSBK1, mAliasList); + + if(tsbkMessage1.isLastBlock()) + { + mComplete = true; + } + else + { + setDUID(DataUnitID.TSBK2); + mMessage.setPointer(TSBK_BEGIN); + } + + dispatch(tsbkMessage1); + } + else + { + mComplete = true; + } + } + else + { + mComplete = true; + } + + /* Set sync match threshold to normal */ + mPrimarySyncDetector.setThreshold(SYNC_MATCH_THRESHOLD); + break; + case TSBK2: + /* Remove interleaving */ + P25Interleave.deinterleaveData(mMessage, TSBK_BEGIN, TSBK_END); + + /* Remove trellis encoding - abort processing if we have an + * unsuccessful decode due to excessive errors */ + if(mHalfRate.decode(mMessage, TSBK_BEGIN, TSBK_END)) + { + mMessage = CRCP25.correctCCITT80(mMessage, + TSBK_BEGIN, TSBK_CRC_START); + + if(mMessage.getCRC() != CRC.FAILED_CRC) + { + BinaryMessage tsbkBuffer2 = mMessage.copy(); + tsbkBuffer2.setSize(TSBK_DECODED_END); + + TSBKMessage tsbkMessage2 = TSBKMessageFactory.getMessage( + tsbkBuffer2, DataUnitID.TSBK2, mAliasList); + + if(tsbkMessage2.isLastBlock()) + { + mComplete = true; + } + else + { + setDUID(DataUnitID.TSBK3); + mMessage.setPointer(TSBK_BEGIN); + } + + dispatch(tsbkMessage2); + } + else + { + mComplete = true; + } + } + else + { + mComplete = true; + } + break; + case TSBK3: + /* Remove interleaving */ + P25Interleave.deinterleaveData(mMessage, TSBK_BEGIN, TSBK_END); + + /* Remove trellis encoding - abort processing if we have an + * unsuccessful decode due to excessive errors */ + if(mHalfRate.decode(mMessage, TSBK_BEGIN, TSBK_END)) + { + mMessage = CRCP25.correctCCITT80(mMessage, + TSBK_BEGIN, TSBK_CRC_START); + + if(mMessage.getCRC() != CRC.FAILED_CRC) + { + BinaryMessage tsbkBuffer3 = mMessage.copy(); + tsbkBuffer3.setSize(TSBK_DECODED_END); + + TSBKMessage tsbkMessage3 = TSBKMessageFactory.getMessage( + tsbkBuffer3, DataUnitID.TSBK3, mAliasList); + + dispatch(tsbkMessage3); + } + } + + mComplete = true; + break; + case VSELP1: + mComplete = true; + dispatch(new VSELP1Message(mMessage.copy(), mDUID, mAliasList)); + break; + case VSELP2: + mComplete = true; + dispatch(new VSELP2Message(mMessage.copy(), mDUID, mAliasList)); + break; + case UNKN: + mComplete = true; + dispatch(new P25Message(mMessage.copy(), mDUID, mAliasList)); + break; + default: + mComplete = true; + break; + } } - + public void dispose() { - mMessage = null; - mHalfRate.dispose(); + mMessage = null; + mHalfRate.dispose(); } /** @@ -663,55 +663,54 @@ public class P25MessageFramer implements Listener { return mComplete; } - + public boolean isActive() { - return mActive; + return mActive; } } - + /** - * Sync pattern detector to listen for costas loop phase lock errors and - * apply a phase correction to the costas loop so that we don't miss any + * Sync pattern detector to listen for costas loop phase lock errors and + * apply a phase correction to the costas loop so that we don't miss any * messages. - * - * When the costas loop locks with a +/- 90 degree or 180 degree phase + * + * When the costas loop locks with a +/- 90 degree or 180 degree phase * error, the slicer will incorrectly apply the symbol pattern rotated left * or right by the phase error. However, we can detect these rotated sync * patterns and apply immediate phase correction so that message processing - * can continue. + * can continue. */ - public class CostasPhaseErrorDetector extends SyncDetector + public class PLLPhaseInversionDetector extends SyncDetector { - private LSMDemodulator mDemodulator; - private double mCorrection; - - public CostasPhaseErrorDetector( FrameSync frameSync, LSMDemodulator demodulator, double correction ) - { - super( frameSync.getSync() ); + private IPhaseLockedLoop mPhaseLockedLoop; + private double mCorrection; - mDemodulator = demodulator; - - mCorrection = correction; + public PLLPhaseInversionDetector(FrameSync frameSync, IPhaseLockedLoop phaseLockedLoop, double correction) + { + super(frameSync.getSync()); - setListener( new ISyncDetectListener() - { - @Override - public void syncDetected() - { - mDemodulator.correctPhaseError( mCorrection ); + mPhaseLockedLoop = phaseLockedLoop; + mCorrection = correction; - /* Since we detected a sync pattern, start a message assembler */ - for( P25MessageAssembler assembler: mAssemblers ) - { - if( !assembler.isActive() ) - { - assembler.setActive( true ); - break; - } - } - } - } ); - } + setListener(new ISyncDetectListener() + { + @Override + public void syncDetected() + { + mPhaseLockedLoop.correctInversion(mCorrection); + + /* Since we detected a sync pattern, start a message assembler */ + for(P25MessageAssembler assembler : mAssemblers) + { + if(!assembler.isActive()) + { + assembler.setActive(true); + break; + } + } + } + }); + } } } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder.java index 13512400..c51a7adf 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder.java @@ -19,10 +19,7 @@ package io.github.dsheirer.module.decode.p25; import io.github.dsheirer.alias.AliasList; -import io.github.dsheirer.instrument.tap.Tap; import io.github.dsheirer.instrument.tap.TapGroup; -import io.github.dsheirer.instrument.tap.stream.DibitTap; -import io.github.dsheirer.instrument.tap.stream.FloatTap; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.real.IFilteredRealBufferListener; import io.github.dsheirer.sample.real.RealBuffer; @@ -30,7 +27,6 @@ import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayList; import java.util.List; import java.util.concurrent.ScheduledExecutorService; @@ -105,93 +101,6 @@ public class P25_C4FMDecoder extends P25Decoder implements IFilteredRealBufferLi return mSymbolFilter; } - /** - * Instrumentation taps for monitoring internal processing - */ - @Override - public List getTapGroups() - { - if( mAvailableTaps == null ) - { - mAvailableTaps = new ArrayList<>(); - - TapGroup group = new TapGroup( "P25 C4FM Decoder" ); - - group.add( new FloatTap( INSTRUMENT_C4FM_SYMBOL_FILTER_OUTPUT, 0, 0.1f ) ); - group.add( new DibitTap( INSTRUMENT_C4FM_SLICER_OUTPUT, 0, 0.1f ) ); - - mAvailableTaps.add( group ); - - if( mSymbolFilter != null ) - { - mAvailableTaps.addAll( mSymbolFilter.getTapGroups() ); - } - } - - return mAvailableTaps; - } - - /** - * Adds an instrumentation tap to monitor internal processing. - */ - @Override - public void registerTap( Tap tap ) - { - if( mSymbolFilter != null ) - { - mSymbolFilter.registerTap( tap ); - } - - switch( tap.getName() ) - { - case INSTRUMENT_C4FM_SYMBOL_FILTER_OUTPUT: - FloatTap symbolTap = (FloatTap)tap; - mSymbolFilter.setListener( symbolTap ); - symbolTap.setListener( mC4FMSlicer ); - break; - case INSTRUMENT_C4FM_SLICER_OUTPUT: - DibitTap slicerTap = (DibitTap)tap; - - if( mC4FMSlicer != null ) - { - mC4FMSlicer.addListener( slicerTap ); - } - break; - default: - break; - } - } - - /** - * Removes the instrumentation tap. - */ - @Override - public void unregisterTap( Tap tap ) - { - if( mSymbolFilter != null ) - { - mSymbolFilter.unregisterTap( tap ); - } - - switch( tap.getName() ) - { - case INSTRUMENT_C4FM_SYMBOL_FILTER_OUTPUT: - mSymbolFilter.setListener( mC4FMSlicer ); - break; - case INSTRUMENT_C4FM_SLICER_OUTPUT: - DibitTap slicerTap = (DibitTap)tap; - - if( mC4FMSlicer != null ) - { - mC4FMSlicer.removeListener( slicerTap ); - } - break; - default: - throw new IllegalArgumentException( "Unrecognized tap: " + - tap.getName() ); - } - } - @Override public void reset() { diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2.java new file mode 100644 index 00000000..5a992365 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2.java @@ -0,0 +1,223 @@ +/******************************************************************************* + * 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.module.decode.p25; + +import io.github.dsheirer.alias.AliasList; +import io.github.dsheirer.dsp.filter.FilterFactory; +import io.github.dsheirer.dsp.filter.design.FilterDesignException; +import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification; +import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter; +import io.github.dsheirer.dsp.gain.ComplexFeedForwardGainControl; +import io.github.dsheirer.dsp.psk.DQPSKSymbolPhaseErrorCalculator; +import io.github.dsheirer.dsp.psk.IQPSKSymbolDecoder; +import io.github.dsheirer.dsp.psk.ISymbolPhaseErrorCalculator; +import io.github.dsheirer.dsp.psk.QPSKDemodulator; +import io.github.dsheirer.dsp.psk.QPSKSymbolDecoder; +import io.github.dsheirer.dsp.psk.pll.CostasLoop; +import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.complex.Complex; +import io.github.dsheirer.sample.complex.ComplexBuffer; +import io.github.dsheirer.sample.complex.ComplexBufferToStreamConverter; +import io.github.dsheirer.sample.complex.IComplexBufferListener; +import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ScheduledExecutorService; + +public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListener, Listener +{ + private final static Logger mLog = LoggerFactory.getLogger(P25_C4FMDecoder2.class); + private static final double SYMBOL_RATE = 4800; + private double mSampleRate; + + private Map mBasebandFilters = new HashMap<>(); + private ComplexFIRFilter mBasebandFilter; + private QPSKDemodulator mQPSKDemodulator; + private IPhaseLockedLoop mPhaseLockedLoop; + private ComplexBufferToStreamConverter mStreamConverter = new ComplexBufferToStreamConverter(); + private ComplexFeedForwardGainControl mAGC = new ComplexFeedForwardGainControl(32); + private QPSKSymbolDecoder mQPSKSlicer = new QPSKSymbolDecoder(); + private P25MessageFramer mMessageFramer; + private ISymbolPhaseErrorCalculator mSymbolPhaseErrorCalculator = new DQPSKSymbolPhaseErrorCalculator(); + private IQPSKSymbolDecoder mSymbolDecoder = new QPSKSymbolDecoder(); + private Listener mSymbolListener; + private Listener mPLLPhaseErrorListener; + + public P25_C4FMDecoder2(AliasList aliasList) + { + super(aliasList); + + setSampleRate(48000.0); + + mStreamConverter.setListener(mAGC); + } + + public void setSampleRate(double sampleRate) + { + if(sampleRate <= SYMBOL_RATE * 2) + { + throw new IllegalArgumentException("Sample rate must be at least twice the symbol rate [4800]"); + } + + mSampleRate = sampleRate; + mBasebandFilter = new ComplexFIRFilter(getBasebandFilter(), 1.0f); + + mPhaseLockedLoop = new CostasLoop(mSampleRate, SYMBOL_RATE); + mQPSKDemodulator = new QPSKDemodulator(mPhaseLockedLoop, mSymbolPhaseErrorCalculator, mSymbolDecoder, + mSampleRate, SYMBOL_RATE); + mQPSKDemodulator.setSymbolListener(mSymbolListener); + mQPSKDemodulator.setPLLErrorListener(mPLLPhaseErrorListener); + + //Message framer can trigger a symbol-inversion correction to the PLL when detected + mMessageFramer = new P25MessageFramer(getAliasList(), mPhaseLockedLoop); + mMessageFramer.setListener(getMessageProcessor()); + mQPSKDemodulator.setDibitListener(mMessageFramer); + } + + public void setSymbolListener(Listener listener) + { + mSymbolListener = listener; + mQPSKDemodulator.setSymbolListener(listener); + } + + public void setPLLPhaseErrorListener(Listener listener) + { + mPLLPhaseErrorListener = listener; + mQPSKDemodulator.setPLLErrorListener(listener); + } + + @Override + public void receive(ComplexBuffer complexBuffer) + { + ComplexBuffer filtered = mBasebandFilter.filter(complexBuffer); + ComplexBuffer amplified = mAGC.filter(filtered); + mQPSKDemodulator.receive(amplified); + } + + private double getSampleRate() + { + return mSampleRate; + } + + private float[] getBasebandFilter() + { + float[] filter = mBasebandFilters.get(getSampleRate()); + + if(filter == null) + { + FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() + .sampleRate((int)getSampleRate()) + .passBandCutoff(6000) + .passBandAmplitude(1.0) + .passBandRipple(0.1) + .stopBandAmplitude(0.0) + .stopBandStart(6250) + .stopBandRipple(0.01) + .build(); + + try + { + filter = FilterFactory.getTaps(specification); + mBasebandFilters.put(getSampleRate(), filter); + + } + catch(FilterDesignException fde) + { + mLog.error("Couldn't design low pass baseband filter for sample rate: " + getSampleRate()); + } + } + + return filter; + } + + public void dispose() + { + super.dispose(); + + mBasebandFilter.dispose(); + mBasebandFilter = null; + + mStreamConverter.dispose(); + mStreamConverter = null; + + mAGC.dispose(); + mAGC = null; + + mQPSKSlicer.dispose(); + mQPSKSlicer = null; + + mMessageFramer.dispose(); + mMessageFramer = null; + } + + @Override + public void setFrequencyChangeListener(Listener listener) + { + } + + @Override + public void removeFrequencyChangeListener() + { + } + + @Override + public Listener getFrequencyChangeListener() + { + return new Listener() + { + @Override + public void receive(FrequencyChangeEvent frequencyChangeEvent) + { + //Ignored + } + }; + } + + @Override + public Listener getComplexBufferListener() + { + return P25_C4FMDecoder2.this; + } + + public Modulation getModulation() + { + return Modulation.C4FM; + } + + @Override + public void reset() + { + // TODO Auto-generated method stub + + } + + @Override + public void start(ScheduledExecutorService executor) + { + // TODO Auto-generated method stub + + } + + @Override + public void stop() + { + // TODO Auto-generated method stub + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25_LSMDecoder.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25_LSMDecoder.java index 806b20e6..00bb781e 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25_LSMDecoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25_LSMDecoder.java @@ -1,17 +1,17 @@ /******************************************************************************* * SDR Trunk * Copyright (C) 2014,2015 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 ******************************************************************************/ @@ -23,13 +23,8 @@ import io.github.dsheirer.dsp.filter.Window.WindowType; import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter_CB_CB; import io.github.dsheirer.dsp.gain.ComplexFeedForwardGainControl; import io.github.dsheirer.dsp.psk.LSMDemodulator; -import io.github.dsheirer.dsp.psk.QPSKPolarSlicer; -import io.github.dsheirer.instrument.tap.Tap; +import io.github.dsheirer.dsp.psk.QPSKSymbolDecoder; import io.github.dsheirer.instrument.tap.TapGroup; -import io.github.dsheirer.instrument.tap.stream.ComplexSampleTap; -import io.github.dsheirer.instrument.tap.stream.ComplexTap; -import io.github.dsheirer.instrument.tap.stream.DibitTap; -import io.github.dsheirer.instrument.tap.stream.QPSKTap; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.complex.ComplexBuffer; import io.github.dsheirer.sample.complex.ComplexBufferToStreamConverter; @@ -38,73 +33,72 @@ import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayList; import java.util.List; import java.util.concurrent.ScheduledExecutorService; public class P25_LSMDecoder extends P25Decoder implements IComplexBufferListener { - private final static Logger mLog = LoggerFactory.getLogger( P25_LSMDecoder.class ); - + private final static Logger mLog = LoggerFactory.getLogger(P25_LSMDecoder.class); + /* Instrumentation Taps */ - private static final String INSTRUMENT_BASEBAND_FILTER_OUTPUT = "Tap Point: Baseband Filter Output"; - private static final String INSTRUMENT_AGC_OUTPUT = "Tap Point: AGC Output"; - private static final String INSTRUMENT_LSM_DEMODULATOR_OUTPUT = "Tap Point: LSM Demodulator Output"; - private static final String INSTRUMENT_QPSK_SLICER_OUTPUT = "Tap Point: QPSK Slicer Output"; + private static final String INSTRUMENT_BASEBAND_FILTER_OUTPUT = "Tap Point: Baseband Filter Output"; + private static final String INSTRUMENT_AGC_OUTPUT = "Tap Point: AGC Output"; + private static final String INSTRUMENT_LSM_DEMODULATOR_OUTPUT = "Tap Point: LSM Demodulator Output"; + private static final String INSTRUMENT_QPSK_SLICER_OUTPUT = "Tap Point: QPSK Slicer Output"; - private List mAvailableTaps; - - private ComplexFIRFilter_CB_CB mBasebandFilter; - private ComplexBufferToStreamConverter mStreamConverter = new ComplexBufferToStreamConverter(); - private ComplexFeedForwardGainControl mAGC = - new ComplexFeedForwardGainControl( 32 ); - private LSMDemodulator mLSMDemodulator = new LSMDemodulator(); - private QPSKPolarSlicer mQPSKSlicer = new QPSKPolarSlicer(); - private P25MessageFramer mMessageFramer; - - public P25_LSMDecoder( AliasList aliasList ) - { - super( aliasList ); - - mBasebandFilter = new ComplexFIRFilter_CB_CB( FilterFactory.getLowPass( - 48000, 7250, 8000, 60, WindowType.HANNING, true ), 1.0f ); - - mBasebandFilter.setListener( mStreamConverter ); - - mStreamConverter.setListener( mAGC ); + private List mAvailableTaps; - mAGC.setListener( mLSMDemodulator ); - - mLSMDemodulator.setSymbolListener( mQPSKSlicer ); - - mMessageFramer = new P25MessageFramer( aliasList, mLSMDemodulator ); - mQPSKSlicer.addListener( mMessageFramer ); - - mMessageFramer.setListener( getMessageProcessor() ); - } - - public void dispose() - { - super.dispose(); - - mBasebandFilter.dispose(); - mBasebandFilter = null; - - mStreamConverter.dispose(); - mStreamConverter = null; - - mAGC.dispose(); - mAGC = null; + private ComplexFIRFilter_CB_CB mBasebandFilter; + private ComplexBufferToStreamConverter mStreamConverter = new ComplexBufferToStreamConverter(); + private ComplexFeedForwardGainControl mAGC = + new ComplexFeedForwardGainControl(32); + private LSMDemodulator mLSMDemodulator = new LSMDemodulator(); + private QPSKSymbolDecoder mQPSKSlicer = new QPSKSymbolDecoder(); + private P25MessageFramer mMessageFramer; - mLSMDemodulator.dispose(); - mLSMDemodulator = null; - - mQPSKSlicer.dispose(); - mQPSKSlicer = null; - - mMessageFramer.dispose(); - mMessageFramer = null; - } + public P25_LSMDecoder(AliasList aliasList) + { + super(aliasList); + + mBasebandFilter = new ComplexFIRFilter_CB_CB(FilterFactory.getLowPass( + 48000, 7250, 8000, 60, WindowType.HANNING, true), 1.0f); + + mBasebandFilter.setListener(mStreamConverter); + + mStreamConverter.setListener(mAGC); + + mAGC.setListener(mLSMDemodulator); + + mLSMDemodulator.setSymbolListener(mQPSKSlicer); + + mMessageFramer = new P25MessageFramer(aliasList, mLSMDemodulator.getCostasLoop()); + mQPSKSlicer.addListener(mMessageFramer); + + mMessageFramer.setListener(getMessageProcessor()); + } + + public void dispose() + { + super.dispose(); + + mBasebandFilter.dispose(); + mBasebandFilter = null; + + mStreamConverter.dispose(); + mStreamConverter = null; + + mAGC.dispose(); + mAGC = null; + + mLSMDemodulator.dispose(); + mLSMDemodulator = null; + + mQPSKSlicer.dispose(); + mQPSKSlicer = null; + + mMessageFramer.dispose(); + mMessageFramer = null; + } @Override public void setFrequencyChangeListener(Listener listener) @@ -130,130 +124,34 @@ public class P25_LSMDecoder extends P25Decoder implements IComplexBufferListener } @Override - public Listener getComplexBufferListener() - { - return mBasebandFilter; - } - - public Modulation getModulation() - { - return Modulation.CQPSK; - } - - /** - * Provides a list of instrumentation taps for monitoring internal processing - */ - @Override - public List getTapGroups() + public Listener getComplexBufferListener() { - if( mAvailableTaps == null ) - { - mAvailableTaps = new ArrayList<>(); - - TapGroup group = new TapGroup( "P25 LSM Decoder" ); - - group.add( new ComplexTap( INSTRUMENT_BASEBAND_FILTER_OUTPUT, 0, 1.0f ) ); - group.add( new ComplexTap( INSTRUMENT_AGC_OUTPUT, 0, 1.0f ) ); - group.add( new QPSKTap( INSTRUMENT_LSM_DEMODULATOR_OUTPUT, 0, 1.0f ) ); - group.add( new DibitTap( INSTRUMENT_QPSK_SLICER_OUTPUT, 0, 0.1f ) ); - - mAvailableTaps.add( group ); - - if( mLSMDemodulator != null ) - { - mAvailableTaps.addAll( mLSMDemodulator.getTapGroups() ); - } - } - - return mAvailableTaps; + return mBasebandFilter; } - /** - * Adds the instrumentation tap - */ - @Override - public void registerTap( Tap tap ) + public Modulation getModulation() { - if( mLSMDemodulator != null ) - { - mLSMDemodulator.registerTap( tap ); - } - - switch( tap.getName() ) - { - case INSTRUMENT_BASEBAND_FILTER_OUTPUT: - ComplexSampleTap baseband = (ComplexSampleTap)tap; - mStreamConverter.setListener( baseband ); - baseband.setListener( mAGC ); - break; - case INSTRUMENT_AGC_OUTPUT: - ComplexSampleTap agcSymbol = (ComplexSampleTap)tap; - mAGC.setListener( agcSymbol ); - agcSymbol.setListener( mLSMDemodulator ); - break; - case INSTRUMENT_LSM_DEMODULATOR_OUTPUT: - QPSKTap qpsk = (QPSKTap)tap; - mLSMDemodulator.setSymbolListener( qpsk ); - qpsk.setListener( mQPSKSlicer ); - break; - case INSTRUMENT_QPSK_SLICER_OUTPUT: - mQPSKSlicer.addListener( (DibitTap)tap ); - break; - default: - throw new IllegalArgumentException( "Unrecognized tap: " + - tap.getName() ); - } + return Modulation.CQPSK; } - /** - * Removes the instrumentation tap - */ - @Override - public void unregisterTap( Tap tap ) + @Override + public void reset() { - if( mLSMDemodulator != null ) - { - mLSMDemodulator.unregisterTap( tap ); - } - - switch( tap.getName() ) - { - case INSTRUMENT_BASEBAND_FILTER_OUTPUT: - mStreamConverter.setListener( mAGC ); - break; - case INSTRUMENT_AGC_OUTPUT: - mAGC.setListener( mLSMDemodulator ); - break; - case INSTRUMENT_LSM_DEMODULATOR_OUTPUT: - mLSMDemodulator.setSymbolListener( mQPSKSlicer ); - break; - case INSTRUMENT_QPSK_SLICER_OUTPUT: - mQPSKSlicer.removeListener( (DibitTap)tap ); - break; - default: - throw new IllegalArgumentException( "Unrecognized tap: " + - tap.getName() ); - } + // TODO Auto-generated method stub + } - @Override - public void reset() - { - // TODO Auto-generated method stub - - } + @Override + public void start(ScheduledExecutorService executor) + { + // TODO Auto-generated method stub - @Override - public void start( ScheduledExecutorService executor ) - { - // TODO Auto-generated method stub - - } + } - @Override - public void stop() - { - // TODO Auto-generated method stub - - } + @Override + public void stop() + { + // TODO Auto-generated method stub + + } } diff --git a/src/main/java/io/github/dsheirer/module/decode/passport/PassportDecoder.java b/src/main/java/io/github/dsheirer/module/decode/passport/PassportDecoder.java index 72ba45a2..9f7980b2 100644 --- a/src/main/java/io/github/dsheirer/module/decode/passport/PassportDecoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/passport/PassportDecoder.java @@ -63,7 +63,7 @@ public class PassportDecoder extends Decoder mPassportMessageProcessor = new PassportMessageProcessor( aliasList ); mPassportMessageFramer.addMessageListener( mPassportMessageProcessor ); - mPassportMessageProcessor.setMessageListener( this ); + mPassportMessageProcessor.setMessageListener( getMessageListener() ); } @Override diff --git a/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200Decoder.java b/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200Decoder.java index 9ccae831..848bb85c 100644 --- a/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200Decoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200Decoder.java @@ -104,8 +104,8 @@ public class Tait1200Decoder extends Decoder implements IFilteredRealBufferListe mMessageFramerGPS.addMessageListener( mMessageAProcessor ); mMessageFramerANI.addMessageListener( mMessageBProcessor ); - mMessageAProcessor.setMessageListener( this ); - mMessageBProcessor.setMessageListener( this ); + mMessageAProcessor.setMessageListener(getMessageListener()); + mMessageBProcessor.setMessageListener(getMessageListener()); } public void dispose() diff --git a/src/main/java/io/github/dsheirer/record/RecorderManager.java b/src/main/java/io/github/dsheirer/record/RecorderManager.java index 738ffe3f..c5845cdc 100644 --- a/src/main/java/io/github/dsheirer/record/RecorderManager.java +++ b/src/main/java/io/github/dsheirer/record/RecorderManager.java @@ -42,6 +42,7 @@ public class RecorderManager implements Listener private static final Logger mLog = LoggerFactory.getLogger(RecorderManager.class); public static final int AUDIO_SAMPLE_RATE = 8000; + public static final int BASEBAND_SAMPLE_RATE = 48000; public static final long IDLE_RECORDER_REMOVAL_THRESHOLD = 6000; //6 seconds private Map mRecorders = new HashMap<>(); @@ -229,7 +230,7 @@ public class RecorderManager implements Listener sb.append(SystemProperties.getInstance().getApplicationFolder("recordings")); sb.append(File.separator).append(channelName).append("_baseband"); - return new ComplexBufferWaveRecorder(AUDIO_SAMPLE_RATE, sb.toString()); + return new ComplexBufferWaveRecorder(BASEBAND_SAMPLE_RATE, sb.toString()); } /** diff --git a/src/main/java/io/github/dsheirer/sample/complex/Complex.java b/src/main/java/io/github/dsheirer/sample/complex/Complex.java index afeaee9f..8b98e9e8 100644 --- a/src/main/java/io/github/dsheirer/sample/complex/Complex.java +++ b/src/main/java/io/github/dsheirer/sample/complex/Complex.java @@ -1,17 +1,17 @@ /******************************************************************************* * 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 ******************************************************************************/ @@ -27,412 +27,427 @@ import java.io.Serializable; */ public class Complex implements Serializable { - private final static Logger mLog = - LoggerFactory.getLogger( Complex.class ); + private final static Logger mLog = LoggerFactory.getLogger(Complex.class); - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; private float mLeft; - private float mRight; - - public Complex( float left, float right ) - { - mLeft = left; - mRight = right; - } - - public Complex() - { - this( 0.0f, 0.0f ); - } - - public void setInphase( float inphase ) - { - mLeft = inphase; - } - - public void setQuadrature( float quadrature ) - { - mRight = quadrature; - } - - public void setValues( float inphase, float quadrature ) - { - mLeft = inphase; - mRight = quadrature; - } + private float mRight; - public Complex copy() - { - return new Complex( mLeft, mRight ); - } - - public String toString() - { - StringBuilder sb = new StringBuilder(); - - sb.append( "I:" ); - sb.append( mLeft ); - sb.append( " Q:" ); - sb.append( mRight ); - - return sb.toString(); - } - - /** - * Returns a new sample representing the conjugate of this one - */ - public Complex conjugate() - { - return new Complex( mLeft, -mRight ); - } + public Complex(float left, float right) + { + mLeft = left; + mRight = right; + } - /** - * Multiplies this sample by the scalor value - */ - public void multiply( float scalor ) - { - mLeft *= scalor; - mRight *= scalor; - } - - public static Complex multiply( Complex sample, float scalor ) - { - return new Complex( sample.left() * scalor, - sample.right() * scalor ); - } - - /** - * Calculates the inphase component of multiplying two complex numbers, A and B. - */ - public static float multiplyInphase( float inphaseA, float quadratureA, float inphaseB, float quadratureB ) - { - return ( inphaseA * inphaseB ) - ( quadratureA * quadratureB ); - } - - /** - * Calculates the quadrature component of multiplying two complex numbers, A and B. - */ - public static float multiplyQuadrature( float inphaseA, float quadratureA, float inphaseB, float quadratureB ) - { - return ( quadratureA * inphaseB ) + ( inphaseA * quadratureB ); - } - - /** - * Multiplies this sample by the multiplier sample - */ - public void multiply( Complex multiplier ) - { - float inphase = multiplyInphase( inphase(), quadrature(), - multiplier.inphase(), multiplier.quadrature() ); - float quadrature = multiplyQuadrature( inphase(), quadrature(), - multiplier.inphase(), multiplier.quadrature() ); - - mLeft = inphase; - mRight = quadrature; - } - - /** - * Multiplies both samples returning a new sample with the results - */ - public static Complex multiply( Complex sample1, Complex sample2 ) - { - float inphase = multiplyInphase( sample1.inphase(), sample1.quadrature(), - sample2.inphase(), sample2.quadrature() ); - float quadrature = multiplyQuadrature( sample1.inphase(), sample1.quadrature(), - sample2.inphase(), sample2.quadrature() ); + public Complex() + { + this(0.0f, 0.0f); + } - return new Complex( inphase, quadrature ); - } + public void setInphase(float inphase) + { + mLeft = inphase; + } - public static Complex multiply( Complex sample, - Float inphase, Float quadrature ) - { - float i = multiplyInphase( sample.inphase(), sample.quadrature(), - inphase, quadrature ); - float q = multiplyQuadrature( sample.inphase(), sample.quadrature(), - inphase, quadrature ); - - return new Complex( i, q ); - } - - public static Complex multiply( Float inphase, Float quadrature, Complex sample ) - { - float i = multiplyInphase( inphase, quadrature, sample.inphase(), sample.quadrature() ); - float q = multiplyQuadrature( inphase, quadrature, sample.inphase(), sample.quadrature() ); - - return new Complex( i, q ); - } + public void setQuadrature(float quadrature) + { + mRight = quadrature; + } - public static Complex multiply( float inphaseA, float quadratureA, float inphaseB, float quadratureB ) - { - float i = multiplyInphase( inphaseA, quadratureA, inphaseB, quadratureB ); - float q = multiplyQuadrature( inphaseA, quadratureA, inphaseB, quadratureB ); - - return new Complex( i, q ); - } - - /** - * Adds the adder sample value to this sample - */ - public void add( Complex adder ) - { - mLeft += adder.left(); - mRight += adder.right(); - } + public void setValues(float inphase, float quadrature) + { + mLeft = inphase; + mRight = quadrature; + } - /** - * Adds the two complex samples returning a new complex sample with the result - */ - public static Complex add( Complex first, Complex second ) - { - return new Complex( first.left() + second.left(), - first.right() + second.right() ); - } + public void setValues(Complex sampleToCopy) + { + mLeft = sampleToCopy.left(); + mRight = sampleToCopy.right(); + } - /** - * Adds the two complex samples returning a new complex sample with the result - */ - public static Complex subtract( Complex first, Complex second ) - { - return new Complex( first.left() - second.left(), - first.right() - second.right() ); - } + public Complex copy() + { + return new Complex(mLeft, mRight); + } - /** - * Magnitude of this sample - */ - public float magnitude() - { - return (float)Math.sqrt( magnitudeSquared() ); - } - - public static float magnitude( float inphase, float quadrature ) - { - return (float)Math.sqrt( ( inphase * inphase ) + ( quadrature * quadrature ) ); - } + public String toString() + { + StringBuilder sb = new StringBuilder(); - /** - * Magnitude squared of this sample - */ - public float magnitudeSquared() - { - return norm(); - } + sb.append("I:"); + sb.append(mLeft); + sb.append(" Q:"); + sb.append(mRight); - /** - * Norm of this sample = ( i * i ) + ( q * q ) - */ - public float norm() - { - return (float)( ( inphase() * inphase() ) + - ( quadrature() * quadrature() ) ); - } - - /** - * Returns the vector length to 1 (unit circle) - */ - public void normalize() - { - float magnitude = magnitude(); - - if( magnitude != 0 ) - { - multiply( (float)( 1.0f / magnitude() ) ); - } - } + return sb.toString(); + } - /** - * Returns the vector length to 1 (unit circle), - * avoiding square root multiplication - */ - public void fastNormalize() - { - multiply( (float)( 1.95f - magnitudeSquared() ) ); - } - - public float left() - { - return mLeft; - } - - public float right() - { - return mRight; - } + /** + * Returns a new sample representing the conjugate of this one + */ + public Complex conjugate() + { + return new Complex(mLeft, -mRight); + } - public float inphase() - { - return mLeft; - } - - /** - * Absolute value of in-phase component - */ - public float inPhaseAbsolute() - { - return Math.abs( mLeft ); - } + /** + * Multiplies this sample by the scalor value + */ + public void multiply(float scalor) + { + mLeft *= scalor; + mRight *= scalor; + } - public float quadrature() - { - return mRight; - } - - /** - * Absolute value of quadrature component - */ - public float quadratureAbsolute() - { - return Math.abs( mRight ); - } + public static Complex multiply(Complex sample, float scalor) + { + return new Complex(sample.left() * scalor, + sample.right() * scalor); + } - public float x() - { - return mLeft; - } - - public float y() - { - return mRight; - } - - public float real() - { - return mLeft; - } + /** + * Calculates the inphase component of multiplying two complex numbers, A and B. + */ + public static float multiplyInphase(float inphaseA, float quadratureA, float inphaseB, float quadratureB) + { + return (inphaseA * inphaseB) - (quadratureA * quadratureB); + } - /** - * Absolute value of real component - */ - public float realAbsolute() - { - return Math.abs( mLeft ); - } + /** + * Calculates the quadrature component of multiplying two complex numbers, A and B. + */ + public static float multiplyQuadrature(float inphaseA, float quadratureA, float inphaseB, float quadratureB) + { + return (quadratureA * inphaseB) + (inphaseA * quadratureB); + } - public float imaginary() - { - return mRight; - } + /** + * Multiplies this sample by the multiplier sample + */ + public void multiply(Complex multiplier) + { + float inphase = multiplyInphase(inphase(), quadrature(), + multiplier.inphase(), multiplier.quadrature()); + float quadrature = multiplyQuadrature(inphase(), quadrature(), + multiplier.inphase(), multiplier.quadrature()); - /** - * Absolute value of imaginary component - */ - public float imaginaryAbsolute() - { - return Math.abs( mRight ); - } + mLeft = inphase; + mRight = quadrature; + } - /** - * Returns the greater absolute value between left and right values - */ - public float maximumAbsolute() - { - if( Math.abs( mLeft ) > Math.abs( mRight ) ) - { - return Math.abs( mLeft ); - } - else - { - return Math.abs( mRight ); - } - } + /** + * Multiplies both samples returning a new sample with the results + */ + public static Complex multiply(Complex sample1, Complex sample2) + { + float inphase = multiplyInphase(sample1.inphase(), sample1.quadrature(), + sample2.inphase(), sample2.quadrature()); + float quadrature = multiplyQuadrature(sample1.inphase(), sample1.quadrature(), + sample2.inphase(), sample2.quadrature()); - /** - * Creates a new complex sample representing the angle with unit circle - * magnitude - * @param angle in radians - * @return - */ - public static Complex fromAngle( float angle ) - { - return new Complex( (float)Math.cos( angle ), - (float)Math.sin( angle ) ); - } + return new Complex(inphase, quadrature); + } - /** - * Angle of this sample in radians - */ - public float angle() - { - return (float)Math.atan2( y(), x() ); - } + public static Complex multiply(Complex sample, + Float inphase, Float quadrature) + { + float i = multiplyInphase(sample.inphase(), sample.quadrature(), + inphase, quadrature); + float q = multiplyQuadrature(sample.inphase(), sample.quadrature(), + inphase, quadrature); - public float angleDegrees() - { - return (float)Math.toDegrees(angle()); - } + return new Complex(i, q); + } - /** - * Constrains the i and q quantities to +/- value. - * - * @param value - maximum absolute value - */ - public void clip( float value ) - { - if( mLeft > value ) - { - mLeft = value; - } - else if( mLeft < -value ) - { - mLeft = -value; - } - - if( mRight > value ) - { - mRight = value; - } - else if( mRight < -value ) - { - mRight = -value; - } - } + public static Complex multiply(Float inphase, Float quadrature, Complex sample) + { + float i = multiplyInphase(inphase, quadrature, sample.inphase(), sample.quadrature()); + float q = multiplyQuadrature(inphase, quadrature, sample.inphase(), sample.quadrature()); - /** - * Angle of this sample in degrees - */ - public float polarAngle() - { - return (float)Math.toDegrees( angle() ); - } + return new Complex(i, q); + } - /** - * Provides an approximate magnitude value for this sample. - */ - public float envelope() - { - return envelope( mLeft, mRight ); - } - - public static float envelope( float inphase, float quadrature ) - { - float inphaseAbsolute = Math.abs( inphase ); - float quadratureAbsolute = Math.abs( quadrature ); - - if( inphaseAbsolute > quadratureAbsolute ) - { - return inphaseAbsolute + ( 0.4f * quadratureAbsolute ); - } - else - { - return quadratureAbsolute + ( 0.4f * inphaseAbsolute ); - } - } - - public static void main( String[] args ) - { - double angle = Math.PI; //180 degrees - - Complex s1 = new Complex( (float)Math.sin( angle ), (float)Math.cos( angle ) ); + public static Complex multiply(float inphaseA, float quadratureA, float inphaseB, float quadratureB) + { + float i = multiplyInphase(inphaseA, quadratureA, inphaseB, quadratureB); + float q = multiplyQuadrature(inphaseA, quadratureA, inphaseB, quadratureB); - Complex tap = new Complex( 1.0f, -0.1f ); + return new Complex(i, q); + } - Complex convolved = Complex.multiply( s1, tap ); - - mLog.debug( "s: " + s1.toString() + - " t: " + tap.toString() + - " convolved: " + convolved.toString() ); - - } + /** + * Adds the adder sample value to this sample + */ + public void add(Complex adder) + { + mLeft += adder.left(); + mRight += adder.right(); + } + + /** + * Adds the two complex samples returning a new complex sample with the result + */ + public static Complex add(Complex first, Complex second) + { + return new Complex(first.left() + second.left(), + first.right() + second.right()); + } + + /** + * Adds the two complex samples returning a new complex sample with the result + */ + public static Complex subtract(Complex first, Complex second) + { + return new Complex(first.left() - second.left(), + first.right() - second.right()); + } + + /** + * Magnitude of this sample + */ + public float magnitude() + { + return (float)Math.sqrt(magnitudeSquared()); + } + + public static float magnitude(float inphase, float quadrature) + { + return (float)Math.sqrt((inphase * inphase) + (quadrature * quadrature)); + } + + /** + * Magnitude squared of this sample + */ + public float magnitudeSquared() + { + return norm(); + } + + /** + * Norm of this sample = ( i * i ) + ( q * q ) + */ + public float norm() + { + return (float)((inphase() * inphase()) + + (quadrature() * quadrature())); + } + + /** + * Returns the vector length to 1 (unit circle) + */ + public void normalize() + { + float magnitude = magnitude(); + + if(magnitude != 0) + { + multiply((float)(1.0f / magnitude())); + } + } + + /** + * Returns the vector length to 1 (unit circle), + * avoiding square root multiplication + */ + public void fastNormalize() + { + multiply((float)(1.95f - magnitudeSquared())); + } + + public float left() + { + return mLeft; + } + + public float right() + { + return mRight; + } + + public float inphase() + { + return mLeft; + } + + /** + * Absolute value of in-phase component + */ + public float inPhaseAbsolute() + { + return Math.abs(mLeft); + } + + public float quadrature() + { + return mRight; + } + + /** + * Absolute value of quadrature component + */ + public float quadratureAbsolute() + { + return Math.abs(mRight); + } + + public float x() + { + return mLeft; + } + + public float y() + { + return mRight; + } + + public float real() + { + return mLeft; + } + + /** + * Absolute value of real component + */ + public float realAbsolute() + { + return Math.abs(mLeft); + } + + public float imaginary() + { + return mRight; + } + + /** + * Absolute value of imaginary component + */ + public float imaginaryAbsolute() + { + return Math.abs(mRight); + } + + /** + * Returns the greater absolute value between left and right values + */ + public float maximumAbsolute() + { + if(Math.abs(mLeft) > Math.abs(mRight)) + { + return Math.abs(mLeft); + } + else + { + return Math.abs(mRight); + } + } + + /** + * Creates a new complex sample representing the angle with unit circle + * magnitude + * + * @param angle in radians + * @return + */ + public static Complex fromAngle(double angle) + { + return new Complex((float)Math.cos(angle), (float)Math.sin(angle)); + } + + /** + * Sets this complex instance with the specified angle in radians. + * @param angle in radians + */ + public void setAngle(double angle) + { + setInphase((float)Math.cos(angle)); + setQuadrature((float)Math.sin(angle)); + } + + /** + * Angle of this sample in radians + */ + public float angle() + { + return (float)Math.atan2(y(), x()); + } + + public float angleDegrees() + { + return (float)Math.toDegrees(angle()); + } + + /** + * Constrains the i and q quantities to +/- value. + * + * @param value - maximum absolute value + */ + public void clip(float value) + { + if(mLeft > value) + { + mLeft = value; + } + else if(mLeft < -value) + { + mLeft = -value; + } + + if(mRight > value) + { + mRight = value; + } + else if(mRight < -value) + { + mRight = -value; + } + } + + /** + * Angle of this sample in degrees + */ + public float polarAngle() + { + return (float)Math.toDegrees(angle()); + } + + /** + * Provides an approximate magnitude value for this sample. + */ + public float envelope() + { + return envelope(mLeft, mRight); + } + + public static float envelope(float inphase, float quadrature) + { + float inphaseAbsolute = Math.abs(inphase); + float quadratureAbsolute = Math.abs(quadrature); + + if(inphaseAbsolute > quadratureAbsolute) + { + return inphaseAbsolute + (0.4f * quadratureAbsolute); + } + else + { + return quadratureAbsolute + (0.4f * inphaseAbsolute); + } + } + + public static void main(String[] args) + { + double angle = Math.PI; //180 degrees + + Complex s1 = new Complex((float)Math.sin(angle), (float)Math.cos(angle)); + + Complex tap = new Complex(1.0f, -0.1f); + + Complex convolved = Complex.multiply(s1, tap); + + mLog.debug("s: " + s1.toString() + + " t: " + tap.toString() + + " convolved: " + convolved.toString()); + + } } diff --git a/src/main/java/io/github/dsheirer/source/IControllableFileSource.java b/src/main/java/io/github/dsheirer/source/IControllableFileSource.java index 13270141..59868d66 100644 --- a/src/main/java/io/github/dsheirer/source/IControllableFileSource.java +++ b/src/main/java/io/github/dsheirer/source/IControllableFileSource.java @@ -1,3 +1,18 @@ +/******************************************************************************* + * 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.source; import javax.sound.sampled.UnsupportedAudioFileException; @@ -10,22 +25,24 @@ import java.io.IOException; */ public interface IControllableFileSource { - /** - * Opens the file source - */ - public void open() throws IOException, UnsupportedAudioFileException; - - public void close() throws IOException; - - public File getFile(); - - public void next( int frames ) throws IOException; - - public void next( int frames, boolean broadcast ) throws IOException; - - public long getFrameCount() throws IOException; - - public void setListener( IFrameLocationListener listener ); - - public void removeListener( IFrameLocationListener listener ); + /** + * Opens the file source + */ + public void open() throws IOException, UnsupportedAudioFileException; + + public void close() throws IOException; + + public File getFile(); + + public void next(int frames) throws IOException; + + public void next(int frames, boolean broadcast) throws IOException; + + public long getFrameCount() throws IOException; + + public int getSampleRate(); + + public void setListener(IFrameLocationListener listener); + + public void removeListener(IFrameLocationListener listener); }