330 WIP on redesigning the P25 C4FM decoder. Creates a new decoder

based on the P25 LSM decoder using a costas loop and a gardner detector.
Updates the Demodulator Viewer FX application to add in a symbol chart
and a phase error chart.  The decoder won't lock on yet ... still a wip.
This commit is contained in:
Denny
2018-02-17 13:41:36 -05:00
parent 80c5ad1bc8
commit eaf7c7d11e
47 changed files with 3926 additions and 2093 deletions

View File

@@ -1,80 +1,168 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 Dennis Sheirer
*
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
******************************************************************************/
package io.github.dsheirer.buffer;
import org.apache.commons.math3.stat.descriptive.moment.Mean;
import org.apache.commons.math3.stat.descriptive.moment.Variance;
import org.apache.commons.math3.stat.descriptive.rank.Max;
import org.apache.commons.math3.stat.descriptive.rank.Min;
import java.util.Arrays;
/**
* Circular sample buffer - allocates a buffer and stores samples in a circular
* fashion, overwriting older samples with newly arrived samples. Initially
* fills buffer with 0-valued samples
*
* Can be used as a delay-type buffer, to delay samples by the 'size' amount
* Circular sample buffer - allocates a buffer and stores samples in a circular fashion, overwriting older samples with
* newly arrived samples. Initially fills buffer with 0-valued (default) samples or with a specified value.
*/
public class DoubleCircularBuffer
{
double[] mBuffer;
int mBufferPointer = 0;
public DoubleCircularBuffer( int size )
{
mBuffer = new double[ size ];
Arrays.fill( mBuffer, 0.0 );
}
private Variance mVariance;
private Max mMax;
private Mean mMean;
private Min mMin;
private double[] mBuffer;
private int mBufferPointer = 0;
/**
* Puts the new value into the buffer and returns the oldest buffer value
* that it replaced
* @param newValue
* @return
*/
public double get( double newValue )
{
double oldestSample = mBuffer[ mBufferPointer ];
/**
* Creates a circular double buffer of the specified size and all entries filled with the specified initial value.
* @param size of the buffer
* @param initialFillValue to fill the buffer entries
*/
public DoubleCircularBuffer(int size, double initialFillValue)
{
mBuffer = new double[size];
Arrays.fill(mBuffer, initialFillValue);
}
mBuffer[ mBufferPointer ] = newValue;
mBufferPointer++;
if( mBufferPointer >= mBuffer.length )
{
mBufferPointer = 0;
}
return oldestSample;
}
/**
* Creates a circular double buffer of the specified size and all entries filled with an initial value of zero.
* @param size of the buffer
*/
public DoubleCircularBuffer(int size)
{
this(size, 0.0);
}
/**
* Returns the maximum from the values currently in the buffer
*/
public double max()
{
double max = 0.0d;
for( int x = 0; x < mBuffer.length; x++ )
{
if( mBuffer[ x ] > max )
{
max = mBuffer[ x ];
}
}
return max;
}
/**
* Puts the new value into the buffer and returns the oldest buffer value that it replaced
*
* @param newValue to add to the buffer
* @return oldest value that was overwritten by the new value
*/
public double get(double newValue)
{
double oldestSample = mBuffer[mBufferPointer];
mBuffer[mBufferPointer] = newValue;
mBufferPointer++;
if(mBufferPointer >= mBuffer.length)
{
mBufferPointer = 0;
}
return oldestSample;
}
public double[] getAll()
{
int valuePointer = 0;
double[] values = new double[mBuffer.length];
int bufferPointer = mBufferPointer;
while(valuePointer < values.length)
{
values[valuePointer++] = mBuffer[bufferPointer++];
if(bufferPointer >= mBuffer.length)
{
bufferPointer = 0;
}
}
return values;
}
/**
* Places the new value into the buffer, overwriting the oldest value
* @param newValue to add to the buffer
*/
public void put(double newValue)
{
mBuffer[mBufferPointer] = newValue;
mBufferPointer++;
if(mBufferPointer >= mBuffer.length)
{
mBufferPointer = 0;
}
}
/**
* Returns the maximum from the values currently in the buffer
*/
public double max()
{
if(mMax == null)
{
mMax = new Max();
}
return mMax.evaluate(mBuffer, 0, mBuffer.length);
}
/**
* Returns the minimum from the values currently in the buffer
*/
public double min()
{
if(mMin == null)
{
mMin = new Min();
}
return mMin.evaluate(mBuffer, 0, mBuffer.length);
}
/**
* Calculates the variance of the values contained in this buffer
*/
public double variance()
{
if(mVariance == null)
{
mVariance = new Variance();
}
return mVariance.evaluate(mBuffer);
}
/**
* Calculates the average/mean of the values contained in this buffer
*/
public double mean()
{
if(mMean == null)
{
mMean = new Mean();
}
return mMean.evaluate(mBuffer, 0, mBuffer.length);
}
}

View File

@@ -39,15 +39,24 @@ public class FilterViewer extends Application
*/
private float[] getFilter()
{
// FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
// .sampleRate(48000)
// .gridDensity(16)
// .passBandCutoff(2500)
// .passBandAmplitude(1.0)
// .passBandRipple(0.01)
// .stopBandStart(4000)
// .stopBandAmplitude(0.0)
// .stopBandRipple(0.008)
// .build();
FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
.sampleRate(48000)
.gridDensity(16)
.passBandCutoff(2500)
.sampleRate((int)48000.0)
.passBandCutoff(6000)
.passBandAmplitude(1.0)
.passBandRipple(0.01)
.stopBandStart(4000)
.passBandRipple(0.1)
.stopBandAmplitude(0.0)
.stopBandRipple(0.008)
.stopBandStart(6250)
.stopBandRipple(0.01)
.build();
// FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder()

View File

@@ -1,54 +1,90 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.filter.fir.complex;
import io.github.dsheirer.dsp.filter.fir.FIRFilter;
import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter;
import io.github.dsheirer.sample.complex.Complex;
import io.github.dsheirer.sample.complex.ComplexBuffer;
public class ComplexFIRFilter extends FIRFilter
{
private RealFIRFilter mIFilter;
private RealFIRFilter mQFilter;
/**
* Complex FIR Filter for processing complex sample pairs. Wraps two real
* FIR filters for processing each of the inphase and quadrature samples.
*
* @param coefficients - filter taps
* @param gain - gain to apply to filtered outputs - use 1.0f for no gain
*/
public ComplexFIRFilter( float[] coefficients, float gain )
{
mIFilter = new RealFIRFilter( coefficients, gain );
mQFilter = new RealFIRFilter( coefficients, gain );
}
public float[] getCoefficients()
{
return mIFilter.getCoefficients();
}
public float filterInphase( float sample )
{
return mIFilter.filter( sample );
}
public float filterQuadrature( float sample )
{
return mQFilter.filter( sample );
}
public Complex filter( Complex sample )
{
float i = filterInphase( sample.inphase() );
float q = filterQuadrature( sample.quadrature() );
return new Complex( i, q );
}
@Override
public void dispose()
{
mIFilter.dispose();
mQFilter.dispose();
}
private RealFIRFilter mIFilter;
private RealFIRFilter mQFilter;
/**
* Complex FIR Filter for processing complex sample pairs. Wraps two real
* FIR filters for processing each of the inphase and quadrature samples.
*
* @param coefficients - filter taps
* @param gain - gain to apply to filtered outputs - use 1.0f for no gain
*/
public ComplexFIRFilter(float[] coefficients, float gain)
{
mIFilter = new RealFIRFilter(coefficients, gain);
mQFilter = new RealFIRFilter(coefficients, gain);
}
public float[] getCoefficients()
{
return mIFilter.getCoefficients();
}
public float filterInphase(float sample)
{
return mIFilter.filter(sample);
}
public float filterQuadrature(float sample)
{
return mQFilter.filter(sample);
}
public Complex filter(Complex sample)
{
float i = filterInphase(sample.inphase());
float q = filterQuadrature(sample.quadrature());
return new Complex(i, q);
}
/**
* Filters the complex samples from the buffer and returns a new complex buffer with the filtered output
*
* @param buffer with complex samples to filter
* @return new buffer containing filtered complex samples
*/
public ComplexBuffer filter(ComplexBuffer buffer)
{
float[] samples = buffer.getSamples();
float[] filtered = new float[samples.length];
for(int x = 0; x < samples.length; x += 2)
{
filtered[x] = filterInphase(samples[x]);
filtered[x + 1] = filterQuadrature(samples[x + 1]);
}
return new ComplexBuffer(filtered);
}
@Override
public void dispose()
{
mIFilter.dispose();
mQFilter.dispose();
}
}

View File

@@ -3,8 +3,7 @@ package io.github.dsheirer.dsp.filter.fir.complex;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.ComplexBuffer;
public class ComplexFIRFilter_CB_CB extends ComplexFIRFilter
implements Listener<ComplexBuffer>
public class ComplexFIRFilter_CB_CB extends ComplexFIRFilter implements Listener<ComplexBuffer>
{
private Listener<ComplexBuffer> mListener;

View File

@@ -1,112 +1,125 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.gain;
import io.github.dsheirer.buffer.FloatCircularBuffer;
import io.github.dsheirer.sample.complex.Complex;
import io.github.dsheirer.sample.complex.ComplexBuffer;
import io.github.dsheirer.sample.complex.ComplexSampleListener;
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2015 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
* This class is modeled using concepts detailed in the GNURadio
* implementation of feed forward AGC class located at:
* https://github.com/gnuradio/gnuradio/blob/master/gr-analog/lib/
* feedforward_agc_cc_impl.cc
*
******************************************************************************/
public class ComplexFeedForwardGainControl implements ComplexSampleListener
{
public static final float OBJECTIVE_ENVELOPE = 1.0f;
public static final float MINIMUM_ENVELOPE = 0.0001f;
public static final float OBJECTIVE_ENVELOPE = 1.0f;
public static final float MINIMUM_ENVELOPE = 0.0001f;
private ComplexSampleListener mListener;
private ComplexSampleListener mListener;
private FloatCircularBuffer mEnvelopeHistory;
private float mMaxEnvelope = 0.0f;
private float mGain = 1.0f;
/**
* Dynamic gain control for incoming sample stream to amplify or attenuate
* all samples toward an objective unity)gain, using the maximum envelope
* value detected in the stream history window.
*
* Uses the specified damping factor to limit gain swings. Damping factor
* is applied against the delta between current gain value and a recalculated
* gain value to limit how quickly the gain value will increase or decrease.
*
* @param window - history size to use in detecting maximum envelope value
* @param damping - a value between 0 < damping <= 1.0;
*/
public ComplexFeedForwardGainControl( int window )
{
mEnvelopeHistory = new FloatCircularBuffer( window );
}
public void dispose()
{
mListener = null;
}
private FloatCircularBuffer mEnvelopeHistory;
@Override
public void receive( float inphase, float quadrature )
{
float envelope = Complex.envelope( inphase, quadrature );
private float mMaxEnvelope = 0.0f;
private float mGain = 1.0f;
if( envelope > mMaxEnvelope )
{
mMaxEnvelope = envelope;
adjustGain();
}
/**
* Dynamic gain control for incoming sample stream to amplify or attenuate
* all samples toward an objective unity)gain, using the maximum envelope
* value detected in the stream history window.
*
* Uses the specified damping factor to limit gain swings. Damping factor
* is applied against the delta between current gain value and a recalculated
* gain value to limit how quickly the gain value will increase or decrease.
*
* @param window - history size to use in detecting maximum envelope value
*/
public ComplexFeedForwardGainControl(int window)
{
mEnvelopeHistory = new FloatCircularBuffer(window);
}
/* Replace oldest envelope value with current envelope value */
float oldestEnvelope = mEnvelopeHistory.get( envelope );
public void dispose()
{
mListener = null;
}
/* If the oldest envelope value was the max envelope value, then we
* have to rediscover the max value from the envelope history */
if( mMaxEnvelope == oldestEnvelope && mMaxEnvelope != envelope )
{
mMaxEnvelope = MINIMUM_ENVELOPE;
for( float value: mEnvelopeHistory.getBuffer() )
{
if( value > mMaxEnvelope )
{
mMaxEnvelope = value;
}
}
adjustGain();
}
/* Apply current gain value to the sample and send to the listener */
if( mListener != null )
{
mListener.receive( inphase *= mGain, quadrature *= mGain );
}
}
private void adjustGain()
{
mGain = OBJECTIVE_ENVELOPE / mMaxEnvelope;
}
@Override
public void receive(float inphase, float quadrature)
{
process(inphase, quadrature);
public void setListener( ComplexSampleListener listener )
{
mListener = listener;
}
/* Apply current gain value to the sample and send to the listener */
if(mListener != null)
{
mListener.receive(inphase *= mGain, quadrature *= mGain);
}
}
private void process(float inphase, float quadrature)
{
float envelope = Complex.envelope(inphase, quadrature);
if(envelope > mMaxEnvelope)
{
mMaxEnvelope = envelope;
adjustGain();
}
/* Replace oldest envelope value with current envelope value */
float oldestEnvelope = mEnvelopeHistory.get(envelope);
/* If the oldest envelope value was the max envelope value, then we
* have to rediscover the max value from the envelope history */
if(mMaxEnvelope == oldestEnvelope && mMaxEnvelope != envelope)
{
mMaxEnvelope = MINIMUM_ENVELOPE;
for(float value : mEnvelopeHistory.getBuffer())
{
if(value > mMaxEnvelope)
{
mMaxEnvelope = value;
}
}
adjustGain();
}
}
private void adjustGain()
{
mGain = OBJECTIVE_ENVELOPE / mMaxEnvelope;
}
public void setListener(ComplexSampleListener listener)
{
mListener = listener;
}
public ComplexBuffer filter(ComplexBuffer complexBuffer)
{
float[] samples = complexBuffer.getSamples();
float[] processed = new float[samples.length];
for(int x = 0; x < samples.length; x += 2)
{
process(samples[x], samples[x + 1]);
processed[x] = samples[x] * mGain;
processed[x + 1] = samples[x + 1] * mGain;
}
return new ComplexBuffer(processed);
}
}

View File

@@ -51,8 +51,7 @@ public class Oscillator
*/
private void update()
{
float anglePerSample =
(float)( 2.0d * Math.PI * mFrequency / mSampleRate );
float anglePerSample = (float)( 2.0d * Math.PI * mFrequency / mSampleRate );
mAnglePerSample = Complex.fromAngle( anglePerSample );
}

View File

@@ -0,0 +1,30 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.psk;
import io.github.dsheirer.sample.complex.Complex;
public class DQPSKSymbolPhaseErrorCalculator extends QPSKSymbolPhaseErrorCalculator
{
//Negative 45 degrees rotation to offset for differential additive 45 degrees of rotation
public static final Complex DIFFERENTIAL_OFFSET = Complex.fromAngle(Math.PI / -4.0d);
@Override
public void adjust(Complex symbol)
{
symbol.multiply(DIFFERENTIAL_OFFSET);
}
}

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 = (previous.inphase() - current.inphase()) * middle.inphase();
mErrorQuadrature = (previous.quadrature() - current.quadrature()) * middle.quadrature();
mError = normalize(mErrorInphase + mErrorQuadrature, 1.0f);
return mError;
}
/**
* Constrains timing error to +/- the maximum value and corrects any floating point invalid numbers
*/
public static float normalize(float error, float maximum)
{
if(Float.isNaN(error))
{
return 0.0f;
}
else
{
return clip(error, maximum);
}
}
/**
* Constrains value to the range of ( -maximum <> maximum )
*/
public static float clip(float value, float maximum)
{
if(value > maximum)
{
return maximum;
}
else if(value < -maximum)
{
return -maximum;
}
return value;
}
}

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,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;
import io.github.dsheirer.sample.complex.Complex;
public interface ISymbolPhaseErrorCalculator
{
/**
* Adjusts the calculated symbol as necessary before calculating any phase error. This might be useful for
* differential encoding to remove a 45 degree rotational spin.
* @param symbol to adjust
*/
void adjust(Complex symbol);
/**
* Calculates the phase error of the symbol relative to an alignment for the constellation
* @param symbol to calculate error
* @return error value
*/
float getPhaseError(Complex symbol);
}

View File

@@ -0,0 +1,187 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <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 InterpolatingSymbolBuffer
{
private Complex mMiddleSample = new Complex(0,0);
private Complex mCurrentSample = new Complex(0,0);
private float[] mDelayLineInphase;
private float[] mDelayLineQuadrature;
private float mMu; //Sample Counter
private float mGainMu = 0.05f; //Sample Counter Gain
private float mOmega;
private float mGainOmega = 0.1f * mGainMu * mGainMu;
private float mOmegaRel = 0.005f;
private float mOmegaMid;
private int mDelayLinePointer = 0;
private int mTwiceSamplesPerSymbol;
private RealInterpolator mInterpolator = new RealInterpolator(1.0f);
public InterpolatingSymbolBuffer(float samplesPerSymbol)
{
mMu = samplesPerSymbol;
mTwiceSamplesPerSymbol = (int)Math.floor(2.0 * samplesPerSymbol);
mDelayLineInphase = new float[2 * mTwiceSamplesPerSymbol];
mDelayLineQuadrature = new float[2 * mTwiceSamplesPerSymbol];
mOmega = samplesPerSymbol;
mOmegaMid = samplesPerSymbol;
}
public void receive(Complex currentSample)
{
mMu--;
//Fill up the delay line to use with the interpolator
mDelayLineInphase[mDelayLinePointer] = currentSample.inphase();
mDelayLineInphase[mDelayLinePointer + mTwiceSamplesPerSymbol] = currentSample.inphase();
mDelayLineQuadrature[mDelayLinePointer] = currentSample.quadrature();
mDelayLineQuadrature[mDelayLinePointer + mTwiceSamplesPerSymbol] = currentSample.quadrature();
//Increment pointer and keep pointer in bounds
mDelayLinePointer = (mDelayLinePointer + 1) % mTwiceSamplesPerSymbol;
}
/**
* Updates the internal sample counter with additional samples to add to the symbol sample counter.
* @param samplesToAdd to the sample counter
*/
public void increaseSampleCounter(float samplesToAdd)
{
mMu += samplesToAdd;
}
/**
* Adjusts samples per symbol and symbol timing counters and increments the sample counter to collect another
* symbol.
*
* @param symbolTimingError from a symbol timing error detector
*/
public void resetAndAdjust(float symbolTimingError)
{
//mOmega is samples per symbol and is constrained to floating between +/- .005 of the nominal samples per
//symbol
mOmega = mOmega + mGainOmega * symbolTimingError;
mOmega = mOmegaMid + clip(mOmega - mOmegaMid, mOmegaRel);
//Add another symbol's worth of samples to the counter and adjust timing based on gardner error
increaseSampleCounter((mOmega + (mGainMu * symbolTimingError)));
}
/**
* Indicates if this buffer has accumulated a enough samples to represent a full symbol
*/
public boolean hasSymbol()
{
return mMu < 1.0f;
}
/**
* Interpolated middle sample for the symbol. Note: this method should only be invoked after testing for a
* complete symbol with the hasSymbol() method.
*/
public Complex getMiddleSample()
{
/* Calculate interpolated middle sample and current sample */
mMiddleSample.setValues(getInphase(mMu), getQuadrature(mMu));
return mMiddleSample;
}
/**
* Interpolated current sample for the symbol. This sample represents the ideal symbol sampling point.
* Note: this method should only be invoked after testing for a complete symbol with the hasSymbol() method.
*/
public Complex getCurrentSample()
{
float half_omega = mOmega / 2.0f;
int half_sps = (int)Math.floor(half_omega);
float half_mu = mMu + half_omega - half_sps;
if(half_mu > 1.0)
{
half_mu -= 1.0;
half_sps += 1;
}
mCurrentSample.setValues(getInphase(half_mu, half_sps), getQuadrature(half_mu, half_sps));
return mCurrentSample;
}
/**
* Returns the interpolated inphase value for the specified offset
* @param interpolation into the buffer to calculate the interpolated sample
* @return inphase value of the interpolated sample
*/
public float getInphase(float interpolation)
{
return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer, interpolation);
}
/**
* Returns the interpolated inphase value for the specified offset
* @param interpolation into the buffer to calculate the interpolated sample
* @return inphase value of the interpolated sample
*/
public float getInphase(float interpolation, int indexOffet)
{
return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer + indexOffet, interpolation);
}
/**
* Returns the interpolated quadrature value for the specified offset
* @param interpolation into the buffer to calculate the interpolated sample
* @return quadrature value of the interpolated sample
*/
public float getQuadrature(float interpolation)
{
return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer, interpolation);
}
/**
* Returns the interpolated quadrature value for the specified offset
* @param interpolation into the buffer to calculate the interpolated sample
* @return quadrature value of the interpolated sample
*/
public float getQuadrature(float interpolation, int indexOffset)
{
return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer + indexOffset, interpolation);
}
/**
* Constrains value to the range of ( -maximum <> maximum )
*/
public static float clip(float value, float maximum)
{
if(value > maximum)
{
return maximum;
}
else if(value < -maximum)
{
return -maximum;
}
return value;
}
}

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;
}
}

File diff suppressed because it is too large Load Diff

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;
import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.Complex;
import io.github.dsheirer.sample.complex.ComplexBuffer;
import io.github.dsheirer.sample.complex.ComplexSampleListener;
public class QPSKDemodulator implements ComplexSampleListener
{
private InterpolatingSymbolBuffer mInterpolatingSymbolBuffer;
private IPhaseLockedLoop mPLL;
private ISymbolPhaseErrorCalculator mSymbolPhaseErrorCalculator;
private IQPSKSymbolDecoder mQPSKSymbolDecoder;
private GardnerDetector mGardnerDetector = new GardnerDetector();
private Listener<Complex> mSymbolListener;
private Listener<Double> mPLLErrorListener;
private Listener<Dibit> mDibitListener;
private Complex mPreviousSample = new Complex(0, 0);
private Complex mPreviousMiddleSample = new Complex(0, 0);
private Complex mCurrentSample = new Complex(0, 0);
private Complex mMiddleSample = new Complex(0, 0);
private Complex mPreviousSymbol = new Complex(0, 0);
private Complex mMiddleSymbol = new Complex(0, 0);
private Complex mCurrentSymbol = new Complex(0, 0);
private float mPhaseError;
private float mSymbolTimingError;
/**
* Decoder for Quaternary Phase Shift Keying (QPSK) and Differential QPSK (DQPSK). This decoder uses both a
* phase-locked loop and a gardner symbol timing error detector to automatically align to the incoming carrier
* frequency and to adjust for any changes in symbol timing.
*
* @param phaseLockedLoop for tracking carrier frequency error
* @param symbolPhaseErrorCalculator to calculate symbol phase errors and optionally adjust for differential encoding
* @param symbolDecoder to decode demodulated symbols into dibits
* @param sampleRate of the incoming complex sample stream
* @param symbolRate of the decoded QPSK symbols.
*/
public QPSKDemodulator(IPhaseLockedLoop phaseLockedLoop, ISymbolPhaseErrorCalculator symbolPhaseErrorCalculator,
IQPSKSymbolDecoder symbolDecoder, double sampleRate, double symbolRate)
{
if(sampleRate < (symbolRate * 2))
{
throw new IllegalArgumentException("Sample rate [" + sampleRate +
"] must be at least 2 x symbol rate [" + symbolRate + "]");
}
mPLL = phaseLockedLoop;
mSymbolPhaseErrorCalculator = symbolPhaseErrorCalculator;
mQPSKSymbolDecoder = symbolDecoder;
float samplesPerSymbol = (float)(sampleRate / symbolRate);
mInterpolatingSymbolBuffer = new InterpolatingSymbolBuffer(samplesPerSymbol);
}
/**
* Submits a buffer containing complex samples for decoding
* @param complexBuffer with complex samples
*/
public void receive(ComplexBuffer complexBuffer)
{
float[] samples = complexBuffer.getSamples();
for(int x = 0; x < samples.length; x += 2)
{
receive(samples[x], samples[x + 1]);
}
}
/**
* Submits a complex sample for decoding.
* @param inphase value for the sample
* @param quadrature value for the sample
*/
@Override
public void receive(float inphase, float quadrature)
{
//Update current sample with values
mCurrentSample.setValues(inphase, quadrature);
//Mix current sample with costas loop to remove any rotation that is present from a mis-tuned carrier frequency
mCurrentSample.multiply(mPLL.incrementAndGetCurrentVector());
mInterpolatingSymbolBuffer.receive(mCurrentSample);
//Calculate the symbol once we've stored enough samples
if(mInterpolatingSymbolBuffer.hasSymbol())
{
//Get middle and current samples from the interpolating buffer
mMiddleSample = mInterpolatingSymbolBuffer.getMiddleSample();
mCurrentSample = mInterpolatingSymbolBuffer.getCurrentSample();
//Calculate middle and current symbols as the delta between the previous and current samples
mMiddleSymbol.setInphase(Complex.multiplyInphase(mMiddleSample.inphase(), mMiddleSample.quadrature(), mPreviousMiddleSample.inphase(), -mPreviousMiddleSample.quadrature()));
mMiddleSymbol.setQuadrature(Complex.multiplyQuadrature(mMiddleSample.inphase(), mMiddleSample.quadrature(), mPreviousMiddleSample.inphase(), -mPreviousMiddleSample.quadrature()));
mCurrentSymbol.setInphase(Complex.multiplyInphase(mCurrentSample.inphase(), mCurrentSample.quadrature(), mPreviousSample.inphase(), -mPreviousSample.quadrature()));
mCurrentSymbol.setQuadrature(Complex.multiplyQuadrature(mCurrentSample.inphase(), mCurrentSample.quadrature(), mPreviousSample.inphase(), -mPreviousSample.quadrature()));
//Set gain to unity before we calculate the error value
mMiddleSymbol.normalize();
mCurrentSymbol.normalize();
//Symbol timing error calculations
mSymbolTimingError = mGardnerDetector.getError(mPreviousSymbol, mMiddleSymbol, mCurrentSymbol);
mInterpolatingSymbolBuffer.resetAndAdjust(mSymbolTimingError);
//Store current samples/symbols to use for the next period
mPreviousSample.setValues(mCurrentSample);
mPreviousMiddleSample.setValues(mMiddleSample);
mPreviousSymbol.setValues(mCurrentSymbol);
//Adjust the current symbol as necessary (e.g. for differential encoding)
mSymbolPhaseErrorCalculator.adjust(mCurrentSymbol);
//Calculate the phase error of the current symbol relative to the expected constellation and provide
//feedback to the PLL
mPhaseError = mSymbolPhaseErrorCalculator.getPhaseError(mCurrentSymbol);
mPLL.adjust(mPhaseError);
if(mPLLErrorListener != null)
{
mPLLErrorListener.receive((double)mPhaseError);
}
//Decode the dibit from the symbol and send to the listener
if(mDibitListener != null)
{
mDibitListener.receive(mQPSKSymbolDecoder.decode(mCurrentSymbol));
}
//Send to an external constellation symbol listener when registered
if(mSymbolListener != null)
{
mSymbolListener.receive(mCurrentSymbol);
}
//TODO: Assemble dibits here and broadcast
}
}
/**
* Registers the listener to receive decoded QPSK symbols
*/
public void setSymbolListener(Listener<Complex> listener)
{
mSymbolListener = listener;
}
/**
* Registers the listener to receive decoded QPSK dibits
*/
public void setDibitListener(Listener<Dibit> listener)
{
mDibitListener = listener;
}
/**
* Registers the listener to receive PLL error values
*/
public void setPLLErrorListener(Listener<Double> listener)
{
mPLLErrorListener = listener;
}
}

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,63 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <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 void adjust(Complex symbol)
{
//No-op - since there is no differential, we don't have to adjust
}
@Override
public float getPhaseError(Complex symbol)
{
mPhaseError = 0.0f;
if(Math.abs(symbol.inphase()) > Math.abs(symbol.quadrature()))
{
if(symbol.inphase() > 0)
{
mPhaseError = -symbol.quadrature();
}
else
{
mPhaseError = symbol.quadrature();
}
}
else
{
if(symbol.quadrature() > 0)
{
mPhaseError = symbol.inphase();
}
else
{
mPhaseError = -symbol.inphase();
}
}
return mPhaseError;
}
}

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,337 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.psk.pll;
import io.github.dsheirer.buffer.DoubleCircularBuffer;
import io.github.dsheirer.dsp.mixer.Oscillator;
import io.github.dsheirer.sample.complex.Complex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.DecimalFormat;
/**
* Costas Loop - phase locked loop designed to automatically synchronize to the incoming carrier frequency in order to
* zeroize any frequency offset inherent in the signal due to either mis-tuning or carrier frequency drift.
*
* Costas loop structure was modeled from KA1RBI's OP25/cqpsk.py
*
* Loop bandwidth and alpha/beta gain formulas derived from:
* http://www.trondeau.com/blog/2011/8/13/control-loop-gain-values.html
*/
public class CostasLoop implements IPhaseLockedLoop
{
private final static Logger mLog = LoggerFactory.getLogger(CostasLoop.class);
public static final double TWO_PI = 2.0 * Math.PI;
private DoubleCircularBuffer mErrorBuffer = new DoubleCircularBuffer(10);
private double mDamping = Math.sqrt(2.0) / 2.0;
private double mAlphaGain;
private double mBetaGain;
private double mLoopPhase = 0.0;
private double mLoopFrequency = 0.0;
private double mMaximumLoopFrequency;
private Complex mCurrentVector = new Complex(0, 0);
private Tracking mTracking = Tracking.SEARCHING;
private ITrackingStateListener mTrackingStateListener;
private boolean mAutomaticTrackingEnabled = true;
/**
* Costas Loop for tracking and correcting frequency error in a received carrier signal.
*
* @param sampleRate of the incoming samples
* @param symbolRate of the digital signal
*/
public CostasLoop(double sampleRate, double symbolRate)
{
mMaximumLoopFrequency = TWO_PI * (symbolRate / 2.0) / sampleRate;
updateLoopBandwidth();
}
/**
* Corrects the current phase tracker when an inverted output is detected. The inversion will take the form of
* +/- 90 degrees or +/- 180 degrees, although the latter correction is equal regardless of the sign.
*
* If the supplied correction value places the loop frequency outside of the max frequency, then the frequency will
* be corrected 360 degrees in the opposite direction to maintain within the max frequency bounds.
*
* @param correction as measured in radians
*/
public void correctInversion(double correction)
{
mLoopFrequency += correction;
if(mLoopFrequency > mMaximumLoopFrequency)
{
mLoopFrequency -= 2.0 * mMaximumLoopFrequency;
}
if(mLoopFrequency < -mMaximumLoopFrequency)
{
mLoopFrequency += 2.0 * mMaximumLoopFrequency;
}
}
/**
* Updates the loop bandwidth and alpha/beta gains according to the current loop synchronization state.
*/
private void updateLoopBandwidth()
{
double bandwidth = TWO_PI / mTracking.getLoopBandwidth();
mAlphaGain = (4.0 * mDamping * bandwidth) / (1.0 + (2.0 * mDamping * bandwidth) + (bandwidth * bandwidth));
mBetaGain = (4.0 * bandwidth * bandwidth) / (1.0 + (2.0 * mDamping * bandwidth) + (bandwidth * bandwidth));
}
/**
* Sets the tracking state for this costas loop. The tracking state affects the aggressiveness of the alpha/beta
* gain values in synchronizing with the signal carrier.
*
* @param tracking
*/
public void setTracking(Tracking tracking)
{
mTracking = tracking;
updateLoopBandwidth();
mLog.debug("Tracking: " + tracking.name());
if(mTrackingStateListener != null)
{
mTrackingStateListener.trackingStateChanged(tracking);
}
}
/**
* Sets the listener to receive notifications when the tracking state changes
*/
public void setTrackingStateListener(ITrackingStateListener listener)
{
mTrackingStateListener = listener;
}
/**
* Enables or disables automatic tracking state updates. When enabled, the loop will automatically increase or
* decrease the loop alpha/beta gain values based on the variance of the error tracking values submitted to the
* loop control. As the variance decreases, so will the gain values, and vice-versa. Setting this to disabled
* will force the tracker to retain the gain values associated with either the default or user-specified tracking
* value.
*
* @param enabled true (default) to enable automatic gain tracking
*/
public void setAutomaticTracking(boolean enabled)
{
mAutomaticTrackingEnabled = enabled;
}
/**
* Increments the phase of the loop for each sample received at the sample rate.
*/
public void increment()
{
mLoopPhase += mLoopFrequency;
/* Keep the loop phase in bounds */
normalizePhase();
}
/**
* Current vector of the loop. Note: this value is updated for the current angle in radians each time this method
* is invoked.
*/
public Complex getCurrentVector()
{
mCurrentVector.setAngle(mLoopPhase);
return mCurrentVector;
}
@Override
public Complex incrementAndGetCurrentVector()
{
increment();
return getCurrentVector();
}
public double getLoopFrequency()
{
return mLoopFrequency;
}
public double getMaximumLoopFrequency()
{
return mMaximumLoopFrequency;
}
/**
* Updates the costas loop frequency and phase to adjust for the phase
* error value
*
* @param phaseError - (-)= late and (+)= early
*/
public void adjust(double phaseError)
{
mLoopFrequency += (mBetaGain * phaseError);
mLoopPhase += mLoopFrequency + (mAlphaGain * phaseError);
/* Maintain phase between +/- 2 * PI */
normalizePhase();
/* Limit frequency to +/- maximum loop frequency */
limitFrequency();
mErrorBuffer.put(phaseError);
double variance = mErrorBuffer.variance();
switch(mTracking)
{
case SEARCHING:
if(Tracking.COARSE.meetsThreshold(variance))
{
setTracking(Tracking.COARSE);
}
break;
case COARSE:
if(Tracking.COARSE.meetsThreshold(variance))
{
if(Tracking.FINE.meetsThreshold(variance))
{
setTracking(Tracking.FINE);
}
}
else
{
setTracking(Tracking.SEARCHING);
}
break;
case FINE:
if(Tracking.FINE.meetsThreshold(variance))
{
if(Tracking.LOCKED.meetsThreshold(variance))
{
setTracking(Tracking.LOCKED);
}
}
else
{
setTracking(Tracking.COARSE);
}
break;
case LOCKED:
if(!Tracking.LOCKED.meetsThreshold(variance))
{
setTracking(Tracking.FINE);
}
break;
}
}
public double getErrorMean()
{
return mErrorBuffer.mean();
}
public double getErrorVariance()
{
return mErrorBuffer.variance();
}
/**
* Normalizes the phase tracker to maintain within +/- 2 Pi
*/
private void normalizePhase()
{
while(mLoopPhase > TWO_PI)
{
mLoopPhase -= TWO_PI;
}
while(mLoopPhase < -TWO_PI)
{
mLoopPhase += TWO_PI;
}
}
/**
* Constrains the frequency within the bounds of +/- loop frequency
*/
private void limitFrequency()
{
if(mLoopFrequency > mMaximumLoopFrequency)
{
mLoopFrequency = mMaximumLoopFrequency;
}
if(mLoopFrequency < -mMaximumLoopFrequency)
{
mLoopFrequency = -mMaximumLoopFrequency;
}
}
/**
* Tracking state listener interface for receiving updates when the tracking state of the costas loop changes
*/
public interface ITrackingStateListener
{
void trackingStateChanged(Tracking tracking);
}
public static void main(String[] arguments)
{
DecimalFormat decimalFormat = new DecimalFormat("0.000000");
double sampleRate = 25000.0;
int errorFrequency = 411;
double loopBandwidth = 50.0;
Oscillator oscillator = new Oscillator(errorFrequency, (int)sampleRate);
CostasLoop costasLoop = new CostasLoop(sampleRate, 4800.0);
costasLoop.setTrackingStateListener(new ITrackingStateListener()
{
@Override
public void trackingStateChanged(Tracking tracking)
{
mLog.debug("Tracking State Changed: " + tracking.name());
}
});
for(int x = 0; x < 1000; x++)
{
Complex complex = oscillator.getComplex();
oscillator.rotate();
costasLoop.increment();
if(x % 5 == 0)
{
complex.multiply(costasLoop.getCurrentVector().conjugate());
double phaseError = complex.angle();
costasLoop.adjust(phaseError);
double loopFrequency = costasLoop.getLoopFrequency() * sampleRate / TWO_PI;
double maxFrequency = costasLoop.getMaximumLoopFrequency() * sampleRate / TWO_PI;
mLog.debug(x + " Frequency: " + errorFrequency +
" Loop:" + decimalFormat.format(loopFrequency) +
" Max:" + maxFrequency +
" Detected Error:" + decimalFormat.format(phaseError) +
" Avg:" + decimalFormat.format(costasLoop.getErrorMean()) +
" Var:" + decimalFormat.format(costasLoop.getErrorVariance()));
}
}
}
}

View File

@@ -0,0 +1,56 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <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();
}

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.pll;
public enum Tracking
{
SEARCHING(50.0, Math.PI),
COARSE(100.0, 0.01),
FINE(150.0, 0.001),
LOCKED(200.0, 0.0001);
private double mLoopBandwidth;
private double mVarianceThreshold;
Tracking(double loopBandwidth, double varianceThreshold)
{
mLoopBandwidth = loopBandwidth;
mVarianceThreshold = varianceThreshold;
}
public double getLoopBandwidth()
{
return mLoopBandwidth;
}
public double getVarianceThreshold()
{
return mVarianceThreshold;
}
public boolean meetsThreshold(double variance)
{
return variance < getVarianceThreshold();
}
}

View File

@@ -15,6 +15,8 @@
******************************************************************************/
package io.github.dsheirer.instrument.gui.viewer;
import io.github.dsheirer.instrument.gui.viewer.decoder.DecoderPaneFactory;
import io.github.dsheirer.module.decode.DecoderType;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.event.ActionEvent;
@@ -44,7 +46,7 @@ public class DemodulatorViewerFX extends Application
public void start(Stage primaryStage) throws Exception
{
mStage = primaryStage;
mStage.setTitle("Demodulator Viewer FX");
setTitle(null);
BorderPane borderPane = new BorderPane();
borderPane.setTop(getMenuBar());
@@ -56,6 +58,18 @@ public class DemodulatorViewerFX extends Application
mStage.show();
}
private void setTitle(DecoderType decoderType)
{
if(decoderType != null)
{
mStage.setTitle("Demodulator Viewer FX - " + decoderType.getDisplayString());
}
else
{
mStage.setTitle("Demodulator Viewer FX");
}
}
private ViewerDesktop getViewerDesktop()
{
if(mViewerDesktop == null)
@@ -72,9 +86,9 @@ public class DemodulatorViewerFX extends Application
{
mMenuBar = new MenuBar();
Menu menuFile = new Menu("File");
MenuItem menuItemOpen = new MenuItem("Open ...");
menuItemOpen.setOnAction(new EventHandler<ActionEvent>()
Menu fileMenu = new Menu("File");
MenuItem openMenuItem = new MenuItem("Open ...");
openMenuItem.setOnAction(new EventHandler<ActionEvent>()
{
@Override
public void handle(ActionEvent event)
@@ -87,8 +101,8 @@ public class DemodulatorViewerFX extends Application
}
});
MenuItem menuItemClose = new MenuItem("Close");
menuItemClose.setOnAction(new EventHandler<ActionEvent>()
MenuItem closeMenuItem = new MenuItem("Close");
closeMenuItem.setOnAction(new EventHandler<ActionEvent>()
{
@Override
public void handle(ActionEvent event)
@@ -97,8 +111,8 @@ public class DemodulatorViewerFX extends Application
}
});
MenuItem menuItemExit = new MenuItem("Exit");
menuItemExit.setOnAction(new EventHandler<ActionEvent>()
MenuItem exitMenuItem = new MenuItem("Exit");
exitMenuItem.setOnAction(new EventHandler<ActionEvent>()
{
@Override
public void handle(ActionEvent event)
@@ -107,10 +121,34 @@ public class DemodulatorViewerFX extends Application
}
});
menuFile.getItems().addAll(menuItemOpen, getRecentFilesMenu(), menuItemClose, new SeparatorMenuItem(),
menuItemExit);
fileMenu.getItems().addAll(openMenuItem, getRecentFilesMenu(), closeMenuItem, new SeparatorMenuItem(),
exitMenuItem);
mMenuBar.getMenus().addAll(menuFile);
mMenuBar.getMenus().add(fileMenu);
Menu decoderMenu = new Menu("Decoder");
for(DecoderType decoderType: DecoderType.values())
{
if(DecoderPaneFactory.isSupported(decoderType))
{
MenuItem decoderMenuItem = new MenuItem(decoderType.getShortDisplayString());
decoderMenuItem.setOnAction(new EventHandler<ActionEvent>()
{
@Override
public void handle(ActionEvent event)
{
getViewerDesktop().setDecoder(decoderType);
setTitle(decoderType);
}
});
decoderMenu.getItems().add(decoderMenuItem);
}
}
mMenuBar.getMenus().add(decoderMenu);
}
return mMenuBar;

View File

@@ -15,6 +15,7 @@
******************************************************************************/
package io.github.dsheirer.instrument.gui.viewer;
import io.github.dsheirer.instrument.gui.viewer.decoder.DecoderPane;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.ComplexBuffer;
@@ -52,6 +53,7 @@ public class PlaybackController extends HBox implements IFrameLocationListener,
private HBox mControlsBox;
private Label mFileLabel;
private EventHandler<ActionEvent> mPlayEventHandler;
private DecoderPane mSampleRateListener;
private IControllableFileSource mControllableFileSource;
private Broadcaster<ComplexBuffer> mComplexBufferBroadcaster = new Broadcaster<>();
@@ -65,6 +67,16 @@ public class PlaybackController extends HBox implements IFrameLocationListener,
setControlsEnabled(false);
}
public void setSampleRateListener(DecoderPane decoderPane)
{
mSampleRateListener = decoderPane;
if(mSampleRateListener != null && mControllableFileSource != null)
{
mSampleRateListener.setSampleRate(mControllableFileSource.getSampleRate());
}
}
public void load(File file)
{
mLog.debug("loading:" + file.getAbsolutePath());
@@ -76,6 +88,11 @@ public class PlaybackController extends HBox implements IFrameLocationListener,
mControllableFileSource.setListener(this);
((ComplexWaveSource)mControllableFileSource).setListener((Listener<ComplexBuffer>)this);
mControllableFileSource.open();
if(mSampleRateListener != null)
{
mSampleRateListener.setSampleRate(mControllableFileSource.getSampleRate());
}
}
catch(UnsupportedAudioFileException e)
{

View File

@@ -15,25 +15,22 @@
******************************************************************************/
package io.github.dsheirer.instrument.gui.viewer;
import io.github.dsheirer.instrument.gui.viewer.decoder.DecoderPane;
import io.github.dsheirer.instrument.gui.viewer.decoder.DecoderPaneFactory;
import io.github.dsheirer.module.decode.DecoderType;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import java.io.File;
public class ViewerDesktop extends BorderPane
{
private PlaybackController mPlaybackController;
private VBox mChartBox;
private ComplexSampleLineChart mSampleLineChart;
private ComplexPhaseLineChart mPhaseLineChart;
private DecoderPane mDecoderPane = DecoderPaneFactory.getDefaultPane();
public ViewerDesktop()
{
setBottom(getPlaybackController());
getPlaybackController().addListener(getSampleLineChart());
getPlaybackController().addListener(getPhaseLineChart());
setCenter(getChartBox());
setCenter(getDecoderPane());
}
public void load(File file)
@@ -42,6 +39,31 @@ public class ViewerDesktop extends BorderPane
getPlaybackController().load(file);
}
public void setDecoder(DecoderType decoderType)
{
getPlaybackController().removeListener(getDecoderPane());
getChildren().remove(getDecoderPane());
mDecoderPane = DecoderPaneFactory.getDecoderPane(decoderType);
setCenter(getDecoderPane());
getPlaybackController().addListener(getDecoderPane());
getPlaybackController().setSampleRateListener(getDecoderPane());
}
/**
* Decoder pane
*/
public DecoderPane getDecoderPane()
{
if(mDecoderPane == null)
{
mDecoderPane = DecoderPaneFactory.getDefaultPane();
}
return mDecoderPane;
}
public void close()
{
getPlaybackController().close();
@@ -56,35 +78,4 @@ public class ViewerDesktop extends BorderPane
return mPlaybackController;
}
private VBox getChartBox()
{
if(mChartBox == null)
{
mChartBox = new VBox();
mChartBox.getChildren().addAll(getSampleLineChart(), getPhaseLineChart());
}
return mChartBox;
}
private ComplexSampleLineChart getSampleLineChart()
{
if(mSampleLineChart == null)
{
mSampleLineChart = new ComplexSampleLineChart(40);
}
return mSampleLineChart;
}
private ComplexPhaseLineChart getPhaseLineChart()
{
if(mPhaseLineChart == null)
{
mPhaseLineChart = new ComplexPhaseLineChart(50);
}
return mPhaseLineChart;
}
}

View File

@@ -13,7 +13,7 @@
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.instrument.gui.viewer;
package io.github.dsheirer.instrument.gui.viewer.chart;
import io.github.dsheirer.buffer.ComplexCircularBuffer;
import io.github.dsheirer.dsp.gain.ComplexGain;
@@ -28,7 +28,6 @@ import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import org.apache.commons.math3.complex.ComplexUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -36,7 +35,7 @@ public class ComplexPhaseLineChart extends LineChart implements Listener<Complex
{
private final static Logger mLog = LoggerFactory.getLogger(ComplexPhaseLineChart.class);
private static final Complex ANGLE_OFFSET_45_DEGREES = Complex.fromAngle((float)(Math.PI / 4.0));
private static final Complex ANGLE_OFFSET_45_DEGREES = Complex.fromAngle(Math.PI / 4.0);
private ComplexCircularBuffer mComplexCircularBuffer;
private ComplexGain mComplexGain = new ComplexGain(600.0f);
private ObservableList<Data<Integer,Float>> mPhaseValues = FXCollections.observableArrayList();
@@ -44,8 +43,8 @@ public class ComplexPhaseLineChart extends LineChart implements Listener<Complex
public ComplexPhaseLineChart(int length)
{
super(new NumberAxis("Time", 1, length - 10, 1),
new NumberAxis("Phase", -Math.PI, Math.PI, 1.0));
super(new NumberAxis("Time", 1, length - 10, 2),
new NumberAxis("Phase", -Math.PI, Math.PI, Math.PI / 2.0));
Series<Integer,Float> phaseSeries = new Series<>("Phase", mPhaseValues);
ObservableList<Series<Integer,Float>> observableList = FXCollections.observableArrayList(phaseSeries);

View File

@@ -13,7 +13,7 @@
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.instrument.gui.viewer;
package io.github.dsheirer.instrument.gui.viewer.chart;
import io.github.dsheirer.buffer.ComplexCircularBuffer;
import io.github.dsheirer.dsp.gain.ComplexGain;
@@ -44,8 +44,8 @@ public class ComplexSampleLineChart extends LineChart implements Listener<Comple
public ComplexSampleLineChart(int length)
{
super(new NumberAxis("Sample Number", 1, length, 1),
new NumberAxis("Value", -1.0, 1.0, 0.1));
super(new NumberAxis("Sample Number", 1, length, 2),
new NumberAxis("Value", -1.0, 1.0, 0.25));
LineChart.Series<Integer,Float> iSampleSeries = new LineChart.Series<>("Inphase", mISamples);
LineChart.Series<Integer,Float> qSampleSeries = new LineChart.Series<>("Quadrature", mQSamples);

View File

@@ -0,0 +1,73 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.instrument.gui.viewer.chart;
import io.github.dsheirer.buffer.DoubleCircularBuffer;
import io.github.dsheirer.sample.Listener;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PhaseLineChart extends LineChart implements Listener<Double>
{
private final static Logger mLog = LoggerFactory.getLogger(PhaseLineChart.class);
private DoubleCircularBuffer mDoubleCircularBuffer;
private ObservableList<Data<Integer,Double>> mPhaseValues = FXCollections.observableArrayList();
public PhaseLineChart(int length)
{
super(new NumberAxis("Time", 1, length, 2),
new NumberAxis("Phase", -Math.PI, Math.PI, Math.PI / 2.0));
Series<Integer,Double> phaseSeries = new Series<>("Phase", mPhaseValues);
ObservableList<Series<Integer,Double>> observableList = FXCollections.observableArrayList(phaseSeries);
setData(observableList);
init(length);
}
private void init(int length)
{
mDoubleCircularBuffer = new DoubleCircularBuffer(length);
for(int x = 1; x <= length; x++)
{
mPhaseValues.add(new Data(x, 0.0));
}
}
@Override
public void receive(Double phase)
{
mDoubleCircularBuffer.put(phase);
updateChart();
}
private void updateChart()
{
double[] values = mDoubleCircularBuffer.getAll();
for(int x = 0; x < values.length; x++)
{
mPhaseValues.get(x).setYValue(values[x]);
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.instrument.gui.viewer.chart;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.Complex;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.ScatterChart;
import javafx.scene.chart.XYChart;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SymbolChart extends ScatterChart implements Listener<Complex>
{
private final static Logger mLog = LoggerFactory.getLogger(SymbolChart.class);
private ObservableList<ScatterChart.Data> mCurrentPoints = FXCollections.observableArrayList();
private ObservableList<ScatterChart.Data> mPreviousPoints = FXCollections.observableArrayList();
private ObservableList<ScatterChart.Data> mOldestPoints = FXCollections.observableArrayList();
private int mDataPointer;
private int mHistory;
/**
* Symbol chart for plotting a series of constellation symbols over time. Maintains three history sets for
* current, previous, and oldest symbols.
*
* @param history size for each of the history sets
*/
public SymbolChart(int history)
{
super(new NumberAxis("I", -1.0, 1.0, 0.2),
new NumberAxis("Q", -1.0, 1.0, 0.2));
mHistory = history;
for(int x = 0; x < mHistory; x++)
{
mCurrentPoints.add(new XYChart.Data<>(0.0, 0.0));
mPreviousPoints.add(new XYChart.Data<>(0.0, 0.0));
mOldestPoints.add(new XYChart.Data<>(0.0, 0.0));
}
ObservableList<XYChart.Series> data = FXCollections.observableArrayList();
data.add(new ScatterChart.Series("Current", mCurrentPoints));
data.add(new ScatterChart.Series("Previous", mPreviousPoints));
data.add(new ScatterChart.Series("Oldest", mOldestPoints));
this.setData(data);
}
@Override
public void receive(Complex complex)
{
//Transfer previous to oldest
XYChart.Data oldest = mOldestPoints.get(mDataPointer);
XYChart.Data previous = mPreviousPoints.get(mDataPointer);
XYChart.Data current = mCurrentPoints.get(mDataPointer);
oldest.setXValue(previous.getXValue());
oldest.setYValue(previous.getYValue());
previous.setXValue(current.getXValue());
previous.setYValue(current.getYValue());
current.setXValue(complex.inphase());
current.setYValue(complex.quadrature());
mDataPointer++;
if(mDataPointer >= mHistory)
{
mDataPointer = 0;
}
}
}

View File

@@ -0,0 +1,62 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.instrument.gui.viewer.decoder;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.ComplexBuffer;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
public class DecoderPane extends VBox implements Listener<ComplexBuffer>
{
private Broadcaster<ComplexBuffer> mComplexBufferBroadcaster = new Broadcaster<>();
private DecoderType mDecoderType;
public DecoderPane(DecoderType decoderType)
{
mDecoderType = decoderType;
}
/**
* Package private - used by factory class to create an empty pane
*/
DecoderPane()
{
getChildren().add(new Text("Please select a decoder from the menu"));
}
@Override
public void receive(ComplexBuffer complexBuffer)
{
mComplexBufferBroadcaster.broadcast(complexBuffer);
}
public void addListener(Listener<ComplexBuffer> listener)
{
mComplexBufferBroadcaster.addListener(listener);
}
public void removeListener(Listener<ComplexBuffer> listener)
{
mComplexBufferBroadcaster.removeListener(listener);
}
public void setSampleRate(double sampleRate)
{
}
}

View File

@@ -0,0 +1,55 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.instrument.gui.viewer.decoder;
import io.github.dsheirer.module.decode.DecoderType;
import java.util.EnumSet;
public class DecoderPaneFactory
{
private static final EnumSet<DecoderType> SUPPORTED_DECODER_TYPES = EnumSet.of(DecoderType.P25_PHASE1);
/**
* Creates a decoder pane for the decoder type
*/
public static DecoderPane getDecoderPane(DecoderType decoderType)
{
switch(decoderType)
{
case P25_PHASE1:
return new P25Phase1Pane();
}
return getDefaultPane();
}
/**
* Creates an empty decoder pane
*/
public static DecoderPane getDefaultPane()
{
return new DecoderPane();
}
/**
* Indicates if the decoder type is supported by this factory
*/
public static boolean isSupported(DecoderType decoderType)
{
return SUPPORTED_DECODER_TYPES.contains(decoderType);
}
}

View File

@@ -0,0 +1,141 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.instrument.gui.viewer.decoder;
import io.github.dsheirer.instrument.gui.viewer.chart.ComplexPhaseLineChart;
import io.github.dsheirer.instrument.gui.viewer.chart.ComplexSampleLineChart;
import io.github.dsheirer.instrument.gui.viewer.chart.PhaseLineChart;
import io.github.dsheirer.instrument.gui.viewer.chart.SymbolChart;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.module.decode.p25.P25_C4FMDecoder2;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class P25Phase1Pane extends DecoderPane
{
private final static Logger mLog = LoggerFactory.getLogger(P25Phase1Pane.class);
private HBox mSampleChartBox;
private ComplexSampleLineChart mSampleLineChart;
private ComplexPhaseLineChart mPhaseLineChart;
private HBox mDecoderChartBox;
private SymbolChart mSymbolChart;
private PhaseLineChart mPLLPhaseErrorLineChart;
private P25_C4FMDecoder2 mDecoder = new P25_C4FMDecoder2(null);
public P25Phase1Pane()
{
super(DecoderType.P25_PHASE1);
init();
}
private void init()
{
addListener(getSampleLineChart());
addListener(getPhaseLineChart());
addListener(getDecoder());
getDecoder().setSymbolListener(getSymbolChart());
getDecoder().setPLLPhaseErrorListener(getPLLPhaseErrorLineChart());
getChildren().addAll(getSampleChartBox(), getDecoderChartBox());
}
@Override
public void setSampleRate(double sampleRate)
{
mLog.debug("Configuring for sample rate: " + sampleRate);
mDecoder.setSampleRate(sampleRate);
}
private P25_C4FMDecoder2 getDecoder()
{
return mDecoder;
}
private SymbolChart getSymbolChart()
{
if(mSymbolChart == null)
{
mSymbolChart = new SymbolChart(20);
}
return mSymbolChart;
}
private HBox getDecoderChartBox()
{
if(mDecoderChartBox == null)
{
mDecoderChartBox = new HBox();
getSymbolChart().setMaxWidth(Double.MAX_VALUE);
getPLLPhaseErrorLineChart().setMaxWidth(Double.MAX_VALUE);
HBox.setHgrow(getSymbolChart(), Priority.ALWAYS);
HBox.setHgrow(getPLLPhaseErrorLineChart(), Priority.ALWAYS);
mDecoderChartBox.getChildren().addAll(getSymbolChart(), getPLLPhaseErrorLineChart());
}
return mDecoderChartBox;
}
private HBox getSampleChartBox()
{
if(mSampleChartBox == null)
{
mSampleChartBox = new HBox();
getSampleLineChart().setMaxWidth(Double.MAX_VALUE);
getPhaseLineChart().setMaxWidth(Double.MAX_VALUE);
HBox.setHgrow(getSampleLineChart(), Priority.ALWAYS);
HBox.setHgrow(getPhaseLineChart(), Priority.ALWAYS);
mSampleChartBox.getChildren().addAll(getSampleLineChart(), getPhaseLineChart());
}
return mSampleChartBox;
}
private ComplexSampleLineChart getSampleLineChart()
{
if(mSampleLineChart == null)
{
mSampleLineChart = new ComplexSampleLineChart(40);
}
return mSampleLineChart;
}
private ComplexPhaseLineChart getPhaseLineChart()
{
if(mPhaseLineChart == null)
{
mPhaseLineChart = new ComplexPhaseLineChart(50);
}
return mPhaseLineChart;
}
private PhaseLineChart getPLLPhaseErrorLineChart()
{
if(mPLLPhaseErrorLineChart == null)
{
mPLLPhaseErrorLineChart = new PhaseLineChart(80);
}
return mPLLPhaseErrorLineChart;
}
}

View File

@@ -1,17 +1,17 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014-2016 Dennis Sheirer
*
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
******************************************************************************/
@@ -22,39 +22,39 @@ import io.github.dsheirer.message.Message;
import io.github.dsheirer.module.Module;
import io.github.dsheirer.sample.Listener;
public abstract class Decoder extends Module
implements IMessageProvider, Listener<Message>
public abstract class Decoder extends Module implements IMessageProvider
{
/* This has to be a broadcaster in order for references to persist */
protected Listener<Message> mMessageListener;
/* This has to be a broadcaster in order for references to persist */
private Listener<Message> mMessageDistributor = new MessageDistributor();
protected Listener<Message> mMessageListener;
/**
* Decoder - parent class for all decoders, demodulators and components.
*/
public Decoder()
{
}
/**
* Decoder - parent class for all decoders, demodulators and components.
*/
public Decoder()
{
}
@Override
public void dispose()
{
mMessageListener = null;
}
@Override
public void dispose()
{
mMessageListener = null;
}
/**
* Identifies the decoder type (ie protocol)
*/
public abstract DecoderType getDecoderType();
/**
* Identifies the decoder type (ie protocol)
*/
public abstract DecoderType getDecoderType();
/**
* Adds a listener for receiving decoded messages from this decoder
*/
@Override
public void setMessageListener( Listener<Message> listener )
public void setMessageListener(Listener<Message> listener)
{
mMessageListener = listener;
}
mMessageListener = listener;
}
/**
* Removes the listener from receiving decoded messages from all attached
@@ -63,23 +63,29 @@ public abstract class Decoder extends Module
@Override
public void removeMessageListener()
{
mMessageListener = null;
mMessageListener = null;
}
/**
* Broadcasts the message to the registered message listener
* Message listener for receiving the output from this decoder
*/
public void broadcast( Message message )
protected Listener<Message> getMessageListener()
{
if( mMessageListener != null )
{
mMessageListener.receive( message );
}
return mMessageDistributor;
}
@Override
public void receive( Message message )
{
broadcast( message );
}
/**
* Distributes/forwards messages from sub-class decoder implementations to the registered message listener.
*/
public class MessageDistributor implements Listener<Message>
{
@Override
public void receive(Message message)
{
if(mMessageListener != null)
{
mMessageListener.receive(message);
}
}
}
}

View File

@@ -90,7 +90,7 @@ public class Fleetsync2Decoder extends Decoder implements IFilteredRealBufferLis
mMessageProcessor = new Fleetsync2MessageProcessor( aliasList );
mMessageFramer.addMessageListener( mMessageProcessor );
mMessageProcessor.setMessageListener( this );
mMessageProcessor.setMessageListener( getMessageListener() );
}
public void dispose()

View File

@@ -100,7 +100,7 @@ public class LJ1200Decoder extends Decoder implements IFilteredRealBufferListene
mMessageProcessor = new LJ1200MessageProcessor( aliasList );
mTowerMessageFramer.addMessageListener( mMessageProcessor );
mTransponderMessageFramer.addMessageListener( mMessageProcessor );
mMessageProcessor.setMessageListener( this );
mMessageProcessor.setMessageListener( getMessageListener() );
}
@Override

View File

@@ -74,7 +74,7 @@ public class LTRNetDecoder extends Decoder
mLTRMessageFramer.addMessageListener( mLTRMessageProcessor );
mLTRMessageProcessor.setMessageListener( this );
mLTRMessageProcessor.setMessageListener( getMessageListener() );
}
@Override

View File

@@ -64,7 +64,7 @@ public class LTRStandardDecoder extends Decoder implements IUnFilteredRealBuffer
mLTRMessageProcessor = new LTRStandardMessageProcessor( direction, aliasList );
mLTRMessageFramer.addMessageListener( mLTRMessageProcessor );
mLTRMessageProcessor.setMessageListener( this );
mLTRMessageProcessor.setMessageListener( getMessageListener() );
}
@Override

View File

@@ -102,7 +102,7 @@ public class MDCDecoder extends Decoder implements IFilteredRealBufferListener,
/* Message Processor */
mMessageProcessor = new MDCMessageProcessor( aliasList );
mMessageFramer.addMessageListener( mMessageProcessor );
mMessageProcessor.addMessageListener( this );
mMessageProcessor.addMessageListener(getMessageListener());
}
public void dispose()

View File

@@ -148,7 +148,7 @@ public class MPT1327Decoder extends Decoder implements IFilteredRealBufferListen
/* Fully decoded and framed messages processor */
mMessageProcessor = new MPT1327MessageProcessor(aliasList);
mMessageProcessor.setMessageListener(this);
mMessageProcessor.setMessageListener(getMessageListener());
mControlMessageFramer.addMessageListener(mMessageProcessor);
mTrafficMessageFramer.addMessageListener(mMessageProcessor);
}

View File

@@ -1,70 +1,91 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.module.decode.p25;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.instrument.Instrumentable;
import io.github.dsheirer.module.decode.Decoder;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeListener;
import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeProvider;
public abstract class P25Decoder extends Decoder implements IFrequencyChangeListener, IFrequencyChangeProvider, Instrumentable
public abstract class P25Decoder extends Decoder implements IFrequencyChangeListener, IFrequencyChangeProvider
{
private P25MessageProcessor mMessageProcessor;
public P25Decoder( AliasList aliasList )
{
mMessageProcessor = new P25MessageProcessor( aliasList );
mMessageProcessor.setMessageListener( this );
}
public void dispose()
{
super.dispose();
mMessageProcessor.dispose();
mMessageProcessor = null;
}
private P25MessageProcessor mMessageProcessor;
private AliasList mAliasList;
@Override
public DecoderType getDecoderType()
public P25Decoder(AliasList aliasList)
{
return DecoderType.P25_PHASE1;
mAliasList = aliasList;
mMessageProcessor = new P25MessageProcessor(mAliasList);
mMessageProcessor.setMessageListener(getMessageListener());
}
protected P25MessageProcessor getMessageProcessor()
{
return mMessageProcessor;
}
public abstract Modulation getModulation();
public enum Modulation
{
CQPSK( "Simulcast (LSM)", "LSM" ),
C4FM( "Normal (C4FM)", "C4FM" );
private String mLabel;
private String mShortLabel;
private Modulation( String label, String shortLabel )
{
mLabel = label;
mShortLabel = shortLabel;
}
public String getLabel()
{
return mLabel;
}
public String getShortLabel()
{
return mShortLabel;
}
public String toString()
{
return getLabel();
}
}
protected AliasList getAliasList()
{
return mAliasList;
}
public void dispose()
{
super.dispose();
mMessageProcessor.dispose();
mMessageProcessor = null;
}
@Override
public DecoderType getDecoderType()
{
return DecoderType.P25_PHASE1;
}
protected P25MessageProcessor getMessageProcessor()
{
return mMessageProcessor;
}
public abstract Modulation getModulation();
public enum Modulation
{
CQPSK("Simulcast (LSM)", "LSM"),
C4FM("Normal (C4FM)", "C4FM");
private String mLabel;
private String mShortLabel;
private Modulation(String label, String shortLabel)
{
mLabel = label;
mShortLabel = shortLabel;
}
public String getLabel()
{
return mLabel;
}
public String getShortLabel()
{
return mShortLabel;
}
public String toString()
{
return getLabel();
}
}
}

View File

@@ -19,10 +19,7 @@
package io.github.dsheirer.module.decode.p25;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.instrument.tap.Tap;
import io.github.dsheirer.instrument.tap.TapGroup;
import io.github.dsheirer.instrument.tap.stream.DibitTap;
import io.github.dsheirer.instrument.tap.stream.FloatTap;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.real.IFilteredRealBufferListener;
import io.github.dsheirer.sample.real.RealBuffer;
@@ -30,7 +27,6 @@ import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
@@ -105,93 +101,6 @@ public class P25_C4FMDecoder extends P25Decoder implements IFilteredRealBufferLi
return mSymbolFilter;
}
/**
* Instrumentation taps for monitoring internal processing
*/
@Override
public List<TapGroup> getTapGroups()
{
if( mAvailableTaps == null )
{
mAvailableTaps = new ArrayList<>();
TapGroup group = new TapGroup( "P25 C4FM Decoder" );
group.add( new FloatTap( INSTRUMENT_C4FM_SYMBOL_FILTER_OUTPUT, 0, 0.1f ) );
group.add( new DibitTap( INSTRUMENT_C4FM_SLICER_OUTPUT, 0, 0.1f ) );
mAvailableTaps.add( group );
if( mSymbolFilter != null )
{
mAvailableTaps.addAll( mSymbolFilter.getTapGroups() );
}
}
return mAvailableTaps;
}
/**
* Adds an instrumentation tap to monitor internal processing.
*/
@Override
public void registerTap( Tap tap )
{
if( mSymbolFilter != null )
{
mSymbolFilter.registerTap( tap );
}
switch( tap.getName() )
{
case INSTRUMENT_C4FM_SYMBOL_FILTER_OUTPUT:
FloatTap symbolTap = (FloatTap)tap;
mSymbolFilter.setListener( symbolTap );
symbolTap.setListener( mC4FMSlicer );
break;
case INSTRUMENT_C4FM_SLICER_OUTPUT:
DibitTap slicerTap = (DibitTap)tap;
if( mC4FMSlicer != null )
{
mC4FMSlicer.addListener( slicerTap );
}
break;
default:
break;
}
}
/**
* Removes the instrumentation tap.
*/
@Override
public void unregisterTap( Tap tap )
{
if( mSymbolFilter != null )
{
mSymbolFilter.unregisterTap( tap );
}
switch( tap.getName() )
{
case INSTRUMENT_C4FM_SYMBOL_FILTER_OUTPUT:
mSymbolFilter.setListener( mC4FMSlicer );
break;
case INSTRUMENT_C4FM_SLICER_OUTPUT:
DibitTap slicerTap = (DibitTap)tap;
if( mC4FMSlicer != null )
{
mC4FMSlicer.removeListener( slicerTap );
}
break;
default:
throw new IllegalArgumentException( "Unrecognized tap: " +
tap.getName() );
}
}
@Override
public void reset()
{

View File

@@ -0,0 +1,223 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.module.decode.p25;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.dsp.filter.FilterFactory;
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter;
import io.github.dsheirer.dsp.gain.ComplexFeedForwardGainControl;
import io.github.dsheirer.dsp.psk.DQPSKSymbolPhaseErrorCalculator;
import io.github.dsheirer.dsp.psk.IQPSKSymbolDecoder;
import io.github.dsheirer.dsp.psk.ISymbolPhaseErrorCalculator;
import io.github.dsheirer.dsp.psk.QPSKDemodulator;
import io.github.dsheirer.dsp.psk.QPSKSymbolDecoder;
import io.github.dsheirer.dsp.psk.pll.CostasLoop;
import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.Complex;
import io.github.dsheirer.sample.complex.ComplexBuffer;
import io.github.dsheirer.sample.complex.ComplexBufferToStreamConverter;
import io.github.dsheirer.sample.complex.IComplexBufferListener;
import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListener, Listener<ComplexBuffer>
{
private final static Logger mLog = LoggerFactory.getLogger(P25_C4FMDecoder2.class);
private static final double SYMBOL_RATE = 4800;
private double mSampleRate;
private Map<Double,float[]> mBasebandFilters = new HashMap<>();
private ComplexFIRFilter mBasebandFilter;
private QPSKDemodulator mQPSKDemodulator;
private IPhaseLockedLoop mPhaseLockedLoop;
private ComplexBufferToStreamConverter mStreamConverter = new ComplexBufferToStreamConverter();
private ComplexFeedForwardGainControl mAGC = new ComplexFeedForwardGainControl(32);
private QPSKSymbolDecoder mQPSKSlicer = new QPSKSymbolDecoder();
private P25MessageFramer mMessageFramer;
private ISymbolPhaseErrorCalculator mSymbolPhaseErrorCalculator = new DQPSKSymbolPhaseErrorCalculator();
private IQPSKSymbolDecoder mSymbolDecoder = new QPSKSymbolDecoder();
private Listener<Complex> mSymbolListener;
private Listener<Double> mPLLPhaseErrorListener;
public P25_C4FMDecoder2(AliasList aliasList)
{
super(aliasList);
setSampleRate(48000.0);
mStreamConverter.setListener(mAGC);
}
public void setSampleRate(double sampleRate)
{
if(sampleRate <= SYMBOL_RATE * 2)
{
throw new IllegalArgumentException("Sample rate must be at least twice the symbol rate [4800]");
}
mSampleRate = sampleRate;
mBasebandFilter = new ComplexFIRFilter(getBasebandFilter(), 1.0f);
mPhaseLockedLoop = new CostasLoop(mSampleRate, SYMBOL_RATE);
mQPSKDemodulator = new QPSKDemodulator(mPhaseLockedLoop, mSymbolPhaseErrorCalculator, mSymbolDecoder,
mSampleRate, SYMBOL_RATE);
mQPSKDemodulator.setSymbolListener(mSymbolListener);
mQPSKDemodulator.setPLLErrorListener(mPLLPhaseErrorListener);
//Message framer can trigger a symbol-inversion correction to the PLL when detected
mMessageFramer = new P25MessageFramer(getAliasList(), mPhaseLockedLoop);
mMessageFramer.setListener(getMessageProcessor());
mQPSKDemodulator.setDibitListener(mMessageFramer);
}
public void setSymbolListener(Listener<Complex> listener)
{
mSymbolListener = listener;
mQPSKDemodulator.setSymbolListener(listener);
}
public void setPLLPhaseErrorListener(Listener<Double> listener)
{
mPLLPhaseErrorListener = listener;
mQPSKDemodulator.setPLLErrorListener(listener);
}
@Override
public void receive(ComplexBuffer complexBuffer)
{
ComplexBuffer filtered = mBasebandFilter.filter(complexBuffer);
ComplexBuffer amplified = mAGC.filter(filtered);
mQPSKDemodulator.receive(amplified);
}
private double getSampleRate()
{
return mSampleRate;
}
private float[] getBasebandFilter()
{
float[] filter = mBasebandFilters.get(getSampleRate());
if(filter == null)
{
FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
.sampleRate((int)getSampleRate())
.passBandCutoff(6000)
.passBandAmplitude(1.0)
.passBandRipple(0.1)
.stopBandAmplitude(0.0)
.stopBandStart(6250)
.stopBandRipple(0.01)
.build();
try
{
filter = FilterFactory.getTaps(specification);
mBasebandFilters.put(getSampleRate(), filter);
}
catch(FilterDesignException fde)
{
mLog.error("Couldn't design low pass baseband filter for sample rate: " + getSampleRate());
}
}
return filter;
}
public void dispose()
{
super.dispose();
mBasebandFilter.dispose();
mBasebandFilter = null;
mStreamConverter.dispose();
mStreamConverter = null;
mAGC.dispose();
mAGC = null;
mQPSKSlicer.dispose();
mQPSKSlicer = null;
mMessageFramer.dispose();
mMessageFramer = null;
}
@Override
public void setFrequencyChangeListener(Listener<FrequencyChangeEvent> listener)
{
}
@Override
public void removeFrequencyChangeListener()
{
}
@Override
public Listener<FrequencyChangeEvent> getFrequencyChangeListener()
{
return new Listener<FrequencyChangeEvent>()
{
@Override
public void receive(FrequencyChangeEvent frequencyChangeEvent)
{
//Ignored
}
};
}
@Override
public Listener<ComplexBuffer> getComplexBufferListener()
{
return P25_C4FMDecoder2.this;
}
public Modulation getModulation()
{
return Modulation.C4FM;
}
@Override
public void reset()
{
// TODO Auto-generated method stub
}
@Override
public void start(ScheduledExecutorService executor)
{
// TODO Auto-generated method stub
}
@Override
public void stop()
{
// TODO Auto-generated method stub
}
}

View File

@@ -1,17 +1,17 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014,2015 Dennis Sheirer
*
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
******************************************************************************/
@@ -23,13 +23,8 @@ import io.github.dsheirer.dsp.filter.Window.WindowType;
import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter_CB_CB;
import io.github.dsheirer.dsp.gain.ComplexFeedForwardGainControl;
import io.github.dsheirer.dsp.psk.LSMDemodulator;
import io.github.dsheirer.dsp.psk.QPSKPolarSlicer;
import io.github.dsheirer.instrument.tap.Tap;
import io.github.dsheirer.dsp.psk.QPSKSymbolDecoder;
import io.github.dsheirer.instrument.tap.TapGroup;
import io.github.dsheirer.instrument.tap.stream.ComplexSampleTap;
import io.github.dsheirer.instrument.tap.stream.ComplexTap;
import io.github.dsheirer.instrument.tap.stream.DibitTap;
import io.github.dsheirer.instrument.tap.stream.QPSKTap;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.ComplexBuffer;
import io.github.dsheirer.sample.complex.ComplexBufferToStreamConverter;
@@ -38,73 +33,72 @@ import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
public class P25_LSMDecoder extends P25Decoder implements IComplexBufferListener
{
private final static Logger mLog = LoggerFactory.getLogger( P25_LSMDecoder.class );
private final static Logger mLog = LoggerFactory.getLogger(P25_LSMDecoder.class);
/* Instrumentation Taps */
private static final String INSTRUMENT_BASEBAND_FILTER_OUTPUT = "Tap Point: Baseband Filter Output";
private static final String INSTRUMENT_AGC_OUTPUT = "Tap Point: AGC Output";
private static final String INSTRUMENT_LSM_DEMODULATOR_OUTPUT = "Tap Point: LSM Demodulator Output";
private static final String INSTRUMENT_QPSK_SLICER_OUTPUT = "Tap Point: QPSK Slicer Output";
private static final String INSTRUMENT_BASEBAND_FILTER_OUTPUT = "Tap Point: Baseband Filter Output";
private static final String INSTRUMENT_AGC_OUTPUT = "Tap Point: AGC Output";
private static final String INSTRUMENT_LSM_DEMODULATOR_OUTPUT = "Tap Point: LSM Demodulator Output";
private static final String INSTRUMENT_QPSK_SLICER_OUTPUT = "Tap Point: QPSK Slicer Output";
private List<TapGroup> mAvailableTaps;
private ComplexFIRFilter_CB_CB mBasebandFilter;
private ComplexBufferToStreamConverter mStreamConverter = new ComplexBufferToStreamConverter();
private ComplexFeedForwardGainControl mAGC =
new ComplexFeedForwardGainControl( 32 );
private LSMDemodulator mLSMDemodulator = new LSMDemodulator();
private QPSKPolarSlicer mQPSKSlicer = new QPSKPolarSlicer();
private P25MessageFramer mMessageFramer;
public P25_LSMDecoder( AliasList aliasList )
{
super( aliasList );
mBasebandFilter = new ComplexFIRFilter_CB_CB( FilterFactory.getLowPass(
48000, 7250, 8000, 60, WindowType.HANNING, true ), 1.0f );
mBasebandFilter.setListener( mStreamConverter );
mStreamConverter.setListener( mAGC );
private List<TapGroup> mAvailableTaps;
mAGC.setListener( mLSMDemodulator );
mLSMDemodulator.setSymbolListener( mQPSKSlicer );
mMessageFramer = new P25MessageFramer( aliasList, mLSMDemodulator );
mQPSKSlicer.addListener( mMessageFramer );
mMessageFramer.setListener( getMessageProcessor() );
}
public void dispose()
{
super.dispose();
mBasebandFilter.dispose();
mBasebandFilter = null;
mStreamConverter.dispose();
mStreamConverter = null;
mAGC.dispose();
mAGC = null;
private ComplexFIRFilter_CB_CB mBasebandFilter;
private ComplexBufferToStreamConverter mStreamConverter = new ComplexBufferToStreamConverter();
private ComplexFeedForwardGainControl mAGC =
new ComplexFeedForwardGainControl(32);
private LSMDemodulator mLSMDemodulator = new LSMDemodulator();
private QPSKSymbolDecoder mQPSKSlicer = new QPSKSymbolDecoder();
private P25MessageFramer mMessageFramer;
mLSMDemodulator.dispose();
mLSMDemodulator = null;
mQPSKSlicer.dispose();
mQPSKSlicer = null;
mMessageFramer.dispose();
mMessageFramer = null;
}
public P25_LSMDecoder(AliasList aliasList)
{
super(aliasList);
mBasebandFilter = new ComplexFIRFilter_CB_CB(FilterFactory.getLowPass(
48000, 7250, 8000, 60, WindowType.HANNING, true), 1.0f);
mBasebandFilter.setListener(mStreamConverter);
mStreamConverter.setListener(mAGC);
mAGC.setListener(mLSMDemodulator);
mLSMDemodulator.setSymbolListener(mQPSKSlicer);
mMessageFramer = new P25MessageFramer(aliasList, mLSMDemodulator.getCostasLoop());
mQPSKSlicer.addListener(mMessageFramer);
mMessageFramer.setListener(getMessageProcessor());
}
public void dispose()
{
super.dispose();
mBasebandFilter.dispose();
mBasebandFilter = null;
mStreamConverter.dispose();
mStreamConverter = null;
mAGC.dispose();
mAGC = null;
mLSMDemodulator.dispose();
mLSMDemodulator = null;
mQPSKSlicer.dispose();
mQPSKSlicer = null;
mMessageFramer.dispose();
mMessageFramer = null;
}
@Override
public void setFrequencyChangeListener(Listener<FrequencyChangeEvent> listener)
@@ -130,130 +124,34 @@ public class P25_LSMDecoder extends P25Decoder implements IComplexBufferListener
}
@Override
public Listener<ComplexBuffer> getComplexBufferListener()
{
return mBasebandFilter;
}
public Modulation getModulation()
{
return Modulation.CQPSK;
}
/**
* Provides a list of instrumentation taps for monitoring internal processing
*/
@Override
public List<TapGroup> getTapGroups()
public Listener<ComplexBuffer> getComplexBufferListener()
{
if( mAvailableTaps == null )
{
mAvailableTaps = new ArrayList<>();
TapGroup group = new TapGroup( "P25 LSM Decoder" );
group.add( new ComplexTap( INSTRUMENT_BASEBAND_FILTER_OUTPUT, 0, 1.0f ) );
group.add( new ComplexTap( INSTRUMENT_AGC_OUTPUT, 0, 1.0f ) );
group.add( new QPSKTap( INSTRUMENT_LSM_DEMODULATOR_OUTPUT, 0, 1.0f ) );
group.add( new DibitTap( INSTRUMENT_QPSK_SLICER_OUTPUT, 0, 0.1f ) );
mAvailableTaps.add( group );
if( mLSMDemodulator != null )
{
mAvailableTaps.addAll( mLSMDemodulator.getTapGroups() );
}
}
return mAvailableTaps;
return mBasebandFilter;
}
/**
* Adds the instrumentation tap
*/
@Override
public void registerTap( Tap tap )
public Modulation getModulation()
{
if( mLSMDemodulator != null )
{
mLSMDemodulator.registerTap( tap );
}
switch( tap.getName() )
{
case INSTRUMENT_BASEBAND_FILTER_OUTPUT:
ComplexSampleTap baseband = (ComplexSampleTap)tap;
mStreamConverter.setListener( baseband );
baseband.setListener( mAGC );
break;
case INSTRUMENT_AGC_OUTPUT:
ComplexSampleTap agcSymbol = (ComplexSampleTap)tap;
mAGC.setListener( agcSymbol );
agcSymbol.setListener( mLSMDemodulator );
break;
case INSTRUMENT_LSM_DEMODULATOR_OUTPUT:
QPSKTap qpsk = (QPSKTap)tap;
mLSMDemodulator.setSymbolListener( qpsk );
qpsk.setListener( mQPSKSlicer );
break;
case INSTRUMENT_QPSK_SLICER_OUTPUT:
mQPSKSlicer.addListener( (DibitTap)tap );
break;
default:
throw new IllegalArgumentException( "Unrecognized tap: " +
tap.getName() );
}
return Modulation.CQPSK;
}
/**
* Removes the instrumentation tap
*/
@Override
public void unregisterTap( Tap tap )
@Override
public void reset()
{
if( mLSMDemodulator != null )
{
mLSMDemodulator.unregisterTap( tap );
}
switch( tap.getName() )
{
case INSTRUMENT_BASEBAND_FILTER_OUTPUT:
mStreamConverter.setListener( mAGC );
break;
case INSTRUMENT_AGC_OUTPUT:
mAGC.setListener( mLSMDemodulator );
break;
case INSTRUMENT_LSM_DEMODULATOR_OUTPUT:
mLSMDemodulator.setSymbolListener( mQPSKSlicer );
break;
case INSTRUMENT_QPSK_SLICER_OUTPUT:
mQPSKSlicer.removeListener( (DibitTap)tap );
break;
default:
throw new IllegalArgumentException( "Unrecognized tap: " +
tap.getName() );
}
// TODO Auto-generated method stub
}
@Override
public void reset()
{
// TODO Auto-generated method stub
}
@Override
public void start(ScheduledExecutorService executor)
{
// TODO Auto-generated method stub
@Override
public void start( ScheduledExecutorService executor )
{
// TODO Auto-generated method stub
}
}
@Override
public void stop()
{
// TODO Auto-generated method stub
}
@Override
public void stop()
{
// TODO Auto-generated method stub
}
}

View File

@@ -63,7 +63,7 @@ public class PassportDecoder extends Decoder
mPassportMessageProcessor = new PassportMessageProcessor( aliasList );
mPassportMessageFramer.addMessageListener( mPassportMessageProcessor );
mPassportMessageProcessor.setMessageListener( this );
mPassportMessageProcessor.setMessageListener( getMessageListener() );
}
@Override

View File

@@ -104,8 +104,8 @@ public class Tait1200Decoder extends Decoder implements IFilteredRealBufferListe
mMessageFramerGPS.addMessageListener( mMessageAProcessor );
mMessageFramerANI.addMessageListener( mMessageBProcessor );
mMessageAProcessor.setMessageListener( this );
mMessageBProcessor.setMessageListener( this );
mMessageAProcessor.setMessageListener(getMessageListener());
mMessageBProcessor.setMessageListener(getMessageListener());
}
public void dispose()

View File

@@ -42,6 +42,7 @@ public class RecorderManager implements Listener<AudioPacket>
private static final Logger mLog = LoggerFactory.getLogger(RecorderManager.class);
public static final int AUDIO_SAMPLE_RATE = 8000;
public static final int BASEBAND_SAMPLE_RATE = 48000;
public static final long IDLE_RECORDER_REMOVAL_THRESHOLD = 6000; //6 seconds
private Map<String,AudioPacketWaveRecorder> mRecorders = new HashMap<>();
@@ -229,7 +230,7 @@ public class RecorderManager implements Listener<AudioPacket>
sb.append(SystemProperties.getInstance().getApplicationFolder("recordings"));
sb.append(File.separator).append(channelName).append("_baseband");
return new ComplexBufferWaveRecorder(AUDIO_SAMPLE_RATE, sb.toString());
return new ComplexBufferWaveRecorder(BASEBAND_SAMPLE_RATE, sb.toString());
}
/**

View File

@@ -1,17 +1,17 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 Dennis Sheirer
*
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
******************************************************************************/
@@ -27,412 +27,427 @@ import java.io.Serializable;
*/
public class Complex implements Serializable
{
private final static Logger mLog =
LoggerFactory.getLogger( Complex.class );
private final static Logger mLog = LoggerFactory.getLogger(Complex.class);
private static final long serialVersionUID = 1L;
private static final long serialVersionUID = 1L;
private float mLeft;
private float mRight;
public Complex( float left, float right )
{
mLeft = left;
mRight = right;
}
public Complex()
{
this( 0.0f, 0.0f );
}
public void setInphase( float inphase )
{
mLeft = inphase;
}
public void setQuadrature( float quadrature )
{
mRight = quadrature;
}
public void setValues( float inphase, float quadrature )
{
mLeft = inphase;
mRight = quadrature;
}
private float mRight;
public Complex copy()
{
return new Complex( mLeft, mRight );
}
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append( "I:" );
sb.append( mLeft );
sb.append( " Q:" );
sb.append( mRight );
return sb.toString();
}
/**
* Returns a new sample representing the conjugate of this one
*/
public Complex conjugate()
{
return new Complex( mLeft, -mRight );
}
public Complex(float left, float right)
{
mLeft = left;
mRight = right;
}
/**
* Multiplies this sample by the scalor value
*/
public void multiply( float scalor )
{
mLeft *= scalor;
mRight *= scalor;
}
public static Complex multiply( Complex sample, float scalor )
{
return new Complex( sample.left() * scalor,
sample.right() * scalor );
}
/**
* Calculates the inphase component of multiplying two complex numbers, A and B.
*/
public static float multiplyInphase( float inphaseA, float quadratureA, float inphaseB, float quadratureB )
{
return ( inphaseA * inphaseB ) - ( quadratureA * quadratureB );
}
/**
* Calculates the quadrature component of multiplying two complex numbers, A and B.
*/
public static float multiplyQuadrature( float inphaseA, float quadratureA, float inphaseB, float quadratureB )
{
return ( quadratureA * inphaseB ) + ( inphaseA * quadratureB );
}
/**
* Multiplies this sample by the multiplier sample
*/
public void multiply( Complex multiplier )
{
float inphase = multiplyInphase( inphase(), quadrature(),
multiplier.inphase(), multiplier.quadrature() );
float quadrature = multiplyQuadrature( inphase(), quadrature(),
multiplier.inphase(), multiplier.quadrature() );
mLeft = inphase;
mRight = quadrature;
}
/**
* Multiplies both samples returning a new sample with the results
*/
public static Complex multiply( Complex sample1, Complex sample2 )
{
float inphase = multiplyInphase( sample1.inphase(), sample1.quadrature(),
sample2.inphase(), sample2.quadrature() );
float quadrature = multiplyQuadrature( sample1.inphase(), sample1.quadrature(),
sample2.inphase(), sample2.quadrature() );
public Complex()
{
this(0.0f, 0.0f);
}
return new Complex( inphase, quadrature );
}
public void setInphase(float inphase)
{
mLeft = inphase;
}
public static Complex multiply( Complex sample,
Float inphase, Float quadrature )
{
float i = multiplyInphase( sample.inphase(), sample.quadrature(),
inphase, quadrature );
float q = multiplyQuadrature( sample.inphase(), sample.quadrature(),
inphase, quadrature );
return new Complex( i, q );
}
public static Complex multiply( Float inphase, Float quadrature, Complex sample )
{
float i = multiplyInphase( inphase, quadrature, sample.inphase(), sample.quadrature() );
float q = multiplyQuadrature( inphase, quadrature, sample.inphase(), sample.quadrature() );
return new Complex( i, q );
}
public void setQuadrature(float quadrature)
{
mRight = quadrature;
}
public static Complex multiply( float inphaseA, float quadratureA, float inphaseB, float quadratureB )
{
float i = multiplyInphase( inphaseA, quadratureA, inphaseB, quadratureB );
float q = multiplyQuadrature( inphaseA, quadratureA, inphaseB, quadratureB );
return new Complex( i, q );
}
/**
* Adds the adder sample value to this sample
*/
public void add( Complex adder )
{
mLeft += adder.left();
mRight += adder.right();
}
public void setValues(float inphase, float quadrature)
{
mLeft = inphase;
mRight = quadrature;
}
/**
* Adds the two complex samples returning a new complex sample with the result
*/
public static Complex add( Complex first, Complex second )
{
return new Complex( first.left() + second.left(),
first.right() + second.right() );
}
public void setValues(Complex sampleToCopy)
{
mLeft = sampleToCopy.left();
mRight = sampleToCopy.right();
}
/**
* Adds the two complex samples returning a new complex sample with the result
*/
public static Complex subtract( Complex first, Complex second )
{
return new Complex( first.left() - second.left(),
first.right() - second.right() );
}
public Complex copy()
{
return new Complex(mLeft, mRight);
}
/**
* Magnitude of this sample
*/
public float magnitude()
{
return (float)Math.sqrt( magnitudeSquared() );
}
public static float magnitude( float inphase, float quadrature )
{
return (float)Math.sqrt( ( inphase * inphase ) + ( quadrature * quadrature ) );
}
public String toString()
{
StringBuilder sb = new StringBuilder();
/**
* Magnitude squared of this sample
*/
public float magnitudeSquared()
{
return norm();
}
sb.append("I:");
sb.append(mLeft);
sb.append(" Q:");
sb.append(mRight);
/**
* Norm of this sample = ( i * i ) + ( q * q )
*/
public float norm()
{
return (float)( ( inphase() * inphase() ) +
( quadrature() * quadrature() ) );
}
/**
* Returns the vector length to 1 (unit circle)
*/
public void normalize()
{
float magnitude = magnitude();
if( magnitude != 0 )
{
multiply( (float)( 1.0f / magnitude() ) );
}
}
return sb.toString();
}
/**
* Returns the vector length to 1 (unit circle),
* avoiding square root multiplication
*/
public void fastNormalize()
{
multiply( (float)( 1.95f - magnitudeSquared() ) );
}
public float left()
{
return mLeft;
}
public float right()
{
return mRight;
}
/**
* Returns a new sample representing the conjugate of this one
*/
public Complex conjugate()
{
return new Complex(mLeft, -mRight);
}
public float inphase()
{
return mLeft;
}
/**
* Absolute value of in-phase component
*/
public float inPhaseAbsolute()
{
return Math.abs( mLeft );
}
/**
* Multiplies this sample by the scalor value
*/
public void multiply(float scalor)
{
mLeft *= scalor;
mRight *= scalor;
}
public float quadrature()
{
return mRight;
}
/**
* Absolute value of quadrature component
*/
public float quadratureAbsolute()
{
return Math.abs( mRight );
}
public static Complex multiply(Complex sample, float scalor)
{
return new Complex(sample.left() * scalor,
sample.right() * scalor);
}
public float x()
{
return mLeft;
}
public float y()
{
return mRight;
}
public float real()
{
return mLeft;
}
/**
* Calculates the inphase component of multiplying two complex numbers, A and B.
*/
public static float multiplyInphase(float inphaseA, float quadratureA, float inphaseB, float quadratureB)
{
return (inphaseA * inphaseB) - (quadratureA * quadratureB);
}
/**
* Absolute value of real component
*/
public float realAbsolute()
{
return Math.abs( mLeft );
}
/**
* Calculates the quadrature component of multiplying two complex numbers, A and B.
*/
public static float multiplyQuadrature(float inphaseA, float quadratureA, float inphaseB, float quadratureB)
{
return (quadratureA * inphaseB) + (inphaseA * quadratureB);
}
public float imaginary()
{
return mRight;
}
/**
* Multiplies this sample by the multiplier sample
*/
public void multiply(Complex multiplier)
{
float inphase = multiplyInphase(inphase(), quadrature(),
multiplier.inphase(), multiplier.quadrature());
float quadrature = multiplyQuadrature(inphase(), quadrature(),
multiplier.inphase(), multiplier.quadrature());
/**
* Absolute value of imaginary component
*/
public float imaginaryAbsolute()
{
return Math.abs( mRight );
}
mLeft = inphase;
mRight = quadrature;
}
/**
* Returns the greater absolute value between left and right values
*/
public float maximumAbsolute()
{
if( Math.abs( mLeft ) > Math.abs( mRight ) )
{
return Math.abs( mLeft );
}
else
{
return Math.abs( mRight );
}
}
/**
* Multiplies both samples returning a new sample with the results
*/
public static Complex multiply(Complex sample1, Complex sample2)
{
float inphase = multiplyInphase(sample1.inphase(), sample1.quadrature(),
sample2.inphase(), sample2.quadrature());
float quadrature = multiplyQuadrature(sample1.inphase(), sample1.quadrature(),
sample2.inphase(), sample2.quadrature());
/**
* Creates a new complex sample representing the angle with unit circle
* magnitude
* @param angle in radians
* @return
*/
public static Complex fromAngle( float angle )
{
return new Complex( (float)Math.cos( angle ),
(float)Math.sin( angle ) );
}
return new Complex(inphase, quadrature);
}
/**
* Angle of this sample in radians
*/
public float angle()
{
return (float)Math.atan2( y(), x() );
}
public static Complex multiply(Complex sample,
Float inphase, Float quadrature)
{
float i = multiplyInphase(sample.inphase(), sample.quadrature(),
inphase, quadrature);
float q = multiplyQuadrature(sample.inphase(), sample.quadrature(),
inphase, quadrature);
public float angleDegrees()
{
return (float)Math.toDegrees(angle());
}
return new Complex(i, q);
}
/**
* Constrains the i and q quantities to +/- value.
*
* @param value - maximum absolute value
*/
public void clip( float value )
{
if( mLeft > value )
{
mLeft = value;
}
else if( mLeft < -value )
{
mLeft = -value;
}
if( mRight > value )
{
mRight = value;
}
else if( mRight < -value )
{
mRight = -value;
}
}
public static Complex multiply(Float inphase, Float quadrature, Complex sample)
{
float i = multiplyInphase(inphase, quadrature, sample.inphase(), sample.quadrature());
float q = multiplyQuadrature(inphase, quadrature, sample.inphase(), sample.quadrature());
/**
* Angle of this sample in degrees
*/
public float polarAngle()
{
return (float)Math.toDegrees( angle() );
}
return new Complex(i, q);
}
/**
* Provides an approximate magnitude value for this sample.
*/
public float envelope()
{
return envelope( mLeft, mRight );
}
public static float envelope( float inphase, float quadrature )
{
float inphaseAbsolute = Math.abs( inphase );
float quadratureAbsolute = Math.abs( quadrature );
if( inphaseAbsolute > quadratureAbsolute )
{
return inphaseAbsolute + ( 0.4f * quadratureAbsolute );
}
else
{
return quadratureAbsolute + ( 0.4f * inphaseAbsolute );
}
}
public static void main( String[] args )
{
double angle = Math.PI; //180 degrees
Complex s1 = new Complex( (float)Math.sin( angle ), (float)Math.cos( angle ) );
public static Complex multiply(float inphaseA, float quadratureA, float inphaseB, float quadratureB)
{
float i = multiplyInphase(inphaseA, quadratureA, inphaseB, quadratureB);
float q = multiplyQuadrature(inphaseA, quadratureA, inphaseB, quadratureB);
Complex tap = new Complex( 1.0f, -0.1f );
return new Complex(i, q);
}
Complex convolved = Complex.multiply( s1, tap );
mLog.debug( "s: " + s1.toString() +
" t: " + tap.toString() +
" convolved: " + convolved.toString() );
}
/**
* Adds the adder sample value to this sample
*/
public void add(Complex adder)
{
mLeft += adder.left();
mRight += adder.right();
}
/**
* Adds the two complex samples returning a new complex sample with the result
*/
public static Complex add(Complex first, Complex second)
{
return new Complex(first.left() + second.left(),
first.right() + second.right());
}
/**
* Adds the two complex samples returning a new complex sample with the result
*/
public static Complex subtract(Complex first, Complex second)
{
return new Complex(first.left() - second.left(),
first.right() - second.right());
}
/**
* Magnitude of this sample
*/
public float magnitude()
{
return (float)Math.sqrt(magnitudeSquared());
}
public static float magnitude(float inphase, float quadrature)
{
return (float)Math.sqrt((inphase * inphase) + (quadrature * quadrature));
}
/**
* Magnitude squared of this sample
*/
public float magnitudeSquared()
{
return norm();
}
/**
* Norm of this sample = ( i * i ) + ( q * q )
*/
public float norm()
{
return (float)((inphase() * inphase()) +
(quadrature() * quadrature()));
}
/**
* Returns the vector length to 1 (unit circle)
*/
public void normalize()
{
float magnitude = magnitude();
if(magnitude != 0)
{
multiply((float)(1.0f / magnitude()));
}
}
/**
* Returns the vector length to 1 (unit circle),
* avoiding square root multiplication
*/
public void fastNormalize()
{
multiply((float)(1.95f - magnitudeSquared()));
}
public float left()
{
return mLeft;
}
public float right()
{
return mRight;
}
public float inphase()
{
return mLeft;
}
/**
* Absolute value of in-phase component
*/
public float inPhaseAbsolute()
{
return Math.abs(mLeft);
}
public float quadrature()
{
return mRight;
}
/**
* Absolute value of quadrature component
*/
public float quadratureAbsolute()
{
return Math.abs(mRight);
}
public float x()
{
return mLeft;
}
public float y()
{
return mRight;
}
public float real()
{
return mLeft;
}
/**
* Absolute value of real component
*/
public float realAbsolute()
{
return Math.abs(mLeft);
}
public float imaginary()
{
return mRight;
}
/**
* Absolute value of imaginary component
*/
public float imaginaryAbsolute()
{
return Math.abs(mRight);
}
/**
* Returns the greater absolute value between left and right values
*/
public float maximumAbsolute()
{
if(Math.abs(mLeft) > Math.abs(mRight))
{
return Math.abs(mLeft);
}
else
{
return Math.abs(mRight);
}
}
/**
* Creates a new complex sample representing the angle with unit circle
* magnitude
*
* @param angle in radians
* @return
*/
public static Complex fromAngle(double angle)
{
return new Complex((float)Math.cos(angle), (float)Math.sin(angle));
}
/**
* Sets this complex instance with the specified angle in radians.
* @param angle in radians
*/
public void setAngle(double angle)
{
setInphase((float)Math.cos(angle));
setQuadrature((float)Math.sin(angle));
}
/**
* Angle of this sample in radians
*/
public float angle()
{
return (float)Math.atan2(y(), x());
}
public float angleDegrees()
{
return (float)Math.toDegrees(angle());
}
/**
* Constrains the i and q quantities to +/- value.
*
* @param value - maximum absolute value
*/
public void clip(float value)
{
if(mLeft > value)
{
mLeft = value;
}
else if(mLeft < -value)
{
mLeft = -value;
}
if(mRight > value)
{
mRight = value;
}
else if(mRight < -value)
{
mRight = -value;
}
}
/**
* Angle of this sample in degrees
*/
public float polarAngle()
{
return (float)Math.toDegrees(angle());
}
/**
* Provides an approximate magnitude value for this sample.
*/
public float envelope()
{
return envelope(mLeft, mRight);
}
public static float envelope(float inphase, float quadrature)
{
float inphaseAbsolute = Math.abs(inphase);
float quadratureAbsolute = Math.abs(quadrature);
if(inphaseAbsolute > quadratureAbsolute)
{
return inphaseAbsolute + (0.4f * quadratureAbsolute);
}
else
{
return quadratureAbsolute + (0.4f * inphaseAbsolute);
}
}
public static void main(String[] args)
{
double angle = Math.PI; //180 degrees
Complex s1 = new Complex((float)Math.sin(angle), (float)Math.cos(angle));
Complex tap = new Complex(1.0f, -0.1f);
Complex convolved = Complex.multiply(s1, tap);
mLog.debug("s: " + s1.toString() +
" t: " + tap.toString() +
" convolved: " + convolved.toString());
}
}

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.source;
import javax.sound.sampled.UnsupportedAudioFileException;
@@ -10,22 +25,24 @@ import java.io.IOException;
*/
public interface IControllableFileSource
{
/**
* Opens the file source
*/
public void open() throws IOException, UnsupportedAudioFileException;
public void close() throws IOException;
public File getFile();
public void next( int frames ) throws IOException;
public void next( int frames, boolean broadcast ) throws IOException;
public long getFrameCount() throws IOException;
public void setListener( IFrameLocationListener listener );
public void removeListener( IFrameLocationListener listener );
/**
* Opens the file source
*/
public void open() throws IOException, UnsupportedAudioFileException;
public void close() throws IOException;
public File getFile();
public void next(int frames) throws IOException;
public void next(int frames, boolean broadcast) throws IOException;
public long getFrameCount() throws IOException;
public int getSampleRate();
public void setListener(IFrameLocationListener listener);
public void removeListener(IFrameLocationListener listener);
}