Merge pull request #338 from DSheirer/330-p25-c4fm-decoder-update

330 p25 c4fm decoder update
This commit is contained in:
Denny Sheirer
2018-03-17 13:51:19 +00:00
committed by GitHub
71 changed files with 6092 additions and 2628 deletions

View File

@@ -54,6 +54,8 @@ dependencies {
compile 'org.apache.axis:axis-jaxrpc:1.4' compile 'org.apache.axis:axis-jaxrpc:1.4'
compile 'org.apache.axis:axis:1.4' compile 'org.apache.axis:axis:1.4'
compile 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.9.3' compile 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.9.3'
compile 'org.kordamp.ikonli:ikonli-javafx:2.1.1'
compile 'org.kordamp.ikonli:ikonli-fontawesome-pack:2.1.1'
} }
task wrapper(type: Wrapper) { task wrapper(type: Wrapper) {

View File

@@ -1,6 +1,6 @@
/******************************************************************************* /*******************************************************************************
* SDR Trunk * SDR Trunk
* Copyright (C) 2014 Dennis Sheirer * Copyright (C) 2014-2018 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@@ -30,61 +30,78 @@ import java.util.Arrays;
*/ */
public class ComplexCircularBuffer public class ComplexCircularBuffer
{ {
Complex[] mBuffer; Complex[] mBuffer;
int mBufferPointer = 0; int mBufferPointer = 0;
public ComplexCircularBuffer( int size ) public ComplexCircularBuffer(int size)
{ {
mBuffer = new Complex[ size ]; mBuffer = new Complex[size];
Arrays.fill( mBuffer, new Complex( 1.0f, 1.0f ) ); Arrays.fill(mBuffer, new Complex(1.0f, 1.0f));
} }
/** public int length()
* Returns the sample currently pointed to in the buffer and stores the {
* new sample in its place return mBuffer.length;
* @param sample }
* @return
*/
public Complex get( Complex sample )
{
Complex retVal = mBuffer[ mBufferPointer ];
put( sample ); /**
* Returns the sample currently pointed to in the buffer and stores the
* new sample in its place
*
* @param sample
* @return
*/
public Complex get(Complex sample)
{
Complex retVal = mBuffer[mBufferPointer];
return retVal; put(sample);
}
public void put( Complex sample ) return retVal;
{ }
mBuffer[ mBufferPointer ] = sample;
mBufferPointer++; public void put(Complex sample)
{
mBuffer[mBufferPointer] = sample;
if( mBufferPointer >= mBuffer.length ) mBufferPointer++;
{
mBufferPointer = 0;
}
}
public Complex[] get( int count ) if(mBufferPointer >= mBuffer.length)
{ {
Complex[] samples = new Complex[ count ]; mBufferPointer = 0;
}
}
int tempCounter = mBufferPointer; /**
* Returns the (count) oldest samples in a time ordered array
*/
public Complex[] get(int count)
{
Complex[] samples = new Complex[count];
for( int x = 0; x < count; x++ ) int tempCounter = mBufferPointer;
{
samples[ x ] = mBuffer[ tempCounter ];
tempCounter++; for(int x = 0; x < count; x++)
{
samples[x] = mBuffer[tempCounter];
if( tempCounter > mBuffer.length - 1 ) tempCounter++;
{
tempCounter = 0;
}
}
return samples; if(tempCounter > mBuffer.length - 1)
} {
tempCounter = 0;
}
}
return samples;
}
/**
* Returns the samples in a time-ordered array
*/
public Complex[] getAll()
{
return get(length());
}
} }

View File

@@ -17,64 +17,164 @@
******************************************************************************/ ******************************************************************************/
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.StandardDeviation;
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 StandardDeviation mStandardDeviation;
int mBufferPointer = 0; private Variance mVariance;
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);
}
public double standardDeviation()
{
if(mStandardDeviation == null)
{
mStandardDeviation = new StandardDeviation();
}
return mStandardDeviation.evaluate(mBuffer);
}
/**
* Calculates the variance of the values contained in this buffer
*/
public double variance()
{
if(mVariance == null)
{
mVariance = new Variance();
}
return mVariance.evaluate(mBuffer);
}
/**
* Calculates the average/mean of the values contained in this buffer
*/
public double mean()
{
if(mMean == null)
{
mMean = new Mean();
}
return mMean.evaluate(mBuffer, 0, mBuffer.length);
}
} }

View File

@@ -25,94 +25,96 @@ import java.util.Arrays;
* fills buffer with 0-valued samples * fills buffer with 0-valued samples
* *
* Can be used as a delay-type buffer, to delay samples by the 'size' amount * Can be used as a delay-type buffer, to delay samples by the 'size' amount
* @author denny
* *
* @author denny
*/ */
public class FloatCircularBuffer public class FloatCircularBuffer
{ {
float[] mBuffer; float[] mBuffer;
int mBufferPointer = 0; int mBufferPointer = 0;
public FloatCircularBuffer( int size ) /**
{ * Creates a circular double buffer of the specified size and all entries filled with the specified initial value.
mBuffer = new float[ size ]; *
* @param size of the buffer
* @param initialFillValue to fill the buffer entries
*/
public FloatCircularBuffer(int size, float initialFillValue)
{
mBuffer = new float[size];
Arrays.fill(mBuffer, initialFillValue);
}
Arrays.fill( mBuffer, (float)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 FloatCircularBuffer(int size)
{
this(size, 0.0f);
}
public int getSize() public float[] getBuffer()
{ {
return mBuffer.length; return mBuffer;
} }
public float[] getBuffer() /**
{ * Puts the new value into the buffer and returns the oldest buffer value that it replaced
return mBuffer; *
} * @param newValue to add to the buffer
* @return oldest value that was overwritten by the new value
*/
public float get(float newValue)
{
float oldestSample = mBuffer[mBufferPointer];
/** mBuffer[mBufferPointer] = newValue;
* Puts the new value into the buffer and returns the oldest buffer value
* that it replaced
* @param newValue
* @return
*/
public float get( float newValue )
{
float oldestSample = mBuffer[ mBufferPointer ];
mBuffer[ mBufferPointer ] = newValue; mBufferPointer++;
mBufferPointer++; if(mBufferPointer >= mBuffer.length)
{
mBufferPointer = 0;
}
if( mBufferPointer >= mBuffer.length ) return oldestSample;
{ }
mBufferPointer = 0;
}
return oldestSample; public float[] getAll()
} {
int valuePointer = 0;
float[] values = new float[mBuffer.length];
int bufferPointer = mBufferPointer;
/** while(valuePointer < values.length)
* Convenience method to put a new value in the buffer, while discarding {
* the old value values[valuePointer++] = mBuffer[bufferPointer++];
*/
public void put( float newValue )
{
get( newValue );
}
/** if(bufferPointer >= mBuffer.length)
* Returns an array of arraySize, loaded with sampleCount of the {
* oldest samples, loaded into the array starting at index 0. bufferPointer = 0;
* }
* This is useful for fetching an array filled half way with samples to }
* use in FFT processing.
* @param arraySize - size of the array (must be larger than sampleCount)
* @param sampleCount - number of the oldest samples in the buffer to load
* @return
*/
public float[] get( int arraySize, int sampleCount )
{
float[] samples = new float[ arraySize ];
if( arraySize > sampleCount ) return values;
{ }
int tempCounter = mBufferPointer;
for( int x = 0; x < sampleCount; x++ ) /**
{ * Places the new value into the buffer, overwriting the oldest value
samples[ x ] = mBuffer[ tempCounter ]; *
* @param newValue to add to the buffer
*/
public void put(float newValue)
{
mBuffer[mBufferPointer] = newValue;
tempCounter++; mBufferPointer++;
if( tempCounter > mBuffer.length )
{
tempCounter = 0;
}
}
}
return samples;
}
if(mBufferPointer >= mBuffer.length)
{
mBufferPointer = 0;
}
}
} }

View File

@@ -666,11 +666,11 @@ public class FilterFactory
* @param alpha - roll-off factor * @param alpha - roll-off factor
* @return - filter coefficients * @return - filter coefficients
*/ */
public static float[] getRootRaisedCosine(int samplesPerSymbol, public static float[] getRootRaisedCosine(float samplesPerSymbol,
int symbols, int symbols,
float alpha) float alpha)
{ {
int taps = samplesPerSymbol * symbols + 1; int taps = (int)(samplesPerSymbol * (float)symbols + 1.0f);
float scale = 0; float scale = 0;
@@ -680,8 +680,8 @@ public class FilterFactory
{ {
float index = (float) x - ((float) taps / 2.0f); float index = (float) x - ((float) taps / 2.0f);
float x1 = (float) Math.PI * index / (float) samplesPerSymbol; float x1 = (float) Math.PI * index / samplesPerSymbol;
float x2 = 4.0f * alpha * index / (float) samplesPerSymbol; float x2 = 4.0f * alpha * index / samplesPerSymbol;
float x3 = x2 * x2 - 1.0f; float x3 = x2 * x2 - 1.0f;
float numerator, denominator; float numerator, denominator;
@@ -692,7 +692,7 @@ public class FilterFactory
{ {
numerator = (float) Math.cos((1.0 + alpha) * x1) + numerator = (float) Math.cos((1.0 + alpha) * x1) +
(float) Math.sin((1.0f - alpha) * x1) / (float) Math.sin((1.0f - alpha) * x1) /
(4.0f * alpha * index / (float) samplesPerSymbol); (4.0f * alpha * index / samplesPerSymbol);
} }
else else
{ {
@@ -713,9 +713,9 @@ public class FilterFactory
x3 = (1.0f - alpha) * x1; x3 = (1.0f - alpha) * x1;
x2 = (1.0f + alpha) * x1; x2 = (1.0f + alpha) * x1;
numerator = (float) (Math.sin(x2) * (1.0f + alpha) * Math.PI - numerator = (float) (Math.sin(x2) * (1.0 + alpha) * Math.PI -
Math.cos(x3) * ((1.0 - alpha) * Math.PI * Math.cos(x3) * ((1.0 - alpha) * Math.PI *
(double) samplesPerSymbol) / (4 * alpha * index) + (double) samplesPerSymbol) / (4.0 * alpha * index) +
Math.sin(x3) * (double) samplesPerSymbol * Math.sin(x3) * (double) samplesPerSymbol *
(double) samplesPerSymbol / (4.0 * alpha * index * index)); (double) samplesPerSymbol / (4.0 * alpha * index * index));

View File

@@ -12,155 +12,169 @@ import org.jtransforms.fft.FloatFFT_1D;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.net.URL;
public class FilterView extends BorderPane public class FilterView extends BorderPane
{ {
private final static Logger mLog = LoggerFactory.getLogger( FilterView.class ); private final static Logger mLog = LoggerFactory.getLogger(FilterView.class);
private CurveFittedAreaChart mImpulseChart; private CurveFittedAreaChart mImpulseChart;
private NumberAxis mImpulseXAxis; private NumberAxis mImpulseXAxis;
private NumberAxis mImpulseYAxis; private NumberAxis mImpulseYAxis;
private CurveFittedAreaChart mDFTChart; private CurveFittedAreaChart mDFTChart;
private NumberAxis mDFTXAxis; private NumberAxis mDFTXAxis;
private NumberAxis mDFTYAxis; private NumberAxis mDFTYAxis;
private static final int FFT_SIZE = 4096; private static final int FFT_SIZE = 4096;
private FloatFFT_1D mFFT = new FloatFFT_1D( FFT_SIZE ); private FloatFFT_1D mFFT = new FloatFFT_1D(FFT_SIZE);
public FilterView( float[] taps ) public FilterView(float[] taps)
{ {
init(); init();
if( taps != null ) if(taps != null)
{ {
setImpulseTaps( taps ); setImpulseTaps(taps);
float[] dft = new float[ FFT_SIZE ]; float[] dft = new float[FFT_SIZE];
System.arraycopy( taps, 0, dft, 0, taps.length ); System.arraycopy(taps, 0, dft, 0, taps.length);
mFFT.realForward( dft ); mFFT.realForward(dft);
setDFTTaps( convertDFTToDecibel( dft, taps.length ) ); setDFTTaps(convertDFTToDecibel(dft, taps.length));
} }
} }
private void init() private void init()
{ {
HBox hbox = new HBox(); HBox hbox = new HBox();
mImpulseXAxis = new NumberAxis( 0.0, 0.5, 1.0 ); mImpulseXAxis = new NumberAxis(0.0, 0.5, 1.0);
mImpulseYAxis = new NumberAxis( -0.2, 1.2, .1 ); mImpulseYAxis = new NumberAxis(-0.2, 1.2, .1);
mImpulseChart = new CurveFittedAreaChart( mImpulseXAxis, mImpulseYAxis ); mImpulseChart = new CurveFittedAreaChart(mImpulseXAxis, mImpulseYAxis);
mImpulseChart.setLegendVisible( false ); mImpulseChart.setLegendVisible(false);
mImpulseChart.setHorizontalGridLinesVisible( false ); mImpulseChart.setHorizontalGridLinesVisible(false);
mImpulseChart.setVerticalGridLinesVisible(false); mImpulseChart.setVerticalGridLinesVisible(false);
mImpulseChart.setAlternativeColumnFillVisible(false); mImpulseChart.setAlternativeColumnFillVisible(false);
mImpulseChart.setAlternativeRowFillVisible(false); mImpulseChart.setAlternativeRowFillVisible(false);
String css = FilterView.class.getResource( "FilterView.css" ).toExternalForm(); URL cssURL = FilterView.class.getResource("FilterView.css");
mImpulseChart.getStylesheets().add( css );
hbox.getChildren().add( mImpulseChart ); String css = null;
HBox.setHgrow( mImpulseChart, Priority.ALWAYS );
mDFTXAxis = new NumberAxis( 0.0, FFT_SIZE / 2, FFT_SIZE / 8 ); if(cssURL != null)
mDFTYAxis = new NumberAxis( -200.0, 10.0, 10.0 );
mDFTChart = new CurveFittedAreaChart( mDFTXAxis, mDFTYAxis );
mDFTChart.setLegendVisible( false );
mDFTChart.setHorizontalGridLinesVisible( false );
mDFTChart.setVerticalGridLinesVisible(false);
mDFTChart.setAlternativeColumnFillVisible(false);
mDFTChart.setAlternativeRowFillVisible(false);
mDFTChart.getStylesheets().add( css );
hbox.getChildren().add( mDFTChart );
HBox.setHgrow( mDFTChart, Priority.ALWAYS );
setCenter( hbox );
}
public void setImpulseTaps( double[] taps )
{
float[] convertedTaps = new float[ taps.length ];
for( int x = 0; x < taps.length; x++ )
{
convertedTaps[ x ] = (float)taps[ x ];
}
setImpulseTaps( convertedTaps );
}
public void setImpulseTaps( float[] taps )
{
double smallest = 0.0;
double largest = 0.0;
final XYChart.Series<Number, Number> series = new XYChart.Series<>();
for( int x = 0; x < taps.length; x++ )
{ {
if( taps[ x ] < smallest ) css = cssURL.toExternalForm();
{
smallest = taps[ x ];
}
if( taps[ x ] > largest )
{
largest = taps[ x ];
}
series.getData().add( new XYChart.Data<Number, Number>( x, taps[ x ] ) );
} }
mImpulseXAxis.setLowerBound( 0.0 ); hbox.getChildren().add(mImpulseChart);
mImpulseXAxis.setUpperBound( taps.length - 1 ); HBox.setHgrow(mImpulseChart, Priority.ALWAYS);
mImpulseXAxis.setTickUnit( 1.0 );
double padding = Math.abs( largest - smallest ) * .1; mDFTXAxis = new NumberAxis(0.0, FFT_SIZE / 2, FFT_SIZE / 8);
mDFTYAxis = new NumberAxis(-200.0, 10.0, 10.0);
mImpulseYAxis.setLowerBound( smallest - padding ); mDFTChart = new CurveFittedAreaChart(mDFTXAxis, mDFTYAxis);
mImpulseYAxis.setUpperBound( largest + padding ); mDFTChart.setLegendVisible(false);
mImpulseYAxis.setTickUnit( padding ); mDFTChart.setHorizontalGridLinesVisible(false);
mDFTChart.setVerticalGridLinesVisible(false);
mDFTChart.setAlternativeColumnFillVisible(false);
mDFTChart.setAlternativeRowFillVisible(false);
if(css != null)
{
mImpulseChart.getStylesheets().add(css);
mDFTChart.getStylesheets().add(css);
}
hbox.getChildren().add(mDFTChart);
HBox.setHgrow(mDFTChart, Priority.ALWAYS);
setCenter(hbox);
}
public void setImpulseTaps(double[] taps)
{
float[] convertedTaps = new float[taps.length];
for(int x = 0; x < taps.length; x++)
{
convertedTaps[x] = (float)taps[x];
}
setImpulseTaps(convertedTaps);
}
public void setImpulseTaps(float[] taps)
{
double smallest = 0.0;
double largest = 0.0;
final XYChart.Series<Number,Number> series = new XYChart.Series<>();
for(int x = 0; x < taps.length; x++)
{
if(taps[x] < smallest)
{
smallest = taps[x];
}
if(taps[x] > largest)
{
largest = taps[x];
}
series.getData().add(new XYChart.Data<Number,Number>(x, taps[x]));
}
mImpulseXAxis.setLowerBound(0.0);
mImpulseXAxis.setUpperBound(taps.length - 1);
mImpulseXAxis.setTickUnit(1.0);
double padding = Math.abs(largest - smallest) * .1;
mImpulseYAxis.setLowerBound(smallest - padding);
mImpulseYAxis.setUpperBound(largest + padding);
mImpulseYAxis.setTickUnit(padding);
ObservableList<Series<Number,Number>> list = mImpulseChart.getData(); ObservableList<Series<Number,Number>> list = mImpulseChart.getData();
list.add( series ); list.add(series);
} }
public void setDFTTaps( float[] taps ) public void setDFTTaps(float[] taps)
{ {
double smallest = 0.0; double smallest = 0.0;
final XYChart.Series<Number, Number> series = new XYChart.Series<>(); final XYChart.Series<Number,Number> series = new XYChart.Series<>();
for( int x = 0; x < taps.length; x++ ) for(int x = 0; x < taps.length; x++)
{ {
if( taps[ x ] < smallest ) if(taps[x] < smallest)
{ {
smallest = taps[ x ]; smallest = taps[x];
} }
series.getData().add( new XYChart.Data<Number, Number>( x, taps[ x ] ) ); series.getData().add(new XYChart.Data<Number,Number>(x, taps[x]));
} }
ObservableList<Series<Number,Number>> list = mDFTChart.getData(); ObservableList<Series<Number,Number>> list = mDFTChart.getData();
list.add( series ); list.add(series);
} }
public float[] convertDFTToDecibel( float[] dft, int tapLength ) public float[] convertDFTToDecibel(float[] dft, int tapLength)
{ {
float[] decibels = new float[ dft.length / 2 ]; float[] decibels = new float[dft.length / 2];
int index = 0; int index = 0;
for( int x = 0; x < decibels.length; x ++ ) for(int x = 0; x < decibels.length; x++)
{ {
index = x * 2; index = x * 2;
decibels[ x ] = 20.0f * (float)Math.log10( ( ( dft[ index ] * dft[ index ] ) + decibels[x] = 20.0f * (float)Math.log10(((dft[index] * dft[index]) +
( dft[ index + 1 ] * dft[ index + 1 ] ) ) ); (dft[index + 1] * dft[index + 1])));
} }
return decibels; return decibels;
} }
} }

View File

@@ -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(48000)
.gridDensity(16) .passBandCutoff(5100)
.passBandCutoff(2500)
.passBandAmplitude(1.0) .passBandAmplitude(1.0)
.passBandRipple(0.01) .passBandRipple(0.01)
.stopBandStart(4000)
.stopBandAmplitude(0.0) .stopBandAmplitude(0.0)
.stopBandRipple(0.008) .stopBandStart(6500)
.stopBandRipple(0.01)
.build(); .build();
// FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder() // FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder()
@@ -72,6 +81,9 @@ public class FilterViewer extends Application
float[] taps = null; float[] taps = null;
// taps = FilterFactory.getSinc(6000, 1920, 47, Window.WindowType.BLACKMAN);
// taps = FilterFactory.getRootRaisedCosine(10, 10, 0.2f);
// taps = FilterFactory.getLowPass(48000, 6750, 7500, 60, // taps = FilterFactory.getLowPass(48000, 6750, 7500, 60,
// Window.WindowType.HAMMING, true); // Window.WindowType.HAMMING, true);

View File

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

View File

@@ -3,8 +3,7 @@ package io.github.dsheirer.dsp.filter.fir.complex;
import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.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;

View File

@@ -5,54 +5,86 @@ import org.apache.commons.lang3.Validate;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.text.DecimalFormat;
import java.util.Arrays;
public class RealInterpolator extends Interpolator public class RealInterpolator extends Interpolator
{ {
private final static Logger mLog = LoggerFactory.getLogger( RealInterpolator.class ); private final static Logger mLog = LoggerFactory.getLogger(RealInterpolator.class);
private float mGain; private float mGain;
public RealInterpolator( float gain ) /**
{ * Provides an interpolated sample point along an eight-sample waveform representation using 128 filters to
mGain = gain; * provide approximate interpolation with a resolution of 1/128th of a sample.
} *
/** * @param gain to apply to the interpolated sample
* Calculates an interpolated value from the 8 samples beginning at the */
* offset index. Uses the mu value to determine which of 128 filter kernels public RealInterpolator(float gain)
* to use in order to closely approximate the ideal frequency response for {
* the value of mu, ranging from 0.0 to 1.0, indicating the interpolated mGain = gain;
* position in the sample set ranging from offset to offset + 7. }
*
* @param samples - sample array of length at least offset + 7
*
* @param mu - interpolated sample position between 0 and 1.0
*
* @return - interpolated sample value
*/
public float filter( float[] samples, int offset, float mu )
{
/* Ensure we have enough samples in the array */
Validate.isTrue(samples.length >= offset + 7);
/* Identify the filter bank that corresponds to mu */ /**
int index = (int)( NSTEPS * mu ); * Calculates an interpolated value from eight samples that start at the offset into the sample array. The
* interpolated sample will fall within the middle of the eight sample array, between indexes offset+3 and
* offset+4. The mu argument is translated into an index position between 0 and 128, where a mu of 0.0 will be
* converted to index zero and will be equal to the sample at index offset+3 and a mu of 1.0 will be equal to
* the sample at offset+4. All mu values between 0.0 and 1.0 will be converted to a 1 - 127 index and will
* produce an approximated value from among 127 interpolated sample points between indexes offset+3 and offset+4.
*
* @param samples - sample array of length at least offset + 7
* @param mu - interpolated sample position between 0.0 and 1.0
* @return - interpolated sample value
*/
public float filter(float[] samples, int offset, float mu)
{
/* Ensure we have enough samples in the array */
Validate.isTrue(samples.length >= offset + 7);
float accumulator = ( TAPS[ index ][ 7 ] * samples[ offset ] ); /* Identify the filter bank that corresponds to mu */
accumulator += ( TAPS[ index ][ 6 ] * samples[ offset + 1 ] ); int index = (int)(NSTEPS * mu);
accumulator += ( TAPS[ index ][ 5 ] * samples[ offset + 2 ] );
accumulator += ( TAPS[ index ][ 4 ] * samples[ offset + 3 ] );
accumulator += ( TAPS[ index ][ 3 ] * samples[ offset + 4 ] );
accumulator += ( TAPS[ index ][ 2 ] * samples[ offset + 5 ] );
accumulator += ( TAPS[ index ][ 1 ] * samples[ offset + 6 ] );
accumulator += ( TAPS[ index ][ 0 ] * samples[ offset + 7 ] );
return accumulator * mGain; float accumulator = (TAPS[index][7] * samples[offset]);
} accumulator += (TAPS[index][6] * samples[offset + 1]);
accumulator += (TAPS[index][5] * samples[offset + 2]);
accumulator += (TAPS[index][4] * samples[offset + 3]);
accumulator += (TAPS[index][3] * samples[offset + 4]);
accumulator += (TAPS[index][2] * samples[offset + 5]);
accumulator += (TAPS[index][1] * samples[offset + 6]);
accumulator += (TAPS[index][0] * samples[offset + 7]);
public Complex filter( float[] iSamples, float[] qSamples, int offset, float mu ) return accumulator * mGain;
{ }
float i = filter( iSamples, offset, mu );
float q = filter( qSamples, offset, mu );
return new Complex( i, q ); public Complex filter(float[] iSamples, float[] qSamples, int offset, float mu)
} {
float i = filter(iSamples, offset, mu);
float q = filter(qSamples, offset, mu);
return new Complex(i, q);
}
public static void main(String[] args)
{
RealInterpolator interpolator = new RealInterpolator(1.0f);
DecimalFormat decimalFormat = new DecimalFormat("0.0000");
double TWO_PI = Math.PI * 2.0;
float[] samples = new float[16];
for(int x = 0; x < 16; x++)
{
samples[x] = (float)Math.sin(TWO_PI * (double)x / 8.0);
}
mLog.debug("Samples: " + Arrays.toString(samples));
for(float x = 0.0f; x <= 1.01f; x += 0.1f)
{
mLog.debug(decimalFormat.format(x) + ": " + decimalFormat.format(interpolator.filter(samples, 1, x)));
}
}
} }

View File

@@ -17,62 +17,87 @@
******************************************************************************/ ******************************************************************************/
package io.github.dsheirer.dsp.fm; package io.github.dsheirer.dsp.fm;
import io.github.dsheirer.sample.complex.Complex;
public abstract class FMDemodulator public abstract class FMDemodulator
{ {
private float mPreviousI = 0.0f; private float mPreviousI = 0.0f;
private float mPreviousQ = 0.0f; private float mPreviousQ = 0.0f;
protected float mGain; protected float mGain;
public FMDemodulator( float gain ) public FMDemodulator(float gain)
{ {
mGain = gain; mGain = gain;
} }
public float demodulate( float currentI, float currentQ ) public float demodulate(float currentI, float currentQ)
{ {
/** /**
* Multiply the current sample against the complex conjugate of the * Multiply the current sample against the complex conjugate of the
* previous sample to derive the phase delta between the two samples * previous sample to derive the phase delta between the two samples
* *
* Negating the previous sample quadrature produces the conjugate * Negating the previous sample quadrature produces the conjugate
*/ */
double inphase = ( currentI * mPreviousI ) - ( currentQ * -mPreviousQ ); double inphase = (currentI * mPreviousI) - (currentQ * -mPreviousQ);
double quadrature = ( currentQ * mPreviousI ) + ( currentI * -mPreviousQ ); double quadrature = (currentQ * mPreviousI) + (currentI * -mPreviousQ);
double angle = 0.0f; double angle = 0.0f;
//Check for divide by zero //Check for divide by zero
if( inphase != 0 ) if(inphase != 0)
{ {
/** /**
* Use the arc-tangent of quadrature divided by inphase to * Use the arc-tangent of quadrature divided by inphase to
* get the phase angle (+/-) which was directly manipulated by the * get the phase angle (+/-) which was directly manipulated by the
* original message waveform during the modulation. This value now * original message waveform during the modulation. This value now
* serves as the instantaneous amplitude of the demodulated signal * serves as the instantaneous amplitude of the demodulated signal
*/ */
double denominator = 1.0d / inphase; double denominator = 1.0d / inphase;
angle = Math.atan( (double)quadrature * denominator ); angle = Math.atan((double)quadrature * denominator);
} }
/** /**
* Store the current sample to use during the next iteration * Store the current sample to use during the next iteration
*/ */
mPreviousI = currentI; mPreviousI = currentI;
mPreviousQ = currentQ; mPreviousQ = currentQ;
return (float)( angle * mGain ); return (float)(angle * mGain);
} }
public abstract void dispose(); public static double demodulate(Complex previous, Complex current)
{
double inphase = (current.inphase() * previous.inphase()) - (current.quadrature() * -previous.quadrature());
double quadrature = (current.quadrature() * previous.inphase()) + (current.inphase() * -previous.quadrature());
public void reset() double angle = 0.0f;
{
mPreviousI = 0.0f;
mPreviousQ = 0.0f;
}
public void setGain( float gain ) //Check for divide by zero
{ if(inphase != 0)
mGain = gain; {
} /**
* Use the arc-tangent of quadrature divided by inphase to
* get the phase angle (+/-) which was directly manipulated by the
* original message waveform during the modulation. This value now
* serves as the instantaneous amplitude of the demodulated signal
*/
double denominator = 1.0d / inphase;
angle = Math.atan((double)quadrature * denominator);
}
return angle;
}
public abstract void dispose();
public void reset()
{
mPreviousI = 0.0f;
mPreviousQ = 0.0f;
}
public void setGain(float gain)
{
mGain = gain;
}
} }

View File

@@ -26,8 +26,7 @@ import org.slf4j.LoggerFactory;
public class FMDemodulator_CB extends FMDemodulator implements Listener<ComplexBuffer> public class FMDemodulator_CB extends FMDemodulator implements Listener<ComplexBuffer>
{ {
private final static Logger mLog = private final static Logger mLog = LoggerFactory.getLogger( FMDemodulator_CB.class );
LoggerFactory.getLogger( FMDemodulator_CB.class );
private Listener<RealBuffer> mListener; private Listener<RealBuffer> mListener;

View File

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

View File

@@ -5,27 +5,34 @@ import io.github.dsheirer.sample.complex.Complex;
public class ComplexGain implements Listener<Complex> public class ComplexGain implements Listener<Complex>
{ {
private float mGain; private float mGain;
private Listener<Complex> mListener; private Listener<Complex> mListener;
public ComplexGain( float gain ) public ComplexGain(float gain)
{ {
mGain = gain; mGain = gain;
} }
@Override public Complex apply(Complex sample)
public void receive( Complex sample ) {
{ sample.multiply(mGain);
if( mListener != null )
{
sample.multiply( mGain );
mListener.receive( sample ); return sample;
} }
}
public void setListener( Listener<Complex> listener ) @Override
{ public void receive(Complex sample)
mListener = listener; {
} if(mListener != null)
{
apply(sample);
mListener.receive(sample);
}
}
public void setListener(Listener<Complex> listener)
{
mListener = listener;
}
} }

View File

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

View File

@@ -0,0 +1,162 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.psk;
import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.Complex;
import io.github.dsheirer.sample.complex.ComplexBuffer;
import io.github.dsheirer.sample.complex.ComplexSampleListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DQPSKDemodulator implements ComplexSampleListener
{
private final static Logger mLog = LoggerFactory.getLogger(DQPSKDemodulator.class);
protected InterpolatingSampleBuffer mInterpolatingSampleBuffer;
protected IPhaseLockedLoop mPLL;
protected DQPSKSymbolEvaluator mSymbolEvaluator = new DQPSKSymbolEvaluator();
private Complex mReceivedSample = new Complex(0, 0);
private Complex mPreviousPrecedingSample = new Complex(0, 0);
private Complex mPreviousCurrentSample = new Complex(0, 0);
private Complex mPrecedingSample = new Complex(0, 0);
private Complex mCurrentSample = new Complex(0, 0);
private Complex mPrecedingSymbol = new Complex(0, 0);
protected Complex mCurrentSymbol = new Complex(0, 0);
private Listener<Dibit> mDibitListener;
/**
* Decoder for Differential Quaternary Phase Shift Keying (DQPSK). This decoder uses both a Costas Loop (PLL) and
* a custom DQPSK symbol timing error detector to automatically align to the incoming carrier frequency and to
* adjust for any changes in symbol timing.
*
* This detector is optimized for constant amplitude DQPSK symbols like C4FM.
*
* @param phaseLockedLoop for tracking carrier frequency error
* @param interpolatingSampleBuffer to hold samples for interpolating a symbol
*/
public DQPSKDemodulator(IPhaseLockedLoop phaseLockedLoop, InterpolatingSampleBuffer interpolatingSampleBuffer)
{
mPLL = phaseLockedLoop;
mInterpolatingSampleBuffer = interpolatingSampleBuffer;
}
/**
* Processes a (filtered) buffer containing complex samples for decoding
* @param complexBuffer with complex samples
*/
public void receive(ComplexBuffer complexBuffer)
{
float[] samples = complexBuffer.getSamples();
for(int x = 0; x < samples.length; x += 2)
{
receive(samples[x], samples[x + 1]);
}
}
/**
* Processes a complex sample for decoding. Once sufficient samples are buffered, a symbol decision is made.
* @param inphase value for the sample
* @param quadrature value for the sample
*/
@Override
public void receive(float inphase, float quadrature)
{
//Update current sample with values
mReceivedSample.setValues(inphase, quadrature);
//Mix current sample with costas loop to remove any rotation that is present from a mis-tuned carrier frequency
mReceivedSample.multiply(mPLL.incrementAndGetCurrentVector());
//Store the sample in the interpolating buffer
mInterpolatingSampleBuffer.receive(mReceivedSample);
//Calculate the symbol once we've stored enough samples
if(mInterpolatingSampleBuffer.hasSymbol())
{
calculateSymbol();
}
}
/**
* Calculates a symbol from the interpolating buffer
*/
protected void calculateSymbol()
{
//Get preceding sample and an interpolated current sample from the interpolating buffer
mPrecedingSample = mInterpolatingSampleBuffer.getPrecedingSample();
mCurrentSample = mInterpolatingSampleBuffer.getCurrentSample();
//Calculate preceding, current and following symbols as the delta rotation compared to the previous samples
mPrecedingSymbol.setInphase(Complex.multiplyInphase(mPrecedingSample.inphase(), mPrecedingSample.quadrature(),
mPreviousPrecedingSample.inphase(), -mPreviousPrecedingSample.quadrature()));
mPrecedingSymbol.setQuadrature(Complex.multiplyQuadrature(mPrecedingSample.inphase(), mPrecedingSample.quadrature(),
mPreviousPrecedingSample.inphase(), -mPreviousPrecedingSample.quadrature()));
mCurrentSymbol.setInphase(Complex.multiplyInphase(mCurrentSample.inphase(), mCurrentSample.quadrature(),
mPreviousCurrentSample.inphase(), -mPreviousCurrentSample.quadrature()));
mCurrentSymbol.setQuadrature(Complex.multiplyQuadrature(mCurrentSample.inphase(), mCurrentSample.quadrature(),
mPreviousCurrentSample.inphase(), -mPreviousCurrentSample.quadrature()));
//Set gain to unity before we calculate the error value
mPrecedingSymbol.normalize();
mCurrentSymbol.normalize();
mSymbolEvaluator.setSymbol(mPrecedingSymbol, mCurrentSymbol);
mInterpolatingSampleBuffer.resetAndAdjust(mSymbolEvaluator.getTimingError());
//Store current samples/symbols to use for the next period
mPreviousPrecedingSample.setValues(mPrecedingSample);
mPreviousCurrentSample.setValues(mCurrentSample);
//Calculate the phase error of the current symbol relative to the expected constellation and provide
//feedback to the PLL
mPLL.adjust(clip(mSymbolEvaluator.getPhaseError(), 0.5f));
if(mDibitListener != null)
{
mDibitListener.receive(mSymbolEvaluator.getSymbolDecision());
}
}
/**
* Registers the listener to receive dibit symbol decisions from this demodulator
*/
public void setDibitListener(Listener<Dibit> listener)
{
mDibitListener = listener;
}
/**
* Constrains value to the range of ( -maximum <> maximum )
*/
public static float clip(float value, float maximum)
{
if(value > maximum)
{
return maximum;
}
else if(value < -maximum)
{
return -maximum;
}
return value;
}
}

View File

@@ -0,0 +1,131 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.psk;
import io.github.dsheirer.dsp.psk.pll.CostasLoop;
import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.Complex;
public class DQPSKDemodulatorInstrumented extends DQPSKDemodulator
{
private Listener<SymbolDecisionData> mSymbolDecisionDataListener;
private Listener<Double> mSamplesPerSymbolListener;
private Listener<Complex> mSymbolListener;
private Listener<Double> mPLLErrorListener;
private Listener<Double> mPLLFrequencyListener;
private double mSampleRate;
/**
* Decoder for Differential Quaternary Phase Shift Keying (DQPSK). This decoder uses both a Costas Loop (PLL) and
* a custom DQPSK symbol timing error detector to automatically align to the incoming carrier frequency and to
* adjust for any changes in symbol timing.
*
* This detector is optimized for constant amplitude DQPSK symbols like C4FM.
*
* @param phaseLockedLoop for tracking carrier frequency error
* @param interpolatingSampleBuffer
* @param sampleRate of the incoming complex sample stream
*/
public DQPSKDemodulatorInstrumented(IPhaseLockedLoop phaseLockedLoop,
InterpolatingSampleBufferInstrumented interpolatingSampleBuffer,
double sampleRate)
{
super(phaseLockedLoop, interpolatingSampleBuffer);
mSampleRate = sampleRate;
}
/**
* Overrides the parent class symbol calculation to capture eye diagram data
*/
protected void calculateSymbol()
{
//Eye diagram listener
if(mSymbolDecisionDataListener != null)
{
mSymbolDecisionDataListener.receive(
((InterpolatingSampleBufferInstrumented)mInterpolatingSampleBuffer).getSymbolDecisionData());
}
super.calculateSymbol();
if(mSamplesPerSymbolListener != null)
{
mSamplesPerSymbolListener.receive((double)mInterpolatingSampleBuffer.getSamplingPoint());
}
//Send to an external constellation symbol listener when registered
if(mSymbolListener != null)
{
mSymbolListener.receive(mCurrentSymbol);
}
if(mPLLErrorListener != null)
{
mPLLErrorListener.receive((double)mSymbolEvaluator.getPhaseError());
}
if(mPLLFrequencyListener != null)
{
double loopFrequency = ((CostasLoop)mPLL).getLoopFrequency();
loopFrequency *= mSampleRate / (2.0 * Math.PI);
mPLLFrequencyListener.receive(loopFrequency);
}
}
/**
* Registers a listener to receive symbol decision data to produce an eye diagram.
*/
public void setSymbolDecisionDataListener(Listener<SymbolDecisionData> listener)
{
mSymbolDecisionDataListener = listener;
}
/**
* Registers the listener to receive PLL error values
*/
public void setSamplesPerSymbolListener(Listener<Double> listener)
{
mSamplesPerSymbolListener = listener;
}
/**
* Registers the listener to receive decoded QPSK symbols
*/
public void setSymbolListener(Listener<Complex> listener)
{
mSymbolListener = listener;
}
/**
* Registers the listener to receive PLL error values
*/
public void setPLLErrorListener(Listener<Double> listener)
{
mPLLErrorListener = listener;
}
/**
* Registers the listener to receive PLL error values
*/
public void setPLLFrequencyListener(Listener<Double> listener)
{
mPLLFrequencyListener = listener;
}
}

View File

@@ -0,0 +1,137 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.psk;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.sample.complex.Complex;
public class DQPSKSymbolEvaluator implements IDQPSKSymbolEvaluator
{
private static final Complex ROTATE_FROM_PLUS_135 = Complex.fromAngle(-3.0 * Math.PI / 4.0);
private static final Complex ROTATE_FROM_PLUS_45 = Complex.fromAngle(-1.0 * Math.PI / 4.0);
private static final Complex ROTATE_FROM_MINUS_45 = Complex.fromAngle(1.0 * Math.PI / 4.0);
private static final Complex ROTATE_FROM_MINUS_135 = Complex.fromAngle(3.0 * Math.PI / 4.0);
private Complex mSymbolError = new Complex(0, 0);
private float mPhaseError = 0.0f;
private float mTimingError = 0.0f;
private float mTimingErrorPolarity = 1.0f;
private Dibit mSymbolDecision = Dibit.D00_PLUS_1;
/**
* Decision-directed symbol phase and timing error detector and symbol decision slicer.
*
* Symbol decision is based on the closest reference quadrant for the sampled symbol.
*
* Phase and timing error are calculated as the angular distance of the sampled symbol from the reference
* symbol. Timing error is further corrected to compensate for clock-wise or counter-clock-wise vector rotation
* relative to the immediately preceding symbol.
*/
public DQPSKSymbolEvaluator()
{
}
/**
* Sets the preceding and current symbols to be evaluated for phase and timing errors and to determine the
* transmitted symbol relative to the closest reference symbol. After invoking this method, you can access the
* phase and timing errors and the symbol decision via their respective accessor methods.
*
* Phase and timing error values are calculated by first determining the symbol and then calculating the phase
* and timing errors relative to the reference symbol. The timing error is corrected with the appropriate sign
* relative to the angular vector rotation so that the error value indicates the correct error direction.
*
* @param preceding differential-decoded sample/symbol used to detect rotation - does NOT need to be interpolated
* since it's only a reference
* @param current symbol to be evaluated - interpolated
*/
@Override
public void setSymbol(Complex preceding, Complex current)
{
mSymbolError.setValues(current);
if(mSymbolError.quadrature() > 0.0f)
{
if(mSymbolError.inphase() > 0.0f)
{
mSymbolDecision = Dibit.D00_PLUS_1;
mTimingErrorPolarity = (preceding.quadrature() > current.quadrature() ? 1.0f : -1.0f);
mSymbolError.multiply(ROTATE_FROM_PLUS_45);
}
else
{
mSymbolDecision = Dibit.D01_PLUS_3;
mTimingErrorPolarity = (preceding.quadrature() < current.quadrature() ? 1.0f : -1.0f);
mSymbolError.multiply(ROTATE_FROM_PLUS_135);
}
}
else
{
if(mSymbolError.inphase() > 0.0f)
{
mSymbolDecision = Dibit.D10_MINUS_1;
mTimingErrorPolarity = (preceding.quadrature() > current.quadrature() ? 1.0f : -1.0f);
mSymbolError.multiply(ROTATE_FROM_MINUS_45);
}
else
{
mSymbolDecision = Dibit.D11_MINUS_3;
mTimingErrorPolarity = (preceding.quadrature() < current.quadrature() ? 1.0f : -1.0f);
mSymbolError.multiply(ROTATE_FROM_MINUS_135);
}
}
//Since we've rotated the error symbol back to 0 radians, the quadrature value closely approximates the
//arctan of the error angle relative to 0 radians and this provides our error value
mPhaseError = -mSymbolError.quadrature();
//Timing error is the same as phase error with the sign corrected according to the vector's angular rotation
mTimingError = mSymbolError.quadrature() * mTimingErrorPolarity;
}
/**
* Phase error of the symbol relative to the nearest reference symbol.
*
* @return phase error in radians of distance from the reference symbol.
*/
@Override
public float getPhaseError()
{
return mPhaseError;
}
/**
* Timing error of the symbol relative to the nearest reference symbol.
*
* @return timing error in radians of angular distance from the reference symbol recognizing that the symbol
* originates at zero radians and rotates toward the intended reference symbol, therefore the error value indicates
* if the symbol was sampled early (-) or late (+) relative to the reference symbol.
*/
@Override
public float getTimingError()
{
return mTimingError;
}
/**
* Reference symbol that is closest to the transmitted/sampled symbol.
*/
@Override
public Dibit getSymbolDecision()
{
return mSymbolDecision;
}
}

View File

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

View File

@@ -0,0 +1,50 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.psk;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.sample.complex.Complex;
public interface IDQPSKSymbolEvaluator
{
/**
* Sets the sampled symbol to be evaluated. Note: the symbol's gain value MUST be normalized to the unit circle.
*
* @param preceding symbol used to detect (counter-)clock-wise rotation
* @param symbol currently being sampled
*/
void setSymbol(Complex preceding, Complex symbol);
/**
* Phase error of the symbol relative to the closest reference symbol
* @return angular phase error in radians
*/
float getPhaseError();
/**
* Timing error of the symbol relative to the closest reference symbol
* @return angular timing error in radians where a positive value indicates early sampling and sample timing needs
* to be increased and a negative value indicates late sampling where sample timing needs to be decreased.
*/
float getTimingError();
/**
* Dibit decision indicating the reference symbol location closest to the symbol.
* @return symbol decision
*/
Dibit getSymbolDecision();
}

View File

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

View File

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

View File

@@ -0,0 +1,167 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.psk;
import io.github.dsheirer.dsp.filter.interpolator.RealInterpolator;
import io.github.dsheirer.sample.complex.Complex;
public class InterpolatingSampleBuffer
{
private static final float MAXIMUM_DEVIATION_SAMPLES_PER_SYMBOL = 0.02f; // +/- 2% deviation
private Complex mPrecedingSample = new Complex(0,0);
private Complex mCurrentSample = new Complex(0,0);
private float[] mDelayLineInphase;
private float[] mDelayLineQuadrature;
private int mDelayLinePointer = 0;
private int mTwiceSamplesPerSymbol;
private float mSamplingPoint;
private float mSampleCounterGain = 1.0f;
private float mDetectedSamplesPerSymbol;
private float mDetectedSamplesPerSymbolGain = 0.1f * mSampleCounterGain * mSampleCounterGain;
private float mMaximumSamplesPerSymbol;
private float mMinimumSamplesPerSymbol;
private RealInterpolator mInterpolator = new RealInterpolator(1.0f);
/**
* Buffer to store complex sample data and produce interpolated samples.
* @param samplesPerSymbol
*/
public InterpolatingSampleBuffer(float samplesPerSymbol)
{
mSamplingPoint = samplesPerSymbol;
mDetectedSamplesPerSymbol = samplesPerSymbol;
mMaximumSamplesPerSymbol = samplesPerSymbol * (1.0f + MAXIMUM_DEVIATION_SAMPLES_PER_SYMBOL);
mMinimumSamplesPerSymbol = samplesPerSymbol * (1.0f - MAXIMUM_DEVIATION_SAMPLES_PER_SYMBOL);
mTwiceSamplesPerSymbol = (int)Math.floor(2.0 * samplesPerSymbol);
mDelayLineInphase = new float[2 * mTwiceSamplesPerSymbol];
mDelayLineQuadrature = new float[2 * mTwiceSamplesPerSymbol];
}
/**
* Stores the sample in the buffer and updates pointers.
* @param sample
*/
public void receive(Complex sample)
{
mSamplingPoint--;
//Fill up the delay line to use with the interpolator
mDelayLineInphase[mDelayLinePointer] = sample.inphase();
mDelayLineInphase[mDelayLinePointer + mTwiceSamplesPerSymbol] = sample.inphase();
mDelayLineQuadrature[mDelayLinePointer] = sample.quadrature();
mDelayLineQuadrature[mDelayLinePointer + mTwiceSamplesPerSymbol] = sample.quadrature();
//Increment pointer and keep pointer in bounds
mDelayLinePointer++;
mDelayLinePointer = mDelayLinePointer % mTwiceSamplesPerSymbol;
}
/**
* Updates the internal sample counter with additional samples to add to the symbol sample counter.
* @param samplesToAdd to the sample counter
*/
public void increaseSampleCounter(float samplesToAdd)
{
mSamplingPoint += samplesToAdd;
}
/**
* Adjusts samples per symbol and symbol timing counters and increments the sample counter to collect another
* symbol.
*
* @param symbolTimingError from a symbol timing error detector
*/
public void resetAndAdjust(float symbolTimingError)
{
//Adjust detected samples per symbol based on timing error
mDetectedSamplesPerSymbol = mDetectedSamplesPerSymbol + (symbolTimingError * mDetectedSamplesPerSymbolGain);
//Constrain detected samples per symbol to min/max values
if(mDetectedSamplesPerSymbol > mMaximumSamplesPerSymbol)
{
mDetectedSamplesPerSymbol = mMaximumSamplesPerSymbol;
}
if(mDetectedSamplesPerSymbol < mMinimumSamplesPerSymbol)
{
mDetectedSamplesPerSymbol = mMinimumSamplesPerSymbol;
}
//Add another symbol's worth of samples to the counter and adjust timing based on gardner error
increaseSampleCounter((mDetectedSamplesPerSymbol + (symbolTimingError * mSampleCounterGain)));
}
/**
* Current value of the detected samples per symbol
*/
public float getSamplingPoint()
{
return mSamplingPoint;
}
/**
* Indicates if this buffer has accumulated a enough samples to represent a full symbol
*/
public boolean hasSymbol()
{
return mSamplingPoint < 1.0f;
}
/**
* Un-interpolated sample that precedes the current interpolated sampling point. The current sampling point falls
* somewhere between sample index 3 and index 4, therefore the preceding sample is located at index 3.
*/
public Complex getPrecedingSample()
{
mPrecedingSample.setValues(mDelayLineInphase[mDelayLinePointer + 4], mDelayLineQuadrature[mDelayLinePointer + 3]);
return mPrecedingSample;
}
/**
* Interpolated middle sample for the symbol. Note: this method should only be invoked after testing for a
* complete symbol with the hasSymbol() method.
*/
public Complex getCurrentSample()
{
/* Calculate interpolated current sample */
mCurrentSample.setValues(getInphase(mSamplingPoint), getQuadrature(mSamplingPoint));
return mCurrentSample;
}
/**
* Returns the interpolated inphase value for the specified offset
* @param interpolation into the buffer to calculate the interpolated sample
* @return inphase value of the interpolated sample
*/
public float getInphase(float interpolation)
{
return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer + 1, interpolation);
}
/**
* Returns the interpolated quadrature value for the specified offset
* @param interpolation into the buffer to calculate the interpolated sample
* @return quadrature value of the interpolated sample
*/
public float getQuadrature(float interpolation)
{
return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer + 1, interpolation);
}
}

View File

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

View File

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

View File

@@ -2,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,402 @@ 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 reset()
while( mLoopPhase > TWO_PI ) {
{ mLoopPhase = 0.0f;
mLoopPhase -= TWO_PI; mLoopFrequency = 0.0f;
} }
while( mLoopPhase < -TWO_PI ) @Override
{ public void adjust(double error)
mLoopPhase += TWO_PI; {
} adjust(error);
} }
/** @Override
* Current vector of the loop rotated by THETA degrees. public void correctInversion(double correction)
*/ {
public Complex getCurrentVector() correctPhaseError(correction);
{ }
private void unwrapPhase()
{
while(mLoopPhase > TWO_PI)
{
mLoopPhase -= TWO_PI;
}
while(mLoopPhase < -TWO_PI)
{
mLoopPhase += TWO_PI;
}
}
/**
* Current vector of the loop rotated by THETA degrees.
*/
public Complex getCurrentVector()
{
// return ComplexSample.fromAngle( mLoopPhase + THETA ); // return 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;
} }
} }
} }

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,93 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.psk;
import io.github.dsheirer.buffer.ComplexCircularBuffer;
import io.github.dsheirer.sample.complex.Complex;
public class SymbolDecisionData
{
//45 degrees rotation to orient the symbol to a polar axis to make error calculation easy/efficient
public static final Complex DIFFERENTIAL_OFFSET = Complex.fromAngle(Math.PI / 4.0d);
private ComplexCircularBuffer mBuffer;
private float mSamplingPoint;
/**
* Circular buffer for capturing two symbols worth of sample data for use in instrumentation.
* @param samplesPerSymbol to size the buffer
*/
public SymbolDecisionData(int samplesPerSymbol)
{
mBuffer = new ComplexCircularBuffer(2 * samplesPerSymbol);
}
/**
* Stores the complex sample in the buffer
*/
public void receive(float inphase, float quadrature)
{
mBuffer.put(new Complex(inphase, quadrature));
}
/**
* Stores the complex sample in the buffer
*/
public void receive(Complex complex)
{
mBuffer.put(complex.copy());
}
/**
* Sets the fractional sampling point for the current symbol to use in interpolating the current symbol.
*/
public void setSamplingPoint(float samplingPoint)
{
mSamplingPoint = samplingPoint;
}
/**
* Fractional sampling point
*/
public float getSamplingPoint()
{
return mSamplingPoint;
}
/**
* Array of demodulated samples representing the current symbol where each current sample is demodulated against
* the previous symbol's sample to produce the differential demodulated sample. Samples are in time order.
*
* Note: differential decoding is on an integral samples-per-symbol basis and does not use fractional interpolation.
*/
public Complex[] getDemodulated()
{
Complex[] samples = mBuffer.getAll();
int length = samples.length / 2;
Complex[] demodulated = new Complex[length];
for(int x = 0; x < samples.length / 2; x++)
{
Complex copy = samples[length + x].copy();
copy.multiply(samples[x].conjugate());
demodulated[x] = copy;
}
return demodulated;
}
}

View File

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

View File

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

View File

@@ -0,0 +1,59 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.psk.pll;
import io.github.dsheirer.sample.complex.Complex;
/**
* Phase-Locked Loop (PLL) interface
*/
public interface IPhaseLockedLoop
{
/**
* Increments the PLL by one sample period
*/
void increment();
/**
* Applies an error value to the PLL.
*
* @param error value in the range of -PI <> +PI
*/
void adjust(double error);
/**
* Adjusts the current phase of the loop to account for +/-90 or 180 degree inverted phase locks when detected
* @param correction to apply to the phase of the loop
*/
void correctInversion(double correction);
/**
* Current vector of the PLL to use in de-spinning the incoming samples.
* @return complex vector
*/
Complex getCurrentVector();
/**
* Increments the phase rotation and gets the current vector of the PLL to use in de-spinning the incoming samples.
* @return complex vector
*/
Complex incrementAndGetCurrentVector();
/**
* Reset tracking to 0
*/
void reset();
}

View File

@@ -0,0 +1,42 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.psk.pll;
/**
* Phase Locked Loop (PLL) tracking bandwidth - controls how fast/slow the PLL tracks the carrier frequency
*/
public enum Tracking
{
FASTEST(50.0),
FAST(100.0),
NORMAL(200.0),
SLOW(400.0);
private double mLoopBandwidth;
Tracking(double loopBandwidth)
{
mLoopBandwidth = loopBandwidth;
}
/**
* Loop bandwidth setting
*/
public double getLoopBandwidth()
{
return mLoopBandwidth;
}
}

View File

@@ -2,15 +2,19 @@ package io.github.dsheirer.instrument.gui;
import io.github.dsheirer.alias.AliasModel; import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.audio.broadcast.BroadcastModel; import io.github.dsheirer.audio.broadcast.BroadcastModel;
import io.github.dsheirer.controller.channel.Channel;
import io.github.dsheirer.controller.channel.ChannelModel; import io.github.dsheirer.controller.channel.ChannelModel;
import io.github.dsheirer.controller.channel.map.ChannelMapModel; import io.github.dsheirer.controller.channel.map.ChannelMapModel;
import io.github.dsheirer.module.decode.DecodeConfigurationEditor; import io.github.dsheirer.module.decode.DecodeConfigurationEditor;
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
import io.github.dsheirer.playlist.PlaylistManager; import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.source.IControllableFileSource; import io.github.dsheirer.source.IControllableFileSource;
import net.miginfocom.swing.MigLayout; import net.miginfocom.swing.MigLayout;
import javax.swing.*; import javax.swing.JButton;
import java.awt.*; import javax.swing.JDesktopPane;
import javax.swing.JInternalFrame;
import java.awt.Dimension;
import java.awt.event.ActionEvent; import java.awt.event.ActionEvent;
import java.awt.event.ActionListener; import java.awt.event.ActionListener;
@@ -18,14 +22,14 @@ public class DecoderSelectionFrame extends JInternalFrame
{ {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private Channel mChannel = new Channel();
private DecodeConfigurationEditor mDecodeEditor = new DecodeConfigurationEditor(null); private DecodeConfigurationEditor mDecodeEditor = new DecodeConfigurationEditor(null);
private PlaylistManager mPlaylistManager; private PlaylistManager mPlaylistManager;
private IControllableFileSource mSource; private IControllableFileSource mSource;
private JDesktopPane mDesktop; private JDesktopPane mDesktop;
public DecoderSelectionFrame(JDesktopPane desktop, public DecoderSelectionFrame(JDesktopPane desktop, IControllableFileSource source)
IControllableFileSource source)
{ {
AliasModel aliasModel = new AliasModel(); AliasModel aliasModel = new AliasModel();
BroadcastModel broadcastModel = new BroadcastModel(null); BroadcastModel broadcastModel = new BroadcastModel(null);
@@ -37,6 +41,7 @@ public class DecoderSelectionFrame extends JInternalFrame
mDesktop = desktop; mDesktop = desktop;
mSource = source; mSource = source;
mDecodeEditor.setItem(mChannel);
initGUI(); initGUI();
} }
@@ -72,17 +77,23 @@ public class DecoderSelectionFrame extends JInternalFrame
@Override @Override
public void actionPerformed(ActionEvent arg0) public void actionPerformed(ActionEvent arg0)
{ {
// DecodeConfiguration config = mDecodeEditor.getDecodeConfig(); mDecodeEditor.save();
//
// if( config != null ) Channel channel = mDecodeEditor.getItem();
// {
// DecoderViewFrame decoderFrame = new DecoderViewFrame( if(channel != null)
// mPlaylistManager, null, mSource ); {
// DecodeConfiguration config = channel.getDecodeConfiguration();
// decoderFrame.setVisible( true );
// if( config != null )
// mDesktop.add( decoderFrame ); {
// } DecoderViewFrame decoderFrame = new DecoderViewFrame(mPlaylistManager, channel, mSource );
decoderFrame.setVisible( true );
mDesktop.add( decoderFrame );
}
}
} }
}); });
} }

View File

@@ -0,0 +1,183 @@
/*******************************************************************************
* 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;
import io.github.dsheirer.instrument.gui.viewer.decoder.DecoderPaneFactory;
import io.github.dsheirer.module.decode.DecoderType;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.layout.BorderPane;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
/**
* Application for viewing and working with demodulators and recording files.
*/
public class DemodulatorViewerFX extends Application
{
private final static Logger mLog = LoggerFactory.getLogger(DemodulatorViewerFX.class);
private Stage mStage;
private MenuBar mMenuBar;
private RecentFilesMenu mRecentFilesMenu;
private ViewerDesktop mViewerDesktop;
@Override
public void start(Stage primaryStage) throws Exception
{
mStage = primaryStage;
setTitle(null);
BorderPane borderPane = new BorderPane();
borderPane.setTop(getMenuBar());
borderPane.setCenter(getViewerDesktop());
Scene scene = new Scene(borderPane, 1500, 900);
mStage.setScene(scene);
mStage.show();
}
private void setTitle(DecoderType decoderType)
{
if(decoderType != null)
{
mStage.setTitle("Demodulator Viewer FX - " + decoderType.getDisplayString());
}
else
{
mStage.setTitle("Demodulator Viewer FX");
}
}
private ViewerDesktop getViewerDesktop()
{
if(mViewerDesktop == null)
{
mViewerDesktop = new ViewerDesktop();
}
return mViewerDesktop;
}
private MenuBar getMenuBar()
{
if(mMenuBar == null)
{
mMenuBar = new MenuBar();
Menu fileMenu = new Menu("File");
MenuItem openMenuItem = new MenuItem("Open ...");
openMenuItem.setOnAction(new EventHandler<ActionEvent>()
{
@Override
public void handle(ActionEvent event)
{
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Open an I/Q recording file");
File file = fileChooser.showOpenDialog(mStage);
getViewerDesktop().load(file);
getRecentFilesMenu().add(file);
}
});
MenuItem closeMenuItem = new MenuItem("Close");
closeMenuItem.setOnAction(new EventHandler<ActionEvent>()
{
@Override
public void handle(ActionEvent event)
{
getViewerDesktop().close();
}
});
MenuItem exitMenuItem = new MenuItem("Exit");
exitMenuItem.setOnAction(new EventHandler<ActionEvent>()
{
@Override
public void handle(ActionEvent event)
{
Platform.exit();
}
});
fileMenu.getItems().addAll(openMenuItem, getRecentFilesMenu(), closeMenuItem, new SeparatorMenuItem(),
exitMenuItem);
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;
}
private RecentFilesMenu getRecentFilesMenu()
{
if(mRecentFilesMenu == null)
{
mRecentFilesMenu = new RecentFilesMenu("Recent","demodulator.viewer", 5);
mRecentFilesMenu.setFileSelectionListener(new RecentFilesMenu.IFileSelectionListener()
{
@Override
public void fileSelected(File file)
{
getViewerDesktop().load(file);
}
});
}
return mRecentFilesMenu;
}
public static void main(String[] args)
{
launch(args);
}
}

View File

@@ -0,0 +1,311 @@
/*******************************************************************************
* 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;
import io.github.dsheirer.instrument.gui.viewer.decoder.DecoderPane;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.ComplexBuffer;
import io.github.dsheirer.source.IControllableFileSource;
import io.github.dsheirer.source.IFrameLocationListener;
import io.github.dsheirer.source.wave.ComplexWaveSource;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import org.kordamp.ikonli.fontawesome.FontAwesome;
import org.kordamp.ikonli.javafx.FontIcon;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.sound.sampled.UnsupportedAudioFileException;
import java.io.File;
import java.io.IOException;
public class PlaybackController extends HBox implements IFrameLocationListener, Listener<ComplexBuffer>
{
private final static Logger mLog = LoggerFactory.getLogger(PlaybackController.class);
private Button mRewindButton;
private TextField mPlaybackPositionText;
private Button mPlay1Button;
private Button mPlay10Button;
private Button mPlay100Button;
private Button mPlay1000Button;
private HBox mControlsBox;
private Label mFileLabel;
private EventHandler<ActionEvent> mPlayEventHandler;
private DecoderPane mSampleRateListener;
private IControllableFileSource mControllableFileSource;
private Broadcaster<ComplexBuffer> mComplexBufferBroadcaster = new Broadcaster<>();
public PlaybackController()
{
super(10);
getChildren().addAll(getControlsBox(), getFileLabel());
setControlsEnabled(false);
}
public void setSampleRateListener(DecoderPane decoderPane)
{
mSampleRateListener = decoderPane;
if(mSampleRateListener != null && mControllableFileSource != null)
{
mSampleRateListener.setSampleRate(mControllableFileSource.getSampleRate());
}
}
public void load(File file)
{
if(file != null && file.isFile())
{
try
{
mControllableFileSource = new ComplexWaveSource(file);
mControllableFileSource.setListener(this);
((ComplexWaveSource)mControllableFileSource).setListener((Listener<ComplexBuffer>)this);
mControllableFileSource.open();
if(mSampleRateListener != null)
{
mSampleRateListener.setSampleRate(mControllableFileSource.getSampleRate());
}
}
catch(UnsupportedAudioFileException e)
{
mLog.error("Unsupported Audio File Type [" + (file != null ? file.getAbsolutePath() : "null") + "]");
Alert alert = new Alert(Alert.AlertType.ERROR, "The file type is unsupported.", ButtonType.OK);
alert.show();
return;
}
catch(IOException ioe)
{
mLog.error("Error opening file [" + (file != null ? file.getAbsolutePath() : "null") + "]", ioe);
Alert alert = new Alert(Alert.AlertType.ERROR, "There was an error opening the file",
ButtonType.OK);
alert.show();
return;
}
getFileLabel().setText(file.getName());
setControlsEnabled(true);
}
}
public void close()
{
if(mControllableFileSource != null)
{
try
{
mControllableFileSource.close();
}
catch(IOException ioe)
{
mLog.error("Error closing file", ioe);
}
}
setControlsEnabled(false);
}
/**
* Adds listener to receive complex buffers from this playback
*/
public void addListener(Listener<ComplexBuffer> listener)
{
mComplexBufferBroadcaster.addListener(listener);
}
/**
* Removes the listener from receiving complex buffers from this playback
*/
public void removeListener(Listener<ComplexBuffer> listener)
{
mComplexBufferBroadcaster.removeListener(listener);
}
/**
* Sets all of the playback controls to the specified enabled state.
*/
private void setControlsEnabled(boolean enabled)
{
getRewindButton().setDisable(!enabled);
getPlaybackPositionText().setDisable(!enabled);
getPlay1Button().setDisable(!enabled);
getPlay10Button().setDisable(!enabled);
getPlay100Button().setDisable(!enabled);
getPlay1000Button().setDisable(!enabled);
}
private HBox getControlsBox()
{
if(mControlsBox == null)
{
mControlsBox = new HBox();
mControlsBox.getChildren().addAll(getRewindButton(), getPlaybackPositionText(), getPlay1Button(),
getPlay10Button(), getPlay100Button(), getPlay1000Button());
}
return mControlsBox;
}
private Button getRewindButton()
{
if(mRewindButton == null)
{
mRewindButton = new Button();
mRewindButton.setGraphic(new FontIcon(FontAwesome.FAST_BACKWARD));
}
return mRewindButton;
}
private Button getPlay1Button()
{
if(mPlay1Button == null)
{
mPlay1Button = new Button("1");
mPlay1Button.setUserData(1);
mPlay1Button.setOnAction(getPlaybackEventHandler());
mPlay1Button.setGraphic(new FontIcon(FontAwesome.PLAY));
}
return mPlay1Button;
}
private Button getPlay10Button()
{
if(mPlay10Button == null)
{
mPlay10Button = new Button("10");
mPlay10Button.setUserData(10);
mPlay10Button.setOnAction(getPlaybackEventHandler());
mPlay10Button.setGraphic(new FontIcon(FontAwesome.FAST_FORWARD));
}
return mPlay10Button;
}
private Button getPlay100Button()
{
if(mPlay100Button == null)
{
mPlay100Button = new Button("100");
mPlay100Button.setUserData(100);
mPlay100Button.setOnAction(getPlaybackEventHandler());
mPlay100Button.setGraphic(new FontIcon(FontAwesome.FAST_FORWARD));
}
return mPlay100Button;
}
private Button getPlay1000Button()
{
if(mPlay1000Button == null)
{
mPlay1000Button = new Button("1000");
mPlay1000Button.setUserData(1000);
mPlay1000Button.setOnAction(getPlaybackEventHandler());
mPlay1000Button.setGraphic(new FontIcon(FontAwesome.FAST_FORWARD));
}
return mPlay1000Button;
}
private EventHandler<ActionEvent> getPlaybackEventHandler()
{
if(mPlayEventHandler == null)
{
mPlayEventHandler = new EventHandler<ActionEvent>()
{
@Override
public void handle(ActionEvent event)
{
int count = (int)((Button)event.getSource()).getUserData();
if(mControllableFileSource != null)
{
try
{
mControllableFileSource.next(count, true);
}
catch(IOException ioe)
{
mLog.error("Error while playing samples");
}
}
}
};
}
return mPlayEventHandler;
}
private TextField getPlaybackPositionText()
{
if(mPlaybackPositionText == null)
{
mPlaybackPositionText = new TextField("0");
mPlaybackPositionText.setAlignment(Pos.CENTER);
mPlaybackPositionText.setPrefWidth(80);
}
return mPlaybackPositionText;
}
private Label getFileLabel()
{
if(mFileLabel == null)
{
mFileLabel = new Label();
}
return mFileLabel;
}
@Override
public void frameLocationUpdated(int location)
{
getPlaybackPositionText().setText(String.valueOf(location));
}
@Override
public void frameLocationReset()
{
mLog.info("Frame location reset ... ignoring?");
}
@Override
public void receive(ComplexBuffer complexBuffer)
{
mComplexBufferBroadcaster.broadcast(complexBuffer);
}
}

View File

@@ -0,0 +1,192 @@
package io.github.dsheirer.instrument.gui.viewer;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SeparatorMenuItem;
import java.io.File;
import java.util.prefs.Preferences;
public class RecentFilesMenu extends Menu implements EventHandler<ActionEvent>
{
private Preferences mPreferences = Preferences.userNodeForPackage(RecentFilesMenu.class);
private IFileSelectionListener mFileSelectionListener;
private String mIdentifier;
private int mSize;
/**
* Recently used files menu. Uses java Preferences to store recently accessed files and provides access to those
* files via child menu items to this menu.
*
* @param label to use for this menu.
* @param uniqueIdentifier to use in addition to the class path of this class to uniquely identify recently accessed
* files in a java Preferences data store.
* @param size of the maximum number of recent files to present to the user
*/
public RecentFilesMenu(String label, String uniqueIdentifier, int size)
{
super(label);
mIdentifier = uniqueIdentifier;
mSize = size;
load();
}
/**
* Registers the listener to receive file selection events when the user selects a recently used file.
* @param listener to receive file selection events
*/
public void setFileSelectionListener(IFileSelectionListener listener)
{
mFileSelectionListener = listener;
}
/**
* Adds the file to the recently accessed files list. If this file is already included in the recently files list,
* it will be promoted to the first menu item and all other recently used files will be demoted.
*
* @param file to add to the recently used files list.
*/
public void add(File file)
{
if(file != null)
{
String filePath = file.getAbsolutePath();
int index = mSize - 1;
//Check to see if this file is already in the recent files list
for(int x = mSize - 1; x >= 0; x--)
{
String existingItem = mPreferences.get(getKey(x), null);
if(existingItem != null && existingItem.equals(filePath))
{
index = x;
break;
}
}
//Shift all recent files down to make room for the newly added file at the top of the list
while(index > 0)
{
String newerItem = mPreferences.get(getKey(index - 1), null);
if(newerItem != null)
{
mPreferences.put(getKey(index), newerItem);
}
index--;
}
mPreferences.put(getKey(0), file.getAbsolutePath());
}
load();
}
/**
* Creates a unique key value for each numbered recently used file item in the preferences.
*
* @param index of recent file item.
* @return unique key to use with the preferences instance.
*/
private String getKey(int index)
{
return mIdentifier + ".recent.file." + index;
}
/**
* Clears and (re)loads all child recent file menu items.
*/
private void load()
{
getItems().clear();
for(int x = 0; x < mSize; x++)
{
String path = mPreferences.get(getKey(x), null);
if(path != null)
{
File file = new File(path);
if(file.isFile())
{
MenuItem menuItem = new MenuItem(file.getName());
menuItem.setUserData(file);
menuItem.setOnAction(this);
getItems().add(menuItem);
}
}
}
if(getItems().size() > 0)
{
setDisable(false);
getItems().add(new SeparatorMenuItem());
MenuItem clearMenuItem = new MenuItem("Clear History");
clearMenuItem.setOnAction(new EventHandler<ActionEvent>()
{
@Override
public void handle(ActionEvent event)
{
clearHistory();
}
});
getItems().add(clearMenuItem);
}
else
{
setDisable(true);
}
}
/**
* Clears the recently used files history and removes all items from the preferences instance
*/
private void clearHistory()
{
for(int x = 0; x < mSize; x++)
{
mPreferences.remove(getKey(x));
}
load();
}
/**
* Event handler for all recent file child menu items. Extracts the selected file stored as the user data object
* in each of the menu items and notifies the listener that the recent file has been selected.
*
* @param event from a recent file menu item child of this menu.
*/
@Override
public void handle(ActionEvent event)
{
if(mFileSelectionListener != null)
{
if(event.getSource() instanceof MenuItem)
{
Object fileObject = ((MenuItem)event.getSource()).getUserData();
if(fileObject instanceof File)
{
mFileSelectionListener.fileSelected((File)fileObject);
}
}
}
}
/**
* Listener interface for file selection events.
*/
public interface IFileSelectionListener
{
void fileSelected(File file);
}
}

View File

@@ -0,0 +1,81 @@
/*******************************************************************************
* 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;
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 java.io.File;
public class ViewerDesktop extends BorderPane
{
private PlaybackController mPlaybackController;
private DecoderPane mDecoderPane = DecoderPaneFactory.getDefaultPane();
public ViewerDesktop()
{
setBottom(getPlaybackController());
setCenter(getDecoderPane());
}
public void load(File file)
{
close();
getPlaybackController().load(file);
}
public void setDecoder(DecoderType decoderType)
{
getPlaybackController().removeListener(getDecoderPane());
getChildren().remove(getDecoderPane());
mDecoderPane = DecoderPaneFactory.getDecoderPane(decoderType);
setCenter(getDecoderPane());
getPlaybackController().addListener(getDecoderPane());
getPlaybackController().setSampleRateListener(getDecoderPane());
}
/**
* Decoder pane
*/
public DecoderPane getDecoderPane()
{
if(mDecoderPane == null)
{
mDecoderPane = DecoderPaneFactory.getDefaultPane();
}
return mDecoderPane;
}
public void close()
{
getPlaybackController().close();
}
private PlaybackController getPlaybackController()
{
if(mPlaybackController == null)
{
mPlaybackController = new PlaybackController();
}
return mPlaybackController;
}
}

View File

@@ -0,0 +1,121 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.instrument.gui.viewer.chart;
import io.github.dsheirer.buffer.ComplexCircularBuffer;
import io.github.dsheirer.dsp.gain.ComplexGain;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.Complex;
import io.github.dsheirer.sample.complex.ComplexBuffer;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
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 ComplexPhaseLineChart extends LineChart implements Listener<ComplexBuffer>
{
private final static Logger mLog = LoggerFactory.getLogger(ComplexPhaseLineChart.class);
private static final Complex ANGLE_OFFSET_45_DEGREES = Complex.fromAngle(Math.PI / 4.0);
private ComplexCircularBuffer mComplexCircularBuffer;
private ComplexGain mComplexGain = new ComplexGain(600.0f);
private ObservableList<Data<Integer,Float>> mPhaseValues = FXCollections.observableArrayList();
private IntegerProperty mLengthProperty = new SimpleIntegerProperty(40);
public ComplexPhaseLineChart(int length)
{
super(new NumberAxis("Time", 1, length - 10, 2),
new NumberAxis("Phase", -Math.PI, Math.PI, Math.PI / 2.0));
Series<Integer,Float> phaseSeries = new Series<>("Phase", mPhaseValues);
ObservableList<Series<Integer,Float>> observableList = FXCollections.observableArrayList(phaseSeries);
setData(observableList);
init(length);
}
private void init(int length)
{
mLengthProperty.addListener(new ChangeListener<Number>()
{
@Override
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue)
{
mComplexCircularBuffer = new ComplexCircularBuffer(newValue.intValue());
}
});
mComplexCircularBuffer = new ComplexCircularBuffer(length);
length().setValue(length);
for(int x = 10; x <= length; x++)
{
mPhaseValues.add(new Data(x - 10, 0.0f));
}
}
/**
* Length of the x-axis for displaying samples
*/
public IntegerProperty length()
{
return mLengthProperty;
}
@Override
public void receive(ComplexBuffer complexBuffer)
{
float[] samples = complexBuffer.getSamples();
Complex sample;
for(int x = 0; x < samples.length; x += 2)
{
sample = new Complex(samples[x], samples[x + 1]);
Complex copy = sample.copy();
mComplexGain.apply(copy);
mComplexCircularBuffer.put(copy);
}
updateChart();
}
private void updateChart()
{
Complex[] samples = mComplexCircularBuffer.getAll();
for(int x = 10; x < samples.length; x++)
{
Complex previous = samples[x - 10].copy();
Complex sample = samples[x].copy();
if(previous != null && sample != null)
{
// previous.normalize();
// sample.normalize();
previous.add(ANGLE_OFFSET_45_DEGREES);
sample.multiply(previous.conjugate());
// sample.normalize();
mPhaseValues.get(x - 9).setYValue(sample.angle());
}
}
}
}

View File

@@ -0,0 +1,131 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.instrument.gui.viewer.chart;
import io.github.dsheirer.buffer.ComplexCircularBuffer;
import io.github.dsheirer.dsp.gain.ComplexGain;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.Complex;
import io.github.dsheirer.sample.complex.ComplexBuffer;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ComplexSampleLineChart extends LineChart implements Listener<ComplexBuffer>
{
private final static Logger mLog = LoggerFactory.getLogger(ComplexSampleLineChart.class);
private ComplexCircularBuffer mComplexCircularBuffer;
private ComplexCircularBuffer mDemodulationCircularBuffer;
private ComplexGain mComplexGain = new ComplexGain(500.0f);
private ObservableList<XYChart.Data<Integer,Float>> mISamples = FXCollections.observableArrayList();
private ObservableList<XYChart.Data<Integer,Float>> mQSamples = FXCollections.observableArrayList();
private IntegerProperty mLengthProperty = new SimpleIntegerProperty(40);
public ComplexSampleLineChart(int length, int samplesPerSymbol)
{
super(new NumberAxis("Differential-Demodulated Samples", 0, length, 10),
new NumberAxis("Value", -1.0, 1.0, 0.25));
LineChart.Series<Integer,Float> iSampleSeries = new LineChart.Series<>("Inphase", mISamples);
LineChart.Series<Integer,Float> qSampleSeries = new LineChart.Series<>("Quadrature", mQSamples);
ObservableList<XYChart.Series<Integer,Float>> observableList =
FXCollections.observableArrayList(iSampleSeries, qSampleSeries);
setData(observableList);
init(length, samplesPerSymbol);
}
public void setSamplesPerSymbol(int samplesPerSymbol)
{
mDemodulationCircularBuffer = new ComplexCircularBuffer(samplesPerSymbol);
}
private void init(int length, int samplesPerSymbol)
{
mLengthProperty.addListener(new ChangeListener<Number>()
{
@Override
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue)
{
mComplexCircularBuffer = new ComplexCircularBuffer(newValue.intValue());
}
});
mComplexCircularBuffer = new ComplexCircularBuffer(length);
mDemodulationCircularBuffer = new ComplexCircularBuffer(samplesPerSymbol);
length().setValue(length);
for(int x = 1; x <= length; x++)
{
mISamples.add(new Data(x, 0.0f));
mQSamples.add(new Data(x, 0.0f));
}
}
/**
* Length of the x-axis for displaying samples
*/
public IntegerProperty length()
{
return mLengthProperty;
}
@Override
public void receive(ComplexBuffer complexBuffer)
{
float[] samples = complexBuffer.getSamples();
Complex sample = new Complex(0,0);
for(int x = 0; x < samples.length; x += 2)
{
sample.setValues(samples[x], samples[x + 1]);
// mComplexGain.apply(sample);
sample.normalize();
Complex previous = mDemodulationCircularBuffer.get(sample.copy());
sample.multiply(previous.conjugate());
mComplexCircularBuffer.put(sample.copy());
}
updateChart();
}
private void updateChart()
{
Complex[] samples = mComplexCircularBuffer.getAll();
for(int x = 0; x < samples.length; x++)
{
Complex sample = samples[x];
if(sample != null)
{
sample.normalize();
mISamples.get(x).setYValue(sample.inphase());
mQSamples.get(x).setYValue(sample.quadrature());
}
}
}
}

View File

@@ -0,0 +1,72 @@
/*******************************************************************************
* 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 DoubleLineChart extends LineChart implements Listener<Double>
{
private final static Logger mLog = LoggerFactory.getLogger(DoubleLineChart.class);
private DoubleCircularBuffer mDoubleCircularBuffer;
private ObservableList<Data<Integer,Double>> mPhaseValues = FXCollections.observableArrayList();
public DoubleLineChart(String seriesLabel, double min, double max, double tick, int length)
{
super(new NumberAxis(1, length, 2), new NumberAxis(min, max, tick));
Series<Integer,Double> phaseSeries = new Series<>(seriesLabel, mPhaseValues);
ObservableList<Series<Integer,Double>> observableList = FXCollections.observableArrayList(phaseSeries);
setData(observableList);
init(length);
}
private void init(int length)
{
mDoubleCircularBuffer = new DoubleCircularBuffer(length);
for(int x = 1; x <= length; x++)
{
mPhaseValues.add(new Data(x, 0.0));
}
}
@Override
public void receive(Double phase)
{
mDoubleCircularBuffer.put(phase);
updateChart();
}
private void updateChart()
{
double[] values = mDoubleCircularBuffer.getAll();
for(int x = 0; x < values.length; x++)
{
mPhaseValues.get(x).setYValue(values[x]);
}
}
}

View File

@@ -0,0 +1,121 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.instrument.gui.viewer.chart;
import io.github.dsheirer.dsp.psk.SymbolDecisionData;
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.LineChart;
import javafx.scene.chart.NumberAxis;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class EyeDiagramChart extends LineChart implements Listener<SymbolDecisionData>
{
private final static Logger mLog = LoggerFactory.getLogger(EyeDiagramChart.class);
private ObservableList<Series<Double,Double>> mData = FXCollections.observableArrayList();
private int mSeriesCount;
private int mSeriesPointer;
private int mSeriesLength;
public EyeDiagramChart(int seriesCount)
{
super(new NumberAxis("Symbol Timing (5 = center)", 1.0, 10.0, 1.0),
new NumberAxis("Value", -1.25, 1.25, 0.25));
mSeriesCount = seriesCount * 2;
init();
}
private void init()
{
for(int x = 0; x < mSeriesCount; x++)
{
ObservableList<Data<Double,Double>> series = FXCollections.observableArrayList();
mData.add(new Series<>(series));
}
setData(mData);
}
private ObservableList<Data<Double,Double>> getSeries(int series, int length)
{
ObservableList<Data<Double,Double>> seriesData = mData.get(series).getData();
while(seriesData.size() > length)
{
seriesData.remove(0);
}
while(seriesData.size() < length)
{
seriesData.add(new Data<Double,Double>(0.0,0.0));
}
return seriesData;
}
private void checkChartLength(int length)
{
if(mSeriesLength != length)
{
((NumberAxis)getXAxis()).setUpperBound(length);
mSeriesLength = length;
}
}
@Override
public void receive(SymbolDecisionData symbolDecisionData)
{
Complex[] demodulated = symbolDecisionData.getDemodulated();
int length = demodulated.length;
checkChartLength(length);
ObservableList<Data<Double,Double>> iSeries = getSeries(mSeriesPointer++, length + 1);
if(mSeriesPointer >= mSeriesCount)
{
mSeriesPointer = 0;
}
ObservableList<Data<Double,Double>> qSeries = getSeries(mSeriesPointer++, length + 1);
if(mSeriesPointer >= mSeriesCount)
{
mSeriesPointer = 0;
}
for(int x = 0; x < length; x++)
{
demodulated[x].normalize();
Data<Double,Double> iDataPoint = iSeries.get(x);
iDataPoint.setXValue((double)x + 1 - symbolDecisionData.getSamplingPoint());
double iValue = demodulated[x].inphase();
iDataPoint.setYValue(iValue);
Data<Double,Double> qDataPoint = qSeries.get(x);
qDataPoint.setXValue((double)x + 1 - symbolDecisionData.getSamplingPoint());
double qValue = demodulated[x].quadrature();
qDataPoint.setYValue(qValue);
}
}
}

View File

@@ -0,0 +1,119 @@
/*******************************************************************************
* 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.dsp.fm.FMDemodulator;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.Complex;
import io.github.dsheirer.sample.complex.ComplexBuffer;
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 FMDemodulatedLineChart extends LineChart implements Listener<ComplexBuffer>
{
private final static Logger mLog = LoggerFactory.getLogger(FMDemodulatedLineChart.class);
private DoubleCircularBuffer mDoubleCircularBuffer;
private int mSamplesPerSymbol;
private int mTwiceSamplesPerSymbol;
private int mSeriesCount;
private ObservableList<Series<Integer,Double>> mData = FXCollections.observableArrayList();
private Complex mPreviousSample = new Complex(0,0);
private int mSeriesPointer;
private int mSamplePointer;
public FMDemodulatedLineChart(int samplesPerSymbol, int seriesCount)
{
super(new NumberAxis("Time", 1, samplesPerSymbol * 2, 2),
new NumberAxis("Demodulated", -1.0, 1.0, 0.25));
mSamplesPerSymbol = samplesPerSymbol;
mTwiceSamplesPerSymbol = mSamplesPerSymbol * 2;
mSeriesCount = seriesCount;
init();
}
private void init()
{
int seriesLength = 2 * mSamplesPerSymbol;
mDoubleCircularBuffer = new DoubleCircularBuffer(seriesLength * mSeriesCount);
for(int x = 0; x < mSeriesCount; x++)
{
ObservableList<Data<Integer,Double>> series = FXCollections.observableArrayList();
for(int y = 0; y < seriesLength; y++)
{
series.add(new Data<Integer,Double>(y + 1, 0.0));
}
mData.add(new Series<>(series));
}
setData(mData);
}
@Override
public void receive(ComplexBuffer complexBuffer)
{
float[] samples = complexBuffer.getSamples();
Complex sample;
for(int x = 0; x < samples.length; x += 2)
{
sample = new Complex(samples[x], samples[x + 1]);
double demodulated = FMDemodulator.demodulate(mPreviousSample, sample);
demodulated *= 2.0;
if(demodulated > 1.0)
{
demodulated = 1.0;
}
else if(demodulated < -1.0)
{
demodulated = -1.0;
}
mDoubleCircularBuffer.put(demodulated);
mPreviousSample = sample;
updateChart(demodulated);
}
}
private void updateChart(double value)
{
Series<Integer,Double> series = mData.get(mSeriesPointer);
Data data = series.getData().get(mSamplePointer++);
data.setYValue(value);
if(mSamplePointer >= mTwiceSamplesPerSymbol)
{
mSamplePointer = 0;
mSeriesPointer++;
if(mSeriesPointer >= mSeriesCount)
{
mSeriesPointer = 0;
}
}
}
}

View File

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

View File

@@ -0,0 +1,90 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.instrument.gui.viewer.chart;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.Complex;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.ScatterChart;
import javafx.scene.chart.XYChart;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SymbolChart extends ScatterChart implements Listener<Complex>
{
private final static Logger mLog = LoggerFactory.getLogger(SymbolChart.class);
private ObservableList<ScatterChart.Data> mCurrentPoints = FXCollections.observableArrayList();
// private ObservableList<ScatterChart.Data> mPreviousPoints = FXCollections.observableArrayList();
// private ObservableList<ScatterChart.Data> mOldestPoints = FXCollections.observableArrayList();
private int mDataPointer;
private int mHistory;
/**
* Symbol chart for plotting a series of constellation symbols over time. Maintains three history sets for
* current, previous, and oldest symbols.
*
* @param history size for each of the history sets
*/
public SymbolChart(int history)
{
super(new NumberAxis("I", -1.1, 1.1, 0.2),
new NumberAxis("Q", -1.1, 1.1, 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("Constellation", mCurrentPoints));
// data.add(new ScatterChart.Series("Previous", mPreviousPoints));
// data.add(new ScatterChart.Series("Oldest", mOldestPoints));
this.setData(data);
}
@Override
public void receive(Complex complex)
{
//Transfer previous to oldest
// XYChart.Data oldest = mOldestPoints.get(mDataPointer);
// XYChart.Data previous = mPreviousPoints.get(mDataPointer);
XYChart.Data current = mCurrentPoints.get(mDataPointer);
// oldest.setXValue(previous.getXValue());
// oldest.setYValue(previous.getYValue());
//
// previous.setXValue(current.getXValue());
// previous.setYValue(current.getYValue());
current.setXValue(complex.inphase());
current.setYValue(complex.quadrature());
mDataPointer++;
if(mDataPointer >= mHistory)
{
mDataPointer = 0;
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,195 @@
/*******************************************************************************
* 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.ComplexSampleLineChart;
import io.github.dsheirer.instrument.gui.viewer.chart.DoubleLineChart;
import io.github.dsheirer.instrument.gui.viewer.chart.EyeDiagramChart;
import io.github.dsheirer.instrument.gui.viewer.chart.PhaseLineChart;
import io.github.dsheirer.instrument.gui.viewer.chart.SymbolChart;
import io.github.dsheirer.message.Message;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.module.decode.p25.P25_C4FMDecoderInstrumented;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.ComplexBuffer;
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 EyeDiagramChart mEyeDiagramChart;
private HBox mDecoderChartBox;
private SymbolChart mSymbolChart;
private PhaseLineChart mPLLPhaseErrorLineChart;
private DoubleLineChart mPLLFrequencyLineChart;
private DoubleLineChart mSamplesPerSymbolLineChart;
private Broadcaster<ComplexBuffer> mFilteredBufferBroadcaster = new Broadcaster<>();
private P25_C4FMDecoderInstrumented mDecoder = new P25_C4FMDecoderInstrumented(null);
public P25Phase1Pane()
{
super(DecoderType.P25_PHASE1);
init();
}
private void init()
{
addListener(getDecoder());
getDecoder().setFilteredBufferListener(mFilteredBufferBroadcaster);
getDecoder().setSymbolListener(getSymbolChart());
getDecoder().setPLLPhaseErrorListener(getPLLPhaseErrorLineChart());
getDecoder().setPLLFrequencyListener(getPLLFrequencyLineChart());
getDecoder().setSymbolDecisionDataListener(getEyeDiagramChart());
getDecoder().setSamplesPerSymbolListener(getSamplesPerSymbolLineChart());
mFilteredBufferBroadcaster.addListener(getSampleLineChart());
HBox.setHgrow(getSampleChartBox(), Priority.ALWAYS);
HBox.setHgrow(getDecoderChartBox(), Priority.ALWAYS);
getChildren().addAll(getSampleChartBox(), getDecoderChartBox());
mDecoder.setMessageListener(new Listener<Message>()
{
@Override
public void receive(Message message)
{
mLog.debug(message.getMessage());
}
});
}
@Override
public void setSampleRate(double sampleRate)
{
mLog.debug("Configuring for sample rate: " + sampleRate);
mDecoder.setSampleRate(sampleRate);
double samplesPerSymbol = sampleRate / 4800.0;
getSampleLineChart().setSamplesPerSymbol((int)samplesPerSymbol);
}
private P25_C4FMDecoderInstrumented getDecoder()
{
return mDecoder;
}
private SymbolChart getSymbolChart()
{
if(mSymbolChart == null)
{
mSymbolChart = new SymbolChart(10);
}
return mSymbolChart;
}
private HBox getDecoderChartBox()
{
if(mDecoderChartBox == null)
{
mDecoderChartBox = new HBox();
mDecoderChartBox.setMaxHeight(Double.MAX_VALUE);
getSymbolChart().setMaxWidth(Double.MAX_VALUE);
getPLLPhaseErrorLineChart().setMaxWidth(Double.MAX_VALUE);
getPLLFrequencyLineChart().setMaxWidth(Double.MAX_VALUE);
HBox.setHgrow(getSymbolChart(), Priority.ALWAYS);
HBox.setHgrow(getPLLPhaseErrorLineChart(), Priority.ALWAYS);
HBox.setHgrow(getPLLFrequencyLineChart(), Priority.ALWAYS);
mDecoderChartBox.getChildren().addAll(getSymbolChart(), getPLLPhaseErrorLineChart(),
getPLLFrequencyLineChart());
}
return mDecoderChartBox;
}
private HBox getSampleChartBox()
{
if(mSampleChartBox == null)
{
mSampleChartBox = new HBox();
mSampleChartBox.setMaxHeight(Double.MAX_VALUE);
getSampleLineChart().setMaxWidth(Double.MAX_VALUE);
getEyeDiagramChart().setMaxWidth(Double.MAX_VALUE);
getSamplesPerSymbolLineChart().setMaxWidth(Double.MAX_VALUE);
HBox.setHgrow(getSampleLineChart(), Priority.ALWAYS);
HBox.setHgrow(getEyeDiagramChart(), Priority.ALWAYS);
HBox.setHgrow(getSamplesPerSymbolLineChart(), Priority.ALWAYS);
mSampleChartBox.getChildren().addAll(getSampleLineChart(), getEyeDiagramChart(),
getSamplesPerSymbolLineChart());
}
return mSampleChartBox;
}
private ComplexSampleLineChart getSampleLineChart()
{
if(mSampleLineChart == null)
{
mSampleLineChart = new ComplexSampleLineChart(100, 10);
}
return mSampleLineChart;
}
private EyeDiagramChart getEyeDiagramChart()
{
if(mEyeDiagramChart == null)
{
mEyeDiagramChart = new EyeDiagramChart(10);
}
return mEyeDiagramChart;
}
private PhaseLineChart getPLLPhaseErrorLineChart()
{
if(mPLLPhaseErrorLineChart == null)
{
mPLLPhaseErrorLineChart = new PhaseLineChart(40);
}
return mPLLPhaseErrorLineChart;
}
private DoubleLineChart getPLLFrequencyLineChart()
{
if(mPLLFrequencyLineChart == null)
{
mPLLFrequencyLineChart = new DoubleLineChart( "PLL Frequency", -500, 500, 50, 40);
}
return mPLLFrequencyLineChart;
}
private DoubleLineChart getSamplesPerSymbolLineChart()
{
if(mSamplesPerSymbolLineChart == null)
{
mSamplesPerSymbolLineChart = new DoubleLineChart( "Sample Point", 9.5, 11.5, 0.1, 40);
}
return mSamplesPerSymbolLineChart;
}
}

View File

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

View File

@@ -246,9 +246,7 @@ public class DecoderFactory
switch(modulation) switch(modulation)
{ {
case C4FM: case C4FM:
modules.add(new FMDemodulatorModule(P25_C4FM_IQ_FILTER)); modules.add(new P25_C4FMDecoder(aliasList));
modules.add(new DemodulatedAudioFilterModule(P25_C4FM_DEMOD_FILTER, 1.0f));
modules.add(new P25_C4FMDecoder(aliasList, decodeConfig.getAFCMaximumCorrection()));
modules.add(new P25DecoderState(aliasList, channelType, P25Decoder.Modulation.C4FM, modules.add(new P25DecoderState(aliasList, channelType, P25Decoder.Modulation.C4FM,
p25Config.getIgnoreDataCalls())); p25Config.getIgnoreDataCalls()));
break; break;

View File

@@ -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()

View File

@@ -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

View File

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

View File

@@ -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

View File

@@ -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()

View File

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

View File

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

View File

@@ -1,244 +1,200 @@
/******************************************************************************* /*******************************************************************************
* sdrtrunk * sdr-trunk
* Copyright (C) 2014-2017 Dennis Sheirer * Copyright (C) 2014-2018 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* it under the terms of the GNU General Public License as published by * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* the Free Software Foundation, either version 3 of the License, or * later version.
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* but WITHOUT ANY WARRANTY; without even the implied warranty of * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
* 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 * You should have received a copy of the GNU General Public License along with this program.
* along with this program. If not, see <http://www.gnu.org/licenses/> * 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.tap.Tap; import io.github.dsheirer.dsp.filter.FilterFactory;
import io.github.dsheirer.instrument.tap.TapGroup; import io.github.dsheirer.dsp.filter.design.FilterDesignException;
import io.github.dsheirer.instrument.tap.stream.DibitTap; import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
import io.github.dsheirer.instrument.tap.stream.FloatTap; import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter;
import io.github.dsheirer.dsp.psk.DQPSKDemodulator;
import io.github.dsheirer.dsp.psk.InterpolatingSampleBuffer;
import io.github.dsheirer.dsp.psk.InterpolatingSampleBufferInstrumented;
import io.github.dsheirer.dsp.psk.pll.CostasLoop;
import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.real.IFilteredRealBufferListener; import io.github.dsheirer.sample.complex.ComplexBuffer;
import io.github.dsheirer.sample.real.RealBuffer; import io.github.dsheirer.sample.complex.IComplexBufferListener;
import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent; 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.HashMap;
import java.util.List; import java.util.Map;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
public class P25_C4FMDecoder extends P25Decoder implements IFilteredRealBufferListener public class P25_C4FMDecoder extends P25Decoder implements IComplexBufferListener, Listener<ComplexBuffer>
{ {
private final static Logger mLog = LoggerFactory.getLogger( P25_C4FMDecoder.class ); private final static Logger mLog = LoggerFactory.getLogger(P25_C4FMDecoder.class);
protected static final double SYMBOL_RATE = 4800;
protected InterpolatingSampleBuffer mInterpolatingSampleBuffer;
protected DQPSKDemodulator mQPSKDemodulator;
protected CostasLoop mCostasLoop;
protected P25MessageFramer mMessageFramer;
protected double mSampleRate;
private Map<Double,float[]> mBasebandFilters = new HashMap<>();
private ComplexFIRFilter mBasebandFilter;
/* Instrumentation Taps */ public P25_C4FMDecoder(AliasList aliasList)
private static final String INSTRUMENT_C4FM_SYMBOL_FILTER_OUTPUT = "Tap Point: Symbol Filter Output";
private static final String INSTRUMENT_C4FM_SLICER_OUTPUT = "Tap Point: C4FM Slicer Output";
private List<TapGroup> mAvailableTaps;
private C4FMSymbolFilter mSymbolFilter;
private C4FMSlicer mC4FMSlicer;
private P25MessageFramer mMessageFramer;
/**
* P25 Phase 1 C4FM Decoder processes real buffers of un-filtered,
* demodulated audio and produces decoded P25 Phase 1 Messages.
*
* Provides Frequency Control to steer external tuner source and incorporates
* internal gain control.
*
* Note: use the P25AudioModule to convert the decoded messages into audio.
*
* @param aliasList - optional (can be null) list of alias values for network
* infrastructure and subscriber identities that will be included in each
* decoded message
*/
public P25_C4FMDecoder(AliasList aliasList, int frequencyCorrectionMaximum )
{
super( aliasList );
/* Shape gain and frequency offsets to optimize sample stream */
mSymbolFilter = new C4FMSymbolFilter( frequencyCorrectionMaximum );
/* Convert samples to symbols */
mC4FMSlicer = new C4FMSlicer();
mSymbolFilter.setListener( mC4FMSlicer );
/* Sync pattern detection and message construction */
mMessageFramer = new P25MessageFramer( aliasList );
mC4FMSlicer.addListener( mMessageFramer );
/* Process and broadcast messages */
mMessageFramer.setListener( getMessageProcessor() );
}
@Override
public void dispose()
{
super.dispose();
mSymbolFilter.dispose();
mSymbolFilter = null;
mMessageFramer.dispose();
mMessageFramer = null;
mC4FMSlicer.dispose();
mC4FMSlicer = null;
}
public Modulation getModulation()
{
return Modulation.C4FM;
}
@Override
public Listener<RealBuffer> getFilteredRealBufferListener()
{
return mSymbolFilter;
}
/**
* Instrumentation taps for monitoring internal processing
*/
@Override
public List<TapGroup> getTapGroups()
{ {
if( mAvailableTaps == null ) super(aliasList);
{ setSampleRate(48000.0);
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;
} }
/** public void setSampleRate(double sampleRate)
* Adds an instrumentation tap to monitor internal processing.
*/
@Override
public void registerTap( Tap tap )
{ {
if( mSymbolFilter != null ) if(sampleRate <= SYMBOL_RATE * 2)
{ {
mSymbolFilter.registerTap( tap ); throw new IllegalArgumentException("Sample rate must be at least twice the symbol rate [4800]");
} }
switch( tap.getName() ) mSampleRate = sampleRate;
{ mBasebandFilter = new ComplexFIRFilter(getBasebandFilter(), 1.0f);
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 ) mCostasLoop = new CostasLoop(mSampleRate, SYMBOL_RATE);
{
mC4FMSlicer.addListener( slicerTap ); mInterpolatingSampleBuffer = new InterpolatingSampleBufferInstrumented((float)(sampleRate / SYMBOL_RATE));
} mQPSKDemodulator = new DQPSKDemodulator(mCostasLoop, mInterpolatingSampleBuffer);
break;
default: //Message framer can trigger a symbol-inversion correction to the PLL when detected
break; mMessageFramer = new P25MessageFramer(getAliasList(), mCostasLoop);
} mMessageFramer.setListener(getMessageProcessor());
mQPSKDemodulator.setDibitListener(mMessageFramer);
} }
/** @Override
* Removes the instrumentation tap. public void receive(ComplexBuffer complexBuffer)
*/
@Override
public void unregisterTap( Tap tap )
{ {
if( mSymbolFilter != null ) ComplexBuffer filtered = filter(complexBuffer);
{ mQPSKDemodulator.receive(filtered);
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 protected ComplexBuffer filter(ComplexBuffer complexBuffer)
public void reset() {
{ return mBasebandFilter.filter(complexBuffer);
// TODO Auto-generated method stub }
} private double getSampleRate()
{
return mSampleRate;
}
@Override private float[] getBasebandFilter()
public void start( ScheduledExecutorService executor ) {
{ float[] filter = mBasebandFilters.get(getSampleRate());
// TODO Auto-generated method stub
} if(filter == null)
{
FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
.sampleRate((int)getSampleRate())
.passBandCutoff(5100)
.passBandAmplitude(1.0)
.passBandRipple(0.01)
.stopBandAmplitude(0.0)
.stopBandStart(6500)
.stopBandRipple(0.01)
.build();
@Override try
public void stop() {
{ filter = FilterFactory.getTaps(specification);
// TODO Auto-generated method stub
} }
catch(FilterDesignException fde)
{
mLog.error("Couldn't design low pass baseband filter for sample rate: " + getSampleRate());
}
if(filter != null)
{
mBasebandFilters.put(getSampleRate(), filter);
}
else
{
throw new IllegalStateException("Couldn't design a C4FM symbol filter for sample rate: " + mSampleRate);
}
}
@Override return filter;
public void setFrequencyChangeListener( Listener<FrequencyChangeEvent> listener ) }
{
if( mSymbolFilter != null )
{
mSymbolFilter.setFrequencyChangeListener( listener );
}
}
@Override public void dispose()
public void removeFrequencyChangeListener() {
{ super.dispose();
if( mSymbolFilter != null )
{
mSymbolFilter.setFrequencyChangeListener( null );
}
}
@Override mBasebandFilter.dispose();
public Listener<FrequencyChangeEvent> getFrequencyChangeListener() mBasebandFilter = null;
{
if( mSymbolFilter != null )
{
return mSymbolFilter.getFrequencyChangeListener();
}
return 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)
{
switch(frequencyChangeEvent.getEvent())
{
case NOTIFICATION_FREQUENCY_CHANGE:
case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
case NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE:
case NOTIFICATION_SAMPLE_RATE_CHANGE:
mCostasLoop.reset();
break;
}
}
};
}
@Override
public Listener<ComplexBuffer> getComplexBufferListener()
{
return P25_C4FMDecoder.this;
}
public Modulation getModulation()
{
return Modulation.C4FM;
}
@Override
public void reset()
{
mCostasLoop.reset();
}
@Override
public void start(ScheduledExecutorService executor)
{
}
@Override
public void stop()
{
}
} }

View File

@@ -0,0 +1,109 @@
/*******************************************************************************
* 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.psk.DQPSKDemodulatorInstrumented;
import io.github.dsheirer.dsp.psk.InterpolatingSampleBufferInstrumented;
import io.github.dsheirer.dsp.psk.SymbolDecisionData;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.Complex;
import io.github.dsheirer.sample.complex.ComplexBuffer;
public class P25_C4FMDecoderInstrumented extends P25_C4FMDecoder
{
private Listener<Double> mPLLPhaseErrorListener;
private Listener<Double> mPLLFrequencyListener;
private Listener<Double> mSamplesPerSymbolListener;
private Listener<Complex> mSymbolListener;
private Listener<ComplexBuffer> mFilteredSymbolListener;
private Listener<SymbolDecisionData> mSymbolDecisionDataListener;
public P25_C4FMDecoderInstrumented(AliasList aliasList)
{
super(aliasList);
}
/**
* Overrides the filter method so that we can capture the filtered samples for instrumentation
*/
protected ComplexBuffer filter(ComplexBuffer complexBuffer)
{
ComplexBuffer filtered = super.filter(complexBuffer);
if(mFilteredSymbolListener != null)
{
mFilteredSymbolListener.receive(filtered);
}
return filtered;
}
/**
* Overrides this method so we can correctly configure for instrumented operations
*/
public void setSampleRate(double sampleRate)
{
super.setSampleRate(sampleRate);
InterpolatingSampleBufferInstrumented instrumentedBuffer = new InterpolatingSampleBufferInstrumented((float)(sampleRate / SYMBOL_RATE));
mInterpolatingSampleBuffer = instrumentedBuffer;
DQPSKDemodulatorInstrumented instrumented = new DQPSKDemodulatorInstrumented(mCostasLoop, instrumentedBuffer, mSampleRate);
mQPSKDemodulator = instrumented;
instrumented.setSymbolListener(mSymbolListener);
instrumented.setPLLErrorListener(mPLLPhaseErrorListener);
instrumented.setPLLFrequencyListener(mPLLFrequencyListener);
instrumented.setSymbolDecisionDataListener(mSymbolDecisionDataListener);
instrumented.setSamplesPerSymbolListener(mSamplesPerSymbolListener);
instrumented.setDibitListener(mMessageFramer);
}
public void setSymbolListener(Listener<Complex> listener)
{
mSymbolListener = listener;
((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setSymbolListener(listener);
}
public void setPLLPhaseErrorListener(Listener<Double> listener)
{
mPLLPhaseErrorListener = listener;
((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setPLLErrorListener(listener);
}
public void setPLLFrequencyListener(Listener<Double> listener)
{
mPLLFrequencyListener = listener;
((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setPLLFrequencyListener(listener);
}
public void setFilteredBufferListener(Listener<ComplexBuffer> listener)
{
mFilteredSymbolListener = listener;
}
public void setSymbolDecisionDataListener(Listener<SymbolDecisionData> listener)
{
mSymbolDecisionDataListener = listener;
((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setSymbolDecisionDataListener(listener);
}
public void setSamplesPerSymbolListener(Listener<Double> listener)
{
mSamplesPerSymbolListener = listener;
((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setSamplesPerSymbolListener(listener);
}
}

View File

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

View File

@@ -78,7 +78,7 @@ public class SyncBroadcast extends TSBKMessage
if( isMicroslotsLockedToMinuteRollover() ) if( isMicroslotsLockedToMinuteRollover() )
{ {
sb.append( " MICROSLOT-MINUTE ROLLOVER:LOCKED" ); sb.append( " MICROSLOT-MINUTE ROLLOVER:SLOW" );
} }
else else
{ {

View File

@@ -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

View File

@@ -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()

View File

@@ -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());
} }
/** /**

View File

@@ -21,14 +21,13 @@ public class ConversionUtils
*/ */
public static float[] convertFromSigned16BitSamples( ByteBuffer buffer ) public static float[] convertFromSigned16BitSamples( ByteBuffer buffer )
{ {
ShortBuffer byteBuffer = ShortBuffer byteBuffer = buffer.order( ByteOrder.LITTLE_ENDIAN ).asShortBuffer();
buffer.order( ByteOrder.LITTLE_ENDIAN ).asShortBuffer();
float[] samples = new float[ buffer.limit() / 2 ]; float[] samples = new float[ buffer.limit() / 2 ];
for( int x = 0; x < samples.length; x++ ) for( int x = 0; x < samples.length; x++ )
{ {
samples[ x ] = (float)byteBuffer.get() / 32767.0f; samples[ x ] = (float)byteBuffer.get() / (float)Short.MAX_VALUE;
} }
return samples; return samples;

View File

@@ -27,407 +27,424 @@ 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));
}
/** /**
* Constrains the i and q quantities to +/- value. * Sets this complex instance with the specified angle in radians.
* * @param angle in radians
* @param value - maximum absolute value */
*/ public void setAngle(double angle)
public void clip( float value ) {
{ setInphase((float)Math.cos(angle));
if( mLeft > value ) setQuadrature((float)Math.sin(angle));
{ }
mLeft = value;
}
else if( mLeft < -value )
{
mLeft = -value;
}
if( mRight > value ) /**
{ * Angle of this sample in radians
mRight = value; */
} public float angle()
else if( mRight < -value ) {
{ return (float)Math.atan2(y(), x());
mRight = -value; }
}
}
/** public float angleDegrees()
* Angle of this sample in degrees {
*/ return (float)Math.toDegrees(angle());
public float polarAngle() }
{
return (float)Math.toDegrees( angle() );
}
/** /**
* Provides an approximate magnitude value for this sample. * Constrains the i and q quantities to +/- value.
*/ *
public float envelope() * @param value - maximum absolute value
{ */
return envelope( mLeft, mRight ); public void clip(float value)
} {
if(mLeft > value)
{
mLeft = value;
}
else if(mLeft < -value)
{
mLeft = -value;
}
public static float envelope( float inphase, float quadrature ) if(mRight > value)
{ {
float inphaseAbsolute = Math.abs( inphase ); mRight = value;
float quadratureAbsolute = Math.abs( quadrature ); }
else if(mRight < -value)
{
mRight = -value;
}
}
if( inphaseAbsolute > quadratureAbsolute ) /**
{ * Angle of this sample in degrees
return inphaseAbsolute + ( 0.4f * quadratureAbsolute ); */
} public float polarAngle()
else {
{ return (float)Math.toDegrees(angle());
return quadratureAbsolute + ( 0.4f * inphaseAbsolute ); }
}
}
public static void main( String[] args ) /**
{ * Provides an approximate magnitude value for this sample.
double angle = Math.PI; //180 degrees */
public float envelope()
{
return envelope(mLeft, mRight);
}
Complex s1 = new Complex( (float)Math.sin( angle ), (float)Math.cos( angle ) ); public static float envelope(float inphase, float quadrature)
{
float inphaseAbsolute = Math.abs(inphase);
float quadratureAbsolute = Math.abs(quadrature);
Complex tap = new Complex( 1.0f, -0.1f ); if(inphaseAbsolute > quadratureAbsolute)
{
return inphaseAbsolute + (0.4f * quadratureAbsolute);
}
else
{
return quadratureAbsolute + (0.4f * inphaseAbsolute);
}
}
Complex convolved = Complex.multiply( s1, tap ); public static void main(String[] args)
{
Complex ROTATE_FROM_MINUS_45 = Complex.fromAngle(1.0 * Math.PI / 4.0);
Complex ROTATE_FROM_PLUS_135 = Complex.fromAngle(-3.0 * Math.PI / 4.0);
mLog.debug("REF: " + ROTATE_FROM_MINUS_45.toString());
mLog.debug( "s: " + s1.toString() + Complex symbol = new Complex(0.7665009f, -0.6422432f);
" t: " + tap.toString() + mLog.debug("SYM: " + symbol.toString());
" convolved: " + convolved.toString() );
} symbol.multiply(ROTATE_FROM_MINUS_45);
mLog.debug("DER: " + symbol.toString());
}
} }

View File

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

View File

@@ -469,6 +469,7 @@ public class TunerChannelSource extends ComplexSource implements IFrequencyChang
{ {
//Frequency correction requests are the only change requests supported from downstream components //Frequency correction requests are the only change requests supported from downstream components
case REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE: case REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE:
mLog.debug("Got a freq corr change request: " + event.getValue().intValue());
setFrequencyCorrection(event.getValue().intValue()); setFrequencyCorrection(event.getValue().intValue());
break; break;
} }

View File

@@ -167,13 +167,8 @@ public class ComplexWaveSource extends ComplexSource implements IControllableFil
if(mInputStream != null) if(mInputStream != null)
{ {
mInputStream.close(); mInputStream.close();
mInputStream = null;
} }
else
{
throw new IOException("Can't close wave source - was not opened");
}
mInputStream = null;
} }
/** /**
@@ -239,8 +234,7 @@ public class ComplexWaveSource extends ComplexSource implements IControllableFil
buffer = Arrays.copyOf(buffer, samplesRead); buffer = Arrays.copyOf(buffer, samplesRead);
} }
float[] samples = ConversionUtils float[] samples = ConversionUtils.convertFromSigned16BitSamples(buffer);
.convertFromSigned16BitSamples(buffer);
mListener.receive(new ComplexBuffer(samples)); mListener.receive(new ComplexBuffer(samples));
} }