mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-15 01:57:39 -05:00
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:
@@ -17,64 +17,152 @@
|
|||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
package io.github.dsheirer.buffer;
|
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;
|
import java.util.Arrays;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Circular sample buffer - allocates a buffer and stores samples in a circular
|
* Circular sample buffer - allocates a buffer and stores samples in a circular fashion, overwriting older samples with
|
||||||
* fashion, overwriting older samples with newly arrived samples. Initially
|
* newly arrived samples. Initially fills buffer with 0-valued (default) samples or with a specified value.
|
||||||
* fills buffer with 0-valued samples
|
|
||||||
*
|
|
||||||
* Can be used as a delay-type buffer, to delay samples by the 'size' amount
|
|
||||||
*/
|
*/
|
||||||
public class DoubleCircularBuffer
|
public class DoubleCircularBuffer
|
||||||
{
|
{
|
||||||
double[] mBuffer;
|
private Variance mVariance;
|
||||||
int mBufferPointer = 0;
|
private Max mMax;
|
||||||
|
private Mean mMean;
|
||||||
|
private Min mMin;
|
||||||
|
private double[] mBuffer;
|
||||||
|
private int mBufferPointer = 0;
|
||||||
|
|
||||||
public DoubleCircularBuffer( int size )
|
/**
|
||||||
{
|
* Creates a circular double buffer of the specified size and all entries filled with the specified initial value.
|
||||||
mBuffer = new double[ size ];
|
* @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);
|
||||||
|
}
|
||||||
|
|
||||||
Arrays.fill( mBuffer, 0.0 );
|
/**
|
||||||
}
|
* 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);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Puts the new value into the buffer and returns the oldest buffer value
|
* Puts the new value into the buffer and returns the oldest buffer value that it replaced
|
||||||
* that it replaced
|
*
|
||||||
* @param newValue
|
* @param newValue to add to the buffer
|
||||||
* @return
|
* @return oldest value that was overwritten by the new value
|
||||||
*/
|
*/
|
||||||
public double get( double newValue )
|
public double get(double newValue)
|
||||||
{
|
{
|
||||||
double oldestSample = mBuffer[ mBufferPointer ];
|
double oldestSample = mBuffer[mBufferPointer];
|
||||||
|
|
||||||
mBuffer[ mBufferPointer ] = newValue;
|
mBuffer[mBufferPointer] = newValue;
|
||||||
|
|
||||||
mBufferPointer++;
|
mBufferPointer++;
|
||||||
|
|
||||||
if( mBufferPointer >= mBuffer.length )
|
if(mBufferPointer >= mBuffer.length)
|
||||||
{
|
{
|
||||||
mBufferPointer = 0;
|
mBufferPointer = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return oldestSample;
|
return oldestSample;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public double[] getAll()
|
||||||
* Returns the maximum from the values currently in the buffer
|
{
|
||||||
*/
|
int valuePointer = 0;
|
||||||
public double max()
|
double[] values = new double[mBuffer.length];
|
||||||
{
|
int bufferPointer = mBufferPointer;
|
||||||
double max = 0.0d;
|
|
||||||
|
|
||||||
for( int x = 0; x < mBuffer.length; x++ )
|
while(valuePointer < values.length)
|
||||||
{
|
{
|
||||||
if( mBuffer[ x ] > max )
|
values[valuePointer++] = mBuffer[bufferPointer++];
|
||||||
{
|
|
||||||
max = mBuffer[ x ];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return max;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,15 +39,24 @@ public class FilterViewer extends Application
|
|||||||
*/
|
*/
|
||||||
private float[] getFilter()
|
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()
|
FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
|
||||||
.sampleRate(48000)
|
.sampleRate((int)48000.0)
|
||||||
.gridDensity(16)
|
.passBandCutoff(6000)
|
||||||
.passBandCutoff(2500)
|
|
||||||
.passBandAmplitude(1.0)
|
.passBandAmplitude(1.0)
|
||||||
.passBandRipple(0.01)
|
.passBandRipple(0.1)
|
||||||
.stopBandStart(4000)
|
|
||||||
.stopBandAmplitude(0.0)
|
.stopBandAmplitude(0.0)
|
||||||
.stopBandRipple(0.008)
|
.stopBandStart(6250)
|
||||||
|
.stopBandRipple(0.01)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder()
|
// FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder()
|
||||||
|
|||||||
@@ -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;
|
package io.github.dsheirer.dsp.filter.fir.complex;
|
||||||
|
|
||||||
import io.github.dsheirer.dsp.filter.fir.FIRFilter;
|
import io.github.dsheirer.dsp.filter.fir.FIRFilter;
|
||||||
import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter;
|
import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter;
|
||||||
import io.github.dsheirer.sample.complex.Complex;
|
import io.github.dsheirer.sample.complex.Complex;
|
||||||
|
import io.github.dsheirer.sample.complex.ComplexBuffer;
|
||||||
|
|
||||||
public class ComplexFIRFilter extends FIRFilter
|
public class ComplexFIRFilter extends FIRFilter
|
||||||
{
|
{
|
||||||
private RealFIRFilter mIFilter;
|
private RealFIRFilter mIFilter;
|
||||||
private RealFIRFilter mQFilter;
|
private RealFIRFilter mQFilter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Complex FIR Filter for processing complex sample pairs. Wraps two real
|
* Complex FIR Filter for processing complex sample pairs. Wraps two real
|
||||||
* FIR filters for processing each of the inphase and quadrature samples.
|
* FIR filters for processing each of the inphase and quadrature samples.
|
||||||
*
|
*
|
||||||
* @param coefficients - filter taps
|
* @param coefficients - filter taps
|
||||||
* @param gain - gain to apply to filtered outputs - use 1.0f for no gain
|
* @param gain - gain to apply to filtered outputs - use 1.0f for no gain
|
||||||
*/
|
*/
|
||||||
public ComplexFIRFilter( float[] coefficients, float gain )
|
public ComplexFIRFilter(float[] coefficients, float gain)
|
||||||
{
|
{
|
||||||
mIFilter = new RealFIRFilter( coefficients, gain );
|
mIFilter = new RealFIRFilter(coefficients, gain);
|
||||||
mQFilter = new RealFIRFilter( coefficients, gain );
|
mQFilter = new RealFIRFilter(coefficients, gain);
|
||||||
}
|
}
|
||||||
|
|
||||||
public float[] getCoefficients()
|
public float[] getCoefficients()
|
||||||
{
|
{
|
||||||
return mIFilter.getCoefficients();
|
return mIFilter.getCoefficients();
|
||||||
}
|
}
|
||||||
|
|
||||||
public float filterInphase( float sample )
|
public float filterInphase(float sample)
|
||||||
{
|
{
|
||||||
return mIFilter.filter( sample );
|
return mIFilter.filter(sample);
|
||||||
}
|
}
|
||||||
|
|
||||||
public float filterQuadrature( float sample )
|
public float filterQuadrature(float sample)
|
||||||
{
|
{
|
||||||
return mQFilter.filter( sample );
|
return mQFilter.filter(sample);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Complex filter( Complex sample )
|
public Complex filter(Complex sample)
|
||||||
{
|
{
|
||||||
float i = filterInphase( sample.inphase() );
|
float i = filterInphase(sample.inphase());
|
||||||
float q = filterQuadrature( sample.quadrature() );
|
float q = filterQuadrature(sample.quadrature());
|
||||||
|
|
||||||
return new Complex( i, q );
|
return new Complex(i, q);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
/**
|
||||||
public void dispose()
|
* Filters the complex samples from the buffer and returns a new complex buffer with the filtered output
|
||||||
{
|
*
|
||||||
mIFilter.dispose();
|
* @param buffer with complex samples to filter
|
||||||
mQFilter.dispose();
|
* @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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ package io.github.dsheirer.dsp.filter.fir.complex;
|
|||||||
import io.github.dsheirer.sample.Listener;
|
import io.github.dsheirer.sample.Listener;
|
||||||
import io.github.dsheirer.sample.complex.ComplexBuffer;
|
import io.github.dsheirer.sample.complex.ComplexBuffer;
|
||||||
|
|
||||||
public class ComplexFIRFilter_CB_CB extends ComplexFIRFilter
|
public class ComplexFIRFilter_CB_CB extends ComplexFIRFilter implements Listener<ComplexBuffer>
|
||||||
implements Listener<ComplexBuffer>
|
|
||||||
{
|
{
|
||||||
private Listener<ComplexBuffer> mListener;
|
private Listener<ComplexBuffer> mListener;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
package io.github.dsheirer.dsp.gain;
|
||||||
|
|
||||||
import io.github.dsheirer.buffer.FloatCircularBuffer;
|
import io.github.dsheirer.buffer.FloatCircularBuffer;
|
||||||
import io.github.dsheirer.sample.complex.Complex;
|
import io.github.dsheirer.sample.complex.Complex;
|
||||||
|
import io.github.dsheirer.sample.complex.ComplexBuffer;
|
||||||
import io.github.dsheirer.sample.complex.ComplexSampleListener;
|
import io.github.dsheirer.sample.complex.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 class ComplexFeedForwardGainControl implements ComplexSampleListener
|
||||||
{
|
{
|
||||||
public static final float OBJECTIVE_ENVELOPE = 1.0f;
|
public static final float OBJECTIVE_ENVELOPE = 1.0f;
|
||||||
public static final float MINIMUM_ENVELOPE = 0.0001f;
|
public static final float MINIMUM_ENVELOPE = 0.0001f;
|
||||||
|
|
||||||
private ComplexSampleListener mListener;
|
private ComplexSampleListener mListener;
|
||||||
|
|
||||||
private FloatCircularBuffer mEnvelopeHistory;
|
private FloatCircularBuffer mEnvelopeHistory;
|
||||||
|
|
||||||
private float mMaxEnvelope = 0.0f;
|
private float mMaxEnvelope = 0.0f;
|
||||||
private float mGain = 1.0f;
|
private float mGain = 1.0f;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dynamic gain control for incoming sample stream to amplify or attenuate
|
* Dynamic gain control for incoming sample stream to amplify or attenuate
|
||||||
* all samples toward an objective unity)gain, using the maximum envelope
|
* all samples toward an objective unity)gain, using the maximum envelope
|
||||||
* value detected in the stream history window.
|
* value detected in the stream history window.
|
||||||
*
|
*
|
||||||
* Uses the specified damping factor to limit gain swings. Damping factor
|
* Uses the specified damping factor to limit gain swings. Damping factor
|
||||||
* is applied against the delta between current gain value and a recalculated
|
* 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.
|
* 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 window - history size to use in detecting maximum envelope value
|
||||||
* @param damping - a value between 0 < damping <= 1.0;
|
*/
|
||||||
*/
|
public ComplexFeedForwardGainControl(int window)
|
||||||
public ComplexFeedForwardGainControl( int window )
|
{
|
||||||
{
|
mEnvelopeHistory = new FloatCircularBuffer(window);
|
||||||
mEnvelopeHistory = new FloatCircularBuffer( window );
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public void dispose()
|
public void dispose()
|
||||||
{
|
{
|
||||||
mListener = null;
|
mListener = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void receive( float inphase, float quadrature )
|
public void receive(float inphase, float quadrature)
|
||||||
{
|
{
|
||||||
float envelope = Complex.envelope( inphase, quadrature );
|
process(inphase, quadrature);
|
||||||
|
|
||||||
if( envelope > mMaxEnvelope )
|
/* Apply current gain value to the sample and send to the listener */
|
||||||
{
|
if(mListener != null)
|
||||||
mMaxEnvelope = envelope;
|
{
|
||||||
|
mListener.receive(inphase *= mGain, quadrature *= mGain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
adjustGain();
|
private void process(float inphase, float quadrature)
|
||||||
}
|
{
|
||||||
|
float envelope = Complex.envelope(inphase, quadrature);
|
||||||
|
|
||||||
/* Replace oldest envelope value with current envelope value */
|
if(envelope > mMaxEnvelope)
|
||||||
float oldestEnvelope = mEnvelopeHistory.get( envelope );
|
{
|
||||||
|
mMaxEnvelope = envelope;
|
||||||
|
|
||||||
/* If the oldest envelope value was the max envelope value, then we
|
adjustGain();
|
||||||
* have to rediscover the max value from the envelope history */
|
}
|
||||||
if( mMaxEnvelope == oldestEnvelope && mMaxEnvelope != envelope )
|
|
||||||
{
|
|
||||||
mMaxEnvelope = MINIMUM_ENVELOPE;
|
|
||||||
|
|
||||||
for( float value: mEnvelopeHistory.getBuffer() )
|
/* Replace oldest envelope value with current envelope value */
|
||||||
{
|
float oldestEnvelope = mEnvelopeHistory.get(envelope);
|
||||||
if( value > mMaxEnvelope )
|
|
||||||
{
|
|
||||||
mMaxEnvelope = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
adjustGain();
|
/* 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;
|
||||||
|
|
||||||
/* Apply current gain value to the sample and send to the listener */
|
for(float value : mEnvelopeHistory.getBuffer())
|
||||||
if( mListener != null )
|
{
|
||||||
{
|
if(value > mMaxEnvelope)
|
||||||
mListener.receive( inphase *= mGain, quadrature *= mGain );
|
{
|
||||||
}
|
mMaxEnvelope = value;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void adjustGain()
|
adjustGain();
|
||||||
{
|
}
|
||||||
mGain = OBJECTIVE_ENVELOPE / mMaxEnvelope;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public void setListener( ComplexSampleListener listener )
|
private void adjustGain()
|
||||||
{
|
{
|
||||||
mListener = listener;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,8 +51,7 @@ public class Oscillator
|
|||||||
*/
|
*/
|
||||||
private void update()
|
private void update()
|
||||||
{
|
{
|
||||||
float anglePerSample =
|
float anglePerSample = (float)( 2.0d * Math.PI * mFrequency / mSampleRate );
|
||||||
(float)( 2.0d * Math.PI * mFrequency / mSampleRate );
|
|
||||||
|
|
||||||
mAnglePerSample = Complex.fromAngle( anglePerSample );
|
mAnglePerSample = Complex.fromAngle( anglePerSample );
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ package io.github.dsheirer.dsp.psk;
|
|||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
* SDR Trunk
|
* SDR Trunk
|
||||||
* Copyright (C) 2014, 2015 Dennis Sheirer
|
* Copyright (C) 2014 - 2018 Dennis Sheirer
|
||||||
*
|
*
|
||||||
* Gardner detector, Costas loop and Interpolator derived from:
|
* Gardner detector, Costas loop and Interpolator derived from:
|
||||||
*
|
*
|
||||||
@@ -32,6 +32,7 @@ package io.github.dsheirer.dsp.psk;
|
|||||||
|
|
||||||
import io.github.dsheirer.buffer.FloatAveragingBuffer;
|
import io.github.dsheirer.buffer.FloatAveragingBuffer;
|
||||||
import io.github.dsheirer.dsp.filter.interpolator.RealInterpolator;
|
import io.github.dsheirer.dsp.filter.interpolator.RealInterpolator;
|
||||||
|
import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop;
|
||||||
import io.github.dsheirer.dsp.symbol.Dibit;
|
import io.github.dsheirer.dsp.symbol.Dibit;
|
||||||
import io.github.dsheirer.instrument.Instrumentable;
|
import io.github.dsheirer.instrument.Instrumentable;
|
||||||
import io.github.dsheirer.instrument.tap.Tap;
|
import io.github.dsheirer.instrument.tap.Tap;
|
||||||
@@ -57,125 +58,130 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
public class LSMDemodulator implements Instrumentable, ComplexSampleListener
|
public class LSMDemodulator implements Instrumentable, ComplexSampleListener
|
||||||
{
|
{
|
||||||
private List<TapGroup> mAvailableTaps;
|
private List<TapGroup> mAvailableTaps;
|
||||||
|
|
||||||
/* 45 degree rotation angle */
|
/* 45 degree rotation angle */
|
||||||
public static final float THETA = (float)( Math.PI / 4.0d );
|
public static final float THETA = (float)(Math.PI / 4.0d);
|
||||||
|
|
||||||
/* 45 degree point */
|
/* 45 degree point */
|
||||||
public static final Complex POINT_45_DEGREES =
|
public static final Complex POINT_45_DEGREES = new Complex((float)Math.sin(THETA), (float)Math.cos(THETA));
|
||||||
new Complex( (float)Math.sin( THETA ), (float)Math.cos( THETA ) );
|
|
||||||
|
|
||||||
private Listener<Complex> mSymbolListener;
|
private Listener<Complex> mSymbolListener;
|
||||||
|
|
||||||
private GardnerSymbolTiming mGardnerDetector = new GardnerSymbolTiming();
|
private GardnerSymbolTiming mGardnerDetector = new GardnerSymbolTiming();
|
||||||
|
|
||||||
private CostasLoop mCostasLoop = new CostasLoop();
|
private CostasLoop mCostasLoop = new CostasLoop();
|
||||||
|
private QPSKSymbolDecoder mQPSKSymbolDecoder = new QPSKSymbolDecoder();
|
||||||
|
|
||||||
private IFrequencyChangeListener mFrequencyChangeListener;
|
private IFrequencyChangeListener mFrequencyChangeListener;
|
||||||
|
|
||||||
private EyeDiagramDataTap mEyeDiagramDataTap;
|
private EyeDiagramDataTap mEyeDiagramDataTap;
|
||||||
|
|
||||||
public LSMDemodulator()
|
public LSMDemodulator()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public void dispose()
|
public CostasLoop getCostasLoop()
|
||||||
{
|
{
|
||||||
mSymbolListener = null;
|
return mCostasLoop;
|
||||||
mFrequencyChangeListener = null;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public void setSymbolListener( Listener<Complex> listener )
|
public void dispose()
|
||||||
{
|
{
|
||||||
mSymbolListener = listener;
|
mSymbolListener = null;
|
||||||
}
|
mFrequencyChangeListener = null;
|
||||||
|
}
|
||||||
|
|
||||||
public void removeSymbolListener()
|
public void setSymbolListener(Listener<Complex> listener)
|
||||||
{
|
{
|
||||||
mSymbolListener = null;
|
mSymbolListener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public void removeSymbolListener()
|
||||||
public void receive( float inphase, float quadrature )
|
{
|
||||||
{
|
mSymbolListener = null;
|
||||||
mGardnerDetector.receive( inphase, quadrature );
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
@Override
|
||||||
* Applies a phase correction value to the costas loop to correct when a
|
public void receive(float inphase, float quadrature)
|
||||||
* phase lock error is detected in the binary output stream.
|
{
|
||||||
*
|
mGardnerDetector.receive(inphase, quadrature);
|
||||||
* @param correction - value in radians
|
}
|
||||||
*/
|
|
||||||
public void correctPhaseError( double correction )
|
|
||||||
{
|
|
||||||
mCostasLoop.correctPhaseError( correction );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constrains value to the range of ( -maximum <> maximum )
|
* Applies a phase correction value to the costas loop to correct when a
|
||||||
*/
|
* phase lock error is detected in the binary output stream.
|
||||||
public static float clip( float value, float maximum )
|
*
|
||||||
{
|
* @param correction - value in radians
|
||||||
if( value > maximum )
|
*/
|
||||||
{
|
public void correctPhaseError(double correction)
|
||||||
return maximum;
|
{
|
||||||
}
|
mCostasLoop.correctPhaseError(correction);
|
||||||
else if( value < -maximum )
|
}
|
||||||
{
|
|
||||||
return -maximum;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
/**
|
||||||
}
|
* Constrains value to the range of ( -maximum <> maximum )
|
||||||
|
*/
|
||||||
|
public static float clip(float value, float maximum)
|
||||||
|
{
|
||||||
|
if(value > maximum)
|
||||||
|
{
|
||||||
|
return maximum;
|
||||||
|
}
|
||||||
|
else if(value < -maximum)
|
||||||
|
{
|
||||||
|
return -maximum;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
return value;
|
||||||
* Constrains timing error to +/- the maximum value and corrects any
|
}
|
||||||
* floating point invalid numbers
|
|
||||||
*/
|
|
||||||
private float normalize( float error, float maximum )
|
|
||||||
{
|
|
||||||
if( Float.isNaN( error ) )
|
|
||||||
{
|
|
||||||
return 0.0f;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return clip( error, maximum );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addListener( IFrequencyChangeListener listener )
|
/**
|
||||||
{
|
* Constrains timing error to +/- the maximum value and corrects any
|
||||||
mFrequencyChangeListener = listener;
|
* floating point invalid numbers
|
||||||
}
|
*/
|
||||||
|
private float normalize(float error, float maximum)
|
||||||
|
{
|
||||||
|
if(Float.isNaN(error))
|
||||||
|
{
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return clip(error, maximum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void removeListener( IFrequencyChangeListener listener )
|
public void addListener(IFrequencyChangeListener listener)
|
||||||
{
|
{
|
||||||
mFrequencyChangeListener = null;
|
mFrequencyChangeListener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public void removeListener(IFrequencyChangeListener listener)
|
||||||
* Processes the current loop frequency of the costas loop and broadcasts
|
{
|
||||||
* tuner frequency offset adjustments 10 times a second as needed to keep
|
mFrequencyChangeListener = null;
|
||||||
* the frequency offset below half of the maximum frequency
|
}
|
||||||
*/
|
|
||||||
public class FrequencyControl
|
|
||||||
{
|
|
||||||
/* Set the trigger threshold at half of the maximum costas loop control
|
|
||||||
* frequency */
|
|
||||||
private static final float CORRECTION_THRESHOLD_FREQUENCY =
|
|
||||||
( 2.0f * (float)Math.PI * 600.0f ) / 48000.0f;
|
|
||||||
|
|
||||||
private FloatAveragingBuffer mBuffer = new FloatAveragingBuffer( 40 );
|
/**
|
||||||
|
* Processes the current loop frequency of the costas loop and broadcasts
|
||||||
|
* tuner frequency offset adjustments 10 times a second as needed to keep
|
||||||
|
* the frequency offset below half of the maximum frequency
|
||||||
|
*/
|
||||||
|
public class FrequencyControl
|
||||||
|
{
|
||||||
|
/* Set the trigger threshold at half of the maximum costas loop control
|
||||||
|
* frequency */
|
||||||
|
private static final float CORRECTION_THRESHOLD_FREQUENCY =
|
||||||
|
(2.0f * (float)Math.PI * 600.0f) / 48000.0f;
|
||||||
|
|
||||||
private long mFrequencyError = 0;
|
private FloatAveragingBuffer mBuffer = new FloatAveragingBuffer(40);
|
||||||
|
|
||||||
private int mCounter = 0;
|
private long mFrequencyError = 0;
|
||||||
|
|
||||||
public void receive( float frequency )
|
private int mCounter = 0;
|
||||||
{
|
|
||||||
|
public void receive(float frequency)
|
||||||
|
{
|
||||||
// mCounter++;
|
// mCounter++;
|
||||||
//
|
//
|
||||||
// if( mCounter >= 480 )
|
// if( mCounter >= 480 )
|
||||||
@@ -212,377 +218,395 @@ public class LSMDemodulator implements Instrumentable, ComplexSampleListener
|
|||||||
// {
|
// {
|
||||||
// mBuffer.get( frequency );
|
// mBuffer.get( frequency );
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gardner symbol timing detector
|
* Gardner symbol timing detector
|
||||||
*/
|
*/
|
||||||
public class GardnerSymbolTiming implements ComplexSampleListener
|
public class GardnerSymbolTiming implements ComplexSampleListener
|
||||||
{
|
{
|
||||||
public static final int HALF_SAMPLES_PER_SYMBOL = 5;
|
public static final int HALF_SAMPLES_PER_SYMBOL = 5;
|
||||||
public static final int SAMPLES_PER_SYMBOL = 10;
|
public static final int SAMPLES_PER_SYMBOL = 10;
|
||||||
public static final int TWICE_SAMPLES_PER_SYMBOL = 20;
|
public static final int TWICE_SAMPLES_PER_SYMBOL = 20;
|
||||||
|
|
||||||
private float[] mDelayLineInphase = new float[ 2 * TWICE_SAMPLES_PER_SYMBOL ];
|
private float[] mDelayLineInphase = new float[2 * TWICE_SAMPLES_PER_SYMBOL];
|
||||||
private float[] mDelayLineQuadrature = new float[ 2 * TWICE_SAMPLES_PER_SYMBOL ];
|
private float[] mDelayLineQuadrature = new float[2 * TWICE_SAMPLES_PER_SYMBOL];
|
||||||
|
|
||||||
private int mDelayLinePointer = 0;
|
private int mDelayLinePointer = 0;
|
||||||
|
|
||||||
/* Sampling point */
|
/* Sampling point */
|
||||||
private float mMu = 10.0f;
|
private float mMu = 10.0f;
|
||||||
private float mGainMu = 0.05f;
|
private float mGainMu = 0.05f;
|
||||||
|
|
||||||
/* Samples per symbol */
|
/* Samples per symbol */
|
||||||
private float mOmega = 10.0f;
|
private float mOmega = 10.0f;
|
||||||
private float mGainOmega = 0.1f * mGainMu * mGainMu;
|
private float mGainOmega = 0.1f * mGainMu * mGainMu;
|
||||||
private float mOmegaRel = 0.005f;
|
private float mOmegaRel = 0.005f;
|
||||||
private float mOmegaMid = 10.0f;
|
private float mOmegaMid = 10.0f;
|
||||||
|
|
||||||
private RealInterpolator mInterpolator = new RealInterpolator( 1.0f );
|
private RealInterpolator mInterpolator = new RealInterpolator(1.0f);
|
||||||
|
|
||||||
private Complex mPreviousSample = new Complex( 0, 0 );
|
private Complex mPreviousSample = new Complex(0, 0);
|
||||||
private Complex mPreviousMiddleSample = new Complex( 0, 0 );
|
private Complex mPreviousMiddleSample = new Complex(0, 0);
|
||||||
private Complex mPreviousSymbol = new Complex( 0, 0 );
|
private Complex mPreviousSymbol = new Complex(0, 0);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides symbol sampling timing control
|
* Provides symbol sampling timing control
|
||||||
*/
|
*/
|
||||||
public GardnerSymbolTiming()
|
public GardnerSymbolTiming()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void receive( float inphase, float quadrature )
|
public void receive(float inphase, float quadrature)
|
||||||
{
|
{
|
||||||
/* Count down samples per symbol until we calculate the symbol */
|
/* Count down samples per symbol until we calculate the symbol */
|
||||||
mMu--;
|
mMu--;
|
||||||
|
|
||||||
mCostasLoop.increment();
|
mCostasLoop.increment();
|
||||||
|
|
||||||
/* Mix incoming sample with costas loop to remove any rotation
|
/* Mix incoming sample with costas loop to remove any rotation
|
||||||
* that is present from a mis-tuned carrier frequency */
|
* that is present from a mis-tuned carrier frequency */
|
||||||
|
|
||||||
Complex costas = mCostasLoop.getCurrentVector();
|
Complex costas = mCostasLoop.getCurrentVector();
|
||||||
|
|
||||||
float derotatedInphase = Complex.multiplyInphase( inphase, quadrature, costas.inphase(), costas.quadrature() );
|
float derotatedInphase = Complex.multiplyInphase(inphase, quadrature, costas.inphase(), costas.quadrature());
|
||||||
float derotatedQuadrature = Complex.multiplyQuadrature( inphase, quadrature, costas.inphase(), costas.quadrature() );
|
float derotatedQuadrature = Complex.multiplyQuadrature(inphase, quadrature, costas.inphase(), costas.quadrature());
|
||||||
|
|
||||||
/* Fill up the delay line to use with the interpolator */
|
/* Fill up the delay line to use with the interpolator */
|
||||||
mDelayLineInphase[ mDelayLinePointer ] = derotatedInphase;
|
mDelayLineInphase[mDelayLinePointer] = derotatedInphase;
|
||||||
mDelayLineInphase[ mDelayLinePointer + TWICE_SAMPLES_PER_SYMBOL ] = derotatedInphase;
|
mDelayLineInphase[mDelayLinePointer + TWICE_SAMPLES_PER_SYMBOL] = derotatedInphase;
|
||||||
|
|
||||||
mDelayLineQuadrature[ mDelayLinePointer ] = derotatedQuadrature;
|
mDelayLineQuadrature[mDelayLinePointer] = derotatedQuadrature;
|
||||||
mDelayLineQuadrature[ mDelayLinePointer + TWICE_SAMPLES_PER_SYMBOL ] = derotatedQuadrature;
|
mDelayLineQuadrature[mDelayLinePointer + TWICE_SAMPLES_PER_SYMBOL] = derotatedQuadrature;
|
||||||
|
|
||||||
/* Increment pointer and keep pointer in bounds */
|
/* Increment pointer and keep pointer in bounds */
|
||||||
mDelayLinePointer = ( mDelayLinePointer + 1 ) % TWICE_SAMPLES_PER_SYMBOL;
|
mDelayLinePointer = (mDelayLinePointer + 1) % TWICE_SAMPLES_PER_SYMBOL;
|
||||||
|
|
||||||
/* Calculate the symbol once we've stored enough samples */
|
/* Calculate the symbol once we've stored enough samples */
|
||||||
if( mMu <= 1.0f )
|
if(mMu <= 1.0f)
|
||||||
{
|
{
|
||||||
float half_omega = mOmega / 2.0f;
|
float half_omega = mOmega / 2.0f;
|
||||||
int half_sps = (int)Math.floor( half_omega );
|
int half_sps = (int)Math.floor(half_omega);
|
||||||
float half_mu = mMu + half_omega - (float)half_sps;
|
float half_mu = mMu + half_omega - (float)half_sps;
|
||||||
|
|
||||||
if( half_mu > 1.0 )
|
if(half_mu > 1.0)
|
||||||
{
|
{
|
||||||
half_mu -= 1.0;
|
half_mu -= 1.0;
|
||||||
half_sps += 1;
|
half_sps += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Calculate interpolated middle sample and current sample */
|
/* Calculate interpolated middle sample and current sample */
|
||||||
float middleSampleInphase = mInterpolator.filter(
|
float middleSampleInphase = mInterpolator.filter(
|
||||||
mDelayLineInphase, mDelayLinePointer, mMu );
|
mDelayLineInphase, mDelayLinePointer, mMu);
|
||||||
float middleSampleQuadrature = mInterpolator.filter(
|
float middleSampleQuadrature = mInterpolator.filter(
|
||||||
mDelayLineQuadrature, mDelayLinePointer, mMu );
|
mDelayLineQuadrature, mDelayLinePointer, mMu);
|
||||||
|
|
||||||
int index = mDelayLinePointer + half_sps;
|
int index = mDelayLinePointer + half_sps;
|
||||||
|
|
||||||
float currentSampleInphase = mInterpolator.filter(
|
float currentSampleInphase = mInterpolator.filter(
|
||||||
mDelayLineInphase, index, half_mu );
|
mDelayLineInphase, index, half_mu);
|
||||||
float currentSampleQuadrature = mInterpolator.filter(
|
float currentSampleQuadrature = mInterpolator.filter(
|
||||||
mDelayLineQuadrature, index, half_mu );
|
mDelayLineQuadrature, index, half_mu);
|
||||||
|
|
||||||
/* Multiply current sample and conjugate (negated quadrature) of
|
/* Multiply current sample and conjugate (negated quadrature) of
|
||||||
* previous sample to get symbols to use for gardner error feedback */
|
* previous sample to get symbols to use for gardner error feedback */
|
||||||
Complex middleSymbol = Complex.multiply( middleSampleInphase,
|
Complex middleSymbol = Complex.multiply(middleSampleInphase,
|
||||||
middleSampleQuadrature, mPreviousMiddleSample.conjugate() );
|
middleSampleQuadrature, mPreviousMiddleSample.conjugate());
|
||||||
Complex currentSymbol = Complex.multiply( currentSampleInphase,
|
Complex currentSymbol = Complex.multiply(currentSampleInphase,
|
||||||
currentSampleQuadrature, mPreviousSample.conjugate() );
|
currentSampleQuadrature, mPreviousSample.conjugate());
|
||||||
|
|
||||||
/* Set gain to unity */
|
/* Set gain to unity */
|
||||||
middleSymbol.normalize();
|
middleSymbol.normalize();
|
||||||
currentSymbol.normalize();
|
currentSymbol.normalize();
|
||||||
|
|
||||||
/* Gardner timing error calculations */
|
/* Gardner timing error calculations */
|
||||||
float errorInphase = ( mPreviousSymbol.inphase() -
|
float errorInphase = (mPreviousSymbol.inphase() -
|
||||||
currentSymbol.inphase() ) * middleSymbol.inphase();
|
currentSymbol.inphase()) * middleSymbol.inphase();
|
||||||
|
|
||||||
float errorQuadrature = ( mPreviousSymbol.quadrature() -
|
float errorQuadrature = (mPreviousSymbol.quadrature() -
|
||||||
currentSymbol.quadrature() ) * middleSymbol.quadrature();
|
currentSymbol.quadrature()) * middleSymbol.quadrature();
|
||||||
|
|
||||||
float gardnerError = normalize( errorInphase + errorQuadrature, 1.0f );
|
float gardnerError = normalize(errorInphase + errorQuadrature, 1.0f);
|
||||||
|
|
||||||
if( mEyeDiagramDataTap != null )
|
if(mEyeDiagramDataTap != null)
|
||||||
{
|
{
|
||||||
mEyeDiagramDataTap.receive(
|
mEyeDiagramDataTap.receive(
|
||||||
new EyeDiagramData(
|
new EyeDiagramData(
|
||||||
Arrays.copyOfRange( mDelayLineInphase, 0, 20 ),
|
Arrays.copyOfRange(mDelayLineInphase, 0, 20),
|
||||||
Arrays.copyOfRange( mDelayLineQuadrature, 0, 20 ),
|
Arrays.copyOfRange(mDelayLineQuadrature, 0, 20),
|
||||||
mMu, (float)half_sps + half_mu, gardnerError ) );
|
mMu, (float)half_sps + half_mu, gardnerError));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* mOmega is samples per symbol and is constrained to floating
|
/* mOmega is samples per symbol and is constrained to floating
|
||||||
* between +/- .005 of the nominal 10.0 samples per symbol */
|
* between +/- .005 of the nominal 10.0 samples per symbol */
|
||||||
mOmega = mOmega + mGainOmega * gardnerError;
|
mOmega = mOmega + mGainOmega * gardnerError;
|
||||||
mOmega = mOmegaMid + clip( mOmega - mOmegaMid, mOmegaRel );
|
mOmega = mOmegaMid + clip(mOmega - mOmegaMid, mOmegaRel);
|
||||||
|
|
||||||
/* Adjust sample timing based on error of current sample */
|
/* Adjust sample timing based on error of current sample */
|
||||||
mMu += mOmega + ( mGainMu * gardnerError );
|
mMu += mOmega + (mGainMu * gardnerError);
|
||||||
|
|
||||||
/* Store current samples/symbols to use for the next period */
|
/* Store current samples/symbols to use for the next period */
|
||||||
mPreviousSample.setValues( currentSampleInphase, currentSampleQuadrature );
|
mPreviousSample.setValues(currentSampleInphase, currentSampleQuadrature);
|
||||||
mPreviousMiddleSample.setValues( middleSampleInphase, middleSampleQuadrature );
|
mPreviousMiddleSample.setValues(middleSampleInphase, middleSampleQuadrature);
|
||||||
mPreviousSymbol = currentSymbol;
|
mPreviousSymbol = currentSymbol;
|
||||||
|
|
||||||
/* Update costas loop using phase error present in current
|
/* Update costas loop using phase error present in current
|
||||||
* symbol. The symbol is rotated from star orientation to polar
|
* symbol. The symbol is rotated from star orientation to polar
|
||||||
* orientation to simplify error calculation */
|
* orientation to simplify error calculation */
|
||||||
mCostasLoop.receive( currentSymbol );
|
mCostasLoop.receive(currentSymbol);
|
||||||
// mCostasLoop.receive(
|
// mCostasLoop.receive(
|
||||||
// ComplexSample.multiply( currentSymbol, POINT_45_DEGREES ) );
|
// ComplexSample.multiply( currentSymbol, POINT_45_DEGREES ) );
|
||||||
|
|
||||||
/* Dispatch the differentiated symbol to the registered listener */
|
/* Dispatch the differentiated symbol to the registered listener */
|
||||||
if( mSymbolListener != null )
|
if(mSymbolListener != null)
|
||||||
{
|
{
|
||||||
mSymbolListener.receive( currentSymbol );
|
mSymbolListener.receive(currentSymbol);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Costas Loop - phase locked loop synchronized to the frequency offset of
|
* Costas Loop - phase locked loop synchronized to the frequency offset of
|
||||||
* the incoming signal to enable the signal to be mixed down to zero offset
|
* the incoming signal to enable the signal to be mixed down to zero offset
|
||||||
* for proper synchronization. The mFrequency value indicates the detected
|
* for proper synchronization. The mFrequency value indicates the detected
|
||||||
* frequency offset. We attempt to keep that value close to zero by issuing
|
* frequency offset. We attempt to keep that value close to zero by issuing
|
||||||
* frequency adjustments to the tuner channel source.
|
* frequency adjustments to the tuner channel source.
|
||||||
*
|
*
|
||||||
* Most of the costas loop code was ported from gnuradio/control_loop and
|
* Most of the costas loop code was ported from gnuradio/control_loop and
|
||||||
* gnuradio/mpsk_receiver_cc using initialization values from KA1RBI's
|
* gnuradio/mpsk_receiver_cc using initialization values from KA1RBI's
|
||||||
* OP25/cqpsk.py
|
* OP25/cqpsk.py
|
||||||
*/
|
*/
|
||||||
public class CostasLoop implements Listener<Complex>
|
public class CostasLoop implements Listener<Complex>, IPhaseLockedLoop
|
||||||
{
|
{
|
||||||
public static final double TWO_PI = 2.0 * Math.PI;
|
public static final double TWO_PI = 2.0 * Math.PI;
|
||||||
|
|
||||||
private static final float MAXIMUM_FREQUENCY =
|
private static final float MAXIMUM_FREQUENCY =
|
||||||
( 2400.0f * (float)TWO_PI ) / 48000.0f;
|
(2400.0f * (float)TWO_PI) / 48000.0f;
|
||||||
|
|
||||||
/* http://www.trondeau.com/blog/2011/8/13/control-loop-gain-values.html */
|
/* http://www.trondeau.com/blog/2011/8/13/control-loop-gain-values.html */
|
||||||
private float mDamping = (float)Math.sqrt( 2.0 ) / 2.0f;
|
private float mDamping = (float)Math.sqrt(2.0) / 2.0f;
|
||||||
|
|
||||||
/* Use denominator between 100 (most) and 400 (least) to adjust costas
|
/* Use denominator between 100 (most) and 400 (least) to adjust costas
|
||||||
* loop control level */
|
* loop control level */
|
||||||
private float mLoopBandwidth = (float)( TWO_PI / 400.0d );
|
private float mLoopBandwidth = (float)(TWO_PI / 400.0d);
|
||||||
|
|
||||||
private float mAlphaGain = ( 4.0f * mDamping * mLoopBandwidth ) /
|
private float mAlphaGain = (4.0f * mDamping * mLoopBandwidth) /
|
||||||
( 1.0f + ( 2.0f * mDamping * mLoopBandwidth ) +
|
(1.0f + (2.0f * mDamping * mLoopBandwidth) +
|
||||||
( mLoopBandwidth * mLoopBandwidth ) );
|
(mLoopBandwidth * mLoopBandwidth));
|
||||||
|
|
||||||
private float mBetaGain = ( 4.0f * mLoopBandwidth * mLoopBandwidth ) /
|
private float mBetaGain = (4.0f * mLoopBandwidth * mLoopBandwidth) /
|
||||||
( 1.0f + ( 2.0f * mDamping * mLoopBandwidth ) +
|
(1.0f + (2.0f * mDamping * mLoopBandwidth) +
|
||||||
( mLoopBandwidth * mLoopBandwidth ) );
|
(mLoopBandwidth * mLoopBandwidth));
|
||||||
|
|
||||||
private float mLoopPhase = 0.0f;
|
private float mLoopPhase = 0.0f;
|
||||||
|
|
||||||
private float mLoopFrequency = 0.0f;
|
private float mLoopFrequency = 0.0f;
|
||||||
|
|
||||||
private FrequencyControl mFrequencyControl = new FrequencyControl();
|
private FrequencyControl mFrequencyControl = new FrequencyControl();
|
||||||
|
|
||||||
public CostasLoop()
|
public CostasLoop()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies a phase correction value to the current phasor. Use this
|
* Applies a phase correction value to the current phasor. Use this
|
||||||
* method to apply correction when a +/- 90, or 180 degree phase error
|
* method to apply correction when a +/- 90, or 180 degree phase error
|
||||||
* is detected in the binary output stream.
|
* is detected in the binary output stream.
|
||||||
*
|
*
|
||||||
* If the supplied correction value places the loop frequency outside
|
* If the supplied correction value places the loop frequency outside
|
||||||
* of the max frequency, then the frequency will be corrected 360
|
* of the max frequency, then the frequency will be corrected 360
|
||||||
* degrees in the opposite direction to maintain within the max
|
* degrees in the opposite direction to maintain within the max
|
||||||
* frequency bounds.
|
* frequency bounds.
|
||||||
*
|
*
|
||||||
* @param correction - correction value in radians
|
* @param correction - correction value in radians
|
||||||
*/
|
*/
|
||||||
public void correctPhaseError( double correction )
|
public void correctPhaseError(double correction)
|
||||||
{
|
{
|
||||||
mLoopFrequency += correction;
|
mLoopFrequency += correction;
|
||||||
|
|
||||||
if( mLoopFrequency > MAXIMUM_FREQUENCY )
|
if(mLoopFrequency > MAXIMUM_FREQUENCY)
|
||||||
{
|
{
|
||||||
mLoopFrequency -= 2.0d * MAXIMUM_FREQUENCY;
|
mLoopFrequency -= 2.0d * MAXIMUM_FREQUENCY;
|
||||||
}
|
}
|
||||||
|
|
||||||
if( mLoopFrequency < -MAXIMUM_FREQUENCY )
|
if(mLoopFrequency < -MAXIMUM_FREQUENCY)
|
||||||
{
|
{
|
||||||
mLoopFrequency += 2.0d * MAXIMUM_FREQUENCY;
|
mLoopFrequency += 2.0d * MAXIMUM_FREQUENCY;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Increments the phase of the loop
|
* Increments the phase of the loop
|
||||||
*/
|
*/
|
||||||
public void increment()
|
public void increment()
|
||||||
{
|
{
|
||||||
mLoopPhase += mLoopFrequency;
|
mLoopPhase += mLoopFrequency;
|
||||||
|
|
||||||
/* Keep the loop phase in bounds */
|
/* Keep the loop phase in bounds */
|
||||||
unwrapPhase();
|
unwrapPhase();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void unwrapPhase()
|
@Override
|
||||||
{
|
public void adjust(double error)
|
||||||
while( mLoopPhase > TWO_PI )
|
{
|
||||||
{
|
adjust(error);
|
||||||
mLoopPhase -= TWO_PI;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
while( mLoopPhase < -TWO_PI )
|
@Override
|
||||||
{
|
public void correctInversion(double correction)
|
||||||
mLoopPhase += TWO_PI;
|
{
|
||||||
}
|
correctPhaseError(correction);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private void unwrapPhase()
|
||||||
* Current vector of the loop rotated by THETA degrees.
|
{
|
||||||
*/
|
while(mLoopPhase > TWO_PI)
|
||||||
public Complex getCurrentVector()
|
{
|
||||||
{
|
mLoopPhase -= TWO_PI;
|
||||||
|
}
|
||||||
|
|
||||||
|
while(mLoopPhase < -TWO_PI)
|
||||||
|
{
|
||||||
|
mLoopPhase += TWO_PI;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current vector of the loop rotated by THETA degrees.
|
||||||
|
*/
|
||||||
|
public Complex getCurrentVector()
|
||||||
|
{
|
||||||
// return ComplexSample.fromAngle( mLoopPhase + THETA );
|
// return ComplexSample.fromAngle( mLoopPhase + THETA );
|
||||||
return Complex.fromAngle( mLoopPhase );
|
return Complex.fromAngle(mLoopPhase);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void receive( Complex complex )
|
public Complex incrementAndGetCurrentVector()
|
||||||
{
|
{
|
||||||
/* Calculate phase error */
|
return null;
|
||||||
float phaseError = getPhaseError( complex );
|
}
|
||||||
|
|
||||||
Dibit d = QPSKPolarSlicer.decide( complex );
|
@Override
|
||||||
|
public void receive(Complex complex)
|
||||||
|
{
|
||||||
|
/* Calculate phase error */
|
||||||
|
float phaseError = getPhaseError(complex);
|
||||||
|
|
||||||
|
Dibit d = mQPSKSymbolDecoder.decode(complex);
|
||||||
// mLog.debug( "Dibit: " + d.name() + " Error: " + phaseError );
|
// mLog.debug( "Dibit: " + d.name() + " Error: " + phaseError );
|
||||||
|
|
||||||
/* Adjust for phase error */
|
/* Adjust for phase error */
|
||||||
adjust( phaseError );
|
adjust(phaseError);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the costas loop frequency and phase to adjust for the phase
|
* Updates the costas loop frequency and phase to adjust for the phase
|
||||||
* error value
|
* error value
|
||||||
*
|
*
|
||||||
* @param phase_error - (-)= late and (+)= early
|
* @param phase_error - (-)= late and (+)= early
|
||||||
*/
|
*/
|
||||||
private void adjust( float phase_error )
|
private void adjust(float phase_error)
|
||||||
{
|
{
|
||||||
mLoopFrequency += mBetaGain * phase_error;
|
mLoopFrequency += mBetaGain * phase_error;
|
||||||
mLoopPhase += mLoopFrequency + mAlphaGain * phase_error;
|
mLoopPhase += mLoopFrequency + mAlphaGain * phase_error;
|
||||||
|
|
||||||
/* Maintain phase between +/- 2 * PI */
|
/* Maintain phase between +/- 2 * PI */
|
||||||
unwrapPhase();
|
unwrapPhase();
|
||||||
|
|
||||||
/* Limit frequency to +/- maximum loop frequency */
|
/* Limit frequency to +/- maximum loop frequency */
|
||||||
limitFrequency();
|
limitFrequency();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constrains the frequency within the bounds of +/- loop frequency
|
* Constrains the frequency within the bounds of +/- loop frequency
|
||||||
*/
|
*/
|
||||||
private void limitFrequency()
|
private void limitFrequency()
|
||||||
{
|
{
|
||||||
/* Check for and issue tuner offset correction */
|
/* Check for and issue tuner offset correction */
|
||||||
mFrequencyControl.receive( mLoopFrequency );
|
mFrequencyControl.receive(mLoopFrequency);
|
||||||
|
|
||||||
if( mLoopFrequency > MAXIMUM_FREQUENCY )
|
if(mLoopFrequency > MAXIMUM_FREQUENCY)
|
||||||
{
|
{
|
||||||
mLoopFrequency = MAXIMUM_FREQUENCY;
|
mLoopFrequency = MAXIMUM_FREQUENCY;
|
||||||
}
|
}
|
||||||
|
|
||||||
if( mLoopFrequency < -MAXIMUM_FREQUENCY )
|
if(mLoopFrequency < -MAXIMUM_FREQUENCY)
|
||||||
{
|
{
|
||||||
mLoopFrequency = -MAXIMUM_FREQUENCY;
|
mLoopFrequency = -MAXIMUM_FREQUENCY;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detects rotational error present when the costas loop is not aligned
|
* Detects rotational error present when the costas loop is not aligned
|
||||||
* with the carrier frequency. Provides error feedback to adjust
|
* with the carrier frequency. Provides error feedback to adjust
|
||||||
* mixer frequency.
|
* mixer frequency.
|
||||||
*/
|
*/
|
||||||
public float getPhaseError( Complex complex )
|
public float getPhaseError(Complex complex)
|
||||||
{
|
{
|
||||||
float phase_error = 0;
|
float phase_error = 0;
|
||||||
|
|
||||||
if( Math.abs( complex.inphase() ) > Math.abs( complex.quadrature() ) )
|
if(Math.abs(complex.inphase()) > Math.abs(complex.quadrature()))
|
||||||
{
|
{
|
||||||
if( complex.inphase() > 0 )
|
if(complex.inphase() > 0)
|
||||||
{
|
{
|
||||||
phase_error = -complex.quadrature();
|
phase_error = -complex.quadrature();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
phase_error = complex.quadrature();
|
phase_error = complex.quadrature();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if( complex.quadrature() > 0 )
|
if(complex.quadrature() > 0)
|
||||||
{
|
{
|
||||||
phase_error = complex.inphase();
|
phase_error = complex.inphase();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
phase_error = -complex.inphase();
|
phase_error = -complex.inphase();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return phase_error;
|
return phase_error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<TapGroup> getTapGroups()
|
public List<TapGroup> getTapGroups()
|
||||||
{
|
{
|
||||||
if( mAvailableTaps == null )
|
if(mAvailableTaps == null)
|
||||||
{
|
{
|
||||||
mAvailableTaps = new ArrayList<>();
|
mAvailableTaps = new ArrayList<>();
|
||||||
|
|
||||||
TapGroup group = new TapGroup( "LSM Demodulator" );
|
TapGroup group = new TapGroup("LSM Demodulator");
|
||||||
|
|
||||||
group.add( new EyeDiagramDataTap( "Eye Diagram", 0, 4800 ) );
|
group.add(new EyeDiagramDataTap("Eye Diagram", 0, 4800));
|
||||||
|
|
||||||
mAvailableTaps.add( group );
|
mAvailableTaps.add(group);
|
||||||
}
|
}
|
||||||
|
|
||||||
return mAvailableTaps;
|
return mAvailableTaps;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void registerTap( Tap tap )
|
public void registerTap(Tap tap)
|
||||||
{
|
{
|
||||||
if( tap instanceof EyeDiagramDataTap )
|
if(tap instanceof EyeDiagramDataTap)
|
||||||
{
|
{
|
||||||
mEyeDiagramDataTap = (EyeDiagramDataTap)tap;
|
mEyeDiagramDataTap = (EyeDiagramDataTap)tap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void unregisterTap( Tap tap )
|
public void unregisterTap(Tap tap)
|
||||||
{
|
{
|
||||||
if( tap instanceof EyeDiagramDataTap )
|
if(tap instanceof EyeDiagramDataTap)
|
||||||
{
|
{
|
||||||
mEyeDiagramDataTap = null;
|
mEyeDiagramDataTap = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
182
src/main/java/io/github/dsheirer/dsp/psk/QPSKDemodulator.java
Normal file
182
src/main/java/io/github/dsheirer/dsp/psk/QPSKDemodulator.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
package io.github.dsheirer.dsp.psk;
|
||||||
|
|
||||||
import io.github.dsheirer.dsp.symbol.Dibit;
|
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.Listener;
|
||||||
import io.github.dsheirer.sample.complex.Complex;
|
import io.github.dsheirer.sample.complex.Complex;
|
||||||
|
|
||||||
/*******************************************************************************
|
public class QPSKSymbolDecoder implements Listener<Complex>, IQPSKSymbolDecoder
|
||||||
* 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>
|
|
||||||
{
|
{
|
||||||
private Broadcaster<Dibit> mBroadcaster = new Broadcaster<Dibit>();
|
private Broadcaster<Dibit> mBroadcaster = new Broadcaster<Dibit>();
|
||||||
|
|
||||||
@@ -37,7 +35,7 @@ public class QPSKPolarSlicer implements Listener<Complex>
|
|||||||
* / \
|
* / \
|
||||||
* / 11 \
|
* / 11 \
|
||||||
*/
|
*/
|
||||||
public QPSKPolarSlicer()
|
public QPSKSymbolDecoder()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,10 +58,10 @@ public class QPSKPolarSlicer implements Listener<Complex>
|
|||||||
@Override
|
@Override
|
||||||
public void receive( Complex complex )
|
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() ) )
|
if( Math.abs( complex.inphase() ) > Math.abs( complex.quadrature() ) )
|
||||||
{
|
{
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
337
src/main/java/io/github/dsheirer/dsp/psk/pll/CostasLoop.java
Normal file
337
src/main/java/io/github/dsheirer/dsp/psk/pll/CostasLoop.java
Normal 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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
48
src/main/java/io/github/dsheirer/dsp/psk/pll/Tracking.java
Normal file
48
src/main/java/io/github/dsheirer/dsp/psk/pll/Tracking.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,8 @@
|
|||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
package io.github.dsheirer.instrument.gui.viewer;
|
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.Application;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.event.ActionEvent;
|
import javafx.event.ActionEvent;
|
||||||
@@ -44,7 +46,7 @@ public class DemodulatorViewerFX extends Application
|
|||||||
public void start(Stage primaryStage) throws Exception
|
public void start(Stage primaryStage) throws Exception
|
||||||
{
|
{
|
||||||
mStage = primaryStage;
|
mStage = primaryStage;
|
||||||
mStage.setTitle("Demodulator Viewer FX");
|
setTitle(null);
|
||||||
|
|
||||||
BorderPane borderPane = new BorderPane();
|
BorderPane borderPane = new BorderPane();
|
||||||
borderPane.setTop(getMenuBar());
|
borderPane.setTop(getMenuBar());
|
||||||
@@ -56,6 +58,18 @@ public class DemodulatorViewerFX extends Application
|
|||||||
mStage.show();
|
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()
|
private ViewerDesktop getViewerDesktop()
|
||||||
{
|
{
|
||||||
if(mViewerDesktop == null)
|
if(mViewerDesktop == null)
|
||||||
@@ -72,9 +86,9 @@ public class DemodulatorViewerFX extends Application
|
|||||||
{
|
{
|
||||||
mMenuBar = new MenuBar();
|
mMenuBar = new MenuBar();
|
||||||
|
|
||||||
Menu menuFile = new Menu("File");
|
Menu fileMenu = new Menu("File");
|
||||||
MenuItem menuItemOpen = new MenuItem("Open ...");
|
MenuItem openMenuItem = new MenuItem("Open ...");
|
||||||
menuItemOpen.setOnAction(new EventHandler<ActionEvent>()
|
openMenuItem.setOnAction(new EventHandler<ActionEvent>()
|
||||||
{
|
{
|
||||||
@Override
|
@Override
|
||||||
public void handle(ActionEvent event)
|
public void handle(ActionEvent event)
|
||||||
@@ -87,8 +101,8 @@ public class DemodulatorViewerFX extends Application
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
MenuItem menuItemClose = new MenuItem("Close");
|
MenuItem closeMenuItem = new MenuItem("Close");
|
||||||
menuItemClose.setOnAction(new EventHandler<ActionEvent>()
|
closeMenuItem.setOnAction(new EventHandler<ActionEvent>()
|
||||||
{
|
{
|
||||||
@Override
|
@Override
|
||||||
public void handle(ActionEvent event)
|
public void handle(ActionEvent event)
|
||||||
@@ -97,8 +111,8 @@ public class DemodulatorViewerFX extends Application
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
MenuItem menuItemExit = new MenuItem("Exit");
|
MenuItem exitMenuItem = new MenuItem("Exit");
|
||||||
menuItemExit.setOnAction(new EventHandler<ActionEvent>()
|
exitMenuItem.setOnAction(new EventHandler<ActionEvent>()
|
||||||
{
|
{
|
||||||
@Override
|
@Override
|
||||||
public void handle(ActionEvent event)
|
public void handle(ActionEvent event)
|
||||||
@@ -107,10 +121,34 @@ public class DemodulatorViewerFX extends Application
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
menuFile.getItems().addAll(menuItemOpen, getRecentFilesMenu(), menuItemClose, new SeparatorMenuItem(),
|
fileMenu.getItems().addAll(openMenuItem, getRecentFilesMenu(), closeMenuItem, new SeparatorMenuItem(),
|
||||||
menuItemExit);
|
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;
|
return mMenuBar;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
package io.github.dsheirer.instrument.gui.viewer;
|
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.Broadcaster;
|
||||||
import io.github.dsheirer.sample.Listener;
|
import io.github.dsheirer.sample.Listener;
|
||||||
import io.github.dsheirer.sample.complex.ComplexBuffer;
|
import io.github.dsheirer.sample.complex.ComplexBuffer;
|
||||||
@@ -52,6 +53,7 @@ public class PlaybackController extends HBox implements IFrameLocationListener,
|
|||||||
private HBox mControlsBox;
|
private HBox mControlsBox;
|
||||||
private Label mFileLabel;
|
private Label mFileLabel;
|
||||||
private EventHandler<ActionEvent> mPlayEventHandler;
|
private EventHandler<ActionEvent> mPlayEventHandler;
|
||||||
|
private DecoderPane mSampleRateListener;
|
||||||
|
|
||||||
private IControllableFileSource mControllableFileSource;
|
private IControllableFileSource mControllableFileSource;
|
||||||
private Broadcaster<ComplexBuffer> mComplexBufferBroadcaster = new Broadcaster<>();
|
private Broadcaster<ComplexBuffer> mComplexBufferBroadcaster = new Broadcaster<>();
|
||||||
@@ -65,6 +67,16 @@ public class PlaybackController extends HBox implements IFrameLocationListener,
|
|||||||
setControlsEnabled(false);
|
setControlsEnabled(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setSampleRateListener(DecoderPane decoderPane)
|
||||||
|
{
|
||||||
|
mSampleRateListener = decoderPane;
|
||||||
|
|
||||||
|
if(mSampleRateListener != null && mControllableFileSource != null)
|
||||||
|
{
|
||||||
|
mSampleRateListener.setSampleRate(mControllableFileSource.getSampleRate());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void load(File file)
|
public void load(File file)
|
||||||
{
|
{
|
||||||
mLog.debug("loading:" + file.getAbsolutePath());
|
mLog.debug("loading:" + file.getAbsolutePath());
|
||||||
@@ -76,6 +88,11 @@ public class PlaybackController extends HBox implements IFrameLocationListener,
|
|||||||
mControllableFileSource.setListener(this);
|
mControllableFileSource.setListener(this);
|
||||||
((ComplexWaveSource)mControllableFileSource).setListener((Listener<ComplexBuffer>)this);
|
((ComplexWaveSource)mControllableFileSource).setListener((Listener<ComplexBuffer>)this);
|
||||||
mControllableFileSource.open();
|
mControllableFileSource.open();
|
||||||
|
|
||||||
|
if(mSampleRateListener != null)
|
||||||
|
{
|
||||||
|
mSampleRateListener.setSampleRate(mControllableFileSource.getSampleRate());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch(UnsupportedAudioFileException e)
|
catch(UnsupportedAudioFileException e)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,25 +15,22 @@
|
|||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
package io.github.dsheirer.instrument.gui.viewer;
|
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.BorderPane;
|
||||||
import javafx.scene.layout.HBox;
|
|
||||||
import javafx.scene.layout.VBox;
|
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
|
||||||
public class ViewerDesktop extends BorderPane
|
public class ViewerDesktop extends BorderPane
|
||||||
{
|
{
|
||||||
private PlaybackController mPlaybackController;
|
private PlaybackController mPlaybackController;
|
||||||
private VBox mChartBox;
|
private DecoderPane mDecoderPane = DecoderPaneFactory.getDefaultPane();
|
||||||
private ComplexSampleLineChart mSampleLineChart;
|
|
||||||
private ComplexPhaseLineChart mPhaseLineChart;
|
|
||||||
|
|
||||||
public ViewerDesktop()
|
public ViewerDesktop()
|
||||||
{
|
{
|
||||||
setBottom(getPlaybackController());
|
setBottom(getPlaybackController());
|
||||||
getPlaybackController().addListener(getSampleLineChart());
|
setCenter(getDecoderPane());
|
||||||
getPlaybackController().addListener(getPhaseLineChart());
|
|
||||||
setCenter(getChartBox());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void load(File file)
|
public void load(File file)
|
||||||
@@ -42,6 +39,31 @@ public class ViewerDesktop extends BorderPane
|
|||||||
getPlaybackController().load(file);
|
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()
|
public void close()
|
||||||
{
|
{
|
||||||
getPlaybackController().close();
|
getPlaybackController().close();
|
||||||
@@ -56,35 +78,4 @@ public class ViewerDesktop extends BorderPane
|
|||||||
|
|
||||||
return mPlaybackController;
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
* If not, see <http://www.gnu.org/licenses/>
|
* 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.buffer.ComplexCircularBuffer;
|
||||||
import io.github.dsheirer.dsp.gain.ComplexGain;
|
import io.github.dsheirer.dsp.gain.ComplexGain;
|
||||||
@@ -28,7 +28,6 @@ import javafx.collections.FXCollections;
|
|||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
import javafx.scene.chart.LineChart;
|
import javafx.scene.chart.LineChart;
|
||||||
import javafx.scene.chart.NumberAxis;
|
import javafx.scene.chart.NumberAxis;
|
||||||
import org.apache.commons.math3.complex.ComplexUtils;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
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 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 ComplexCircularBuffer mComplexCircularBuffer;
|
||||||
private ComplexGain mComplexGain = new ComplexGain(600.0f);
|
private ComplexGain mComplexGain = new ComplexGain(600.0f);
|
||||||
private ObservableList<Data<Integer,Float>> mPhaseValues = FXCollections.observableArrayList();
|
private ObservableList<Data<Integer,Float>> mPhaseValues = FXCollections.observableArrayList();
|
||||||
@@ -44,8 +43,8 @@ public class ComplexPhaseLineChart extends LineChart implements Listener<Complex
|
|||||||
|
|
||||||
public ComplexPhaseLineChart(int length)
|
public ComplexPhaseLineChart(int length)
|
||||||
{
|
{
|
||||||
super(new NumberAxis("Time", 1, length - 10, 1),
|
super(new NumberAxis("Time", 1, length - 10, 2),
|
||||||
new NumberAxis("Phase", -Math.PI, Math.PI, 1.0));
|
new NumberAxis("Phase", -Math.PI, Math.PI, Math.PI / 2.0));
|
||||||
|
|
||||||
Series<Integer,Float> phaseSeries = new Series<>("Phase", mPhaseValues);
|
Series<Integer,Float> phaseSeries = new Series<>("Phase", mPhaseValues);
|
||||||
ObservableList<Series<Integer,Float>> observableList = FXCollections.observableArrayList(phaseSeries);
|
ObservableList<Series<Integer,Float>> observableList = FXCollections.observableArrayList(phaseSeries);
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
* If not, see <http://www.gnu.org/licenses/>
|
* 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.buffer.ComplexCircularBuffer;
|
||||||
import io.github.dsheirer.dsp.gain.ComplexGain;
|
import io.github.dsheirer.dsp.gain.ComplexGain;
|
||||||
@@ -44,8 +44,8 @@ public class ComplexSampleLineChart extends LineChart implements Listener<Comple
|
|||||||
|
|
||||||
public ComplexSampleLineChart(int length)
|
public ComplexSampleLineChart(int length)
|
||||||
{
|
{
|
||||||
super(new NumberAxis("Sample Number", 1, length, 1),
|
super(new NumberAxis("Sample Number", 1, length, 2),
|
||||||
new NumberAxis("Value", -1.0, 1.0, 0.1));
|
new NumberAxis("Value", -1.0, 1.0, 0.25));
|
||||||
|
|
||||||
LineChart.Series<Integer,Float> iSampleSeries = new LineChart.Series<>("Inphase", mISamples);
|
LineChart.Series<Integer,Float> iSampleSeries = new LineChart.Series<>("Inphase", mISamples);
|
||||||
LineChart.Series<Integer,Float> qSampleSeries = new LineChart.Series<>("Quadrature", mQSamples);
|
LineChart.Series<Integer,Float> qSampleSeries = new LineChart.Series<>("Quadrature", mQSamples);
|
||||||
@@ -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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,39 +22,39 @@ import io.github.dsheirer.message.Message;
|
|||||||
import io.github.dsheirer.module.Module;
|
import io.github.dsheirer.module.Module;
|
||||||
import io.github.dsheirer.sample.Listener;
|
import io.github.dsheirer.sample.Listener;
|
||||||
|
|
||||||
public abstract class Decoder extends Module
|
public abstract class Decoder extends Module implements IMessageProvider
|
||||||
implements IMessageProvider, Listener<Message>
|
|
||||||
{
|
{
|
||||||
/* This has to be a broadcaster in order for references to persist */
|
/* This has to be a broadcaster in order for references to persist */
|
||||||
protected Listener<Message> mMessageListener;
|
private Listener<Message> mMessageDistributor = new MessageDistributor();
|
||||||
|
protected Listener<Message> mMessageListener;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decoder - parent class for all decoders, demodulators and components.
|
* Decoder - parent class for all decoders, demodulators and components.
|
||||||
*/
|
*/
|
||||||
public Decoder()
|
public Decoder()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void dispose()
|
public void dispose()
|
||||||
{
|
{
|
||||||
mMessageListener = null;
|
mMessageListener = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Identifies the decoder type (ie protocol)
|
* Identifies the decoder type (ie protocol)
|
||||||
*/
|
*/
|
||||||
public abstract DecoderType getDecoderType();
|
public abstract DecoderType getDecoderType();
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a listener for receiving decoded messages from this decoder
|
* Adds a listener for receiving decoded messages from this decoder
|
||||||
*/
|
*/
|
||||||
@Override
|
@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
|
* Removes the listener from receiving decoded messages from all attached
|
||||||
@@ -63,23 +63,29 @@ public abstract class Decoder extends Module
|
|||||||
@Override
|
@Override
|
||||||
public void removeMessageListener()
|
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 )
|
return mMessageDistributor;
|
||||||
{
|
|
||||||
mMessageListener.receive( message );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
/**
|
||||||
public void receive( Message message )
|
* Distributes/forwards messages from sub-class decoder implementations to the registered message listener.
|
||||||
{
|
*/
|
||||||
broadcast( message );
|
public class MessageDistributor implements Listener<Message>
|
||||||
}
|
{
|
||||||
|
@Override
|
||||||
|
public void receive(Message message)
|
||||||
|
{
|
||||||
|
if(mMessageListener != null)
|
||||||
|
{
|
||||||
|
mMessageListener.receive(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ public class Fleetsync2Decoder extends Decoder implements IFilteredRealBufferLis
|
|||||||
mMessageProcessor = new Fleetsync2MessageProcessor( aliasList );
|
mMessageProcessor = new Fleetsync2MessageProcessor( aliasList );
|
||||||
mMessageFramer.addMessageListener( mMessageProcessor );
|
mMessageFramer.addMessageListener( mMessageProcessor );
|
||||||
|
|
||||||
mMessageProcessor.setMessageListener( this );
|
mMessageProcessor.setMessageListener( getMessageListener() );
|
||||||
}
|
}
|
||||||
|
|
||||||
public void dispose()
|
public void dispose()
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ public class LJ1200Decoder extends Decoder implements IFilteredRealBufferListene
|
|||||||
mMessageProcessor = new LJ1200MessageProcessor( aliasList );
|
mMessageProcessor = new LJ1200MessageProcessor( aliasList );
|
||||||
mTowerMessageFramer.addMessageListener( mMessageProcessor );
|
mTowerMessageFramer.addMessageListener( mMessageProcessor );
|
||||||
mTransponderMessageFramer.addMessageListener( mMessageProcessor );
|
mTransponderMessageFramer.addMessageListener( mMessageProcessor );
|
||||||
mMessageProcessor.setMessageListener( this );
|
mMessageProcessor.setMessageListener( getMessageListener() );
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ public class LTRNetDecoder extends Decoder
|
|||||||
|
|
||||||
mLTRMessageFramer.addMessageListener( mLTRMessageProcessor );
|
mLTRMessageFramer.addMessageListener( mLTRMessageProcessor );
|
||||||
|
|
||||||
mLTRMessageProcessor.setMessageListener( this );
|
mLTRMessageProcessor.setMessageListener( getMessageListener() );
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ public class LTRStandardDecoder extends Decoder implements IUnFilteredRealBuffer
|
|||||||
|
|
||||||
mLTRMessageProcessor = new LTRStandardMessageProcessor( direction, aliasList );
|
mLTRMessageProcessor = new LTRStandardMessageProcessor( direction, aliasList );
|
||||||
mLTRMessageFramer.addMessageListener( mLTRMessageProcessor );
|
mLTRMessageFramer.addMessageListener( mLTRMessageProcessor );
|
||||||
mLTRMessageProcessor.setMessageListener( this );
|
mLTRMessageProcessor.setMessageListener( getMessageListener() );
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ public class MDCDecoder extends Decoder implements IFilteredRealBufferListener,
|
|||||||
/* Message Processor */
|
/* Message Processor */
|
||||||
mMessageProcessor = new MDCMessageProcessor( aliasList );
|
mMessageProcessor = new MDCMessageProcessor( aliasList );
|
||||||
mMessageFramer.addMessageListener( mMessageProcessor );
|
mMessageFramer.addMessageListener( mMessageProcessor );
|
||||||
mMessageProcessor.addMessageListener( this );
|
mMessageProcessor.addMessageListener(getMessageListener());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void dispose()
|
public void dispose()
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ public class MPT1327Decoder extends Decoder implements IFilteredRealBufferListen
|
|||||||
|
|
||||||
/* Fully decoded and framed messages processor */
|
/* Fully decoded and framed messages processor */
|
||||||
mMessageProcessor = new MPT1327MessageProcessor(aliasList);
|
mMessageProcessor = new MPT1327MessageProcessor(aliasList);
|
||||||
mMessageProcessor.setMessageListener(this);
|
mMessageProcessor.setMessageListener(getMessageListener());
|
||||||
mControlMessageFramer.addMessageListener(mMessageProcessor);
|
mControlMessageFramer.addMessageListener(mMessageProcessor);
|
||||||
mTrafficMessageFramer.addMessageListener(mMessageProcessor);
|
mTrafficMessageFramer.addMessageListener(mMessageProcessor);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
package io.github.dsheirer.module.decode.p25;
|
||||||
|
|
||||||
import io.github.dsheirer.alias.AliasList;
|
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.Decoder;
|
||||||
import io.github.dsheirer.module.decode.DecoderType;
|
import io.github.dsheirer.module.decode.DecoderType;
|
||||||
import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeListener;
|
import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeListener;
|
||||||
import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeProvider;
|
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;
|
private P25MessageProcessor mMessageProcessor;
|
||||||
|
private AliasList mAliasList;
|
||||||
|
|
||||||
public P25Decoder( AliasList aliasList )
|
public P25Decoder(AliasList aliasList)
|
||||||
{
|
|
||||||
mMessageProcessor = new P25MessageProcessor( aliasList );
|
|
||||||
mMessageProcessor.setMessageListener( this );
|
|
||||||
}
|
|
||||||
|
|
||||||
public void dispose()
|
|
||||||
{
|
|
||||||
super.dispose();
|
|
||||||
|
|
||||||
mMessageProcessor.dispose();
|
|
||||||
mMessageProcessor = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DecoderType getDecoderType()
|
|
||||||
{
|
{
|
||||||
return DecoderType.P25_PHASE1;
|
mAliasList = aliasList;
|
||||||
|
mMessageProcessor = new P25MessageProcessor(mAliasList);
|
||||||
|
mMessageProcessor.setMessageListener(getMessageListener());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected P25MessageProcessor getMessageProcessor()
|
protected AliasList getAliasList()
|
||||||
{
|
{
|
||||||
return mMessageProcessor;
|
return mAliasList;
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract Modulation getModulation();
|
public void dispose()
|
||||||
|
{
|
||||||
|
super.dispose();
|
||||||
|
|
||||||
public enum Modulation
|
mMessageProcessor.dispose();
|
||||||
{
|
mMessageProcessor = null;
|
||||||
CQPSK( "Simulcast (LSM)", "LSM" ),
|
}
|
||||||
C4FM( "Normal (C4FM)", "C4FM" );
|
|
||||||
|
|
||||||
private String mLabel;
|
@Override
|
||||||
private String mShortLabel;
|
public DecoderType getDecoderType()
|
||||||
|
{
|
||||||
|
return DecoderType.P25_PHASE1;
|
||||||
|
}
|
||||||
|
|
||||||
private Modulation( String label, String shortLabel )
|
protected P25MessageProcessor getMessageProcessor()
|
||||||
{
|
{
|
||||||
mLabel = label;
|
return mMessageProcessor;
|
||||||
mShortLabel = shortLabel;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public String getLabel()
|
public abstract Modulation getModulation();
|
||||||
{
|
|
||||||
return mLabel;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getShortLabel()
|
public enum Modulation
|
||||||
{
|
{
|
||||||
return mShortLabel;
|
CQPSK("Simulcast (LSM)", "LSM"),
|
||||||
}
|
C4FM("Normal (C4FM)", "C4FM");
|
||||||
|
|
||||||
public String toString()
|
private String mLabel;
|
||||||
{
|
private String mShortLabel;
|
||||||
return getLabel();
|
|
||||||
}
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -19,10 +19,7 @@
|
|||||||
package io.github.dsheirer.module.decode.p25;
|
package io.github.dsheirer.module.decode.p25;
|
||||||
|
|
||||||
import io.github.dsheirer.alias.AliasList;
|
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.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.Listener;
|
||||||
import io.github.dsheirer.sample.real.IFilteredRealBufferListener;
|
import io.github.dsheirer.sample.real.IFilteredRealBufferListener;
|
||||||
import io.github.dsheirer.sample.real.RealBuffer;
|
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.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
|
||||||
@@ -105,93 +101,6 @@ public class P25_C4FMDecoder extends P25Decoder implements IFilteredRealBufferLi
|
|||||||
return mSymbolFilter;
|
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
|
@Override
|
||||||
public void reset()
|
public void reset()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.filter.fir.complex.ComplexFIRFilter_CB_CB;
|
||||||
import io.github.dsheirer.dsp.gain.ComplexFeedForwardGainControl;
|
import io.github.dsheirer.dsp.gain.ComplexFeedForwardGainControl;
|
||||||
import io.github.dsheirer.dsp.psk.LSMDemodulator;
|
import io.github.dsheirer.dsp.psk.LSMDemodulator;
|
||||||
import io.github.dsheirer.dsp.psk.QPSKPolarSlicer;
|
import io.github.dsheirer.dsp.psk.QPSKSymbolDecoder;
|
||||||
import io.github.dsheirer.instrument.tap.Tap;
|
|
||||||
import io.github.dsheirer.instrument.tap.TapGroup;
|
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.Listener;
|
||||||
import io.github.dsheirer.sample.complex.ComplexBuffer;
|
import io.github.dsheirer.sample.complex.ComplexBuffer;
|
||||||
import io.github.dsheirer.sample.complex.ComplexBufferToStreamConverter;
|
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.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
|
||||||
public class P25_LSMDecoder extends P25Decoder implements IComplexBufferListener
|
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 */
|
/* Instrumentation Taps */
|
||||||
private static final String INSTRUMENT_BASEBAND_FILTER_OUTPUT = "Tap Point: Baseband Filter 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_AGC_OUTPUT = "Tap Point: AGC Output";
|
||||||
private static final String INSTRUMENT_LSM_DEMODULATOR_OUTPUT = "Tap Point: LSM Demodulator 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_QPSK_SLICER_OUTPUT = "Tap Point: QPSK Slicer Output";
|
||||||
|
|
||||||
private List<TapGroup> mAvailableTaps;
|
private List<TapGroup> mAvailableTaps;
|
||||||
|
|
||||||
private ComplexFIRFilter_CB_CB mBasebandFilter;
|
private ComplexFIRFilter_CB_CB mBasebandFilter;
|
||||||
private ComplexBufferToStreamConverter mStreamConverter = new ComplexBufferToStreamConverter();
|
private ComplexBufferToStreamConverter mStreamConverter = new ComplexBufferToStreamConverter();
|
||||||
private ComplexFeedForwardGainControl mAGC =
|
private ComplexFeedForwardGainControl mAGC =
|
||||||
new ComplexFeedForwardGainControl( 32 );
|
new ComplexFeedForwardGainControl(32);
|
||||||
private LSMDemodulator mLSMDemodulator = new LSMDemodulator();
|
private LSMDemodulator mLSMDemodulator = new LSMDemodulator();
|
||||||
private QPSKPolarSlicer mQPSKSlicer = new QPSKPolarSlicer();
|
private QPSKSymbolDecoder mQPSKSlicer = new QPSKSymbolDecoder();
|
||||||
private P25MessageFramer mMessageFramer;
|
private P25MessageFramer mMessageFramer;
|
||||||
|
|
||||||
public P25_LSMDecoder( AliasList aliasList )
|
public P25_LSMDecoder(AliasList aliasList)
|
||||||
{
|
{
|
||||||
super( aliasList );
|
super(aliasList);
|
||||||
|
|
||||||
mBasebandFilter = new ComplexFIRFilter_CB_CB( FilterFactory.getLowPass(
|
mBasebandFilter = new ComplexFIRFilter_CB_CB(FilterFactory.getLowPass(
|
||||||
48000, 7250, 8000, 60, WindowType.HANNING, true ), 1.0f );
|
48000, 7250, 8000, 60, WindowType.HANNING, true), 1.0f);
|
||||||
|
|
||||||
mBasebandFilter.setListener( mStreamConverter );
|
mBasebandFilter.setListener(mStreamConverter);
|
||||||
|
|
||||||
mStreamConverter.setListener( mAGC );
|
mStreamConverter.setListener(mAGC);
|
||||||
|
|
||||||
mAGC.setListener( mLSMDemodulator );
|
mAGC.setListener(mLSMDemodulator);
|
||||||
|
|
||||||
mLSMDemodulator.setSymbolListener( mQPSKSlicer );
|
mLSMDemodulator.setSymbolListener(mQPSKSlicer);
|
||||||
|
|
||||||
mMessageFramer = new P25MessageFramer( aliasList, mLSMDemodulator );
|
mMessageFramer = new P25MessageFramer(aliasList, mLSMDemodulator.getCostasLoop());
|
||||||
mQPSKSlicer.addListener( mMessageFramer );
|
mQPSKSlicer.addListener(mMessageFramer);
|
||||||
|
|
||||||
mMessageFramer.setListener( getMessageProcessor() );
|
mMessageFramer.setListener(getMessageProcessor());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void dispose()
|
public void dispose()
|
||||||
{
|
{
|
||||||
super.dispose();
|
super.dispose();
|
||||||
|
|
||||||
mBasebandFilter.dispose();
|
mBasebandFilter.dispose();
|
||||||
mBasebandFilter = null;
|
mBasebandFilter = null;
|
||||||
|
|
||||||
mStreamConverter.dispose();
|
mStreamConverter.dispose();
|
||||||
mStreamConverter = null;
|
mStreamConverter = null;
|
||||||
|
|
||||||
mAGC.dispose();
|
mAGC.dispose();
|
||||||
mAGC = null;
|
mAGC = null;
|
||||||
|
|
||||||
mLSMDemodulator.dispose();
|
mLSMDemodulator.dispose();
|
||||||
mLSMDemodulator = null;
|
mLSMDemodulator = null;
|
||||||
|
|
||||||
mQPSKSlicer.dispose();
|
mQPSKSlicer.dispose();
|
||||||
mQPSKSlicer = null;
|
mQPSKSlicer = null;
|
||||||
|
|
||||||
mMessageFramer.dispose();
|
mMessageFramer.dispose();
|
||||||
mMessageFramer = null;
|
mMessageFramer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setFrequencyChangeListener(Listener<FrequencyChangeEvent> listener)
|
public void setFrequencyChangeListener(Listener<FrequencyChangeEvent> listener)
|
||||||
@@ -130,130 +124,34 @@ public class P25_LSMDecoder extends P25Decoder implements IComplexBufferListener
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Listener<ComplexBuffer> getComplexBufferListener()
|
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()
|
|
||||||
{
|
{
|
||||||
if( mAvailableTaps == null )
|
return mBasebandFilter;
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public Modulation getModulation()
|
||||||
* Adds the instrumentation tap
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void registerTap( Tap tap )
|
|
||||||
{
|
{
|
||||||
if( mLSMDemodulator != null )
|
return Modulation.CQPSK;
|
||||||
{
|
|
||||||
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() );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Override
|
||||||
* Removes the instrumentation tap
|
public void reset()
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void unregisterTap( Tap tap )
|
|
||||||
{
|
{
|
||||||
if( mLSMDemodulator != null )
|
// TODO Auto-generated method stub
|
||||||
{
|
|
||||||
mLSMDemodulator.unregisterTap( tap );
|
|
||||||
}
|
|
||||||
|
|
||||||
switch( tap.getName() )
|
|
||||||
{
|
|
||||||
case INSTRUMENT_BASEBAND_FILTER_OUTPUT:
|
|
||||||
mStreamConverter.setListener( mAGC );
|
|
||||||
break;
|
|
||||||
case INSTRUMENT_AGC_OUTPUT:
|
|
||||||
mAGC.setListener( mLSMDemodulator );
|
|
||||||
break;
|
|
||||||
case INSTRUMENT_LSM_DEMODULATOR_OUTPUT:
|
|
||||||
mLSMDemodulator.setSymbolListener( mQPSKSlicer );
|
|
||||||
break;
|
|
||||||
case INSTRUMENT_QPSK_SLICER_OUTPUT:
|
|
||||||
mQPSKSlicer.removeListener( (DibitTap)tap );
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new IllegalArgumentException( "Unrecognized tap: " +
|
|
||||||
tap.getName() );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void reset()
|
public void start(ScheduledExecutorService executor)
|
||||||
{
|
{
|
||||||
// TODO Auto-generated method stub
|
// TODO Auto-generated method stub
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void start( ScheduledExecutorService executor )
|
public void stop()
|
||||||
{
|
{
|
||||||
// TODO Auto-generated method stub
|
// TODO Auto-generated method stub
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void stop()
|
|
||||||
{
|
|
||||||
// TODO Auto-generated method stub
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ public class PassportDecoder extends Decoder
|
|||||||
|
|
||||||
mPassportMessageProcessor = new PassportMessageProcessor( aliasList );
|
mPassportMessageProcessor = new PassportMessageProcessor( aliasList );
|
||||||
mPassportMessageFramer.addMessageListener( mPassportMessageProcessor );
|
mPassportMessageFramer.addMessageListener( mPassportMessageProcessor );
|
||||||
mPassportMessageProcessor.setMessageListener( this );
|
mPassportMessageProcessor.setMessageListener( getMessageListener() );
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -104,8 +104,8 @@ public class Tait1200Decoder extends Decoder implements IFilteredRealBufferListe
|
|||||||
mMessageFramerGPS.addMessageListener( mMessageAProcessor );
|
mMessageFramerGPS.addMessageListener( mMessageAProcessor );
|
||||||
mMessageFramerANI.addMessageListener( mMessageBProcessor );
|
mMessageFramerANI.addMessageListener( mMessageBProcessor );
|
||||||
|
|
||||||
mMessageAProcessor.setMessageListener( this );
|
mMessageAProcessor.setMessageListener(getMessageListener());
|
||||||
mMessageBProcessor.setMessageListener( this );
|
mMessageBProcessor.setMessageListener(getMessageListener());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void dispose()
|
public void dispose()
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ public class RecorderManager implements Listener<AudioPacket>
|
|||||||
private static final Logger mLog = LoggerFactory.getLogger(RecorderManager.class);
|
private static final Logger mLog = LoggerFactory.getLogger(RecorderManager.class);
|
||||||
|
|
||||||
public static final int AUDIO_SAMPLE_RATE = 8000;
|
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
|
public static final long IDLE_RECORDER_REMOVAL_THRESHOLD = 6000; //6 seconds
|
||||||
|
|
||||||
private Map<String,AudioPacketWaveRecorder> mRecorders = new HashMap<>();
|
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(SystemProperties.getInstance().getApplicationFolder("recordings"));
|
||||||
sb.append(File.separator).append(channelName).append("_baseband");
|
sb.append(File.separator).append(channelName).append("_baseband");
|
||||||
|
|
||||||
return new ComplexBufferWaveRecorder(AUDIO_SAMPLE_RATE, sb.toString());
|
return new ComplexBufferWaveRecorder(BASEBAND_SAMPLE_RATE, sb.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -27,412 +27,427 @@ import java.io.Serializable;
|
|||||||
*/
|
*/
|
||||||
public class Complex implements Serializable
|
public class Complex implements Serializable
|
||||||
{
|
{
|
||||||
private final static Logger mLog =
|
private final static Logger mLog = LoggerFactory.getLogger(Complex.class);
|
||||||
LoggerFactory.getLogger( Complex.class );
|
|
||||||
|
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
private float mLeft;
|
private float mLeft;
|
||||||
private float mRight;
|
private float mRight;
|
||||||
|
|
||||||
public Complex( float left, float right )
|
public Complex(float left, float right)
|
||||||
{
|
{
|
||||||
mLeft = left;
|
mLeft = left;
|
||||||
mRight = right;
|
mRight = right;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Complex()
|
public Complex()
|
||||||
{
|
{
|
||||||
this( 0.0f, 0.0f );
|
this(0.0f, 0.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setInphase( float inphase )
|
public void setInphase(float inphase)
|
||||||
{
|
{
|
||||||
mLeft = inphase;
|
mLeft = inphase;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setQuadrature( float quadrature )
|
public void setQuadrature(float quadrature)
|
||||||
{
|
{
|
||||||
mRight = quadrature;
|
mRight = quadrature;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setValues( float inphase, float quadrature )
|
public void setValues(float inphase, float quadrature)
|
||||||
{
|
{
|
||||||
mLeft = inphase;
|
mLeft = inphase;
|
||||||
mRight = quadrature;
|
mRight = quadrature;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Complex copy()
|
public void setValues(Complex sampleToCopy)
|
||||||
{
|
{
|
||||||
return new Complex( mLeft, mRight );
|
mLeft = sampleToCopy.left();
|
||||||
}
|
mRight = sampleToCopy.right();
|
||||||
|
}
|
||||||
|
|
||||||
public String toString()
|
public Complex copy()
|
||||||
{
|
{
|
||||||
StringBuilder sb = new StringBuilder();
|
return new Complex(mLeft, mRight);
|
||||||
|
}
|
||||||
|
|
||||||
sb.append( "I:" );
|
public String toString()
|
||||||
sb.append( mLeft );
|
{
|
||||||
sb.append( " Q:" );
|
StringBuilder sb = new StringBuilder();
|
||||||
sb.append( mRight );
|
|
||||||
|
|
||||||
return sb.toString();
|
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 );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Multiplies this sample by the scalor value
|
* Returns a new sample representing the conjugate of this one
|
||||||
*/
|
*/
|
||||||
public void multiply( float scalor )
|
public Complex conjugate()
|
||||||
{
|
{
|
||||||
mLeft *= scalor;
|
return new Complex(mLeft, -mRight);
|
||||||
mRight *= scalor;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public static Complex multiply( Complex sample, float scalor )
|
/**
|
||||||
{
|
* Multiplies this sample by the scalor value
|
||||||
return new Complex( sample.left() * scalor,
|
*/
|
||||||
sample.right() * scalor );
|
public void multiply(float scalor)
|
||||||
}
|
{
|
||||||
|
mLeft *= scalor;
|
||||||
|
mRight *= scalor;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
public static Complex multiply(Complex sample, float scalor)
|
||||||
* Calculates the inphase component of multiplying two complex numbers, A and B.
|
{
|
||||||
*/
|
return new Complex(sample.left() * scalor,
|
||||||
public static float multiplyInphase( float inphaseA, float quadratureA, float inphaseB, float quadratureB )
|
sample.right() * scalor);
|
||||||
{
|
}
|
||||||
return ( inphaseA * inphaseB ) - ( quadratureA * quadratureB );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates the quadrature component of multiplying two complex numbers, A and B.
|
* Calculates the inphase component of multiplying two complex numbers, A and B.
|
||||||
*/
|
*/
|
||||||
public static float multiplyQuadrature( float inphaseA, float quadratureA, float inphaseB, float quadratureB )
|
public static float multiplyInphase(float inphaseA, float quadratureA, float inphaseB, float quadratureB)
|
||||||
{
|
{
|
||||||
return ( quadratureA * inphaseB ) + ( inphaseA * quadratureB );
|
return (inphaseA * inphaseB) - (quadratureA * quadratureB);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Multiplies this sample by the multiplier sample
|
* Calculates the quadrature component of multiplying two complex numbers, A and B.
|
||||||
*/
|
*/
|
||||||
public void multiply( Complex multiplier )
|
public static float multiplyQuadrature(float inphaseA, float quadratureA, float inphaseB, float quadratureB)
|
||||||
{
|
{
|
||||||
float inphase = multiplyInphase( inphase(), quadrature(),
|
return (quadratureA * inphaseB) + (inphaseA * quadratureB);
|
||||||
multiplier.inphase(), multiplier.quadrature() );
|
}
|
||||||
float quadrature = multiplyQuadrature( inphase(), quadrature(),
|
|
||||||
multiplier.inphase(), multiplier.quadrature() );
|
|
||||||
|
|
||||||
mLeft = inphase;
|
/**
|
||||||
mRight = quadrature;
|
* 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;
|
||||||
* Multiplies both samples returning a new sample with the results
|
mRight = quadrature;
|
||||||
*/
|
}
|
||||||
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() );
|
|
||||||
|
|
||||||
return new Complex( inphase, 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 static Complex multiply( Complex sample,
|
return new Complex(inphase, quadrature);
|
||||||
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(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 static Complex multiply( Float inphase, Float quadrature, Complex sample )
|
return new Complex(i, q);
|
||||||
{
|
}
|
||||||
float i = multiplyInphase( inphase, quadrature, sample.inphase(), sample.quadrature() );
|
|
||||||
float q = multiplyQuadrature( inphase, quadrature, sample.inphase(), sample.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());
|
||||||
|
|
||||||
public static Complex multiply( float inphaseA, float quadratureA, float inphaseB, float quadratureB )
|
return new Complex(i, q);
|
||||||
{
|
}
|
||||||
float i = multiplyInphase( inphaseA, quadratureA, inphaseB, quadratureB );
|
|
||||||
float q = multiplyQuadrature( inphaseA, quadratureA, inphaseB, quadratureB );
|
|
||||||
|
|
||||||
return new Complex( i, q );
|
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds the two complex samples returning a new complex sample with the result
|
* Adds the adder sample value to this sample
|
||||||
*/
|
*/
|
||||||
public static Complex add( Complex first, Complex second )
|
public void add(Complex adder)
|
||||||
{
|
{
|
||||||
return new Complex( first.left() + second.left(),
|
mLeft += adder.left();
|
||||||
first.right() + second.right() );
|
mRight += adder.right();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds the two complex samples returning a new complex sample with the result
|
* Adds the two complex samples returning a new complex sample with the result
|
||||||
*/
|
*/
|
||||||
public static Complex subtract( Complex first, Complex second )
|
public static Complex add(Complex first, Complex second)
|
||||||
{
|
{
|
||||||
return new Complex( first.left() - second.left(),
|
return new Complex(first.left() + second.left(),
|
||||||
first.right() - second.right() );
|
first.right() + second.right());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Magnitude of this sample
|
* Adds the two complex samples returning a new complex sample with the result
|
||||||
*/
|
*/
|
||||||
public float magnitude()
|
public static Complex subtract(Complex first, Complex second)
|
||||||
{
|
{
|
||||||
return (float)Math.sqrt( magnitudeSquared() );
|
return new Complex(first.left() - second.left(),
|
||||||
}
|
first.right() - second.right());
|
||||||
|
}
|
||||||
|
|
||||||
public static float magnitude( float inphase, float quadrature )
|
/**
|
||||||
{
|
* Magnitude of this sample
|
||||||
return (float)Math.sqrt( ( inphase * inphase ) + ( quadrature * quadrature ) );
|
*/
|
||||||
}
|
public float magnitude()
|
||||||
|
{
|
||||||
|
return (float)Math.sqrt(magnitudeSquared());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
public static float magnitude(float inphase, float quadrature)
|
||||||
* Magnitude squared of this sample
|
{
|
||||||
*/
|
return (float)Math.sqrt((inphase * inphase) + (quadrature * quadrature));
|
||||||
public float magnitudeSquared()
|
}
|
||||||
{
|
|
||||||
return norm();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Norm of this sample = ( i * i ) + ( q * q )
|
* Magnitude squared of this sample
|
||||||
*/
|
*/
|
||||||
public float norm()
|
public float magnitudeSquared()
|
||||||
{
|
{
|
||||||
return (float)( ( inphase() * inphase() ) +
|
return norm();
|
||||||
( quadrature() * quadrature() ) );
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the vector length to 1 (unit circle)
|
* Norm of this sample = ( i * i ) + ( q * q )
|
||||||
*/
|
*/
|
||||||
public void normalize()
|
public float norm()
|
||||||
{
|
{
|
||||||
float magnitude = magnitude();
|
return (float)((inphase() * inphase()) +
|
||||||
|
(quadrature() * quadrature()));
|
||||||
|
}
|
||||||
|
|
||||||
if( magnitude != 0 )
|
/**
|
||||||
{
|
* Returns the vector length to 1 (unit circle)
|
||||||
multiply( (float)( 1.0f / magnitude() ) );
|
*/
|
||||||
}
|
public void normalize()
|
||||||
}
|
{
|
||||||
|
float magnitude = magnitude();
|
||||||
|
|
||||||
/**
|
if(magnitude != 0)
|
||||||
* Returns the vector length to 1 (unit circle),
|
{
|
||||||
* avoiding square root multiplication
|
multiply((float)(1.0f / magnitude()));
|
||||||
*/
|
}
|
||||||
public void fastNormalize()
|
}
|
||||||
{
|
|
||||||
multiply( (float)( 1.95f - magnitudeSquared() ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
public float left()
|
/**
|
||||||
{
|
* Returns the vector length to 1 (unit circle),
|
||||||
return mLeft;
|
* avoiding square root multiplication
|
||||||
}
|
*/
|
||||||
|
public void fastNormalize()
|
||||||
|
{
|
||||||
|
multiply((float)(1.95f - magnitudeSquared()));
|
||||||
|
}
|
||||||
|
|
||||||
public float right()
|
public float left()
|
||||||
{
|
{
|
||||||
return mRight;
|
return mLeft;
|
||||||
}
|
}
|
||||||
|
|
||||||
public float inphase()
|
public float right()
|
||||||
{
|
{
|
||||||
return mLeft;
|
return mRight;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public float inphase()
|
||||||
* Absolute value of in-phase component
|
{
|
||||||
*/
|
return mLeft;
|
||||||
public float inPhaseAbsolute()
|
}
|
||||||
{
|
|
||||||
return Math.abs( mLeft );
|
|
||||||
}
|
|
||||||
|
|
||||||
public float quadrature()
|
/**
|
||||||
{
|
* Absolute value of in-phase component
|
||||||
return mRight;
|
*/
|
||||||
}
|
public float inPhaseAbsolute()
|
||||||
|
{
|
||||||
|
return Math.abs(mLeft);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
public float quadrature()
|
||||||
* Absolute value of quadrature component
|
{
|
||||||
*/
|
return mRight;
|
||||||
public float quadratureAbsolute()
|
}
|
||||||
{
|
|
||||||
return Math.abs( mRight );
|
|
||||||
}
|
|
||||||
|
|
||||||
public float x()
|
/**
|
||||||
{
|
* Absolute value of quadrature component
|
||||||
return mLeft;
|
*/
|
||||||
}
|
public float quadratureAbsolute()
|
||||||
|
{
|
||||||
|
return Math.abs(mRight);
|
||||||
|
}
|
||||||
|
|
||||||
public float y()
|
public float x()
|
||||||
{
|
{
|
||||||
return mRight;
|
return mLeft;
|
||||||
}
|
}
|
||||||
|
|
||||||
public float real()
|
public float y()
|
||||||
{
|
{
|
||||||
return mLeft;
|
return mRight;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public float real()
|
||||||
* Absolute value of real component
|
{
|
||||||
*/
|
return mLeft;
|
||||||
public float realAbsolute()
|
}
|
||||||
{
|
|
||||||
return Math.abs( mLeft );
|
|
||||||
}
|
|
||||||
|
|
||||||
public float imaginary()
|
/**
|
||||||
{
|
* Absolute value of real component
|
||||||
return mRight;
|
*/
|
||||||
}
|
public float realAbsolute()
|
||||||
|
{
|
||||||
|
return Math.abs(mLeft);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
public float imaginary()
|
||||||
* Absolute value of imaginary component
|
{
|
||||||
*/
|
return mRight;
|
||||||
public float imaginaryAbsolute()
|
}
|
||||||
{
|
|
||||||
return Math.abs( mRight );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the greater absolute value between left and right values
|
* Absolute value of imaginary component
|
||||||
*/
|
*/
|
||||||
public float maximumAbsolute()
|
public float imaginaryAbsolute()
|
||||||
{
|
{
|
||||||
if( Math.abs( mLeft ) > Math.abs( mRight ) )
|
return Math.abs(mRight);
|
||||||
{
|
}
|
||||||
return Math.abs( mLeft );
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return Math.abs( mRight );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new complex sample representing the angle with unit circle
|
* Returns the greater absolute value between left and right values
|
||||||
* magnitude
|
*/
|
||||||
* @param angle in radians
|
public float maximumAbsolute()
|
||||||
* @return
|
{
|
||||||
*/
|
if(Math.abs(mLeft) > Math.abs(mRight))
|
||||||
public static Complex fromAngle( float angle )
|
{
|
||||||
{
|
return Math.abs(mLeft);
|
||||||
return new Complex( (float)Math.cos( angle ),
|
}
|
||||||
(float)Math.sin( angle ) );
|
else
|
||||||
}
|
{
|
||||||
|
return Math.abs(mRight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Angle of this sample in radians
|
* Creates a new complex sample representing the angle with unit circle
|
||||||
*/
|
* magnitude
|
||||||
public float angle()
|
*
|
||||||
{
|
* @param angle in radians
|
||||||
return (float)Math.atan2( y(), x() );
|
* @return
|
||||||
}
|
*/
|
||||||
|
public static Complex fromAngle(double angle)
|
||||||
|
{
|
||||||
|
return new Complex((float)Math.cos(angle), (float)Math.sin(angle));
|
||||||
|
}
|
||||||
|
|
||||||
public float angleDegrees()
|
/**
|
||||||
{
|
* Sets this complex instance with the specified angle in radians.
|
||||||
return (float)Math.toDegrees(angle());
|
* @param angle in radians
|
||||||
}
|
*/
|
||||||
|
public void setAngle(double angle)
|
||||||
|
{
|
||||||
|
setInphase((float)Math.cos(angle));
|
||||||
|
setQuadrature((float)Math.sin(angle));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constrains the i and q quantities to +/- value.
|
* Angle of this sample in radians
|
||||||
*
|
*/
|
||||||
* @param value - maximum absolute value
|
public float angle()
|
||||||
*/
|
{
|
||||||
public void clip( float value )
|
return (float)Math.atan2(y(), x());
|
||||||
{
|
}
|
||||||
if( mLeft > value )
|
|
||||||
{
|
|
||||||
mLeft = value;
|
|
||||||
}
|
|
||||||
else if( mLeft < -value )
|
|
||||||
{
|
|
||||||
mLeft = -value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if( mRight > value )
|
public float angleDegrees()
|
||||||
{
|
{
|
||||||
mRight = value;
|
return (float)Math.toDegrees(angle());
|
||||||
}
|
}
|
||||||
else if( mRight < -value )
|
|
||||||
{
|
|
||||||
mRight = -value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Angle of this sample in degrees
|
* Constrains the i and q quantities to +/- value.
|
||||||
*/
|
*
|
||||||
public float polarAngle()
|
* @param value - maximum absolute value
|
||||||
{
|
*/
|
||||||
return (float)Math.toDegrees( angle() );
|
public void clip(float value)
|
||||||
}
|
{
|
||||||
|
if(mLeft > value)
|
||||||
|
{
|
||||||
|
mLeft = value;
|
||||||
|
}
|
||||||
|
else if(mLeft < -value)
|
||||||
|
{
|
||||||
|
mLeft = -value;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
if(mRight > value)
|
||||||
* Provides an approximate magnitude value for this sample.
|
{
|
||||||
*/
|
mRight = value;
|
||||||
public float envelope()
|
}
|
||||||
{
|
else if(mRight < -value)
|
||||||
return envelope( mLeft, mRight );
|
{
|
||||||
}
|
mRight = -value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static float envelope( float inphase, float quadrature )
|
/**
|
||||||
{
|
* Angle of this sample in degrees
|
||||||
float inphaseAbsolute = Math.abs( inphase );
|
*/
|
||||||
float quadratureAbsolute = Math.abs( quadrature );
|
public float polarAngle()
|
||||||
|
{
|
||||||
|
return (float)Math.toDegrees(angle());
|
||||||
|
}
|
||||||
|
|
||||||
if( inphaseAbsolute > quadratureAbsolute )
|
/**
|
||||||
{
|
* Provides an approximate magnitude value for this sample.
|
||||||
return inphaseAbsolute + ( 0.4f * quadratureAbsolute );
|
*/
|
||||||
}
|
public float envelope()
|
||||||
else
|
{
|
||||||
{
|
return envelope(mLeft, mRight);
|
||||||
return quadratureAbsolute + ( 0.4f * inphaseAbsolute );
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void main( String[] args )
|
public static float envelope(float inphase, float quadrature)
|
||||||
{
|
{
|
||||||
double angle = Math.PI; //180 degrees
|
float inphaseAbsolute = Math.abs(inphase);
|
||||||
|
float quadratureAbsolute = Math.abs(quadrature);
|
||||||
|
|
||||||
Complex s1 = new Complex( (float)Math.sin( angle ), (float)Math.cos( angle ) );
|
if(inphaseAbsolute > quadratureAbsolute)
|
||||||
|
{
|
||||||
|
return inphaseAbsolute + (0.4f * quadratureAbsolute);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return quadratureAbsolute + (0.4f * inphaseAbsolute);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Complex tap = new Complex( 1.0f, -0.1f );
|
public static void main(String[] args)
|
||||||
|
{
|
||||||
|
double angle = Math.PI; //180 degrees
|
||||||
|
|
||||||
Complex convolved = Complex.multiply( s1, tap );
|
Complex s1 = new Complex((float)Math.sin(angle), (float)Math.cos(angle));
|
||||||
|
|
||||||
mLog.debug( "s: " + s1.toString() +
|
Complex tap = new Complex(1.0f, -0.1f);
|
||||||
" t: " + tap.toString() +
|
|
||||||
" convolved: " + convolved.toString() );
|
|
||||||
|
|
||||||
}
|
Complex convolved = Complex.multiply(s1, tap);
|
||||||
|
|
||||||
|
mLog.debug("s: " + s1.toString() +
|
||||||
|
" t: " + tap.toString() +
|
||||||
|
" convolved: " + convolved.toString());
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
package io.github.dsheirer.source;
|
||||||
|
|
||||||
import javax.sound.sampled.UnsupportedAudioFileException;
|
import javax.sound.sampled.UnsupportedAudioFileException;
|
||||||
@@ -10,22 +25,24 @@ import java.io.IOException;
|
|||||||
*/
|
*/
|
||||||
public interface IControllableFileSource
|
public interface IControllableFileSource
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Opens the file source
|
* Opens the file source
|
||||||
*/
|
*/
|
||||||
public void open() throws IOException, UnsupportedAudioFileException;
|
public void open() throws IOException, UnsupportedAudioFileException;
|
||||||
|
|
||||||
public void close() throws IOException;
|
public void close() throws IOException;
|
||||||
|
|
||||||
public File getFile();
|
public File getFile();
|
||||||
|
|
||||||
public void next( int frames ) throws IOException;
|
public void next(int frames) throws IOException;
|
||||||
|
|
||||||
public void next( int frames, boolean broadcast ) throws IOException;
|
public void next(int frames, boolean broadcast) throws IOException;
|
||||||
|
|
||||||
public long getFrameCount() throws IOException;
|
public long getFrameCount() throws IOException;
|
||||||
|
|
||||||
public void setListener( IFrameLocationListener listener );
|
public int getSampleRate();
|
||||||
|
|
||||||
public void removeListener( IFrameLocationListener listener );
|
public void setListener(IFrameLocationListener listener);
|
||||||
|
|
||||||
|
public void removeListener(IFrameLocationListener listener);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user