diff --git a/src/main/java/io/github/dsheirer/buffer/FloatCircularBuffer.java b/src/main/java/io/github/dsheirer/buffer/FloatCircularBuffer.java index e69de29b..640e6506 100644 --- a/src/main/java/io/github/dsheirer/buffer/FloatCircularBuffer.java +++ b/src/main/java/io/github/dsheirer/buffer/FloatCircularBuffer.java @@ -0,0 +1,120 @@ +/******************************************************************************* + * 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 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 + * + * @author denny + */ +public class FloatCircularBuffer +{ + float[] mBuffer; + int mBufferPointer = 0; + + /** + * 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 FloatCircularBuffer(int size, float initialFillValue) + { + mBuffer = new float[size]; + Arrays.fill(mBuffer, initialFillValue); + } + + /** + * 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 FloatCircularBuffer(int size) + { + this(size, 0.0f); + } + + public float[] getBuffer() + { + return mBuffer; + } + + /** + * 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 float get(float newValue) + { + float oldestSample = mBuffer[mBufferPointer]; + + mBuffer[mBufferPointer] = newValue; + + mBufferPointer++; + + if(mBufferPointer >= mBuffer.length) + { + mBufferPointer = 0; + } + + return oldestSample; + } + + public float[] getAll() + { + int valuePointer = 0; + float[] values = new float[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(float newValue) + { + mBuffer[mBufferPointer] = newValue; + + mBufferPointer++; + + if(mBufferPointer >= mBuffer.length) + { + mBufferPointer = 0; + } + } +} 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 6ac9a164..64cb1d0f 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 @@ -18,10 +18,12 @@ 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; +import io.github.dsheirer.sample.complex.reusable.ReusableBufferQueue; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; public class ComplexFIRFilter extends FIRFilter { + private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue(); private RealFIRFilter mIFilter; private RealFIRFilter mQFilter; @@ -61,24 +63,43 @@ public class ComplexFIRFilter extends FIRFilter 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) + public float[] filter(float[] samples) { - float[] samples = buffer.getSamples(); - float[] filtered = new float[samples.length]; + float[] filteredSamples = 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]); + filteredSamples[x] = filterInphase(samples[x]); + filteredSamples[x + 1] = filterQuadrature(samples[x + 1]); } - return new ComplexBuffer(filtered); + return filteredSamples; + } + + /** + * Filters the complex samples from the buffer and returns a new complex buffer with the filtered output + * + * @param originalBuffer with complex samples to filter + * @return new buffer containing filtered complex samples + */ + public ReusableComplexBuffer filter(ReusableComplexBuffer originalBuffer) + { + ReusableComplexBuffer filteredBuffer = mReusableBufferQueue.getBuffer(originalBuffer.getSampleLength()); + filteredBuffer.setTimestamp(originalBuffer.getTimestamp()); + + float[] samples = originalBuffer.getSamples(); + float[] filteredSamples = filteredBuffer.getSamples(); + + for(int x = 0; x < originalBuffer.getSampleLength(); x += 2) + { + filteredSamples[x] = filterInphase(samples[x]); + filteredSamples[x + 1] = filterQuadrature(samples[x + 1]); + } + + originalBuffer.decrementUserCount(); + filteredBuffer.incrementUserCount(); + + return filteredBuffer; } @Override @@ -86,5 +107,6 @@ public class ComplexFIRFilter extends FIRFilter { mIFilter.dispose(); mQFilter.dispose(); + mReusableBufferQueue.dispose(); } } 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 215d7b46..560655b4 100644 --- a/src/main/java/io/github/dsheirer/dsp/gain/ComplexFeedForwardGainControl.java +++ b/src/main/java/io/github/dsheirer/dsp/gain/ComplexFeedForwardGainControl.java @@ -19,6 +19,8 @@ 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; +import io.github.dsheirer.sample.complex.reusable.ReusableBufferQueue; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; public class ComplexFeedForwardGainControl implements ComplexSampleListener { @@ -26,8 +28,8 @@ public class ComplexFeedForwardGainControl implements ComplexSampleListener public static final float MINIMUM_ENVELOPE = 0.0001f; private ComplexSampleListener mListener; - private FloatCircularBuffer mEnvelopeHistory; + private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue(); private float mMaxEnvelope = 0.0f; private float mGain = 1.0f; @@ -107,19 +109,59 @@ public class ComplexFeedForwardGainControl implements ComplexSampleListener mListener = listener; } - public ComplexBuffer filter(ComplexBuffer complexBuffer) + public float[] filter(float[] complesSamples) { - float[] samples = complexBuffer.getSamples(); - float[] processed = new float[samples.length]; + mMaxEnvelope = MINIMUM_ENVELOPE; + float currentEnvelope; - for(int x = 0; x < samples.length; x += 2) + for(int x = 0; x < complesSamples.length; x += 2) { - process(samples[x], samples[x + 1]); + currentEnvelope = Complex.envelope(complesSamples[x], complesSamples[x + 1]); - processed[x] = samples[x] * mGain; - processed[x + 1] = samples[x + 1] * mGain; + if(currentEnvelope > mMaxEnvelope) + { + mMaxEnvelope = currentEnvelope; + } } - return new ComplexBuffer(processed); + adjustGain(); + + float[] processed = new float[complesSamples.length]; + + for(int x = 0; x < complesSamples.length; x += 2) + { + processed[x] = complesSamples[x] * mGain; + processed[x + 1] = complesSamples[x + 1] * mGain; + } + + return processed; + } + + /** + * Applies gain to the complex sample buffer + * @param buffer to apply gain + * @return buffer with gain applied samples + */ + public ComplexBuffer filter(ComplexBuffer buffer) + { + float[] filtered = filter(buffer.getSamples()); + return new ComplexBuffer(filtered); + } + + /** + * Applies gain to the complex sample buffer + * @param buffer to apply gain + * @return reusable buffer with the user count incremented to 1 + */ + public ReusableComplexBuffer filter(ReusableComplexBuffer buffer) + { + float[] filtered = filter(buffer.getSamples()); + + ReusableComplexBuffer filteredBuffer = mReusableBufferQueue.getBuffer(filtered.length); + + filteredBuffer.reloadFrom(filtered, buffer.getTimestamp()); + filteredBuffer.incrementUserCount(); + + return filteredBuffer; } } diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDecisionDirectedDemodulator.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDecisionDirectedDemodulator.java new file mode 100644 index 00000000..7eebc342 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDecisionDirectedDemodulator.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.dsp.psk; + +import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop; +import io.github.dsheirer.dsp.symbol.Dibit; +import io.github.dsheirer.sample.complex.Complex; + +public class DQPSKDecisionDirectedDemodulator extends PSKDemodulator +{ + protected DQPSKDecisionDirectedSymbolEvaluator mSymbolEvaluator = new DQPSKDecisionDirectedSymbolEvaluator(); + private Complex mPreviousPrecedingSample = new Complex(0, 0); + private Complex mPreviousCurrentSample = new Complex(0, 0); + private Complex mPrecedingSample = new Complex(0, 0); + private Complex mCurrentSample = new Complex(0, 0); + private Complex mPrecedingSymbol = new Complex(0, 0); + protected Complex mCurrentSymbol = new Complex(0, 0); + + /** + * Decoder for Differential Quaternary Phase Shift Keying (DQPSK). This decoder uses both a Costas Loop (PLL) and + * a decision-directed DQPSK symbol timing and phase error detector to automatically align to the incoming carrier + * frequency and to adjust for any changes in symbol timing. + * + * This detector is optimized for constant amplitude DQPSK symbols like C4FM. + * + * @param phaseLockedLoop for tracking carrier frequency error + * @param interpolatingSampleBuffer to hold samples for interpolating a symbol + */ + public DQPSKDecisionDirectedDemodulator(IPhaseLockedLoop phaseLockedLoop, InterpolatingSampleBuffer interpolatingSampleBuffer) + { + super(interpolatingSampleBuffer, phaseLockedLoop); + } + + /** + * Calculates a symbol from the interpolating buffer + */ + protected void calculateSymbol() + { + //Get preceding sample and an interpolated current sample from the interpolating buffer + mPrecedingSample = getInterpolatingSampleBuffer().getPrecedingSample(); + mCurrentSample = getInterpolatingSampleBuffer().getCurrentSample(); + + //Differential decode preceding and current symbols by calculating the angular rotation between the previous and + //current samples (current sample x complex conjugate of previous sample). + + //Note: preceding symbol is a preceding measurement of the current symbol that is simply used as a reference + //point to determine vector rotation to the current symbol -- it is not the true predecessor symbol + mPrecedingSymbol.setInphase(Complex.multiplyInphase(mPrecedingSample.inphase(), mPrecedingSample.quadrature(), + mPreviousPrecedingSample.inphase(), -mPreviousPrecedingSample.quadrature())); + mPrecedingSymbol.setQuadrature(Complex.multiplyQuadrature(mPrecedingSample.inphase(), mPrecedingSample.quadrature(), + mPreviousPrecedingSample.inphase(), -mPreviousPrecedingSample.quadrature())); + + mCurrentSymbol.setInphase(Complex.multiplyInphase(mCurrentSample.inphase(), mCurrentSample.quadrature(), + mPreviousCurrentSample.inphase(), -mPreviousCurrentSample.quadrature())); + mCurrentSymbol.setQuadrature(Complex.multiplyQuadrature(mCurrentSample.inphase(), mCurrentSample.quadrature(), + mPreviousCurrentSample.inphase(), -mPreviousCurrentSample.quadrature())); + + //Set gain to unity before we calculate the error value + mPrecedingSymbol.normalize(); + mCurrentSymbol.normalize(); + + //Apply symbols to evaluator to calculate phase and timing error and make a symbol decision + mSymbolEvaluator.setSymbol(mPrecedingSymbol, mCurrentSymbol); + + //Update the symbol timing error + getInterpolatingSampleBuffer().resetAndAdjust(mSymbolEvaluator.getTimingError()); + + //Update the costas loop (PLL) with any measured phase error + getPLL().adjust(clip(mSymbolEvaluator.getPhaseError(), 0.5f)); + + //Store current samples/symbols to use for the next symbol period + mPreviousPrecedingSample.setValues(mPrecedingSample); + mPreviousCurrentSample.setValues(mCurrentSample); + + broadcast(mSymbolEvaluator.getSymbolDecision()); + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulatorInstrumented.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDecisionDirectedDemodulatorInstrumented.java similarity index 79% rename from src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulatorInstrumented.java rename to src/main/java/io/github/dsheirer/dsp/psk/DQPSKDecisionDirectedDemodulatorInstrumented.java index cc90a742..819a9124 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulatorInstrumented.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDecisionDirectedDemodulatorInstrumented.java @@ -20,11 +20,11 @@ import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.complex.Complex; -public class DQPSKDemodulatorInstrumented extends DQPSKDemodulator +public class DQPSKDecisionDirectedDemodulatorInstrumented extends DQPSKDecisionDirectedDemodulator { private Listener mSymbolDecisionDataListener; private Listener mSamplesPerSymbolListener; - private Listener mSymbolListener; + private Listener mComplexSymbolListener; private Listener mPLLErrorListener; private Listener mPLLFrequencyListener; private double mSampleRate; @@ -40,9 +40,9 @@ public class DQPSKDemodulatorInstrumented extends DQPSKDemodulator * @param interpolatingSampleBuffer * @param sampleRate of the incoming complex sample stream */ - public DQPSKDemodulatorInstrumented(IPhaseLockedLoop phaseLockedLoop, - InterpolatingSampleBufferInstrumented interpolatingSampleBuffer, - double sampleRate) + public DQPSKDecisionDirectedDemodulatorInstrumented(IPhaseLockedLoop phaseLockedLoop, + InterpolatingSampleBufferInstrumented interpolatingSampleBuffer, + double sampleRate) { super(phaseLockedLoop, interpolatingSampleBuffer); mSampleRate = sampleRate; @@ -57,20 +57,20 @@ public class DQPSKDemodulatorInstrumented extends DQPSKDemodulator if(mSymbolDecisionDataListener != null) { mSymbolDecisionDataListener.receive( - ((InterpolatingSampleBufferInstrumented)mInterpolatingSampleBuffer).getSymbolDecisionData()); + ((InterpolatingSampleBufferInstrumented)getInterpolatingSampleBuffer()).getSymbolDecisionData()); } super.calculateSymbol(); if(mSamplesPerSymbolListener != null) { - mSamplesPerSymbolListener.receive((double)mInterpolatingSampleBuffer.getSamplingPoint()); + mSamplesPerSymbolListener.receive((double)getInterpolatingSampleBuffer().getSamplingPoint()); } //Send to an external constellation symbol listener when registered - if(mSymbolListener != null) + if(mComplexSymbolListener != null) { - mSymbolListener.receive(mCurrentSymbol); + mComplexSymbolListener.receive(mCurrentSymbol); } if(mPLLErrorListener != null) @@ -80,7 +80,7 @@ public class DQPSKDemodulatorInstrumented extends DQPSKDemodulator if(mPLLFrequencyListener != null) { - double loopFrequency = ((CostasLoop)mPLL).getLoopFrequency(); + double loopFrequency = ((CostasLoop)getPLL()).getLoopFrequency(); loopFrequency *= mSampleRate / (2.0 * Math.PI); @@ -107,9 +107,9 @@ public class DQPSKDemodulatorInstrumented extends DQPSKDemodulator /** * Registers the listener to receive decoded QPSK symbols */ - public void setSymbolListener(Listener listener) + public void setComplexSymbolListener(Listener listener) { - mSymbolListener = listener; + mComplexSymbolListener = listener; } /** diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolEvaluator.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDecisionDirectedSymbolEvaluator.java similarity index 95% rename from src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolEvaluator.java rename to src/main/java/io/github/dsheirer/dsp/psk/DQPSKDecisionDirectedSymbolEvaluator.java index cab0fd22..077010dc 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolEvaluator.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDecisionDirectedSymbolEvaluator.java @@ -18,7 +18,7 @@ package io.github.dsheirer.dsp.psk; import io.github.dsheirer.dsp.symbol.Dibit; import io.github.dsheirer.sample.complex.Complex; -public class DQPSKSymbolEvaluator implements IDQPSKSymbolEvaluator +public class DQPSKDecisionDirectedSymbolEvaluator implements IPSKSymbolEvaluator { private static final Complex ROTATE_FROM_PLUS_135 = Complex.fromAngle(-3.0 * Math.PI / 4.0); private static final Complex ROTATE_FROM_PLUS_45 = Complex.fromAngle(-1.0 * Math.PI / 4.0); @@ -32,7 +32,7 @@ public class DQPSKSymbolEvaluator implements IDQPSKSymbolEvaluator private Dibit mSymbolDecision = Dibit.D00_PLUS_1; /** - * Decision-directed symbol phase and timing error detector and symbol decision slicer. + * Differential QPSK Decision-directed symbol phase and timing error detector and symbol decision slicer. * * Symbol decision is based on the closest reference quadrant for the sampled symbol. * @@ -40,7 +40,7 @@ public class DQPSKSymbolEvaluator implements IDQPSKSymbolEvaluator * symbol. Timing error is further corrected to compensate for clock-wise or counter-clock-wise vector rotation * relative to the immediately preceding symbol. */ - public DQPSKSymbolEvaluator() + public DQPSKDecisionDirectedSymbolEvaluator() { } @@ -57,7 +57,6 @@ public class DQPSKSymbolEvaluator implements IDQPSKSymbolEvaluator * since it's only a reference * @param current symbol to be evaluated - interpolated */ - @Override public void setSymbol(Complex preceding, Complex current) { mSymbolError.setValues(current); diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulator.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulator.java deleted file mode 100644 index f8b6aabd..00000000 --- a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulator.java +++ /dev/null @@ -1,162 +0,0 @@ -/******************************************************************************* - * 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; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class DQPSKDemodulator implements ComplexSampleListener -{ - private final static Logger mLog = LoggerFactory.getLogger(DQPSKDemodulator.class); - protected InterpolatingSampleBuffer mInterpolatingSampleBuffer; - protected IPhaseLockedLoop mPLL; - protected DQPSKSymbolEvaluator mSymbolEvaluator = new DQPSKSymbolEvaluator(); - private Complex mReceivedSample = new Complex(0, 0); - private Complex mPreviousPrecedingSample = new Complex(0, 0); - private Complex mPreviousCurrentSample = new Complex(0, 0); - private Complex mPrecedingSample = new Complex(0, 0); - private Complex mCurrentSample = new Complex(0, 0); - private Complex mPrecedingSymbol = new Complex(0, 0); - protected Complex mCurrentSymbol = new Complex(0, 0); - private Listener mDibitListener; - - /** - * Decoder for Differential Quaternary Phase Shift Keying (DQPSK). This decoder uses both a Costas Loop (PLL) and - * a custom DQPSK symbol timing error detector to automatically align to the incoming carrier frequency and to - * adjust for any changes in symbol timing. - * - * This detector is optimized for constant amplitude DQPSK symbols like C4FM. - * - * @param phaseLockedLoop for tracking carrier frequency error - * @param interpolatingSampleBuffer to hold samples for interpolating a symbol - */ - public DQPSKDemodulator(IPhaseLockedLoop phaseLockedLoop, InterpolatingSampleBuffer interpolatingSampleBuffer) - { - mPLL = phaseLockedLoop; - mInterpolatingSampleBuffer = interpolatingSampleBuffer; - } - - /** - * Processes a (filtered) 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]); - } - } - - /** - * Processes a complex sample for decoding. Once sufficient samples are buffered, a symbol decision is made. - * @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 - mReceivedSample.setValues(inphase, quadrature); - - //Mix current sample with costas loop to remove any rotation that is present from a mis-tuned carrier frequency - mReceivedSample.multiply(mPLL.incrementAndGetCurrentVector()); - - //Store the sample in the interpolating buffer - mInterpolatingSampleBuffer.receive(mReceivedSample); - - //Calculate the symbol once we've stored enough samples - if(mInterpolatingSampleBuffer.hasSymbol()) - { - calculateSymbol(); - } - } - - /** - * Calculates a symbol from the interpolating buffer - */ - protected void calculateSymbol() - { - //Get preceding sample and an interpolated current sample from the interpolating buffer - mPrecedingSample = mInterpolatingSampleBuffer.getPrecedingSample(); - mCurrentSample = mInterpolatingSampleBuffer.getCurrentSample(); - - //Calculate preceding, current and following symbols as the delta rotation compared to the previous samples - mPrecedingSymbol.setInphase(Complex.multiplyInphase(mPrecedingSample.inphase(), mPrecedingSample.quadrature(), - mPreviousPrecedingSample.inphase(), -mPreviousPrecedingSample.quadrature())); - mPrecedingSymbol.setQuadrature(Complex.multiplyQuadrature(mPrecedingSample.inphase(), mPrecedingSample.quadrature(), - mPreviousPrecedingSample.inphase(), -mPreviousPrecedingSample.quadrature())); - - mCurrentSymbol.setInphase(Complex.multiplyInphase(mCurrentSample.inphase(), mCurrentSample.quadrature(), - mPreviousCurrentSample.inphase(), -mPreviousCurrentSample.quadrature())); - mCurrentSymbol.setQuadrature(Complex.multiplyQuadrature(mCurrentSample.inphase(), mCurrentSample.quadrature(), - mPreviousCurrentSample.inphase(), -mPreviousCurrentSample.quadrature())); - - //Set gain to unity before we calculate the error value - mPrecedingSymbol.normalize(); - mCurrentSymbol.normalize(); - - mSymbolEvaluator.setSymbol(mPrecedingSymbol, mCurrentSymbol); - - mInterpolatingSampleBuffer.resetAndAdjust(mSymbolEvaluator.getTimingError()); - - //Store current samples/symbols to use for the next period - mPreviousPrecedingSample.setValues(mPrecedingSample); - mPreviousCurrentSample.setValues(mCurrentSample); - - //Calculate the phase error of the current symbol relative to the expected constellation and provide - //feedback to the PLL - mPLL.adjust(clip(mSymbolEvaluator.getPhaseError(), 0.5f)); - - if(mDibitListener != null) - { - mDibitListener.receive(mSymbolEvaluator.getSymbolDecision()); - } - } - - /** - * Registers the listener to receive dibit symbol decisions from this demodulator - */ - public void setDibitListener(Listener listener) - { - mDibitListener = listener; - } - - /** - * 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/DQPSKGardnerDemodulator.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKGardnerDemodulator.java new file mode 100644 index 00000000..4e8033ee --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKGardnerDemodulator.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.dsp.psk; + +import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop; +import io.github.dsheirer.dsp.symbol.Dibit; +import io.github.dsheirer.sample.complex.Complex; + +public class DQPSKGardnerDemodulator extends PSKDemodulator +{ + protected DQPSKGardnerSymbolEvaluator mSymbolEvaluator = new DQPSKGardnerSymbolEvaluator(); + private Complex mPreviousCurrentSample = new Complex(0, 0); + private Complex mPreviousMiddleSample = new Complex(0, 0); + private Complex mMiddleSymbol = new Complex(0, 0); + protected Complex mCurrentSymbol = new Complex(0, 0); + + /** + * Implements a Differential QPSK demodulator using a Costas Loop (PLL) and a Gardner timing error detector. + * + * This decoder is optimized for P25 Linear Simulcast Modulation (LSM). + * + * @param phaseLockedLoop for tracking carrier frequency error + * @param interpolatingSampleBuffer to hold samples for interpolating a symbol + */ + public DQPSKGardnerDemodulator(IPhaseLockedLoop phaseLockedLoop, InterpolatingSampleBuffer interpolatingSampleBuffer) + { + super(interpolatingSampleBuffer, phaseLockedLoop); + } + + public void dispose() + { + } + + @Override + protected void calculateSymbol() + { + //Note: the interpolating sample buffer holds 2 symbols worth of samples and the current sample method points + //to the sample at the mid-point between those 2 symbol periods and the middle sample method points to the + //sample that is half a symbol period after the current sample. Since we need a middle sample and a current + //symbol sample for the gardner calculation, we'll treat the interpolating buffer's current sample as the + //gardner mid-point and we'll treat the interpolating buffer's mid-point sample as the current symbol + //sample (ie flip-flopped) + Complex middleSample = getInterpolatingSampleBuffer().getCurrentSample(); + Complex currentSample = getInterpolatingSampleBuffer().getMiddleSample(); + + //Differential decode middle and current symbols by calculating the angular rotation between the previous and + //current samples (current sample x complex conjugate of previous sample). + mMiddleSymbol.setInphase(Complex.multiplyInphase(middleSample.inphase(), middleSample.quadrature(), + mPreviousMiddleSample.inphase(), -mPreviousMiddleSample.quadrature())); + mMiddleSymbol.setQuadrature(Complex.multiplyQuadrature(middleSample.inphase(), middleSample.quadrature(), + mPreviousMiddleSample.inphase(), -mPreviousMiddleSample.quadrature())); + + mCurrentSymbol.setInphase(Complex.multiplyInphase(currentSample.inphase(), currentSample.quadrature(), + mPreviousCurrentSample.inphase(), -mPreviousCurrentSample.quadrature())); + mCurrentSymbol.setQuadrature(Complex.multiplyQuadrature(currentSample.inphase(), currentSample.quadrature(), + mPreviousCurrentSample.inphase(), -mPreviousCurrentSample.quadrature())); + + //Set gain to unity before we calculate the error value + mMiddleSymbol.normalize(); + mCurrentSymbol.normalize(); + + //Pass symbols to evaluator to determine timing and phase error and make symbol decision + mSymbolEvaluator.setSymbols(mMiddleSymbol, mCurrentSymbol); + + //Update symbol timing error + getInterpolatingSampleBuffer().resetAndAdjust(mSymbolEvaluator.getTimingError()); + + //Update PLL phase error + getPLL().adjust(mSymbolEvaluator.getPhaseError()); + + //Store current samples/symbols for next symbol calculation + mPreviousMiddleSample.setValues(middleSample); + mPreviousCurrentSample.setValues(currentSample); + + broadcast(mSymbolEvaluator.getSymbolDecision()); + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKGardnerDemodulatorInstrumented.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKGardnerDemodulatorInstrumented.java new file mode 100644 index 00000000..272b0534 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKGardnerDemodulatorInstrumented.java @@ -0,0 +1,127 @@ +/******************************************************************************* + * 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.CostasLoop; +import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.complex.Complex; + +public class DQPSKGardnerDemodulatorInstrumented extends DQPSKGardnerDemodulator +{ + private Listener mSymbolDecisionDataListener; + private Listener mSamplesPerSymbolListener; + private Listener mComplexSymbolListener; + private Listener mPLLErrorListener; + private Listener mPLLFrequencyListener; + private double mSampleRate; + + /** + * Implements a Differential QPSK demodulator using a Costas Loop (PLL) and a Gardner timing error detector. + * + * This decoder is optimized for P25 Linear Simulcast Modulation (LSM). + * + * @param phaseLockedLoop for tracking carrier frequency error + * @param interpolatingSampleBuffer to hold samples for interpolating a symbol + */ + public DQPSKGardnerDemodulatorInstrumented(IPhaseLockedLoop phaseLockedLoop, + InterpolatingSampleBuffer interpolatingSampleBuffer, + double sampleRate) + { + super(phaseLockedLoop, interpolatingSampleBuffer); + mSampleRate = sampleRate; + } + + /** + * Overrides the parent class symbol calculation to capture eye diagram data + */ + protected void calculateSymbol() + { + //Eye diagram listener + if(mSymbolDecisionDataListener != null) + { + mSymbolDecisionDataListener.receive( + ((InterpolatingSampleBufferInstrumented)getInterpolatingSampleBuffer()).getSymbolDecisionData()); + } + + super.calculateSymbol(); + + if(mSamplesPerSymbolListener != null) + { + mSamplesPerSymbolListener.receive((double)getInterpolatingSampleBuffer().getSamplingPoint()); + } + + //Send to an external constellation symbol listener when registered + if(mComplexSymbolListener != null) + { + mComplexSymbolListener.receive(mCurrentSymbol); + } + + if(mPLLErrorListener != null) + { + mPLLErrorListener.receive((double)mSymbolEvaluator.getPhaseError()); + } + + if(mPLLFrequencyListener != null) + { + double loopFrequency = ((CostasLoop)getPLL()).getLoopFrequency(); + + loopFrequency *= mSampleRate / (2.0 * Math.PI); + + mPLLFrequencyListener.receive(loopFrequency); + } + } + + /** + * Registers a listener to receive symbol decision data to produce an eye diagram. + */ + public void setSymbolDecisionDataListener(Listener listener) + { + mSymbolDecisionDataListener = listener; + } + + /** + * Registers the listener to receive PLL error values + */ + public void setSamplesPerSymbolListener(Listener listener) + { + mSamplesPerSymbolListener = listener; + } + + /** + * Registers the listener to receive decoded QPSK symbols + */ + public void setComplexSymbolListener(Listener listener) + { + mComplexSymbolListener = listener; + } + + /** + * Registers the listener to receive PLL error values + */ + public void setPLLErrorListener(Listener listener) + { + mPLLErrorListener = listener; + } + + /** + * Registers the listener to receive PLL error values + */ + public void setPLLFrequencyListener(Listener listener) + { + mPLLFrequencyListener = listener; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKGardnerSymbolEvaluator.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKGardnerSymbolEvaluator.java new file mode 100644 index 00000000..e54ae90a --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKGardnerSymbolEvaluator.java @@ -0,0 +1,158 @@ +/******************************************************************************* + * 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 DQPSKGardnerSymbolEvaluator implements IPSKSymbolEvaluator +{ + private float mPhaseError = 0.0f; + private float mTimingError = 0.0f; + private Dibit mSymbolDecision = Dibit.D00_PLUS_1; + private Complex mPreviousSymbol = new Complex(0, 0); + + /** + * Differential QPSK Decision-directed symbol phase and timing error detector and symbol decision slicer. + * + * Symbol decision is based on the closest reference quadrant for the sampled symbol. + * + * Phase error is calculated as the angular distance of the sampled symbol from the reference symbol. + * + * Timing error is calculated using the Gardner method by comparing the previous symbol to the current symbol and + * amplifying the delta between the two using the intra-symbol sample to form the timing error. + */ + public DQPSKGardnerSymbolEvaluator() + { + } + + /** + * Sets the middle and current symbols to be evaluated for phase and timing errors and to determine the + * transmitted symbol relative to the closest reference symbol. After invoking this method, you can access the + * phase and timing errors and the symbol decision via their respective accessor methods. + * + * Phase and timing error values are calculated by first determining the symbol and then calculating the phase + * and timing errors relative to the reference symbol. The timing error is corrected with the appropriate sign + * relative to the angular vector rotation so that the error value indicates the correct error direction. + * + * @param middle interpolated differentially-decoded sample that falls midway between previous/current symbols + * @param current interpolated differentially-decoded symbol + */ + public void setSymbols(Complex middle, Complex current) + { + /* Gardner timing error calculations */ + float errorInphase = (mPreviousSymbol.inphase() - current.inphase()) * middle.inphase(); + float errorQuadrature = (mPreviousSymbol.quadrature() - current.quadrature()) * middle.quadrature(); + mTimingError = normalize(errorInphase + errorQuadrature, 1.0f); + + //Calculate phase error and determine the transmitted dibit from the symbol + if(Math.abs(current.inphase()) > Math.abs(current.quadrature())) + { + if(current.inphase() > 0) + { + mSymbolDecision = Dibit.D10_MINUS_1; + mPhaseError = -current.quadrature(); + } + else + { + mSymbolDecision = Dibit.D01_PLUS_3; + mPhaseError = current.quadrature(); + } + } + else + { + if(current.quadrature() > 0) + { + mSymbolDecision = Dibit.D00_PLUS_1; + mPhaseError = current.inphase(); + } + else + { + mSymbolDecision = Dibit.D11_MINUS_3; + mPhaseError = -current.inphase(); + } + } + + mPreviousSymbol.setValues(current); + } + + /** + * 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); + } + } + + + /** + * Phase error of the symbol relative to the nearest reference symbol. + * + * @return phase error in radians of distance from the reference symbol. + */ + @Override + public float getPhaseError() + { + return mPhaseError; + } + + /** + * Timing error of the symbol relative to the nearest reference symbol. + * + * @return timing error in radians of angular distance from the reference symbol recognizing that the symbol + * originates at zero radians and rotates toward the intended reference symbol, therefore the error value indicates + * if the symbol was sampled early (-) or late (+) relative to the reference symbol. + */ + @Override + public float getTimingError() + { + return mTimingError; + } + + /** + * Reference symbol that is closest to the transmitted/sampled symbol. + */ + @Override + public Dibit getSymbolDecision() + { + return mSymbolDecision; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/psk/IDQPSKSymbolEvaluator.java b/src/main/java/io/github/dsheirer/dsp/psk/IPSKSymbolEvaluator.java similarity index 71% rename from src/main/java/io/github/dsheirer/dsp/psk/IDQPSKSymbolEvaluator.java rename to src/main/java/io/github/dsheirer/dsp/psk/IPSKSymbolEvaluator.java index d0213a0d..fa1d9d12 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/IDQPSKSymbolEvaluator.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/IPSKSymbolEvaluator.java @@ -15,20 +15,8 @@ ******************************************************************************/ package io.github.dsheirer.dsp.psk; -import io.github.dsheirer.dsp.symbol.Dibit; -import io.github.dsheirer.sample.complex.Complex; - - -public interface IDQPSKSymbolEvaluator +public interface IPSKSymbolEvaluator { - /** - * Sets the sampled symbol to be evaluated. Note: the symbol's gain value MUST be normalized to the unit circle. - * - * @param preceding symbol used to detect (counter-)clock-wise rotation - * @param symbol currently being sampled - */ - void setSymbol(Complex preceding, Complex symbol); - /** * Phase error of the symbol relative to the closest reference symbol * @return angular phase error in radians @@ -43,8 +31,8 @@ public interface IDQPSKSymbolEvaluator float getTimingError(); /** - * Dibit decision indicating the reference symbol location closest to the symbol. + * Symbol decision indicating the reference symbol location closest to the symbol. * @return symbol decision */ - Dibit getSymbolDecision(); + T getSymbolDecision(); } diff --git a/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBuffer.java b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBuffer.java index 15adbd9a..5b64f338 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBuffer.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBuffer.java @@ -24,6 +24,7 @@ public class InterpolatingSampleBuffer private Complex mPrecedingSample = new Complex(0,0); private Complex mCurrentSample = new Complex(0,0); + private Complex mMiddleSample = new Complex(0,0); private float[] mDelayLineInphase; private float[] mDelayLineQuadrature; @@ -42,8 +43,9 @@ public class InterpolatingSampleBuffer /** * Buffer to store complex sample data and produce interpolated samples. * @param samplesPerSymbol + * @param symbolTimingGain for the symbol timing error adjustments */ - public InterpolatingSampleBuffer(float samplesPerSymbol) + public InterpolatingSampleBuffer(float samplesPerSymbol, float symbolTimingGain) { mSamplingPoint = samplesPerSymbol; mDetectedSamplesPerSymbol = samplesPerSymbol; @@ -52,6 +54,7 @@ public class InterpolatingSampleBuffer mTwiceSamplesPerSymbol = (int)Math.floor(2.0 * samplesPerSymbol); mDelayLineInphase = new float[2 * mTwiceSamplesPerSymbol]; mDelayLineQuadrature = new float[2 * mTwiceSamplesPerSymbol]; + mDetectedSamplesPerSymbolGain = symbolTimingGain * mSampleCounterGain * mSampleCounterGain; } /** @@ -125,18 +128,21 @@ public class InterpolatingSampleBuffer } /** - * Un-interpolated sample that precedes the current interpolated sampling point. The current sampling point falls - * somewhere between sample index 3 and index 4, therefore the preceding sample is located at index 3. + * Un-interpolated sample that precedes the current interpolated sampling point. The current interpolated sampling + * point falls somewhere between sample index 3 and index 4 and the sample is interpolated from sample + * indexes 0 - 7, therefore the uninterpolated sample that immediately precedes the current sample is + * located at index 3. */ public Complex getPrecedingSample() { - mPrecedingSample.setValues(mDelayLineInphase[mDelayLinePointer + 4], mDelayLineQuadrature[mDelayLinePointer + 3]); + mPrecedingSample.setValues(mDelayLineInphase[mDelayLinePointer + 3], mDelayLineQuadrature[mDelayLinePointer + 3]); return mPrecedingSample; } /** - * Interpolated middle sample for the symbol. Note: this method should only be invoked after testing for a - * complete symbol with the hasSymbol() method. + * Interpolated current sample for the symbol. + * + * Note: this method should only be invoked after testing for a complete symbol with the hasSymbol() method. */ public Complex getCurrentSample() { @@ -145,6 +151,21 @@ public class InterpolatingSampleBuffer return mCurrentSample; } + /** + * Interpolated sample that is 1/2 symbol away from (after) the current sample. + * + * Note: this method should only be invoked after testing for a complete symbol with the hasSymbol() method. + */ + public Complex getMiddleSample() + { + float halfDetectedSamplesPerSymbol = mDetectedSamplesPerSymbol / 2.0f; + + //Interpolated sample that is half a symbol away from (occurred before) the current sample. + mMiddleSample.setValues(getInphase(halfDetectedSamplesPerSymbol), + getQuadrature(halfDetectedSamplesPerSymbol)); + return mMiddleSample; + } + /** * Returns the interpolated inphase value for the specified offset * @param interpolation into the buffer to calculate the interpolated sample @@ -152,7 +173,15 @@ public class InterpolatingSampleBuffer */ public float getInphase(float interpolation) { - return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer + 1, interpolation); + if(interpolation < 1.0f) + { + return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer, interpolation); + } + else + { + int offset = (int)Math.floor(interpolation); + return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer + offset, interpolation - offset); + } } /** @@ -162,6 +191,14 @@ public class InterpolatingSampleBuffer */ public float getQuadrature(float interpolation) { - return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer + 1, interpolation); + if(interpolation < 1.0f) + { + return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer, interpolation); + } + else + { + int offset = (int)Math.floor(interpolation); + return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer + offset, interpolation - offset); + } } } diff --git a/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBufferInstrumented.java b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBufferInstrumented.java index 6c71fc25..dcdef399 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBufferInstrumented.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBufferInstrumented.java @@ -21,10 +21,9 @@ public class InterpolatingSampleBufferInstrumented extends InterpolatingSampleBu { private SymbolDecisionData mSymbolDecisionData; - public InterpolatingSampleBufferInstrumented(float samplesPerSymbol) + public InterpolatingSampleBufferInstrumented(float samplesPerSymbol, float symbolTimingGain) { - super(samplesPerSymbol); - + super(samplesPerSymbol, symbolTimingGain); mSymbolDecisionData = new SymbolDecisionData((int)samplesPerSymbol); } diff --git a/src/main/java/io/github/dsheirer/dsp/psk/LSMDemodulator.java b/src/main/java/io/github/dsheirer/dsp/psk/LSMDemodulator.java deleted file mode 100644 index 0536f7f2..00000000 --- a/src/main/java/io/github/dsheirer/dsp/psk/LSMDemodulator.java +++ /dev/null @@ -1,614 +0,0 @@ -package io.github.dsheirer.dsp.psk; - -/******************************************************************************* - * SDR Trunk - * 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; -import io.github.dsheirer.instrument.tap.TapGroup; -import io.github.dsheirer.instrument.tap.stream.EyeDiagramData; -import io.github.dsheirer.instrument.tap.stream.EyeDiagramDataTap; -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.Complex; -import io.github.dsheirer.sample.complex.ComplexSampleListener; -import io.github.dsheirer.source.ISourceEventListener; - -import java.util.ArrayList; -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 - * 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); - - /* 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 ISourceEventListener 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( ISourceEventListener listener ) - { - mFrequencyChangeListener = listener; - } - - public void removeListener( ISourceEventListener 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 ) -// { -// float average = mBuffer.get( frequency ); -// mLog.debug( "Avg:" + average + " Threshold:" + CORRECTION_THRESHOLD_FREQUENCY); -// -// boolean correctionNeeded = false; -// -// if( average > CORRECTION_THRESHOLD_FREQUENCY ) -// { -// mFrequencyError--; -// -// correctionNeeded = true; -// } -// else if( average < -CORRECTION_THRESHOLD_FREQUENCY ) -// { -// mFrequencyError++; -// -// correctionNeeded = true; -// } -// -// if( correctionNeeded && mFrequencyChangeListener != null ) -// { -// mLog.debug( "Issuing Frequency Correction: " + mFrequencyError ); -// mFrequencyChangeListener.process( -// new SourceEvent( Attribute.FREQUENCY_ERROR, -// mFrequencyError ) ); -// } -// -// mCounter = 0; -// } -// else -// { -// 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; - - private float[] mDelayLineInphase = new float[2 * TWICE_SAMPLES_PER_SYMBOL]; - private float[] mDelayLineQuadrature = new float[2 * TWICE_SAMPLES_PER_SYMBOL]; - - 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; - - 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() - { - } - - @Override - public void receive(float inphase, float quadrature) - { - /* Count down samples per symbol until we calculate the symbol */ - mMu--; - - 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; - - mDelayLineQuadrature[mDelayLinePointer] = derotatedQuadrature; - mDelayLineQuadrature[mDelayLinePointer + TWICE_SAMPLES_PER_SYMBOL] = derotatedQuadrature; - - /* 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; - } - - /* 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, IPhaseLockedLoop - { - 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)); - - 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 reset() - { - mLoopPhase = 0.0f; - mLoopFrequency = 0.0f; - } - - @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 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); - } - - /** - * 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(); - - /* 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; - } - } - - /** - * 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; - } - } - - @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/PSKDemodulator.java b/src/main/java/io/github/dsheirer/dsp/psk/PSKDemodulator.java new file mode 100644 index 00000000..c187a408 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/PSKDemodulator.java @@ -0,0 +1,137 @@ +/******************************************************************************* + * 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.sample.Listener; +import io.github.dsheirer.sample.complex.Complex; +import io.github.dsheirer.sample.complex.ComplexSampleListener; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; + +public abstract class PSKDemodulator implements ComplexSampleListener +{ + private InterpolatingSampleBuffer mInterpolatingSampleBuffer; + private IPhaseLockedLoop mPLL; + private Complex mReceivedSample = new Complex(0, 0); + private Listener mSymbolListener; + + /** + * Abstract Phase Shift Keyed (PSK) demodulator + * @param interpolatingSampleBuffer to store complex samples and provide access to interpolated samples + * @param phaseLockedLoop to track and remove frequency offset in the incoming samples + */ + public PSKDemodulator(InterpolatingSampleBuffer interpolatingSampleBuffer, IPhaseLockedLoop phaseLockedLoop) + { + mInterpolatingSampleBuffer = interpolatingSampleBuffer; + mPLL = phaseLockedLoop; + } + + /** + * Registers the listener to receive symbol decisions from this demodulator + */ + public void setSymbolListener(Listener listener) + { + mSymbolListener = listener; + } + + /** + * Broadcasts the symbol decision to the registered symbol listener + * @param symbol + */ + protected void broadcast(T symbol) + { + if(mSymbolListener != null) + { + mSymbolListener.receive(symbol); + } + } + + /** + * Interpolating sample buffer for receiving the incoming complex sample stream and providing access to + * indexed and interpolated samples + */ + protected InterpolatingSampleBuffer getInterpolatingSampleBuffer() + { + return mInterpolatingSampleBuffer; + } + + /** + * Phase Locked Loop (PLL) + */ + protected IPhaseLockedLoop getPLL() + { + return mPLL; + } + + /** + * Processes a (filtered) buffer containing complex samples for decoding + * @param reusableComplexBuffer with complex samples + */ + public void receive(ReusableComplexBuffer reusableComplexBuffer) + { + float[] samples = reusableComplexBuffer.getSamples(); + + for(int x = 0; x < samples.length; x += 2) + { + receive(samples[x], samples[x + 1]); + } + + reusableComplexBuffer.decrementUserCount(); + } + + /** + * Processes a complex sample for decoding. Once sufficient samples are buffered, a symbol decision is made. + * @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 + mReceivedSample.setValues(inphase, quadrature); + + //Mix current sample with costas loop to remove any rotation that is present from a mis-tuned carrier frequency + mReceivedSample.multiply(mPLL.incrementAndGetCurrentVector()); + + //Store the sample in the interpolating buffer + mInterpolatingSampleBuffer.receive(mReceivedSample); + + //Calculate the symbol once we've stored enough samples + if(mInterpolatingSampleBuffer.hasSymbol()) + { + calculateSymbol(); + } + } + + protected abstract void calculateSymbol(); + + /** + * 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/instrument/gui/viewer/DemodulatorViewerFX.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/DemodulatorViewerFX.java index 5e9745a6..5af21746 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 @@ -17,6 +17,7 @@ package io.github.dsheirer.instrument.gui.viewer; import io.github.dsheirer.instrument.gui.viewer.decoder.DecoderPaneFactory; import io.github.dsheirer.module.decode.DecoderType; +import io.github.dsheirer.module.decode.p25.P25Decoder; import javafx.application.Application; import javafx.application.Platform; import javafx.event.ActionEvent; @@ -50,7 +51,7 @@ public class DemodulatorViewerFX extends Application public void start(Stage primaryStage) throws Exception { mStage = primaryStage; - setTitle(null); + setTitle(); BorderPane borderPane = new BorderPane(); borderPane.setTop(getMenuBar()); @@ -62,6 +63,11 @@ public class DemodulatorViewerFX extends Application mStage.show(); } + private void setTitle() + { + mStage.setTitle("Demodulator Viewer FX"); + } + private void setTitle(DecoderType decoderType) { if(decoderType != null) @@ -70,7 +76,19 @@ public class DemodulatorViewerFX extends Application } else { - mStage.setTitle("Demodulator Viewer FX"); + setTitle(); + } + } + + private void setTitle(P25Decoder.Modulation modulation) + { + if(modulation != null) + { + mStage.setTitle("Demodulator Viewer FX - P25 " + modulation.getLabel()); + } + else + { + setTitle(); } } @@ -136,19 +154,52 @@ public class DemodulatorViewerFX extends Application { if(DecoderPaneFactory.isSupported(decoderType)) { - MenuItem decoderMenuItem = new MenuItem(decoderType.getShortDisplayString()); - - decoderMenuItem.setOnAction(new EventHandler() + if(decoderType == DecoderType.P25_PHASE1) { - @Override - public void handle(ActionEvent event) - { - getViewerDesktop().setDecoder(decoderType); - setTitle(decoderType); - } - }); + MenuItem c4fmDecoderMenuItem = new MenuItem("P25 Phase 1 C4FM"); - decoderMenu.getItems().add(decoderMenuItem); + c4fmDecoderMenuItem.setOnAction(new EventHandler() + { + @Override + public void handle(ActionEvent event) + { + getViewerDesktop().setP25Phase1Decoder(P25Decoder.Modulation.C4FM); + setTitle(decoderType); + } + }); + + decoderMenu.getItems().add(c4fmDecoderMenuItem); + + MenuItem lsmDecoderMenuItem = new MenuItem("P25 Phase 1 LSM"); + + lsmDecoderMenuItem.setOnAction(new EventHandler() + { + @Override + public void handle(ActionEvent event) + { + getViewerDesktop().setP25Phase1Decoder(P25Decoder.Modulation.CQPSK); + setTitle(decoderType); + } + }); + + decoderMenu.getItems().add(lsmDecoderMenuItem); + } + else + { + 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); + } } } 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 1ea7a687..6462901f 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 @@ -16,9 +16,9 @@ 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; +import io.github.dsheirer.sample.complex.reusable.ReusableBufferBroadcaster; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; import io.github.dsheirer.source.IControllableFileSource; import io.github.dsheirer.source.IFrameLocationListener; import io.github.dsheirer.source.wave.ComplexWaveSource; @@ -40,7 +40,7 @@ import javax.sound.sampled.UnsupportedAudioFileException; import java.io.File; import java.io.IOException; -public class PlaybackController extends HBox implements IFrameLocationListener, Listener +public class PlaybackController extends HBox implements IFrameLocationListener, Listener { private final static Logger mLog = LoggerFactory.getLogger(PlaybackController.class); @@ -56,7 +56,7 @@ public class PlaybackController extends HBox implements IFrameLocationListener, private DecoderPane mSampleRateListener; private IControllableFileSource mControllableFileSource; - private Broadcaster mComplexBufferBroadcaster = new Broadcaster<>(); + private ReusableBufferBroadcaster mComplexBufferBroadcaster = new ReusableBufferBroadcaster(); public PlaybackController() { @@ -85,7 +85,7 @@ public class PlaybackController extends HBox implements IFrameLocationListener, { mControllableFileSource = new ComplexWaveSource(file); mControllableFileSource.setListener(this); - ((ComplexWaveSource)mControllableFileSource).setListener((Listener)this); + ((ComplexWaveSource)mControllableFileSource).setListener((Listener)this); mControllableFileSource.open(); if(mSampleRateListener != null) @@ -136,7 +136,7 @@ public class PlaybackController extends HBox implements IFrameLocationListener, /** * Adds listener to receive complex buffers from this playback */ - public void addListener(Listener listener) + public void addListener(Listener listener) { mComplexBufferBroadcaster.addListener(listener); } @@ -144,7 +144,7 @@ public class PlaybackController extends HBox implements IFrameLocationListener, /** * Removes the listener from receiving complex buffers from this playback */ - public void removeListener(Listener listener) + public void removeListener(Listener listener) { mComplexBufferBroadcaster.removeListener(listener); } @@ -304,7 +304,7 @@ public class PlaybackController extends HBox implements IFrameLocationListener, } @Override - public void receive(ComplexBuffer complexBuffer) + public void receive(ReusableComplexBuffer complexBuffer) { mComplexBufferBroadcaster.broadcast(complexBuffer); } 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 bdd01f5d..fcdef3f9 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 @@ -18,6 +18,7 @@ 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 io.github.dsheirer.module.decode.p25.P25Decoder; import javafx.scene.layout.BorderPane; import java.io.File; @@ -40,14 +41,22 @@ public class ViewerDesktop extends BorderPane } public void setDecoder(DecoderType decoderType) + { + setDecoderPane(DecoderPaneFactory.getDecoderPane(decoderType)); + } + + public void setP25Phase1Decoder(P25Decoder.Modulation modulation) + { + setDecoderPane(DecoderPaneFactory.getP25DecoderPane(modulation)); + } + + private void setDecoderPane(DecoderPane decoderPane) { getPlaybackController().removeListener(getDecoderPane()); getChildren().remove(getDecoderPane()); - - mDecoderPane = DecoderPaneFactory.getDecoderPane(decoderType); + mDecoderPane = decoderPane; setCenter(getDecoderPane()); getPlaybackController().addListener(getDecoderPane()); - getPlaybackController().setSampleRateListener(getDecoderPane()); } diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexSampleLineChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexSampleLineChart.java index d2b25b4b..bec81eef 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexSampleLineChart.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexSampleLineChart.java @@ -19,7 +19,7 @@ import io.github.dsheirer.buffer.ComplexCircularBuffer; import io.github.dsheirer.dsp.gain.ComplexGain; 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.reusable.ReusableComplexBuffer; import javafx.beans.property.IntegerProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.value.ChangeListener; @@ -32,7 +32,7 @@ import javafx.scene.chart.XYChart; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class ComplexSampleLineChart extends LineChart implements Listener +public class ComplexSampleLineChart extends LineChart implements Listener { private final static Logger mLog = LoggerFactory.getLogger(ComplexSampleLineChart.class); @@ -92,7 +92,7 @@ public class ComplexSampleLineChart extends LineChart implements Listener +public class DecoderPane extends VBox implements Listener { - private Broadcaster mComplexBufferBroadcaster = new Broadcaster<>(); + private ReusableBufferBroadcaster mComplexBufferBroadcaster = new ReusableBufferBroadcaster(); private DecoderType mDecoderType; public DecoderPane(DecoderType decoderType) @@ -41,17 +41,17 @@ public class DecoderPane extends VBox implements Listener } @Override - public void receive(ComplexBuffer complexBuffer) + public void receive(ReusableComplexBuffer reusableComplexBuffer) { - mComplexBufferBroadcaster.broadcast(complexBuffer); + mComplexBufferBroadcaster.broadcast(reusableComplexBuffer); } - public void addListener(Listener listener) + public void addListener(Listener listener) { mComplexBufferBroadcaster.addListener(listener); } - public void removeListener(Listener listener) + public void removeListener(Listener listener) { mComplexBufferBroadcaster.removeListener(listener); } 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 index b069204e..03590922 100644 --- 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 @@ -16,6 +16,7 @@ package io.github.dsheirer.instrument.gui.viewer.decoder; import io.github.dsheirer.module.decode.DecoderType; +import io.github.dsheirer.module.decode.p25.P25Decoder; import java.util.EnumSet; @@ -31,7 +32,23 @@ public class DecoderPaneFactory switch(decoderType) { case P25_PHASE1: - return new P25Phase1Pane(); + throw new IllegalArgumentException("Use the getP25DecoderPane() method for P25 decoder type"); + } + + return getDefaultPane(); + } + + /** + * Creates a decoder pane for the P25 decoder type + */ + public static DecoderPane getP25DecoderPane(P25Decoder.Modulation modulation) + { + switch(modulation) + { + case C4FM: + return new P25Phase1C4FMPane(); + case CQPSK: + return new P25Phase1LSMPane(); } return getDefaultPane(); diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1C4FMPane.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1C4FMPane.java new file mode 100644 index 00000000..66ac5153 --- /dev/null +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1C4FMPane.java @@ -0,0 +1,194 @@ +/******************************************************************************* + * 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.ComplexSampleLineChart; +import io.github.dsheirer.instrument.gui.viewer.chart.DoubleLineChart; +import io.github.dsheirer.instrument.gui.viewer.chart.EyeDiagramChart; +import io.github.dsheirer.instrument.gui.viewer.chart.PhaseLineChart; +import io.github.dsheirer.instrument.gui.viewer.chart.SymbolChart; +import io.github.dsheirer.message.Message; +import io.github.dsheirer.module.decode.DecoderType; +import io.github.dsheirer.module.decode.p25.P25DecoderC4FMInstrumented; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.complex.reusable.ReusableBufferBroadcaster; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class P25Phase1C4FMPane extends DecoderPane +{ + private final static Logger mLog = LoggerFactory.getLogger(P25Phase1C4FMPane.class); + + private HBox mSampleChartBox; + private ComplexSampleLineChart mSampleLineChart; + private EyeDiagramChart mEyeDiagramChart; + private HBox mDecoderChartBox; + private SymbolChart mSymbolChart; + private PhaseLineChart mPLLPhaseErrorLineChart; + private DoubleLineChart mPLLFrequencyLineChart; + private DoubleLineChart mSamplesPerSymbolLineChart; + private ReusableBufferBroadcaster mFilteredBufferBroadcaster = new ReusableBufferBroadcaster(); + private P25DecoderC4FMInstrumented mDecoder = new P25DecoderC4FMInstrumented(null); + + public P25Phase1C4FMPane() + { + super(DecoderType.P25_PHASE1); + init(); + } + + private void init() + { + addListener(getDecoder()); + + getDecoder().setFilteredBufferListener(mFilteredBufferBroadcaster); + getDecoder().setComplexSymbolListener(getSymbolChart()); + getDecoder().setPLLPhaseErrorListener(getPLLPhaseErrorLineChart()); + getDecoder().setPLLFrequencyListener(getPLLFrequencyLineChart()); + getDecoder().setSymbolDecisionDataListener(getEyeDiagramChart()); + getDecoder().setSamplesPerSymbolListener(getSamplesPerSymbolLineChart()); + mFilteredBufferBroadcaster.addListener(getSampleLineChart()); + + HBox.setHgrow(getSampleChartBox(), Priority.ALWAYS); + HBox.setHgrow(getDecoderChartBox(), Priority.ALWAYS); + getChildren().addAll(getSampleChartBox(), getDecoderChartBox()); + + mDecoder.setMessageListener(new Listener() + { + @Override + public void receive(Message message) + { + mLog.debug(message.getMessage()); + } + }); + + } + + @Override + public void setSampleRate(double sampleRate) + { + mLog.debug("Configuring for sample rate: " + sampleRate); + + mDecoder.setSampleRate(sampleRate); + double samplesPerSymbol = sampleRate / 4800.0; + + getSampleLineChart().setSamplesPerSymbol((int)samplesPerSymbol); + } + + private P25DecoderC4FMInstrumented getDecoder() + { + return mDecoder; + } + + private SymbolChart getSymbolChart() + { + if(mSymbolChart == null) + { + mSymbolChart = new SymbolChart(10); + } + + return mSymbolChart; + } + + private HBox getDecoderChartBox() + { + if(mDecoderChartBox == null) + { + mDecoderChartBox = new HBox(); + mDecoderChartBox.setMaxHeight(Double.MAX_VALUE); + getSymbolChart().setMaxWidth(Double.MAX_VALUE); + getPLLPhaseErrorLineChart().setMaxWidth(Double.MAX_VALUE); + getPLLFrequencyLineChart().setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(getSymbolChart(), Priority.ALWAYS); + HBox.setHgrow(getPLLPhaseErrorLineChart(), Priority.ALWAYS); + HBox.setHgrow(getPLLFrequencyLineChart(), Priority.ALWAYS); + mDecoderChartBox.getChildren().addAll(getSymbolChart(), getPLLPhaseErrorLineChart(), + getPLLFrequencyLineChart()); + } + + return mDecoderChartBox; + } + + private HBox getSampleChartBox() + { + if(mSampleChartBox == null) + { + mSampleChartBox = new HBox(); + mSampleChartBox.setMaxHeight(Double.MAX_VALUE); + getSampleLineChart().setMaxWidth(Double.MAX_VALUE); + getEyeDiagramChart().setMaxWidth(Double.MAX_VALUE); + getSamplesPerSymbolLineChart().setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(getSampleLineChart(), Priority.ALWAYS); + HBox.setHgrow(getEyeDiagramChart(), Priority.ALWAYS); + HBox.setHgrow(getSamplesPerSymbolLineChart(), Priority.ALWAYS); + mSampleChartBox.getChildren().addAll(getSampleLineChart(), getEyeDiagramChart(), + getSamplesPerSymbolLineChart()); + } + + return mSampleChartBox; + } + + private ComplexSampleLineChart getSampleLineChart() + { + if(mSampleLineChart == null) + { + mSampleLineChart = new ComplexSampleLineChart(100, 10); + } + + return mSampleLineChart; + } + + private EyeDiagramChart getEyeDiagramChart() + { + if(mEyeDiagramChart == null) + { + mEyeDiagramChart = new EyeDiagramChart(10); + } + + return mEyeDiagramChart; + } + + private PhaseLineChart getPLLPhaseErrorLineChart() + { + if(mPLLPhaseErrorLineChart == null) + { + mPLLPhaseErrorLineChart = new PhaseLineChart(40); + } + + return mPLLPhaseErrorLineChart; + } + + private DoubleLineChart getPLLFrequencyLineChart() + { + if(mPLLFrequencyLineChart == null) + { + mPLLFrequencyLineChart = new DoubleLineChart( "PLL Frequency", -500, 500, 50, 40); + } + + return mPLLFrequencyLineChart; + } + + private DoubleLineChart getSamplesPerSymbolLineChart() + { + if(mSamplesPerSymbolLineChart == null) + { + mSamplesPerSymbolLineChart = new DoubleLineChart( "Sample Point", 9.5, 11.5, 0.1, 40); + } + + return mSamplesPerSymbolLineChart; + } +} 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/P25Phase1LSMPane.java similarity index 91% rename from src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1Pane.java rename to src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1LSMPane.java index 526d3e7f..a69538a7 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1Pane.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1LSMPane.java @@ -22,18 +22,17 @@ import io.github.dsheirer.instrument.gui.viewer.chart.PhaseLineChart; import io.github.dsheirer.instrument.gui.viewer.chart.SymbolChart; import io.github.dsheirer.message.Message; import io.github.dsheirer.module.decode.DecoderType; -import io.github.dsheirer.module.decode.p25.P25_C4FMDecoderInstrumented; -import io.github.dsheirer.sample.Broadcaster; +import io.github.dsheirer.module.decode.p25.P25DecoderLSMInstrumented; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.ComplexBuffer; +import io.github.dsheirer.sample.complex.reusable.ReusableBufferBroadcaster; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class P25Phase1Pane extends DecoderPane +public class P25Phase1LSMPane extends DecoderPane { - private final static Logger mLog = LoggerFactory.getLogger(P25Phase1Pane.class); + private final static Logger mLog = LoggerFactory.getLogger(P25Phase1LSMPane.class); private HBox mSampleChartBox; private ComplexSampleLineChart mSampleLineChart; @@ -43,10 +42,10 @@ public class P25Phase1Pane extends DecoderPane private PhaseLineChart mPLLPhaseErrorLineChart; private DoubleLineChart mPLLFrequencyLineChart; private DoubleLineChart mSamplesPerSymbolLineChart; - private Broadcaster mFilteredBufferBroadcaster = new Broadcaster<>(); - private P25_C4FMDecoderInstrumented mDecoder = new P25_C4FMDecoderInstrumented(null); + private ReusableBufferBroadcaster mFilteredBufferBroadcaster = new ReusableBufferBroadcaster(); + private P25DecoderLSMInstrumented mDecoder = new P25DecoderLSMInstrumented(null); - public P25Phase1Pane() + public P25Phase1LSMPane() { super(DecoderType.P25_PHASE1); init(); @@ -57,7 +56,7 @@ public class P25Phase1Pane extends DecoderPane addListener(getDecoder()); getDecoder().setFilteredBufferListener(mFilteredBufferBroadcaster); - getDecoder().setSymbolListener(getSymbolChart()); + getDecoder().setComplexSymbolListener(getSymbolChart()); getDecoder().setPLLPhaseErrorListener(getPLLPhaseErrorLineChart()); getDecoder().setPLLFrequencyListener(getPLLFrequencyLineChart()); getDecoder().setSymbolDecisionDataListener(getEyeDiagramChart()); @@ -90,7 +89,7 @@ public class P25Phase1Pane extends DecoderPane getSampleLineChart().setSamplesPerSymbol((int)samplesPerSymbol); } - private P25_C4FMDecoderInstrumented getDecoder() + private P25DecoderLSMInstrumented getDecoder() { return mDecoder; } 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 f8f4953b..00859c78 100644 --- a/src/main/java/io/github/dsheirer/module/decode/Decoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/Decoder.java @@ -41,6 +41,21 @@ public abstract class Decoder extends Module implements IMessageProvider mMessageListener = null; } + public void start() + { + //no-op + } + + public void stop() + { + //no-op + } + + public void reset() + { + //no-op + } + /** * Identifies the decoder type (ie protocol) */ diff --git a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java index 0c19e47c..146e8f3a 100644 --- a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java @@ -75,10 +75,10 @@ import io.github.dsheirer.module.decode.nbfm.NBFMDecoder; import io.github.dsheirer.module.decode.nbfm.NBFMDecoderEditor; import io.github.dsheirer.module.decode.p25.DecodeConfigP25Phase1; import io.github.dsheirer.module.decode.p25.P25Decoder.Modulation; +import io.github.dsheirer.module.decode.p25.P25DecoderC4FM; import io.github.dsheirer.module.decode.p25.P25DecoderEditor; +import io.github.dsheirer.module.decode.p25.P25DecoderLSM; import io.github.dsheirer.module.decode.p25.P25DecoderState; -import io.github.dsheirer.module.decode.p25.P25_C4FMDecoder; -import io.github.dsheirer.module.decode.p25.P25_LSMDecoder; import io.github.dsheirer.module.decode.p25.audio.P25AudioModule; import io.github.dsheirer.module.decode.p25.message.filter.P25MessageFilterSet; import io.github.dsheirer.module.decode.passport.DecodeConfigPassport; @@ -247,12 +247,12 @@ public class DecoderFactory switch(modulation) { case C4FM: - modules.add(new P25_C4FMDecoder(aliasList)); - modules.add(new P25DecoderState(aliasList, channelType, P25Decoder.Modulation.C4FM, + modules.add(new P25DecoderC4FM(aliasList)); + modules.add(new P25DecoderState(aliasList, channelType, Modulation.C4FM, p25Config.getIgnoreDataCalls())); break; case CQPSK: - modules.add(new P25_LSMDecoder(aliasList)); + modules.add(new P25DecoderLSM(aliasList)); modules.add(new P25DecoderState(aliasList, channelType, Modulation.CQPSK, p25Config.getIgnoreDataCalls())); break; default: diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/DecodeConfigP25Phase1.java b/src/main/java/io/github/dsheirer/module/decode/p25/DecodeConfigP25Phase1.java index 03aec81b..d0dc5620 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/DecodeConfigP25Phase1.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/DecodeConfigP25Phase1.java @@ -25,7 +25,7 @@ import io.github.dsheirer.module.decode.config.DecodeConfiguration; public class DecodeConfigP25Phase1 extends DecodeConfiguration { - private P25_LSMDecoder.Modulation mModulation = P25Decoder.Modulation.C4FM; + private P25DecoderLSM.Modulation mModulation = P25Decoder.Modulation.C4FM; private int mCallTimeout = 1; private int mTrafficChannelPoolSize = TRAFFIC_CHANNEL_LIMIT_DEFAULT; @@ -48,12 +48,12 @@ public class DecodeConfigP25Phase1 extends DecodeConfiguration } @JacksonXmlProperty(isAttribute = true, localName = "modulation") - public P25_LSMDecoder.Modulation getModulation() + public P25DecoderLSM.Modulation getModulation() { return mModulation; } - public void setModulation(P25_LSMDecoder.Modulation modulation) + public void setModulation(P25DecoderLSM.Modulation modulation) { mModulation = modulation; } 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 e85dec3c..dfcf230b 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 @@ -18,26 +18,70 @@ package io.github.dsheirer.module.decode.p25; import io.github.dsheirer.alias.AliasList; import io.github.dsheirer.module.decode.Decoder; import io.github.dsheirer.module.decode.DecoderType; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.complex.reusable.IReusableComplexBufferListener; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; import io.github.dsheirer.source.ISourceEventListener; import io.github.dsheirer.source.ISourceEventProvider; +import io.github.dsheirer.source.SourceEvent; -public abstract class P25Decoder extends Decoder implements ISourceEventListener, ISourceEventProvider, Instrumentable +public abstract class P25Decoder extends Decoder implements ISourceEventListener, ISourceEventProvider, + IReusableComplexBufferListener, Listener { + private double mSampleRate; private P25MessageProcessor mMessageProcessor; private AliasList mAliasList; + private Listener mSourceEventListener; + private double mSymbolRate; - public P25Decoder(AliasList aliasList) + public P25Decoder(double symbolRate, AliasList aliasList) { + mSymbolRate = symbolRate; mAliasList = aliasList; mMessageProcessor = new P25MessageProcessor(mAliasList); mMessageProcessor.setMessageListener(getMessageListener()); } + protected double getSymbolRate() + { + return mSymbolRate; + } + protected AliasList getAliasList() { return mAliasList; } + /** + * Current sample rate for this decoder + */ + protected double getSampleRate() + { + return mSampleRate; + } + + /** + * Sets current sample rate for this decoder + */ + public void setSampleRate(double sampleRate) + { + if(sampleRate <= getSymbolRate() * 2) + { + throw new IllegalArgumentException("Sample rate [" + sampleRate + "] must be >9600 (2 * " + + getSymbolRate() + " symbol rate)"); + } + + mSampleRate = sampleRate; + } + + /** + * Samples per symbol based on current sample rate and symbol rate. + */ + public float getSamplesPerSymbol() + { + return (float)(getSampleRate() / getSymbolRate()); + } + public void dispose() { super.dispose(); @@ -46,6 +90,83 @@ public abstract class P25Decoder extends Decoder implements ISourceEventListener mMessageProcessor = null; } + /** + * Sets the source event listener to receive source events from this decoder. + */ + @Override + public void setSourceEventListener(Listener listener) + { + mSourceEventListener = listener; + } + + /** + * Removes a registered source event listener from receiving source events from this decoder + */ + @Override + public void removeSourceEventListener() + { + mSourceEventListener = null; + } + + @Override + public Listener getSourceEventListener() + { + return new Listener() + { + @Override + public void receive(SourceEvent sourceEvent) + { + process(sourceEvent); + } + }; + } + + /** + * Sub-class processing of received source events + * @param sourceEvent + */ + protected abstract void process(SourceEvent sourceEvent); + + /** + * Broadcasts the source event to an optional registered listener. This method should primarily be used to + * issue frequency correction requests to the channel source. + * @param sourceEvent to broadcast + */ + protected void broadcast(SourceEvent sourceEvent) + { + if(mSourceEventListener != null) + { + mSourceEventListener.receive(sourceEvent); + } + } + + /** + * Listener interface to receive reusable complex buffers + */ + @Override + public Listener getReusableComplexBufferListener() + { + return P25Decoder.this; + } + + /** + * Starts the decoder + */ + @Override + public void start() + { + //No-op + } + + /** + * Stops the decoder + */ + @Override + public void stop() + { + //No-op + } + @Override public DecoderType getDecoderType() { 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/P25DecoderC4FM.java similarity index 56% rename from src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder.java rename to src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FM.java index 6923bc37..cc787e5a 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FM.java @@ -20,79 +20,88 @@ 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.psk.DQPSKDemodulator; +import io.github.dsheirer.dsp.psk.DQPSKDecisionDirectedDemodulator; import io.github.dsheirer.dsp.psk.InterpolatingSampleBuffer; -import io.github.dsheirer.dsp.psk.InterpolatingSampleBufferInstrumented; import io.github.dsheirer.dsp.psk.pll.CostasLoop; -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.ComplexBuffer; -import io.github.dsheirer.sample.complex.IComplexBufferListener; -import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; +import io.github.dsheirer.source.SourceEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.ScheduledExecutorService; -public class P25_C4FMDecoder extends P25Decoder implements IComplexBufferListener, Listener +public class P25DecoderC4FM extends P25Decoder { - private final static Logger mLog = LoggerFactory.getLogger(P25_C4FMDecoder.class); - protected static final double SYMBOL_RATE = 4800; + private final static Logger mLog = LoggerFactory.getLogger(P25DecoderC4FM.class); + protected static final float SYMBOL_TIMING_GAIN = 0.1f; protected InterpolatingSampleBuffer mInterpolatingSampleBuffer; - protected DQPSKDemodulator mQPSKDemodulator; + protected DQPSKDecisionDirectedDemodulator mQPSKDemodulator; protected CostasLoop mCostasLoop; protected P25MessageFramer mMessageFramer; - protected double mSampleRate; private Map mBasebandFilters = new HashMap<>(); private ComplexFIRFilter mBasebandFilter; - public P25_C4FMDecoder(AliasList aliasList) + /** + * P25 Phase 1 - standard C4FM modulation decoder. Uses Differential QPSK decoding with a Costas PLL and a + * decision-directed phase and timing error detector. + * + * @param aliasList + */ + public P25DecoderC4FM(AliasList aliasList) { - super(aliasList); + super(4800.0, aliasList); setSampleRate(48000.0); } public void setSampleRate(double sampleRate) { - if(sampleRate <= SYMBOL_RATE * 2) - { - throw new IllegalArgumentException("Sample rate must be at least twice the symbol rate [4800]"); - } + super.setSampleRate(sampleRate); - mSampleRate = sampleRate; mBasebandFilter = new ComplexFIRFilter(getBasebandFilter(), 1.0f); - mCostasLoop = new CostasLoop(mSampleRate, SYMBOL_RATE); + mCostasLoop = new CostasLoop(getSampleRate(), getSymbolRate()); + mInterpolatingSampleBuffer = new InterpolatingSampleBuffer(getSamplesPerSymbol(), SYMBOL_TIMING_GAIN); - mInterpolatingSampleBuffer = new InterpolatingSampleBufferInstrumented((float)(sampleRate / SYMBOL_RATE)); - mQPSKDemodulator = new DQPSKDemodulator(mCostasLoop, mInterpolatingSampleBuffer); + mQPSKDemodulator = new DQPSKDecisionDirectedDemodulator(mCostasLoop, mInterpolatingSampleBuffer); - //Message framer can trigger a symbol-inversion correction to the PLL when detected + //Message framer can issue a symbol-inversion correction request to the PLL when detected mMessageFramer = new P25MessageFramer(getAliasList(), mCostasLoop); mMessageFramer.setListener(getMessageProcessor()); - mQPSKDemodulator.setDibitListener(mMessageFramer); + mQPSKDemodulator.setSymbolListener(mMessageFramer); } + /** + * Primary method for processing incoming complex sample buffers + * @param reusableComplexBuffer containing channelized complex samples + */ @Override - public void receive(ComplexBuffer complexBuffer) + public void receive(ReusableComplexBuffer reusableComplexBuffer) { - ComplexBuffer filtered = filter(complexBuffer); + //User accounting of the incoming buffer is handled by the filter + ReusableComplexBuffer filtered = filter(reusableComplexBuffer); + + //User accounting of the filtered buffer is handled by the demodulator mQPSKDemodulator.receive(filtered); } - protected ComplexBuffer filter(ComplexBuffer complexBuffer) + /** + * Filters the complex buffer and returns a new reusable complex buffer with the filtered contents. + * @param reusableComplexBuffer to filter + * @return filtered complex buffer + */ + protected ReusableComplexBuffer filter(ReusableComplexBuffer reusableComplexBuffer) { - return mBasebandFilter.filter(complexBuffer); - } - - private double getSampleRate() - { - return mSampleRate; + //User accounting of the incoming buffer is handled by the filter + return mBasebandFilter.filter(reusableComplexBuffer); } + /** + * Constructs a baseband filter for this decoder using the current sample rate + */ private float[] getBasebandFilter() { + //Attempt to reuse a cached (ie already-designed) filter if available float[] filter = mBasebandFilters.get(getSampleRate()); if(filter == null) @@ -110,19 +119,19 @@ public class P25_C4FMDecoder extends P25Decoder implements IComplexBufferListene try { filter = FilterFactory.getTaps(specification); - } catch(FilterDesignException fde) { mLog.error("Couldn't design low pass baseband filter for sample rate: " + getSampleRate()); } + if(filter != null) { mBasebandFilters.put(getSampleRate(), filter); } else { - throw new IllegalStateException("Couldn't design a C4FM symbol filter for sample rate: " + mSampleRate); + throw new IllegalStateException("Couldn't design a C4FM baseband filter for sample rate: " + getSampleRate()); } } @@ -141,60 +150,36 @@ public class P25_C4FMDecoder extends P25Decoder implements IComplexBufferListene } @Override - public void setFrequencyChangeListener(Listener listener) + protected void process(SourceEvent sourceEvent) { - } - - @Override - public void removeFrequencyChangeListener() - { - } - - @Override - public Listener getFrequencyChangeListener() - { - return new Listener() + switch(sourceEvent.getEvent()) { - @Override - public void receive(FrequencyChangeEvent frequencyChangeEvent) - { - switch(frequencyChangeEvent.getEvent()) - { - case NOTIFICATION_FREQUENCY_CHANGE: - case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE: - case NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE: - case NOTIFICATION_SAMPLE_RATE_CHANGE: - mCostasLoop.reset(); - break; - } - } - }; - } - - @Override - public Listener getComplexBufferListener() - { - return P25_C4FMDecoder.this; + case NOTIFICATION_FREQUENCY_CHANGE: + case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE: + case NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE: + mCostasLoop.reset(); + break; + case NOTIFICATION_SAMPLE_RATE_CHANGE: + mCostasLoop.reset(); + setSampleRate(sourceEvent.getValue().doubleValue()); + break; + } } + /** + * P25 modulation supported by this decoder + */ public Modulation getModulation() { return Modulation.C4FM; } + /** + * Resets this decoder to prepare for processing a new channel + */ @Override public void reset() { mCostasLoop.reset(); } - - @Override - public void start(ScheduledExecutorService executor) - { - } - - @Override - public void stop() - { - } } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FMInstrumented.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FMInstrumented.java new file mode 100644 index 00000000..c7b329bc --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FMInstrumented.java @@ -0,0 +1,121 @@ +/******************************************************************************* + * 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.psk.DQPSKDecisionDirectedDemodulatorInstrumented; +import io.github.dsheirer.dsp.psk.InterpolatingSampleBufferInstrumented; +import io.github.dsheirer.dsp.psk.SymbolDecisionData; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.complex.Complex; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; + +public class P25DecoderC4FMInstrumented extends P25DecoderC4FM +{ + private Listener mPLLPhaseErrorListener; + private Listener mPLLFrequencyListener; + private Listener mSamplesPerSymbolListener; + private Listener mComplexSymbolListener; + private Listener mFilteredSymbolListener; + private Listener mSymbolDecisionDataListener; + + /** + * Instrumented version of the P25 C4FM decoder that supports registering listeners to provide access to data as + * it is being processed by the decoder. + * + * @param aliasList + */ + public P25DecoderC4FMInstrumented(AliasList aliasList) + { + super(aliasList); + } + + /** + * Overrides the filter method so that we can capture the filtered samples for instrumentation + */ + protected ReusableComplexBuffer filter(ReusableComplexBuffer reusableComplexBuffer) + { + ReusableComplexBuffer filtered = super.filter(reusableComplexBuffer); + + //Increment user count before we hand to another listener or return from this method + filtered.incrementUserCount(); + + if(mFilteredSymbolListener != null) + { + filtered.incrementUserCount(); + mFilteredSymbolListener.receive(filtered); + } + + return filtered; + } + + /** + * Overrides this method so we can correctly configure for instrumented operations + */ + public void setSampleRate(double sampleRate) + { + super.setSampleRate(sampleRate); + + InterpolatingSampleBufferInstrumented instrumentedBuffer = + new InterpolatingSampleBufferInstrumented(getSamplesPerSymbol(), SYMBOL_TIMING_GAIN); + mInterpolatingSampleBuffer = instrumentedBuffer; + + DQPSKDecisionDirectedDemodulatorInstrumented instrumented = new DQPSKDecisionDirectedDemodulatorInstrumented(mCostasLoop, instrumentedBuffer, getSampleRate()); + mQPSKDemodulator = instrumented; + + instrumented.setComplexSymbolListener(mComplexSymbolListener); + instrumented.setPLLErrorListener(mPLLPhaseErrorListener); + instrumented.setPLLFrequencyListener(mPLLFrequencyListener); + instrumented.setSymbolDecisionDataListener(mSymbolDecisionDataListener); + instrumented.setSamplesPerSymbolListener(mSamplesPerSymbolListener); + instrumented.setSymbolListener(mMessageFramer); + } + + public void setComplexSymbolListener(Listener listener) + { + mComplexSymbolListener = listener; + ((DQPSKDecisionDirectedDemodulatorInstrumented)mQPSKDemodulator).setComplexSymbolListener(listener); + } + + public void setPLLPhaseErrorListener(Listener listener) + { + mPLLPhaseErrorListener = listener; + ((DQPSKDecisionDirectedDemodulatorInstrumented)mQPSKDemodulator).setPLLErrorListener(listener); + } + + public void setPLLFrequencyListener(Listener listener) + { + mPLLFrequencyListener = listener; + ((DQPSKDecisionDirectedDemodulatorInstrumented)mQPSKDemodulator).setPLLFrequencyListener(listener); + } + + public void setFilteredBufferListener(Listener listener) + { + mFilteredSymbolListener = listener; + } + + public void setSymbolDecisionDataListener(Listener listener) + { + mSymbolDecisionDataListener = listener; + ((DQPSKDecisionDirectedDemodulatorInstrumented)mQPSKDemodulator).setSymbolDecisionDataListener(listener); + } + + public void setSamplesPerSymbolListener(Listener listener) + { + mSamplesPerSymbolListener = listener; + ((DQPSKDecisionDirectedDemodulatorInstrumented)mQPSKDemodulator).setSamplesPerSymbolListener(listener); + } +} \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderEditor.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderEditor.java index e283a708..d6d4df83 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderEditor.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderEditor.java @@ -31,7 +31,11 @@ import net.miginfocom.swing.MigLayout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.swing.*; +import javax.swing.DefaultComboBoxModel; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JSlider; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import java.awt.event.ActionEvent; @@ -43,7 +47,7 @@ public class P25DecoderEditor extends ValidatingEditor private static final long serialVersionUID = 1L; - private JComboBox mComboModulation; + private JComboBox mComboModulation; private JCheckBox mIgnoreDataCalls; private JLabel mTrafficChannelPoolSizeLabel; private JSlider mTrafficChannelPoolSize; @@ -57,9 +61,9 @@ public class P25DecoderEditor extends ValidatingEditor { setLayout( new MigLayout( "insets 0 0 0 0,wrap 3", "[right][grow,fill][]", "" ) ); - mComboModulation = new JComboBox(); - mComboModulation.setModel( new DefaultComboBoxModel( - P25_LSMDecoder.Modulation.values() ) ); + mComboModulation = new JComboBox(); + mComboModulation.setModel( new DefaultComboBoxModel( + P25DecoderLSM.Modulation.values() ) ); mComboModulation.setEnabled( false ); mComboModulation.addActionListener( new ActionListener() { diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSM.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSM.java new file mode 100644 index 00000000..74a8cceb --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSM.java @@ -0,0 +1,177 @@ +/******************************************************************************* + * 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.Window.WindowType; +import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter; +import io.github.dsheirer.dsp.gain.ComplexFeedForwardGainControl; +import io.github.dsheirer.dsp.psk.DQPSKGardnerDemodulator; +import io.github.dsheirer.dsp.psk.InterpolatingSampleBuffer; +import io.github.dsheirer.dsp.psk.pll.CostasLoop; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; +import io.github.dsheirer.source.SourceEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.Map; + +public class P25DecoderLSM extends P25Decoder +{ + private final static Logger mLog = LoggerFactory.getLogger(P25DecoderLSM.class); + protected static final float SYMBOL_TIMING_GAIN = 0.1f; + + private Map mBasebandFilters = new HashMap<>(); + private ComplexFIRFilter mBasebandFilter; + private ComplexFeedForwardGainControl mAGC = new ComplexFeedForwardGainControl(32); + protected DQPSKGardnerDemodulator mQPSKDemodulator; + protected P25MessageFramer mMessageFramer; + protected CostasLoop mCostasLoop; + protected InterpolatingSampleBuffer mInterpolatingSampleBuffer; + + /** + * P25 Phase 1 - linear simulcast modulation (LSM) decoder. Uses Differential QPSK decoding with a Costas PLL and + * a gardner timing error detector. + * + * @param aliasList + */ + public P25DecoderLSM(AliasList aliasList) + { + super(4800.0, aliasList); + setSampleRate(48000.0); + } + + /** + * Sets or changes the channel sample rate + * @param sampleRate in hertz + */ + public void setSampleRate(double sampleRate) + { + super.setSampleRate(sampleRate); + + mBasebandFilter = new ComplexFIRFilter(getBasebandFilter(), 1.0f); + + mCostasLoop = new CostasLoop(getSampleRate(), getSymbolRate()); + mInterpolatingSampleBuffer = new InterpolatingSampleBuffer(getSamplesPerSymbol(), SYMBOL_TIMING_GAIN); + + mQPSKDemodulator = new DQPSKGardnerDemodulator(mCostasLoop, mInterpolatingSampleBuffer); + + //Message framer can issue a symbol-inversion correction request to the PLL when detected + mMessageFramer = new P25MessageFramer(getAliasList(), mCostasLoop); + mMessageFramer.setListener(getMessageProcessor()); + mQPSKDemodulator.setSymbolListener(mMessageFramer); + } + + /** + * Primary method for receiving incoming channel samples + */ + @Override + public void receive(ReusableComplexBuffer reusableComplexBuffer) + { + //The filter will decrement the user count when finished + ReusableComplexBuffer basebandFiltered = filter(reusableComplexBuffer); + + //AGC will decrement the user count when finished + ReusableComplexBuffer gainApplied = mAGC.filter(basebandFiltered); + + //Decoder will decrement the user count when finished + mQPSKDemodulator.receive(gainApplied); + } + + /** + * Filters the complex buffer and returns a new reusable complex buffer with the filtered contents. + * @param reusableComplexBuffer to filter + * @return filtered complex buffer + */ + protected ReusableComplexBuffer filter(ReusableComplexBuffer reusableComplexBuffer) + { + //User accounting of the incoming buffer is handled by the filter + return mBasebandFilter.filter(reusableComplexBuffer); + } + + /** + * Processes source events - updates buffer and decoder when sample rate is established/changed and resets the + * PLL after frequency changes/corrections + */ + @Override + protected void process(SourceEvent sourceEvent) + { + switch(sourceEvent.getEvent()) + { + case NOTIFICATION_FREQUENCY_CHANGE: + case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE: + case NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE: + mCostasLoop.reset(); + break; + case NOTIFICATION_SAMPLE_RATE_CHANGE: + mCostasLoop.reset(); + setSampleRate(sourceEvent.getValue().doubleValue()); + break; + } + } + + /** + * Resets this decoder to prepare for processing a new channel + */ + @Override + public void reset() + { + mCostasLoop.reset(); + } + + public void dispose() + { + super.dispose(); + + mBasebandFilter.dispose(); + mBasebandFilter = null; + + mAGC.dispose(); + mAGC = null; + + mQPSKDemodulator.dispose(); + mQPSKDemodulator = null; + + mMessageFramer.dispose(); + mMessageFramer = null; + } + + public Modulation getModulation() + { + return Modulation.CQPSK; + } + + /** + * Constructs a baseband filter for this decoder using the current sample rate + */ + private float[] getBasebandFilter() + { + //Attempt to reuse a cached (ie already-designed) filter if available + float[] filter = mBasebandFilters.get(getSampleRate()); + + if(filter == null) + { + filter = FilterFactory.getLowPass(48000, 7250, 8000, 60, + WindowType.HANN, true); + + mBasebandFilters.put(getSampleRate(), filter); + } + + return filter; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoderInstrumented.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSMInstrumented.java similarity index 59% rename from src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoderInstrumented.java rename to src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSMInstrumented.java index 903396a7..179d3503 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoderInstrumented.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSMInstrumented.java @@ -16,23 +16,31 @@ package io.github.dsheirer.module.decode.p25; import io.github.dsheirer.alias.AliasList; -import io.github.dsheirer.dsp.psk.DQPSKDemodulatorInstrumented; +import io.github.dsheirer.dsp.psk.DQPSKGardnerDemodulatorInstrumented; import io.github.dsheirer.dsp.psk.InterpolatingSampleBufferInstrumented; import io.github.dsheirer.dsp.psk.SymbolDecisionData; 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.reusable.ReusableComplexBuffer; -public class P25_C4FMDecoderInstrumented extends P25_C4FMDecoder +public class P25DecoderLSMInstrumented extends P25DecoderLSM { private Listener mPLLPhaseErrorListener; private Listener mPLLFrequencyListener; private Listener mSamplesPerSymbolListener; - private Listener mSymbolListener; - private Listener mFilteredSymbolListener; + private Listener mComplexSymbolListener; + private Listener mFilteredSymbolListener; private Listener mSymbolDecisionDataListener; - public P25_C4FMDecoderInstrumented(AliasList aliasList) + /** + * P25 Phase 1 - linear simulcast modulation (LSM) decoder. Uses Differential QPSK decoding with a Costas PLL and + * a gardner timing error detector. + * + * Instrumented decoder instance. + * + * @param aliasList + */ + public P25DecoderLSMInstrumented(AliasList aliasList) { super(aliasList); } @@ -40,12 +48,16 @@ public class P25_C4FMDecoderInstrumented extends P25_C4FMDecoder /** * Overrides the filter method so that we can capture the filtered samples for instrumentation */ - protected ComplexBuffer filter(ComplexBuffer complexBuffer) + protected ReusableComplexBuffer filter(ReusableComplexBuffer reusableComplexBuffer) { - ComplexBuffer filtered = super.filter(complexBuffer); + ReusableComplexBuffer filtered = super.filter(reusableComplexBuffer); + + //Increment user count before we hand to another listener or return from this method + filtered.incrementUserCount(); if(mFilteredSymbolListener != null) { + filtered.incrementUserCount(); mFilteredSymbolListener.receive(filtered); } @@ -58,39 +70,40 @@ public class P25_C4FMDecoderInstrumented extends P25_C4FMDecoder public void setSampleRate(double sampleRate) { super.setSampleRate(sampleRate); - InterpolatingSampleBufferInstrumented instrumentedBuffer = new InterpolatingSampleBufferInstrumented((float)(sampleRate / SYMBOL_RATE)); + + InterpolatingSampleBufferInstrumented instrumentedBuffer = new InterpolatingSampleBufferInstrumented(getSamplesPerSymbol(), SYMBOL_TIMING_GAIN); mInterpolatingSampleBuffer = instrumentedBuffer; - DQPSKDemodulatorInstrumented instrumented = new DQPSKDemodulatorInstrumented(mCostasLoop, instrumentedBuffer, mSampleRate); + DQPSKGardnerDemodulatorInstrumented instrumented = new DQPSKGardnerDemodulatorInstrumented(mCostasLoop, instrumentedBuffer, getSampleRate()); mQPSKDemodulator = instrumented; - instrumented.setSymbolListener(mSymbolListener); + instrumented.setComplexSymbolListener(mComplexSymbolListener); instrumented.setPLLErrorListener(mPLLPhaseErrorListener); instrumented.setPLLFrequencyListener(mPLLFrequencyListener); instrumented.setSymbolDecisionDataListener(mSymbolDecisionDataListener); instrumented.setSamplesPerSymbolListener(mSamplesPerSymbolListener); - instrumented.setDibitListener(mMessageFramer); + instrumented.setSymbolListener(mMessageFramer); } - public void setSymbolListener(Listener listener) + public void setComplexSymbolListener(Listener listener) { - mSymbolListener = listener; - ((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setSymbolListener(listener); + mComplexSymbolListener = listener; + ((DQPSKGardnerDemodulatorInstrumented)mQPSKDemodulator).setComplexSymbolListener(listener); } public void setPLLPhaseErrorListener(Listener listener) { mPLLPhaseErrorListener = listener; - ((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setPLLErrorListener(listener); + ((DQPSKGardnerDemodulatorInstrumented)mQPSKDemodulator).setPLLErrorListener(listener); } public void setPLLFrequencyListener(Listener listener) { mPLLFrequencyListener = listener; - ((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setPLLFrequencyListener(listener); + ((DQPSKGardnerDemodulatorInstrumented)mQPSKDemodulator).setPLLFrequencyListener(listener); } - public void setFilteredBufferListener(Listener listener) + public void setFilteredBufferListener(Listener listener) { mFilteredSymbolListener = listener; } @@ -98,12 +111,12 @@ public class P25_C4FMDecoderInstrumented extends P25_C4FMDecoder public void setSymbolDecisionDataListener(Listener listener) { mSymbolDecisionDataListener = listener; - ((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setSymbolDecisionDataListener(listener); + ((DQPSKGardnerDemodulatorInstrumented)mQPSKDemodulator).setSymbolDecisionDataListener(listener); } public void setSamplesPerSymbolListener(Listener listener) { mSamplesPerSymbolListener = listener; - ((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setSamplesPerSymbolListener(listener); + ((DQPSKGardnerDemodulatorInstrumented)mQPSKDemodulator).setSamplesPerSymbolListener(listener); } } \ No newline at end of file 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 deleted file mode 100644 index 20460d76..00000000 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25_LSMDecoder.java +++ /dev/null @@ -1,240 +0,0 @@ -/******************************************************************************* - * 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.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.QPSKSymbolDecoder; -import io.github.dsheirer.instrument.tap.TapGroup; -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.ComplexBuffer; -import io.github.dsheirer.sample.complex.ComplexBufferToStreamConverter; -import io.github.dsheirer.sample.complex.reusable.IReusableComplexBufferListener; -import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; -import io.github.dsheirer.source.SourceEvent; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.List; - -public class P25_LSMDecoder extends P25Decoder implements IReusableComplexBufferListener -{ - 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 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; - private ReusableBufferListener mReusableBufferListener = new ReusableBufferListener(); - - public P25_LSMDecoder(AliasList aliasList) - { - super(aliasList); - - mBasebandFilter = new ComplexFIRFilter_CB_CB(FilterFactory.getLowPass( - 48000, 7250, 8000, 60, WindowType.HANN, true), 1.0f); - - mBasebandFilter.setListener(mStreamConverter); - - mStreamConverter.setListener(mAGC); - - 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; - - mLSMDemodulator.dispose(); - mLSMDemodulator = null; - - mQPSKSlicer.dispose(); - mQPSKSlicer = null; - - mMessageFramer.dispose(); - mMessageFramer = null; - } - - @Override - public Listener getReusableComplexBufferListener() - { - return mReusableBufferListener; - } - - public Modulation getModulation() - { - return Modulation.CQPSK; - } - - /** - * Provides a list of instrumentation taps for monitoring internal processing - */ - @Override - public List getTapGroups() - { - 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; - } - - /** - * Adds the instrumentation tap - */ - @Override - public void registerTap(Tap tap) - { - 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()); - } - } - - /** - * Removes the instrumentation tap - */ - @Override - public void unregisterTap(Tap tap) - { - 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()); - } - } - - @Override - public Listener getSourceEventListener() - { - return null; - } - - @Override - public void setSourceEventListener(Listener listener) - { - - } - - @Override - public void removeSourceEventListener() - { - - } - - public class ReusableBufferListener implements Listener - { - @Override - public void receive(ReusableComplexBuffer reusableComplexBuffer) - { - float[] samples = reusableComplexBuffer.getSamplesCopy(); - - //TODO: redesign the filter chain so that we can simply pass a float array ... - - mBasebandFilter.receive(new ComplexBuffer(samples)); - reusableComplexBuffer.decrementUserCount(); - } - } -} diff --git a/src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableBufferQueue.java b/src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableBufferQueue.java index 655c6967..89f7e81b 100644 --- a/src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableBufferQueue.java +++ b/src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableBufferQueue.java @@ -38,6 +38,11 @@ public class ReusableBufferQueue implements IReusableBufferDisposedListener } + public void dispose() + { + mReusableBufferQueue.clear(); + } + /** * Implements buffer disposed listener interface. Disposed (ie user count = 0) buffers will automatically callback * to this method to indicate when they are disposed. diff --git a/src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableComplexBuffer.java b/src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableComplexBuffer.java index 00345429..151852af 100644 --- a/src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableComplexBuffer.java +++ b/src/main/java/io/github/dsheirer/sample/complex/reusable/ReusableComplexBuffer.java @@ -30,6 +30,10 @@ public class ReusableComplexBuffer extends ComplexBuffer /** * Creates a reusable, timestamped complex buffer using the specified time in milliseconds. * + * NOTE: reusability of this buffer requires strict user count tracking. Each component that receives this + * buffer should not modify the buffer contents. Each component should also increment the user count before + * sending this buffer to another component and should decrement the user count when finished using this buffer. + * * @param disposalListener to be notified when all consumers/users are finished using the buffer * @param samples of data * @param timestamp in millis for the buffer @@ -46,6 +50,10 @@ public class ReusableComplexBuffer extends ComplexBuffer /** * Constructs a timestamped complex buffer using the current system time in milliseconds. * + * NOTE: reusability of this buffer requires strict user count tracking. Each component that receives this + * buffer should not modify the buffer contents. Each component should also increment the user count before + * sending this buffer to another component and should decrement the user count when finished using this buffer. + * * @param disposalListener to be notified when all consumers are finished using the buffer * @param samples of data */ @@ -82,6 +90,11 @@ public class ReusableComplexBuffer extends ComplexBuffer return mTimestamp; } + public void setTimestamp(long timestamp) + { + mTimestamp = timestamp; + } + /** * Reloads this buffer by copying the sample array into this buffer. Internal buffer length will be automatically * resized to fit the new sample array length. diff --git a/src/main/java/io/github/dsheirer/source/IControllableFileSource.java b/src/main/java/io/github/dsheirer/source/IControllableFileSource.java index 59868d66..583b8007 100644 --- a/src/main/java/io/github/dsheirer/source/IControllableFileSource.java +++ b/src/main/java/io/github/dsheirer/source/IControllableFileSource.java @@ -40,7 +40,7 @@ public interface IControllableFileSource public long getFrameCount() throws IOException; - public int getSampleRate(); + public double getSampleRate(); public void setListener(IFrameLocationListener listener); diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerModel.java b/src/main/java/io/github/dsheirer/source/tuner/TunerModel.java index a88db737..615e20bb 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/TunerModel.java +++ b/src/main/java/io/github/dsheirer/source/tuner/TunerModel.java @@ -77,11 +77,11 @@ public class TunerModel extends AbstractTableModel implements Listener Tuners ) - { - for( Tuner tuner: Tuners ) - { - addTuner( tuner ); - } - } + public void addTuners(List Tuners) + { + for(Tuner tuner : Tuners) + { + addTuner(tuner); + } + } /** * Adds the Tuner to this model @@ -284,17 +284,17 @@ public class TunerModel extends AbstractTableModel implements Listener it = mTuners.iterator(); - - Tuner tuner; + TunerChannelSource retVal = null; - if(config.hasPreferredTuner()) + TunerChannel tunerChannel = config.getTunerChannel(); + + tunerChannel.setBandwidth(bandwidth); + + Iterator it = mTuners.iterator(); + + Tuner tuner; + + if(config.hasPreferredTuner()) { tuner = getTuner(config.getPreferredTuner()); @@ -302,7 +302,7 @@ public class TunerModel extends AbstractTableModel implements Listener setLayout(new MigLayout("insets 0 0 0 0", "[left]", "")); mFrequencyControl = new JFrequencyControl(); mFrequencyControl.setEnabled(false); - mFrequencyControl.addListener(new IFrequencyChangeProcessor() + mFrequencyControl.addListener(new ISourceEventProcessor() { @Override - public void frequencyChanged(FrequencyChangeEvent event) + public void process(SourceEvent event) throws SourceException { setModified(true); } diff --git a/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java b/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java index 63453767..0abc2824 100644 --- a/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java +++ b/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java @@ -217,7 +217,9 @@ public class ComplexWaveSource extends ComplexSource implements IControllableFil float[] samples = ConversionUtils.convertFromSigned16BitSamples(buffer); ReusableComplexBuffer reusableBuffer = mReusableBufferQueue.getBuffer(samples.length); - mListener.receive(reusableBuffer.incrementUserCount()); + reusableBuffer.reloadFrom(samples, System.currentTimeMillis()); + reusableBuffer.incrementUserCount(); + mListener.receive(reusableBuffer); } } } diff --git a/src/main/java/io/github/dsheirer/spectrum/ChannelSpectrumPanel.java b/src/main/java/io/github/dsheirer/spectrum/ChannelSpectrumPanel.java index 879a7963..0431ff26 100644 --- a/src/main/java/io/github/dsheirer/spectrum/ChannelSpectrumPanel.java +++ b/src/main/java/io/github/dsheirer/spectrum/ChannelSpectrumPanel.java @@ -1,6 +1,6 @@ /******************************************************************************* * sdr-trunk - * Copyright (C) 2014-2017 Dennis Sheirer + * 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 @@ -26,15 +26,15 @@ import io.github.dsheirer.dsp.filter.smoothing.SmoothingFilter.SmoothingType; import io.github.dsheirer.module.ProcessingChain; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.SampleType; -import io.github.dsheirer.sample.complex.ComplexBuffer; +import io.github.dsheirer.sample.complex.reusable.ReusableBufferQueue; +import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer; import io.github.dsheirer.sample.real.RealBuffer; import io.github.dsheirer.settings.ColorSetting.ColorSettingName; import io.github.dsheirer.settings.ColorSettingMenuItem; import io.github.dsheirer.settings.Setting; import io.github.dsheirer.settings.SettingChangeListener; import io.github.dsheirer.settings.SettingsManager; -import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent; -import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent.Event; +import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.spectrum.converter.DFTResultsConverter; import io.github.dsheirer.spectrum.converter.RealDecibelConverter; import io.github.dsheirer.spectrum.menu.AveragingItem; @@ -45,8 +45,13 @@ import io.github.dsheirer.spectrum.menu.SmoothingItem; import io.github.dsheirer.spectrum.menu.SmoothingTypeItem; import net.miginfocom.swing.MigLayout; -import javax.swing.*; -import java.awt.*; +import javax.swing.JLayeredPane; +import javax.swing.JMenu; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JSeparator; +import javax.swing.SwingUtilities; +import java.awt.Component; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.event.MouseEvent; @@ -54,22 +59,20 @@ import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.util.concurrent.atomic.AtomicBoolean; -public class ChannelSpectrumPanel extends JPanel - implements ChannelEventListener, - Listener, - SettingChangeListener, - SpectralDisplayAdjuster +public class ChannelSpectrumPanel extends JPanel implements ChannelEventListener, + Listener, SettingChangeListener, SpectralDisplayAdjuster { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - private DFTProcessor mDFTProcessor = new DFTProcessor( SampleType.REAL ); + private DFTProcessor mDFTProcessor = new DFTProcessor(SampleType.REAL); private DFTResultsConverter mDFTConverter = new RealDecibelConverter(); private JLayeredPane mLayeredPane; private SpectrumPanel mSpectrumPanel; private ChannelOverlayPanel mOverlayPanel; - + private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue(); + private Channel mCurrentChannel; - + private int mSampleBufferSize = 2400; private HalfBandFilter_RB_RB mDecimatingFilter = new HalfBandFilter_RB_RB( @@ -98,11 +101,10 @@ public class ChannelSpectrumPanel extends JPanel mDFTProcessor.addConverter( mDFTConverter ); mDFTConverter.addListener( mSpectrumPanel ); - /* Set the DFTProcessor to the decimated 24kHz sample rate */ - mDFTProcessor.frequencyChanged( - new FrequencyChangeEvent( Event.NOTIFICATION_SAMPLE_RATE_CHANGE, 24000 ) ); - - initGui(); + /* Set the DFTProcessor to the decimated 24kHz sample rate */ + mDFTProcessor.process(SourceEvent.sampleRateChange(24000.0)); + + initGui(); } public void dispose() @@ -272,9 +274,11 @@ public class ChannelSpectrumPanel extends JPanel { RealBuffer decimated = mDecimatingFilter.filter( buffer ); - //Hack: we're placing real samples in a complex buffer that the DFT - //processor is expecting. - mDFTProcessor.receive( new ComplexBuffer( decimated.getSamples() ) ); + //Hack: we're placing real samples in a complex buffer that the DFT + //processor is expecting. + ReusableComplexBuffer reusableComplexBuffer = mReusableBufferQueue.getBuffer(decimated.getSamples().length); + reusableComplexBuffer.reloadFrom(decimated.getSamples(), System.currentTimeMillis()); + mDFTProcessor.receive(reusableComplexBuffer.incrementUserCount()); } /**