Merge branch 'master' into 39-polyphase-channelizer

# Conflicts:
#	src/main/java/io/github/dsheirer/buffer/FloatCircularBuffer.java
#	src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java
#	src/main/java/io/github/dsheirer/dsp/filter/design/FilterView.java
#	src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java
#	src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java
#	src/main/java/io/github/dsheirer/dsp/gain/ComplexFeedForwardGainControl.java
#	src/main/java/io/github/dsheirer/dsp/mixer/Oscillator.java
#	src/main/java/io/github/dsheirer/dsp/psk/LSMDemodulator.java
#	src/main/java/io/github/dsheirer/module/decode/Decoder.java
#	src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java
#	src/main/java/io/github/dsheirer/module/decode/p25/P25Decoder.java
#	src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder.java
#	src/main/java/io/github/dsheirer/module/decode/p25/P25_LSMDecoder.java
#	src/main/java/io/github/dsheirer/source/tuner/TunerChannelSource.java
#	src/main/java/io/github/dsheirer/source/tuner/TunerModel.java
#	src/main/java/io/github/dsheirer/source/tuner/TunerSourceEditor.java
#	src/main/java/io/github/dsheirer/spectrum/ChannelSpectrumPanel.java
This commit is contained in:
Denny
2018-03-18 05:19:33 -04:00
89 changed files with 7253 additions and 2885 deletions

View File

@@ -0,0 +1,162 @@
/*******************************************************************************
* 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 <http://www.gnu.org/licenses/>
*
******************************************************************************/
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<Dibit> 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<Dibit> 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;
}
}

View File

@@ -0,0 +1,131 @@
/*******************************************************************************
* 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 <http://www.gnu.org/licenses/>
*
******************************************************************************/
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 DQPSKDemodulatorInstrumented extends DQPSKDemodulator
{
private Listener<SymbolDecisionData> mSymbolDecisionDataListener;
private Listener<Double> mSamplesPerSymbolListener;
private Listener<Complex> mSymbolListener;
private Listener<Double> mPLLErrorListener;
private Listener<Double> mPLLFrequencyListener;
private double mSampleRate;
/**
* 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
* @param sampleRate of the incoming complex sample stream
*/
public DQPSKDemodulatorInstrumented(IPhaseLockedLoop phaseLockedLoop,
InterpolatingSampleBufferInstrumented 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)mInterpolatingSampleBuffer).getSymbolDecisionData());
}
super.calculateSymbol();
if(mSamplesPerSymbolListener != null)
{
mSamplesPerSymbolListener.receive((double)mInterpolatingSampleBuffer.getSamplingPoint());
}
//Send to an external constellation symbol listener when registered
if(mSymbolListener != null)
{
mSymbolListener.receive(mCurrentSymbol);
}
if(mPLLErrorListener != null)
{
mPLLErrorListener.receive((double)mSymbolEvaluator.getPhaseError());
}
if(mPLLFrequencyListener != null)
{
double loopFrequency = ((CostasLoop)mPLL).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<SymbolDecisionData> listener)
{
mSymbolDecisionDataListener = listener;
}
/**
* Registers the listener to receive PLL error values
*/
public void setSamplesPerSymbolListener(Listener<Double> listener)
{
mSamplesPerSymbolListener = listener;
}
/**
* Registers the listener to receive decoded QPSK symbols
*/
public void setSymbolListener(Listener<Complex> listener)
{
mSymbolListener = listener;
}
/**
* Registers the listener to receive PLL error values
*/
public void setPLLErrorListener(Listener<Double> listener)
{
mPLLErrorListener = listener;
}
/**
* Registers the listener to receive PLL error values
*/
public void setPLLFrequencyListener(Listener<Double> listener)
{
mPLLFrequencyListener = listener;
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
*
******************************************************************************/
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
{
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);
private static final Complex ROTATE_FROM_MINUS_45 = Complex.fromAngle(1.0 * Math.PI / 4.0);
private static final Complex ROTATE_FROM_MINUS_135 = Complex.fromAngle(3.0 * Math.PI / 4.0);
private Complex mSymbolError = new Complex(0, 0);
private float mPhaseError = 0.0f;
private float mTimingError = 0.0f;
private float mTimingErrorPolarity = 1.0f;
private Dibit mSymbolDecision = Dibit.D00_PLUS_1;
/**
* 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 and timing error are calculated as the angular distance of the sampled symbol from the reference
* 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()
{
}
/**
* Sets the preceding 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 preceding differential-decoded sample/symbol used to detect rotation - does NOT need to be interpolated
* since it's only a reference
* @param current symbol to be evaluated - interpolated
*/
@Override
public void setSymbol(Complex preceding, Complex current)
{
mSymbolError.setValues(current);
if(mSymbolError.quadrature() > 0.0f)
{
if(mSymbolError.inphase() > 0.0f)
{
mSymbolDecision = Dibit.D00_PLUS_1;
mTimingErrorPolarity = (preceding.quadrature() > current.quadrature() ? 1.0f : -1.0f);
mSymbolError.multiply(ROTATE_FROM_PLUS_45);
}
else
{
mSymbolDecision = Dibit.D01_PLUS_3;
mTimingErrorPolarity = (preceding.quadrature() < current.quadrature() ? 1.0f : -1.0f);
mSymbolError.multiply(ROTATE_FROM_PLUS_135);
}
}
else
{
if(mSymbolError.inphase() > 0.0f)
{
mSymbolDecision = Dibit.D10_MINUS_1;
mTimingErrorPolarity = (preceding.quadrature() > current.quadrature() ? 1.0f : -1.0f);
mSymbolError.multiply(ROTATE_FROM_MINUS_45);
}
else
{
mSymbolDecision = Dibit.D11_MINUS_3;
mTimingErrorPolarity = (preceding.quadrature() < current.quadrature() ? 1.0f : -1.0f);
mSymbolError.multiply(ROTATE_FROM_MINUS_135);
}
}
//Since we've rotated the error symbol back to 0 radians, the quadrature value closely approximates the
//arctan of the error angle relative to 0 radians and this provides our error value
mPhaseError = -mSymbolError.quadrature();
//Timing error is the same as phase error with the sign corrected according to the vector's angular rotation
mTimingError = mSymbolError.quadrature() * mTimingErrorPolarity;
}
/**
* 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;
}
}

View File

@@ -0,0 +1,82 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.psk;
import io.github.dsheirer.sample.complex.Complex;
public class GardnerDetector
{
private float mErrorInphase;
private float mErrorQuadrature;
private float mError;
/**
* Gardner detector for recovering symbol timing error.
*/
public GardnerDetector()
{
}
/**
* Calculates the symbol timing error by evaluating the previous, middle and current symbol instants.
*
* @param previous symbol
* @param middle symbol (ie midway between the previous and current)
* @param current symbol
* @return symbol timing error
*/
public float getError(Complex previous, Complex middle, Complex current)
{
mErrorInphase = (Math.abs(previous.inphase()) - Math.abs(current.inphase())) * Math.abs(middle.inphase());
mErrorQuadrature = (Math.abs(previous.quadrature()) - Math.abs(current.quadrature())) * Math.abs(middle.quadrature());
mError = normalize(mErrorInphase + mErrorQuadrature, 1.0f);
return mError;
}
/**
* Constrains timing error to +/- the maximum value and corrects any floating point invalid numbers
*/
public static float normalize(float error, float maximum)
{
if(Float.isNaN(error))
{
return 0.0f;
}
else
{
return clip(error, maximum);
}
}
/**
* Constrains value to the range of ( -maximum <> maximum )
*/
public static float clip(float value, float maximum)
{
if(value > maximum)
{
return maximum;
}
else if(value < -maximum)
{
return -maximum;
}
return value;
}
}

View File

@@ -0,0 +1,50 @@
/*******************************************************************************
* 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 <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.psk;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.sample.complex.Complex;
public interface IDQPSKSymbolEvaluator
{
/**
* 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
*/
float getPhaseError();
/**
* Timing error of the symbol relative to the closest reference symbol
* @return angular timing error in radians where a positive value indicates early sampling and sample timing needs
* to be increased and a negative value indicates late sampling where sample timing needs to be decreased.
*/
float getTimingError();
/**
* Dibit decision indicating the reference symbol location closest to the symbol.
* @return symbol decision
*/
Dibit getSymbolDecision();
}

View File

@@ -0,0 +1,29 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.psk;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.sample.complex.Complex;
public interface IQPSKSymbolDecoder
{
/**
* Decodes the symbol and converts it into a Dibit
* @param symbol to decode
* @return decoded dibit
*/
Dibit decode(Complex symbol);
}

View File

@@ -0,0 +1,28 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.psk;
import io.github.dsheirer.sample.complex.Complex;
public interface ISymbolPhaseErrorCalculator
{
/**
* Calculates the phase error of the symbol relative to an alignment for the constellation
* @param symbol to calculate error
* @return error value
*/
float getPhaseError(Complex symbol);
}

View File

@@ -0,0 +1,167 @@
/*******************************************************************************
* 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 <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.psk;
import io.github.dsheirer.dsp.filter.interpolator.RealInterpolator;
import io.github.dsheirer.sample.complex.Complex;
public class InterpolatingSampleBuffer
{
private static final float MAXIMUM_DEVIATION_SAMPLES_PER_SYMBOL = 0.02f; // +/- 2% deviation
private Complex mPrecedingSample = new Complex(0,0);
private Complex mCurrentSample = new Complex(0,0);
private float[] mDelayLineInphase;
private float[] mDelayLineQuadrature;
private int mDelayLinePointer = 0;
private int mTwiceSamplesPerSymbol;
private float mSamplingPoint;
private float mSampleCounterGain = 1.0f;
private float mDetectedSamplesPerSymbol;
private float mDetectedSamplesPerSymbolGain = 0.1f * mSampleCounterGain * mSampleCounterGain;
private float mMaximumSamplesPerSymbol;
private float mMinimumSamplesPerSymbol;
private RealInterpolator mInterpolator = new RealInterpolator(1.0f);
/**
* Buffer to store complex sample data and produce interpolated samples.
* @param samplesPerSymbol
*/
public InterpolatingSampleBuffer(float samplesPerSymbol)
{
mSamplingPoint = samplesPerSymbol;
mDetectedSamplesPerSymbol = samplesPerSymbol;
mMaximumSamplesPerSymbol = samplesPerSymbol * (1.0f + MAXIMUM_DEVIATION_SAMPLES_PER_SYMBOL);
mMinimumSamplesPerSymbol = samplesPerSymbol * (1.0f - MAXIMUM_DEVIATION_SAMPLES_PER_SYMBOL);
mTwiceSamplesPerSymbol = (int)Math.floor(2.0 * samplesPerSymbol);
mDelayLineInphase = new float[2 * mTwiceSamplesPerSymbol];
mDelayLineQuadrature = new float[2 * mTwiceSamplesPerSymbol];
}
/**
* Stores the sample in the buffer and updates pointers.
* @param sample
*/
public void receive(Complex sample)
{
mSamplingPoint--;
//Fill up the delay line to use with the interpolator
mDelayLineInphase[mDelayLinePointer] = sample.inphase();
mDelayLineInphase[mDelayLinePointer + mTwiceSamplesPerSymbol] = sample.inphase();
mDelayLineQuadrature[mDelayLinePointer] = sample.quadrature();
mDelayLineQuadrature[mDelayLinePointer + mTwiceSamplesPerSymbol] = sample.quadrature();
//Increment pointer and keep pointer in bounds
mDelayLinePointer++;
mDelayLinePointer = mDelayLinePointer % mTwiceSamplesPerSymbol;
}
/**
* Updates the internal sample counter with additional samples to add to the symbol sample counter.
* @param samplesToAdd to the sample counter
*/
public void increaseSampleCounter(float samplesToAdd)
{
mSamplingPoint += samplesToAdd;
}
/**
* Adjusts samples per symbol and symbol timing counters and increments the sample counter to collect another
* symbol.
*
* @param symbolTimingError from a symbol timing error detector
*/
public void resetAndAdjust(float symbolTimingError)
{
//Adjust detected samples per symbol based on timing error
mDetectedSamplesPerSymbol = mDetectedSamplesPerSymbol + (symbolTimingError * mDetectedSamplesPerSymbolGain);
//Constrain detected samples per symbol to min/max values
if(mDetectedSamplesPerSymbol > mMaximumSamplesPerSymbol)
{
mDetectedSamplesPerSymbol = mMaximumSamplesPerSymbol;
}
if(mDetectedSamplesPerSymbol < mMinimumSamplesPerSymbol)
{
mDetectedSamplesPerSymbol = mMinimumSamplesPerSymbol;
}
//Add another symbol's worth of samples to the counter and adjust timing based on gardner error
increaseSampleCounter((mDetectedSamplesPerSymbol + (symbolTimingError * mSampleCounterGain)));
}
/**
* Current value of the detected samples per symbol
*/
public float getSamplingPoint()
{
return mSamplingPoint;
}
/**
* Indicates if this buffer has accumulated a enough samples to represent a full symbol
*/
public boolean hasSymbol()
{
return mSamplingPoint < 1.0f;
}
/**
* 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.
*/
public Complex getPrecedingSample()
{
mPrecedingSample.setValues(mDelayLineInphase[mDelayLinePointer + 4], 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.
*/
public Complex getCurrentSample()
{
/* Calculate interpolated current sample */
mCurrentSample.setValues(getInphase(mSamplingPoint), getQuadrature(mSamplingPoint));
return mCurrentSample;
}
/**
* Returns the interpolated inphase value for the specified offset
* @param interpolation into the buffer to calculate the interpolated sample
* @return inphase value of the interpolated sample
*/
public float getInphase(float interpolation)
{
return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer + 1, interpolation);
}
/**
* Returns the interpolated quadrature value for the specified offset
* @param interpolation into the buffer to calculate the interpolated sample
* @return quadrature value of the interpolated sample
*/
public float getQuadrature(float interpolation)
{
return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer + 1, interpolation);
}
}

View File

@@ -0,0 +1,48 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.psk;
import io.github.dsheirer.sample.complex.Complex;
public class InterpolatingSampleBufferInstrumented extends InterpolatingSampleBuffer
{
private SymbolDecisionData mSymbolDecisionData;
public InterpolatingSampleBufferInstrumented(float samplesPerSymbol)
{
super(samplesPerSymbol);
mSymbolDecisionData = new SymbolDecisionData((int)samplesPerSymbol);
}
public void receive(Complex sample)
{
super.receive(sample);
mSymbolDecisionData.receive(sample);
}
/**
* Contents of the interpolating buffer and the current buffer index and symbol decision offset. This data can
* be used to support an external eye-diagram chart.
* @return symbol decision data.
*/
public SymbolDecisionData getSymbolDecisionData()
{
mSymbolDecisionData.setSamplingPoint(getSamplingPoint());
return mSymbolDecisionData;
}
}

View File

@@ -0,0 +1,28 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.psk;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.sample.complex.Complex;
public class LSMConstellationDecoder implements IQPSKSymbolDecoder
{
@Override
public Dibit decode(Complex symbol)
{
return null;
}
}

View File

@@ -2,36 +2,37 @@ package io.github.dsheirer.dsp.psk;
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014, 2015 Dennis Sheirer
*
* Copyright (C) 2014 - 2018 Dennis Sheirer
*
* Gardner detector, Costas loop and Interpolator derived from:
*
*
* OP25 - gardner_costas_cc_impl.cc
* Copyright 2010, 2011, 2012, 2013 KA1RBI (gardner_costas_cc_impl.cc)
*
*
* OP25 - scope.py
* Copyright 2008-2011 Steve Glass
* Copyright 2011, 2012, 2013 KA1RBI
*
*
* GNURadio - control_loop and mpsk_receiver_cc
* Copyright 2011,2013 Free Software Foundation, Inc.
*
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
******************************************************************************/
import io.github.dsheirer.buffer.FloatAveragingBuffer;
import io.github.dsheirer.dsp.filter.interpolator.RealInterpolator;
import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.instrument.Instrumentable;
import io.github.dsheirer.instrument.tap.Tap;
@@ -48,19 +49,19 @@ import java.util.Arrays;
import java.util.List;
/**
* Implements a LSM (Pi/4) demodulator using a Gardner Detector to determine
* optimal sampling timing and a Costas Loop as a phase locked loop synchronized
* Implements a LSM (Pi/4) demodulator using a Gardner Detector to determine
* optimal sampling timing and a Costas Loop as a phase locked loop synchronized
* with the incoming signal carrier frequency.
*
*
* Sample Rate: 48000
* Symbol Rate: 4800
*/
public class LSMDemodulator implements Instrumentable, ComplexSampleListener
{
private List<TapGroup> mAvailableTaps;
/* 45 degree rotation angle */
public static final float THETA = (float)( Math.PI / 4.0d );
private List<TapGroup> 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 =
@@ -73,46 +74,46 @@ public class LSMDemodulator implements Instrumentable, ComplexSampleListener
private CostasLoop mCostasLoop = new CostasLoop();
private ISourceEventListener mFrequencyChangeListener;
private EyeDiagramDataTap mEyeDiagramDataTap;
public LSMDemodulator()
{
}
public void dispose()
{
mSymbolListener = null;
mFrequencyChangeListener = null;
}
public void setSymbolListener( Listener<Complex> 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 )
*/
@@ -126,10 +127,10 @@ public class LSMDemodulator implements Instrumentable, ComplexSampleListener
{
return -maximum;
}
return value;
}
/**
* Constrains timing error to +/- the maximum value and corrects any
* floating point invalid numbers
@@ -145,37 +146,37 @@ public class LSMDemodulator implements Instrumentable, ComplexSampleListener
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;
/**
* 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 )
{
private FloatAveragingBuffer mBuffer = new FloatAveragingBuffer(40);
private long mFrequencyError = 0;
private int mCounter = 0;
public void receive(float frequency)
{
// mCounter++;
//
// if( mCounter >= 480 )
@@ -212,377 +213,402 @@ public class LSMDemodulator implements Instrumentable, ComplexSampleListener
// {
// mBuffer.get( frequency );
// }
}
}
}
}
/**
* Gardner symbol timing detector
*/
public class GardnerSymbolTiming implements ComplexSampleListener
{
public static final int HALF_SAMPLES_PER_SYMBOL = 5;
public static final int SAMPLES_PER_SYMBOL = 10;
public static final int TWICE_SAMPLES_PER_SYMBOL = 20;
/**
* Gardner symbol timing detector
*/
public class GardnerSymbolTiming implements ComplexSampleListener
{
public static final int HALF_SAMPLES_PER_SYMBOL = 5;
public static final int SAMPLES_PER_SYMBOL = 10;
public static final int TWICE_SAMPLES_PER_SYMBOL = 20;
private float[] mDelayLineInphase = new float[ 2 * TWICE_SAMPLES_PER_SYMBOL ];
private float[] mDelayLineQuadrature = new float[ 2 * TWICE_SAMPLES_PER_SYMBOL ];
private float[] mDelayLineInphase = new float[2 * TWICE_SAMPLES_PER_SYMBOL];
private float[] mDelayLineQuadrature = new float[2 * TWICE_SAMPLES_PER_SYMBOL];
private int mDelayLinePointer = 0;
private int mDelayLinePointer = 0;
/* Sampling point */
private float mMu = 10.0f;
private float mGainMu = 0.05f;
/* Samples per symbol */
private float mOmega = 10.0f;
private float mGainOmega = 0.1f * mGainMu * mGainMu;
private float mOmegaRel = 0.005f;
private float mOmegaMid = 10.0f;
/* Sampling point */
private float mMu = 10.0f;
private float mGainMu = 0.05f;
private RealInterpolator mInterpolator = new RealInterpolator( 1.0f );
private Complex mPreviousSample = new Complex( 0, 0 );
private Complex mPreviousMiddleSample = new Complex( 0, 0 );
private Complex mPreviousSymbol = new Complex( 0, 0 );
/**
* Provides symbol sampling timing control
*/
public GardnerSymbolTiming()
{
}
/* Samples per symbol */
private float mOmega = 10.0f;
private float mGainOmega = 0.1f * mGainMu * mGainMu;
private float mOmegaRel = 0.005f;
private float mOmegaMid = 10.0f;
@Override
public void receive( float inphase, float quadrature )
{
/* Count down samples per symbol until we calculate the symbol */
mMu--;
private RealInterpolator mInterpolator = new RealInterpolator(1.0f);
mCostasLoop.increment();
/* Mix incoming sample with costas loop to remove any rotation
* that is present from a mis-tuned carrier frequency */
Complex costas = mCostasLoop.getCurrentVector();
float derotatedInphase = Complex.multiplyInphase( inphase, quadrature, costas.inphase(), costas.quadrature() );
float derotatedQuadrature = Complex.multiplyQuadrature( inphase, quadrature, costas.inphase(), costas.quadrature() );
/* Fill up the delay line to use with the interpolator */
mDelayLineInphase[ mDelayLinePointer ] = derotatedInphase;
mDelayLineInphase[ mDelayLinePointer + TWICE_SAMPLES_PER_SYMBOL ] = derotatedInphase;
private Complex mPreviousSample = new Complex(0, 0);
private Complex mPreviousMiddleSample = new Complex(0, 0);
private Complex mPreviousSymbol = new Complex(0, 0);
mDelayLineQuadrature[ mDelayLinePointer ] = derotatedQuadrature;
mDelayLineQuadrature[ mDelayLinePointer + TWICE_SAMPLES_PER_SYMBOL ] = derotatedQuadrature;
/**
* Provides symbol sampling timing control
*/
public GardnerSymbolTiming()
{
}
/* Increment pointer and keep pointer in bounds */
mDelayLinePointer = ( mDelayLinePointer + 1 ) % TWICE_SAMPLES_PER_SYMBOL;
/* Calculate the symbol once we've stored enough samples */
if( mMu <= 1.0f )
{
float half_omega = mOmega / 2.0f;
int half_sps = (int)Math.floor( half_omega );
float half_mu = mMu + half_omega - (float)half_sps;
if( half_mu > 1.0 )
{
half_mu -= 1.0;
half_sps += 1;
}
@Override
public void receive(float inphase, float quadrature)
{
/* Count down samples per symbol until we calculate the symbol */
mMu--;
/* Calculate interpolated middle sample and current sample */
float middleSampleInphase = mInterpolator.filter(
mDelayLineInphase, mDelayLinePointer, mMu );
float middleSampleQuadrature = mInterpolator.filter(
mDelayLineQuadrature, mDelayLinePointer, mMu );
mCostasLoop.increment();
int index = mDelayLinePointer + half_sps;
float currentSampleInphase = mInterpolator.filter(
mDelayLineInphase, index, half_mu );
float currentSampleQuadrature = mInterpolator.filter(
mDelayLineQuadrature, index, half_mu );
/* Mix incoming sample with costas loop to remove any rotation
* that is present from a mis-tuned carrier frequency */
/* Multiply current sample and conjugate (negated quadrature) of
* previous sample to get symbols to use for gardner error feedback */
Complex middleSymbol = Complex.multiply( middleSampleInphase,
middleSampleQuadrature, mPreviousMiddleSample.conjugate() );
Complex currentSymbol = Complex.multiply( currentSampleInphase,
currentSampleQuadrature, mPreviousSample.conjugate() );
Complex costas = mCostasLoop.getCurrentVector();
/* Set gain to unity */
middleSymbol.normalize();
currentSymbol.normalize();
float derotatedInphase = Complex.multiplyInphase(inphase, quadrature, costas.inphase(), costas.quadrature());
float derotatedQuadrature = Complex.multiplyQuadrature(inphase, quadrature, costas.inphase(), costas.quadrature());
/* Gardner timing error calculations */
float errorInphase = ( mPreviousSymbol.inphase() -
currentSymbol.inphase() ) * middleSymbol.inphase();
/* Fill up the delay line to use with the interpolator */
mDelayLineInphase[mDelayLinePointer] = derotatedInphase;
mDelayLineInphase[mDelayLinePointer + TWICE_SAMPLES_PER_SYMBOL] = derotatedInphase;
float errorQuadrature = ( mPreviousSymbol.quadrature() -
currentSymbol.quadrature() ) * middleSymbol.quadrature();
mDelayLineQuadrature[mDelayLinePointer] = derotatedQuadrature;
mDelayLineQuadrature[mDelayLinePointer + TWICE_SAMPLES_PER_SYMBOL] = derotatedQuadrature;
float gardnerError = normalize( errorInphase + errorQuadrature, 1.0f );
if( mEyeDiagramDataTap != null )
{
mEyeDiagramDataTap.receive(
new EyeDiagramData(
Arrays.copyOfRange( mDelayLineInphase, 0, 20 ),
Arrays.copyOfRange( mDelayLineQuadrature, 0, 20 ),
mMu, (float)half_sps + half_mu, gardnerError ) );
}
/* mOmega is samples per symbol and is constrained to floating
* between +/- .005 of the nominal 10.0 samples per symbol */
mOmega = mOmega + mGainOmega * gardnerError;
mOmega = mOmegaMid + clip( mOmega - mOmegaMid, mOmegaRel );
/* Increment pointer and keep pointer in bounds */
mDelayLinePointer = (mDelayLinePointer + 1) % TWICE_SAMPLES_PER_SYMBOL;
/* Adjust sample timing based on error of current sample */
mMu += mOmega + ( mGainMu * gardnerError );
/* Calculate the symbol once we've stored enough samples */
if(mMu <= 1.0f)
{
float half_omega = mOmega / 2.0f;
int half_sps = (int)Math.floor(half_omega);
float half_mu = mMu + half_omega - (float)half_sps;
/* Store current samples/symbols to use for the next period */
mPreviousSample.setValues( currentSampleInphase, currentSampleQuadrature );
mPreviousMiddleSample.setValues( middleSampleInphase, middleSampleQuadrature );
mPreviousSymbol = currentSymbol;
if(half_mu > 1.0)
{
half_mu -= 1.0;
half_sps += 1;
}
/* Update costas loop using phase error present in current
* symbol. The symbol is rotated from star orientation to polar
* orientation to simplify error calculation */
mCostasLoop.receive( currentSymbol );
/* Calculate interpolated middle sample and current sample */
float middleSampleInphase = mInterpolator.filter(
mDelayLineInphase, mDelayLinePointer, mMu);
float middleSampleQuadrature = mInterpolator.filter(
mDelayLineQuadrature, mDelayLinePointer, mMu);
int index = mDelayLinePointer + half_sps;
float currentSampleInphase = mInterpolator.filter(
mDelayLineInphase, index, half_mu);
float currentSampleQuadrature = mInterpolator.filter(
mDelayLineQuadrature, index, half_mu);
/* Multiply current sample and conjugate (negated quadrature) of
* previous sample to get symbols to use for gardner error feedback */
Complex middleSymbol = Complex.multiply(middleSampleInphase,
middleSampleQuadrature, mPreviousMiddleSample.conjugate());
Complex currentSymbol = Complex.multiply(currentSampleInphase,
currentSampleQuadrature, mPreviousSample.conjugate());
/* Set gain to unity */
middleSymbol.normalize();
currentSymbol.normalize();
/* Gardner timing error calculations */
float errorInphase = (mPreviousSymbol.inphase() -
currentSymbol.inphase()) * middleSymbol.inphase();
float errorQuadrature = (mPreviousSymbol.quadrature() -
currentSymbol.quadrature()) * middleSymbol.quadrature();
float gardnerError = normalize(errorInphase + errorQuadrature, 1.0f);
if(mEyeDiagramDataTap != null)
{
mEyeDiagramDataTap.receive(
new EyeDiagramData(
Arrays.copyOfRange(mDelayLineInphase, 0, 20),
Arrays.copyOfRange(mDelayLineQuadrature, 0, 20),
mMu, (float)half_sps + half_mu, gardnerError));
}
/* mOmega is samples per symbol and is constrained to floating
* between +/- .005 of the nominal 10.0 samples per symbol */
mOmega = mOmega + mGainOmega * gardnerError;
mOmega = mOmegaMid + clip(mOmega - mOmegaMid, mOmegaRel);
/* Adjust sample timing based on error of current sample */
mMu += mOmega + (mGainMu * gardnerError);
/* Store current samples/symbols to use for the next period */
mPreviousSample.setValues(currentSampleInphase, currentSampleQuadrature);
mPreviousMiddleSample.setValues(middleSampleInphase, middleSampleQuadrature);
mPreviousSymbol = currentSymbol;
/* Update costas loop using phase error present in current
* symbol. The symbol is rotated from star orientation to polar
* orientation to simplify error calculation */
mCostasLoop.receive(currentSymbol);
// mCostasLoop.receive(
// ComplexSample.multiply( currentSymbol, POINT_45_DEGREES ) );
/* Dispatch the differentiated symbol to the registered listener */
if( mSymbolListener != null )
{
mSymbolListener.receive( currentSymbol );
}
}
}
}
/**
* Costas Loop - phase locked loop synchronized to the frequency offset of
* the incoming signal to enable the signal to be mixed down to zero offset
* for proper synchronization. The mFrequency value indicates the detected
* frequency offset. We attempt to keep that value close to zero by issuing
* frequency adjustments to the tuner channel source.
*
* Most of the costas loop code was ported from gnuradio/control_loop and
* gnuradio/mpsk_receiver_cc using initialization values from KA1RBI's
* OP25/cqpsk.py
*/
public class CostasLoop implements Listener<Complex>
{
public static final double TWO_PI = 2.0 * Math.PI;
private static final float MAXIMUM_FREQUENCY =
( 2400.0f * (float)TWO_PI ) / 48000.0f;
/* http://www.trondeau.com/blog/2011/8/13/control-loop-gain-values.html */
private float mDamping = (float)Math.sqrt( 2.0 ) / 2.0f;
/* Use denominator between 100 (most) and 400 (least) to adjust costas
* loop control level */
private float mLoopBandwidth = (float)( TWO_PI / 400.0d );
private float mAlphaGain = ( 4.0f * mDamping * mLoopBandwidth ) /
( 1.0f + ( 2.0f * mDamping * mLoopBandwidth ) +
( mLoopBandwidth * mLoopBandwidth ) );
private float mBetaGain = ( 4.0f * mLoopBandwidth * mLoopBandwidth ) /
( 1.0f + ( 2.0f * mDamping * mLoopBandwidth ) +
( mLoopBandwidth * mLoopBandwidth ) );
/* Dispatch the differentiated symbol to the registered listener */
if(mSymbolListener != null)
{
mSymbolListener.receive(currentSymbol);
}
}
}
}
private float mLoopPhase = 0.0f;
private float mLoopFrequency = 0.0f;
private FrequencyControl mFrequencyControl = new FrequencyControl();
public CostasLoop()
{
}
/**
* Applies a phase correction value to the current phasor. Use this
* method to apply correction when a +/- 90, or 180 degree phase error
* is detected in the binary output stream.
*
* If the supplied correction value places the loop frequency outside
* of the max frequency, then the frequency will be corrected 360
* degrees in the opposite direction to maintain within the max
* frequency bounds.
*
* @param correction - correction value in radians
*/
public void correctPhaseError( double correction )
{
mLoopFrequency += correction;
if( mLoopFrequency > MAXIMUM_FREQUENCY )
{
mLoopFrequency -= 2.0d * MAXIMUM_FREQUENCY;
}
if( mLoopFrequency < -MAXIMUM_FREQUENCY )
{
mLoopFrequency += 2.0d * MAXIMUM_FREQUENCY;
}
}
/**
* Increments the phase of the loop
*/
public void increment()
{
mLoopPhase += mLoopFrequency;
/**
* Costas Loop - phase locked loop synchronized to the frequency offset of
* the incoming signal to enable the signal to be mixed down to zero offset
* for proper synchronization. The mFrequency value indicates the detected
* frequency offset. We attempt to keep that value close to zero by issuing
* frequency adjustments to the tuner channel source.
*
* Most of the costas loop code was ported from gnuradio/control_loop and
* gnuradio/mpsk_receiver_cc using initialization values from KA1RBI's
* OP25/cqpsk.py
*/
public class CostasLoop implements Listener<Complex>, IPhaseLockedLoop
{
public static final double TWO_PI = 2.0 * Math.PI;
/* Keep the loop phase in bounds */
unwrapPhase();
}
private static final float MAXIMUM_FREQUENCY =
(2400.0f * (float)TWO_PI) / 48000.0f;
private void unwrapPhase()
{
while( mLoopPhase > TWO_PI )
{
mLoopPhase -= TWO_PI;
}
while( mLoopPhase < -TWO_PI )
{
mLoopPhase += TWO_PI;
}
}
/**
* Current vector of the loop rotated by THETA degrees.
*/
public Complex getCurrentVector()
{
/* http://www.trondeau.com/blog/2011/8/13/control-loop-gain-values.html */
private float mDamping = (float)Math.sqrt(2.0) / 2.0f;
/* Use denominator between 100 (most) and 400 (least) to adjust costas
* loop control level */
private float mLoopBandwidth = (float)(TWO_PI / 400.0d);
private float mAlphaGain = (4.0f * mDamping * mLoopBandwidth) /
(1.0f + (2.0f * mDamping * mLoopBandwidth) +
(mLoopBandwidth * mLoopBandwidth));
private float mBetaGain = (4.0f * mLoopBandwidth * mLoopBandwidth) /
(1.0f + (2.0f * mDamping * mLoopBandwidth) +
(mLoopBandwidth * mLoopBandwidth));
private float mLoopPhase = 0.0f;
private float mLoopFrequency = 0.0f;
private FrequencyControl mFrequencyControl = new FrequencyControl();
public CostasLoop()
{
}
/**
* Applies a phase correction value to the current phasor. Use this
* method to apply correction when a +/- 90, or 180 degree phase error
* is detected in the binary output stream.
*
* If the supplied correction value places the loop frequency outside
* of the max frequency, then the frequency will be corrected 360
* degrees in the opposite direction to maintain within the max
* frequency bounds.
*
* @param correction - correction value in radians
*/
public void correctPhaseError(double correction)
{
mLoopFrequency += correction;
if(mLoopFrequency > MAXIMUM_FREQUENCY)
{
mLoopFrequency -= 2.0d * MAXIMUM_FREQUENCY;
}
if(mLoopFrequency < -MAXIMUM_FREQUENCY)
{
mLoopFrequency += 2.0d * MAXIMUM_FREQUENCY;
}
}
/**
* Increments the phase of the loop
*/
public void increment()
{
mLoopPhase += mLoopFrequency;
/* Keep the loop phase in bounds */
unwrapPhase();
}
@Override
public void 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 void receive( Complex complex )
{
/* Calculate phase error */
float phaseError = getPhaseError( complex );
Dibit d = QPSKPolarSlicer.decide( complex );
return Complex.fromAngle(mLoopPhase);
}
@Override
public Complex incrementAndGetCurrentVector()
{
return null;
}
@Override
public void receive(Complex complex)
{
/* Calculate phase error */
float phaseError = getPhaseError(complex);
Dibit d = mQPSKSymbolDecoder.decode(complex);
// mLog.debug( "Dibit: " + d.name() + " Error: " + phaseError );
/* Adjust for phase error */
adjust( phaseError );
}
/* Adjust for phase error */
adjust(phaseError);
}
/**
* Updates the costas loop frequency and phase to adjust for the phase
* error value
*
* @param phase_error - (-)= late and (+)= early
*/
private void adjust( float phase_error )
{
mLoopFrequency += mBetaGain * phase_error;
mLoopPhase += mLoopFrequency + mAlphaGain * phase_error;
/**
* Updates the costas loop frequency and phase to adjust for the phase
* error value
*
* @param phase_error - (-)= late and (+)= early
*/
private void adjust(float phase_error)
{
mLoopFrequency += mBetaGain * phase_error;
mLoopPhase += mLoopFrequency + mAlphaGain * phase_error;
/* Maintain phase between +/- 2 * PI */
unwrapPhase();
/* Maintain phase between +/- 2 * PI */
unwrapPhase();
/* Limit frequency to +/- maximum loop frequency */
limitFrequency();
}
/* Limit frequency to +/- maximum loop frequency */
limitFrequency();
}
/**
* Constrains the frequency within the bounds of +/- loop frequency
*/
private void limitFrequency()
{
/* Check for and issue tuner offset correction */
mFrequencyControl.receive( mLoopFrequency );
if( mLoopFrequency > MAXIMUM_FREQUENCY )
{
mLoopFrequency = MAXIMUM_FREQUENCY;
}
if( mLoopFrequency < -MAXIMUM_FREQUENCY )
{
mLoopFrequency = -MAXIMUM_FREQUENCY;
}
}
/**
* Constrains the frequency within the bounds of +/- loop frequency
*/
private void limitFrequency()
{
/* Check for and issue tuner offset correction */
mFrequencyControl.receive(mLoopFrequency);
/**
* Detects rotational error present when the costas loop is not aligned
* with the carrier frequency. Provides error feedback to adjust
* mixer frequency.
*/
public float getPhaseError( Complex complex )
{
float phase_error = 0;
if( Math.abs( complex.inphase() ) > Math.abs( complex.quadrature() ) )
{
if( complex.inphase() > 0 )
{
phase_error = -complex.quadrature();
}
else
{
phase_error = complex.quadrature();
}
}
else
{
if( complex.quadrature() > 0 )
{
phase_error = complex.inphase();
}
else
{
phase_error = -complex.inphase();
}
}
return phase_error;
}
}
if(mLoopFrequency > MAXIMUM_FREQUENCY)
{
mLoopFrequency = MAXIMUM_FREQUENCY;
}
@Override
public List<TapGroup> getTapGroups()
{
if( mAvailableTaps == null )
{
mAvailableTaps = new ArrayList<>();
TapGroup group = new TapGroup( "LSM Demodulator" );
group.add( new EyeDiagramDataTap( "Eye Diagram", 0, 4800 ) );
if(mLoopFrequency < -MAXIMUM_FREQUENCY)
{
mLoopFrequency = -MAXIMUM_FREQUENCY;
}
}
mAvailableTaps.add( group );
}
return mAvailableTaps;
}
/**
* Detects rotational error present when the costas loop is not aligned
* with the carrier frequency. Provides error feedback to adjust
* mixer frequency.
*/
public float getPhaseError(Complex complex)
{
float phase_error = 0;
@Override
public void registerTap( Tap tap )
{
if( tap instanceof EyeDiagramDataTap )
{
mEyeDiagramDataTap = (EyeDiagramDataTap)tap;
}
}
if(Math.abs(complex.inphase()) > Math.abs(complex.quadrature()))
{
if(complex.inphase() > 0)
{
phase_error = -complex.quadrature();
}
else
{
phase_error = complex.quadrature();
}
}
else
{
if(complex.quadrature() > 0)
{
phase_error = complex.inphase();
}
else
{
phase_error = -complex.inphase();
}
}
@Override
public void unregisterTap( Tap tap )
{
if( tap instanceof EyeDiagramDataTap )
{
mEyeDiagramDataTap = null;
}
}
return phase_error;
}
}
@Override
public List<TapGroup> 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;
}
}
}

View File

@@ -1,3 +1,18 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.psk;
import io.github.dsheirer.dsp.symbol.Dibit;
@@ -5,54 +20,54 @@ import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.Complex;
public class QPSKStarSlicer implements Listener<Complex>
public class QPSKStarSlicer implements IQPSKSymbolDecoder, Listener<Complex>
{
private Broadcaster<Dibit> mBroadcaster = new Broadcaster<Dibit>();
private Broadcaster<Dibit> mBroadcaster = new Broadcaster<Dibit>();
/**
* Slices a ComplexSample representing a phase shifted symbol according to
* the following constellation pattern:
*
* 00 | 01
* ---|---
* 10 | 11
*/
public QPSKStarSlicer()
{
}
public void dispose()
{
mBroadcaster.dispose();
mBroadcaster = null;
}
/**
* Slices a Complex sample representing a phase shifted symbol according to the following constellation pattern:
*
* 00 +3 | 01 +1
* ------|------
* 10 -1 | 11 -1
*/
public QPSKStarSlicer()
{
}
public void addListener( Listener<Dibit> listener )
{
mBroadcaster.addListener( listener );
}
public void dispose()
{
mBroadcaster.dispose();
mBroadcaster = null;
}
public void removeListener( Listener<Dibit> listener )
{
mBroadcaster.removeListener( listener );
}
public void addListener(Listener<Dibit> listener)
{
mBroadcaster.addListener(listener);
}
public void removeListener(Listener<Dibit> listener)
{
mBroadcaster.removeListener(listener);
}
@Override
public void receive(Complex sample)
{
mBroadcaster.broadcast(decode(sample));
}
@Override
public Dibit decode(Complex sample)
{
if(sample.inphase() > 0)
{
return sample.quadrature() > 0 ? Dibit.D00_PLUS_1 : Dibit.D10_MINUS_1;
}
else
{
return sample.quadrature() > 0 ? Dibit.D01_PLUS_3 : Dibit.D11_MINUS_3;
}
}
@Override
public void receive( Complex sample )
{
mBroadcaster.broadcast( decide( sample ) );
}
public static Dibit decide( Complex sample )
{
if( sample.inphase() > 0 )
{
return sample.quadrature() > 0 ? Dibit.D00_PLUS_1 : Dibit.D10_MINUS_1;
}
else
{
return sample.quadrature() > 0 ? Dibit.D01_PLUS_3 : Dibit.D11_MINUS_3;
}
}
}

View File

@@ -1,3 +1,18 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.psk;
import io.github.dsheirer.dsp.symbol.Dibit;
@@ -5,24 +20,7 @@ import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.Complex;
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014,2015 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
******************************************************************************/
public class QPSKPolarSlicer implements Listener<Complex>
public class QPSKSymbolDecoder implements Listener<Complex>, IQPSKSymbolDecoder
{
private Broadcaster<Dibit> mBroadcaster = new Broadcaster<Dibit>();
@@ -37,7 +35,7 @@ public class QPSKPolarSlicer implements Listener<Complex>
* / \
* / 11 \
*/
public QPSKPolarSlicer()
public QPSKSymbolDecoder()
{
}
@@ -60,10 +58,10 @@ public class QPSKPolarSlicer implements Listener<Complex>
@Override
public void receive( Complex complex )
{
mBroadcaster.broadcast( decide( complex ) );
mBroadcaster.broadcast( decode( complex ) );
}
public static Dibit decide( Complex complex )
public Dibit decode(Complex complex )
{
if( Math.abs( complex.inphase() ) > Math.abs( complex.quadrature() ) )
{

View File

@@ -0,0 +1,57 @@
/*******************************************************************************
* 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 <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.psk;
import io.github.dsheirer.sample.complex.Complex;
/**
* Calculates the phase error for a QPSK polar-orientation constellation
*/
public class QPSKSymbolPhaseErrorCalculator implements ISymbolPhaseErrorCalculator
{
private float mPhaseError;
@Override
public float getPhaseError(Complex symbol)
{
mPhaseError = 0.0f;
if(Math.abs(symbol.inphase()) > Math.abs(symbol.quadrature()))
{
if(symbol.inphase() > 0)
{
mPhaseError = -symbol.quadrature();
}
else
{
mPhaseError = symbol.quadrature();
}
}
else
{
if(symbol.quadrature() > 0)
{
mPhaseError = symbol.inphase();
}
else
{
mPhaseError = -symbol.inphase();
}
}
return mPhaseError;
}
}

View File

@@ -0,0 +1,93 @@
/*******************************************************************************
* 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 <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.psk;
import io.github.dsheirer.buffer.ComplexCircularBuffer;
import io.github.dsheirer.sample.complex.Complex;
public class SymbolDecisionData
{
//45 degrees rotation to orient the symbol to a polar axis to make error calculation easy/efficient
public static final Complex DIFFERENTIAL_OFFSET = Complex.fromAngle(Math.PI / 4.0d);
private ComplexCircularBuffer mBuffer;
private float mSamplingPoint;
/**
* Circular buffer for capturing two symbols worth of sample data for use in instrumentation.
* @param samplesPerSymbol to size the buffer
*/
public SymbolDecisionData(int samplesPerSymbol)
{
mBuffer = new ComplexCircularBuffer(2 * samplesPerSymbol);
}
/**
* Stores the complex sample in the buffer
*/
public void receive(float inphase, float quadrature)
{
mBuffer.put(new Complex(inphase, quadrature));
}
/**
* Stores the complex sample in the buffer
*/
public void receive(Complex complex)
{
mBuffer.put(complex.copy());
}
/**
* Sets the fractional sampling point for the current symbol to use in interpolating the current symbol.
*/
public void setSamplingPoint(float samplingPoint)
{
mSamplingPoint = samplingPoint;
}
/**
* Fractional sampling point
*/
public float getSamplingPoint()
{
return mSamplingPoint;
}
/**
* Array of demodulated samples representing the current symbol where each current sample is demodulated against
* the previous symbol's sample to produce the differential demodulated sample. Samples are in time order.
*
* Note: differential decoding is on an integral samples-per-symbol basis and does not use fractional interpolation.
*/
public Complex[] getDemodulated()
{
Complex[] samples = mBuffer.getAll();
int length = samples.length / 2;
Complex[] demodulated = new Complex[length];
for(int x = 0; x < samples.length / 2; x++)
{
Complex copy = samples[length + x].copy();
copy.multiply(samples[x].conjugate());
demodulated[x] = copy;
}
return demodulated;
}
}

View File

@@ -0,0 +1,35 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.psk;
public enum SymbolInversion
{
DEGREES_90_PLUS(Math.PI / 2.0),
DEGREES_90_MINUS(Math.PI / -2.0),
DEGREES_180(Math.PI);
private double mAngleRadians;
SymbolInversion(double angleRadians)
{
mAngleRadians = angleRadians;
}
public double getError()
{
return mAngleRadians;
}
}

View File

@@ -0,0 +1,182 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.psk.pll;
import io.github.dsheirer.sample.complex.Complex;
/**
* Costas Loop - phase locked loop designed to automatically synchronize to the incoming carrier frequency in order to
* zeroize any frequency offset inherent in the signal due to either mis-tuning or carrier frequency drift.
*
* Costas loop structure was modeled from KA1RBI's OP25/cqpsk.py
*
* Loop bandwidth and alpha/beta gain formulas derived from:
* http://www.trondeau.com/blog/2011/8/13/control-loop-gain-values.html
*/
public class CostasLoop implements IPhaseLockedLoop
{
public static final double TWO_PI = 2.0 * Math.PI;
private Complex mCurrentVector = new Complex(0, 0);
private double mLoopPhase = 0.0;
private double mLoopFrequency = 0.0;
private double mMaximumLoopFrequency;
private double mDamping = Math.sqrt(2.0) / 2.0;
private double mAlphaGain;
private double mBetaGain;
private Tracking mTracking = Tracking.NORMAL;
/**
* Costas Loop for tracking and correcting frequency error in a received carrier signal.
*
* @param sampleRate of the incoming samples
* @param symbolRate of the digital signal
*/
public CostasLoop(double sampleRate, double symbolRate)
{
mMaximumLoopFrequency = TWO_PI * (symbolRate / 2.0) / sampleRate;
updateLoopBandwidth();
}
/**
* Corrects the current phase tracker when an inverted output is detected. The inversion will take the form of
* +/- 90 degrees or +/- 180 degrees, although the latter correction is equal regardless of the sign.
*
* If the supplied correction value places the loop frequency outside of the max frequency, then the frequency will
* be corrected 360 degrees in the opposite direction to maintain within the max frequency bounds.
*
* @param correction as measured in radians
*/
public void correctInversion(double correction)
{
mLoopFrequency += correction;
while(mLoopFrequency > mMaximumLoopFrequency)
{
mLoopFrequency -= 2.0 * mMaximumLoopFrequency;
}
while(mLoopFrequency < -mMaximumLoopFrequency)
{
mLoopFrequency += 2.0 * mMaximumLoopFrequency;
}
}
/**
* Updates the loop bandwidth and alpha/beta gains according to the current loop synchronization state.
*/
private void updateLoopBandwidth()
{
double bandwidth = TWO_PI / mTracking.getLoopBandwidth();
mAlphaGain = (4.0 * mDamping * bandwidth) / (1.0 + (2.0 * mDamping * bandwidth) + (bandwidth * bandwidth));
mBetaGain = (4.0 * bandwidth * bandwidth) / (1.0 + (2.0 * mDamping * bandwidth) + (bandwidth * bandwidth));
}
/**
* Sets the tracking state for this costas loop. The tracking state affects the aggressiveness of the alpha/beta
* gain values in synchronizing with the signal carrier.
*
* @param tracking
*/
public void setTracking(Tracking tracking)
{
mTracking = tracking;
updateLoopBandwidth();
}
/**
* Increments the phase of the loop for each sample received at the sample rate.
*/
public void increment()
{
mLoopPhase += mLoopFrequency;
/* Normalize phase between +/- 2 * PI */
if(mLoopPhase > TWO_PI)
{
mLoopPhase -= TWO_PI;
}
if(mLoopPhase < -TWO_PI)
{
mLoopPhase += TWO_PI;
}
}
/**
* Current vector of the loop. Note: this value is updated for the current angle in radians each time this method
* is invoked.
*/
public Complex getCurrentVector()
{
mCurrentVector.setAngle(mLoopPhase);
return mCurrentVector;
}
@Override
public Complex incrementAndGetCurrentVector()
{
increment();
return getCurrentVector();
}
public double getLoopFrequency()
{
return mLoopFrequency;
}
/**
* Updates the costas loop frequency and phase to adjust for the phase
* error value
*
* @param phaseError - (-)= late and (+)= early
*/
public void adjust(double phaseError)
{
mLoopFrequency += (mBetaGain * phaseError);
mLoopPhase += mLoopFrequency + (mAlphaGain * phaseError);
/* Normalize phase between +/- 2 * PI */
if(mLoopPhase > TWO_PI)
{
mLoopPhase -= TWO_PI;
}
if(mLoopPhase < -TWO_PI)
{
mLoopPhase += TWO_PI;
}
/* Limit frequency to +/- maximum loop frequency */
if(mLoopFrequency > mMaximumLoopFrequency)
{
mLoopFrequency = mMaximumLoopFrequency;
}
if(mLoopFrequency < -mMaximumLoopFrequency)
{
mLoopFrequency = -mMaximumLoopFrequency;
}
}
@Override
public void reset()
{
mLoopPhase = 0.0;
mLoopFrequency = 0.0;
}
}

View File

@@ -0,0 +1,59 @@
/*******************************************************************************
* 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 <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.psk.pll;
import io.github.dsheirer.sample.complex.Complex;
/**
* Phase-Locked Loop (PLL) interface
*/
public interface IPhaseLockedLoop
{
/**
* Increments the PLL by one sample period
*/
void increment();
/**
* Applies an error value to the PLL.
*
* @param error value in the range of -PI <> +PI
*/
void adjust(double error);
/**
* Adjusts the current phase of the loop to account for +/-90 or 180 degree inverted phase locks when detected
* @param correction to apply to the phase of the loop
*/
void correctInversion(double correction);
/**
* Current vector of the PLL to use in de-spinning the incoming samples.
* @return complex vector
*/
Complex getCurrentVector();
/**
* Increments the phase rotation and gets the current vector of the PLL to use in de-spinning the incoming samples.
* @return complex vector
*/
Complex incrementAndGetCurrentVector();
/**
* Reset tracking to 0
*/
void reset();
}

View File

@@ -0,0 +1,42 @@
/*******************************************************************************
* 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 <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.psk.pll;
/**
* Phase Locked Loop (PLL) tracking bandwidth - controls how fast/slow the PLL tracks the carrier frequency
*/
public enum Tracking
{
FASTEST(50.0),
FAST(100.0),
NORMAL(200.0),
SLOW(400.0);
private double mLoopBandwidth;
Tracking(double loopBandwidth)
{
mLoopBandwidth = loopBandwidth;
}
/**
* Loop bandwidth setting
*/
public double getLoopBandwidth()
{
return mLoopBandwidth;
}
}