diff --git a/CHANGELOG b/CHANGELOG
index 89e692b0..d9f7b9d7 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,8 @@
+0.3.4 20180210
+ - Adds channel auto-start and ordering capability with startup dialog allowing user to invoke or bypass channel
+ auto-start. Provides user with more control over startup configuration.
+ - Adds preferred tuner attribute to channel configuration allowing user to select a preferred tuner for the channel
+
0.3.3 20171225
- Changes build system from ant to gradle. Thanks to: Roman Tsypuk https://github.com/tsypuk
- Adds LIST and ID3 metadata to wav audio recordings
diff --git a/build.gradle b/build.gradle
index 5039a823..284b6999 100644
--- a/build.gradle
+++ b/build.gradle
@@ -15,7 +15,7 @@ repositories {
mavenCentral()
}
-version = '0.3.3-beta.3'
+version = '0.3.4-beta.1'
sourceCompatibility = 1.8
sourceSets {
@@ -54,6 +54,8 @@ dependencies {
compile 'org.apache.axis:axis-jaxrpc:1.4'
compile 'org.apache.axis:axis:1.4'
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) {
diff --git a/sdr-trunk.ipr b/sdr-trunk.ipr
new file mode 100644
index 00000000..912b20d7
--- /dev/null
+++ b/sdr-trunk.ipr
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1.6
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/java/io/github/dsheirer/buffer/ComplexCircularBuffer.java b/src/main/java/io/github/dsheirer/buffer/ComplexCircularBuffer.java
index 7dbbed04..db56da97 100644
--- a/src/main/java/io/github/dsheirer/buffer/ComplexCircularBuffer.java
+++ b/src/main/java/io/github/dsheirer/buffer/ComplexCircularBuffer.java
@@ -1,17 +1,17 @@
/*******************************************************************************
* 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
* 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
******************************************************************************/
@@ -25,66 +25,83 @@ import java.util.Arrays;
* Circular sample buffer - allocates a buffer and stores samples in a circular
* fashion, overwriting older samples with newly arrived samples. Initially
* fills buffer with 0-valued samples
- *
+ *
* Can be used as a delay-type buffer, to delay samples by the 'size' amount
*/
public class ComplexCircularBuffer
{
- Complex[] mBuffer;
- int mBufferPointer = 0;
-
- public ComplexCircularBuffer( int size )
- {
- mBuffer = new Complex[ size ];
-
- Arrays.fill( mBuffer, new Complex( 1.0f, 1.0f ) );
- }
+ Complex[] mBuffer;
+ int mBufferPointer = 0;
- /**
- * 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 ];
+ public ComplexCircularBuffer(int size)
+ {
+ mBuffer = new Complex[size];
- put( sample );
+ Arrays.fill(mBuffer, new Complex(1.0f, 1.0f));
+ }
- return retVal;
- }
-
- public void put( Complex sample )
- {
- mBuffer[ mBufferPointer ] = sample;
+ public int length()
+ {
+ return mBuffer.length;
+ }
- mBufferPointer++;
-
- if( mBufferPointer >= mBuffer.length )
- {
- mBufferPointer = 0;
- }
- }
-
- public Complex[] get( int count )
- {
- Complex[] samples = new Complex[ count ];
-
- int tempCounter = mBufferPointer;
+ /**
+ * 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];
- for( int x = 0; x < count; x++ )
- {
- samples[ x ] = mBuffer[ tempCounter ];
-
- tempCounter++;
-
- if( tempCounter > mBuffer.length - 1 )
- {
- tempCounter = 0;
- }
- }
-
- return samples;
- }
+ put(sample);
+
+ return retVal;
+ }
+
+ public void put(Complex sample)
+ {
+ mBuffer[mBufferPointer] = sample;
+
+ mBufferPointer++;
+
+ if(mBufferPointer >= mBuffer.length)
+ {
+ mBufferPointer = 0;
+ }
+ }
+
+ /**
+ * Returns the (count) oldest samples in a time ordered array
+ */
+ public Complex[] get(int count)
+ {
+ Complex[] samples = new Complex[count];
+
+ int tempCounter = mBufferPointer;
+
+ for(int x = 0; x < count; x++)
+ {
+ samples[x] = mBuffer[tempCounter];
+
+ tempCounter++;
+
+ if(tempCounter > mBuffer.length - 1)
+ {
+ tempCounter = 0;
+ }
+ }
+
+ return samples;
+ }
+
+ /**
+ * Returns the samples in a time-ordered array
+ */
+ public Complex[] getAll()
+ {
+ return get(length());
+ }
}
diff --git a/src/main/java/io/github/dsheirer/buffer/DoubleCircularBuffer.java b/src/main/java/io/github/dsheirer/buffer/DoubleCircularBuffer.java
index 0c9172fe..b65f2f67 100644
--- a/src/main/java/io/github/dsheirer/buffer/DoubleCircularBuffer.java
+++ b/src/main/java/io/github/dsheirer/buffer/DoubleCircularBuffer.java
@@ -1,80 +1,180 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 Dennis Sheirer
- *
+ *
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
- *
+ *
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
- *
+ *
* You should have received a copy of the GNU General Public License
* along with this program. If not, see
******************************************************************************/
package io.github.dsheirer.buffer;
+import 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;
/**
- * Circular sample buffer - allocates a buffer and stores samples in a circular
- * fashion, overwriting older samples with newly arrived samples. Initially
- * fills buffer with 0-valued samples
- *
- * Can be used as a delay-type buffer, to delay samples by the 'size' amount
+ * Circular sample buffer - allocates a buffer and stores samples in a circular fashion, overwriting older samples with
+ * newly arrived samples. Initially fills buffer with 0-valued (default) samples or with a specified value.
*/
public class DoubleCircularBuffer
{
- double[] mBuffer;
- int mBufferPointer = 0;
-
- public DoubleCircularBuffer( int size )
- {
- mBuffer = new double[ size ];
-
- Arrays.fill( mBuffer, 0.0 );
- }
+ private StandardDeviation mStandardDeviation;
+ private Variance mVariance;
+ private Max mMax;
+ private Mean mMean;
+ private Min mMin;
+ private double[] mBuffer;
+ private int mBufferPointer = 0;
- /**
- * Puts the new value into the buffer and returns the oldest buffer value
- * that it replaced
- * @param newValue
- * @return
- */
- public double get( double newValue )
- {
- double oldestSample = mBuffer[ mBufferPointer ];
+ /**
+ * Creates a circular double buffer of the specified size and all entries filled with the specified initial value.
+ * @param size of the buffer
+ * @param initialFillValue to fill the buffer entries
+ */
+ public DoubleCircularBuffer(int size, double initialFillValue)
+ {
+ mBuffer = new double[size];
+ Arrays.fill(mBuffer, initialFillValue);
+ }
- mBuffer[ mBufferPointer ] = newValue;
-
- mBufferPointer++;
-
- if( mBufferPointer >= mBuffer.length )
- {
- mBufferPointer = 0;
- }
-
- return oldestSample;
- }
+ /**
+ * Creates a circular double buffer of the specified size and all entries filled with an initial value of zero.
+ * @param size of the buffer
+ */
+ public DoubleCircularBuffer(int size)
+ {
+ this(size, 0.0);
+ }
- /**
- * Returns the maximum from the values currently in the buffer
- */
- public double max()
- {
- double max = 0.0d;
-
- for( int x = 0; x < mBuffer.length; x++ )
- {
- if( mBuffer[ x ] > max )
- {
- max = mBuffer[ x ];
- }
- }
-
- return max;
- }
+ /**
+ * Puts the new value into the buffer and returns the oldest buffer value that it replaced
+ *
+ * @param newValue to add to the buffer
+ * @return oldest value that was overwritten by the new value
+ */
+ public double get(double newValue)
+ {
+ double oldestSample = mBuffer[mBufferPointer];
+
+ mBuffer[mBufferPointer] = newValue;
+
+ mBufferPointer++;
+
+ if(mBufferPointer >= mBuffer.length)
+ {
+ mBufferPointer = 0;
+ }
+
+ return oldestSample;
+ }
+
+ public double[] getAll()
+ {
+ int valuePointer = 0;
+ double[] values = new double[mBuffer.length];
+ int bufferPointer = mBufferPointer;
+
+ while(valuePointer < values.length)
+ {
+ values[valuePointer++] = mBuffer[bufferPointer++];
+
+ if(bufferPointer >= mBuffer.length)
+ {
+ bufferPointer = 0;
+ }
+ }
+
+ return values;
+ }
+
+ /**
+ * Places the new value into the buffer, overwriting the oldest value
+ * @param newValue to add to the buffer
+ */
+ public void put(double newValue)
+ {
+ mBuffer[mBufferPointer] = newValue;
+
+ mBufferPointer++;
+
+ if(mBufferPointer >= mBuffer.length)
+ {
+ mBufferPointer = 0;
+ }
+ }
+
+ /**
+ * Returns the maximum from the values currently in the buffer
+ */
+ public double max()
+ {
+ if(mMax == null)
+ {
+ mMax = new Max();
+ }
+
+ return mMax.evaluate(mBuffer, 0, mBuffer.length);
+ }
+
+ /**
+ * Returns the minimum from the values currently in the buffer
+ */
+ public double min()
+ {
+ if(mMin == null)
+ {
+ mMin = new Min();
+ }
+
+ return mMin.evaluate(mBuffer, 0, mBuffer.length);
+ }
+
+ 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);
+ }
}
diff --git a/src/main/java/io/github/dsheirer/buffer/FloatCircularBuffer.java b/src/main/java/io/github/dsheirer/buffer/FloatCircularBuffer.java
new file mode 100644
index 00000000..e69de29b
diff --git a/src/main/java/io/github/dsheirer/channel/traffic/TrafficChannelManager.java b/src/main/java/io/github/dsheirer/channel/traffic/TrafficChannelManager.java
index 6c284f3c..80721e1a 100644
--- a/src/main/java/io/github/dsheirer/channel/traffic/TrafficChannelManager.java
+++ b/src/main/java/io/github/dsheirer/channel/traffic/TrafficChannelManager.java
@@ -128,7 +128,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
{
for(Channel configuredChannel : mTrafficChannelPool)
{
- if(!configuredChannel.getEnabled())
+ if(!configuredChannel.isProcessing())
{
channel = configuredChannel;
break;
@@ -196,7 +196,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
//Request to enable the channel
mChannelModel.broadcast(trafficChannelEvent);
- if(channel.getEnabled())
+ if(channel.isProcessing())
{
mTrafficChannelsInUse.put(callEvent.getChannel(), channel);
}
diff --git a/src/main/java/io/github/dsheirer/controller/channel/AutoStartChannelModel.java b/src/main/java/io/github/dsheirer/controller/channel/AutoStartChannelModel.java
new file mode 100644
index 00000000..403a2fc7
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/controller/channel/AutoStartChannelModel.java
@@ -0,0 +1,105 @@
+/*******************************************************************************
+ * sdr-trunk
+ * Copyright (C) 2014-2018 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with this program.
+ * If not, see
+ *
+ ******************************************************************************/
+package io.github.dsheirer.controller.channel;
+
+import javax.swing.table.AbstractTableModel;
+import java.util.List;
+
+/**
+ * Auto-Start Channel Model
+ */
+public class AutoStartChannelModel extends AbstractTableModel
+{
+ private static final long serialVersionUID = 1L;
+
+ public static final int COLUMN_ORDER = 0;
+ public static final int COLUMN_SYSTEM = 1;
+ public static final int COLUMN_SITE = 2;
+ public static final int COLUMN_NAME = 3;
+ public static final int COLUMN_DECODER = 4;
+
+ private static final String[] COLUMN_NAMES = new String[] {"Order", "System", "Site", "Name", "Decoder"};
+
+ private List mChannels;
+
+ public AutoStartChannelModel(List autoStartChannels)
+ {
+ mChannels = autoStartChannels;
+ }
+
+ @Override
+ public int getRowCount()
+ {
+ return mChannels.size();
+ }
+
+ @Override
+ public int getColumnCount()
+ {
+ return COLUMN_NAMES.length;
+ }
+
+ @Override
+ public String getColumnName(int columnIndex)
+ {
+ if(columnIndex < COLUMN_NAMES.length)
+ {
+ return COLUMN_NAMES[columnIndex];
+ }
+
+ return null;
+ }
+
+ @Override
+ public Class> getColumnClass(int columnIndex)
+ {
+ return String.class;
+ }
+
+ @Override
+ public boolean isCellEditable(int rowIndex, int columnIndex)
+ {
+ return false;
+ }
+
+ @Override
+ public Object getValueAt(int rowIndex, int columnIndex)
+ {
+ Channel channel = mChannels.get(rowIndex);
+
+ switch(columnIndex)
+ {
+ case COLUMN_ORDER:
+ return channel.getAutoStartOrder();
+ case COLUMN_SYSTEM:
+ return channel.getSystem();
+ case COLUMN_SITE:
+ return channel.getSite();
+ case COLUMN_NAME:
+ return channel.getName();
+ case COLUMN_DECODER:
+ return channel.getDecodeConfiguration().getDecoderType().getShortDisplayString();
+ }
+
+ return null;
+ }
+
+ @Override
+ public void setValueAt(Object aValue, int rowIndex, int columnIndex)
+ {
+ throw new IllegalArgumentException("Not yet implemented");
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/controller/channel/Channel.java b/src/main/java/io/github/dsheirer/controller/channel/Channel.java
index 4f66c37f..f19e56d7 100644
--- a/src/main/java/io/github/dsheirer/controller/channel/Channel.java
+++ b/src/main/java/io/github/dsheirer/controller/channel/Channel.java
@@ -1,6 +1,6 @@
/*******************************************************************************
* sdrtrunk
- * Copyright (C) 2014-2017 Dennis Sheirer
+ * Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -43,7 +43,10 @@ import io.github.dsheirer.source.tuner.channel.TunerChannel;
public class Channel extends Configuration implements Listener
{
// Standard channels are persisted and traffic channels are temporary
- public enum ChannelType {STANDARD, TRAFFIC}
+ public enum ChannelType
+ {
+ STANDARD, TRAFFIC
+ }
// Static unique channel identifier tracking
private static int UNIQUE_ID = 0;
@@ -59,7 +62,9 @@ public class Channel extends Configuration implements Listener
private String mSite = "Site";
private String mName = "Channel";
- private boolean mEnabled;
+ private boolean mProcessing;
+ private boolean mAutoStart;
+ private Integer mAutoStartOrder;
private boolean mSelected;
private TunerChannel mTunerChannel = null;
@@ -108,6 +113,8 @@ public class Channel extends Configuration implements Listener
channel.setSystem(mSystem);
channel.setSite(mSite);
channel.setAliasListName(mAliasListName);
+ channel.setAutoStart(mAutoStart);
+ channel.setAutoStartOrder(mAutoStartOrder);
AuxDecodeConfiguration aux = new AuxDecodeConfiguration();
@@ -253,27 +260,90 @@ public class Channel extends Configuration implements Listener
}
/**
- * Indicates if this channel is enabled for processing.
+ * Indicates if this channel is currently started and processing.
*/
- @JacksonXmlProperty(isAttribute = true, localName = "enabled")
- public boolean getEnabled()
+ @JsonIgnore
+ public boolean isProcessing()
{
- return mEnabled;
+ return mProcessing;
}
/**
- * Indicates if this channel will automatically start processing on
- * application startup or if this channel is currently processing after
- * application startup.
+ * Indicates if this channel is currently started and processing.
*
- * Note: this method is package private and is intended to be managed
- * by a co-package central processing manager
+ * Note: this method is package private and is intended to be managed by a channel processing manager instance
*
* @see ChannelProcessingManager
*/
- void setEnabled(boolean enabled)
+ void setProcessing(boolean processing)
{
- mEnabled = enabled;
+ mProcessing = processing;
+ }
+
+ /**
+ * Indicates if this channel has auto-start enabled.
+ *
+ * Note: the Json (de)serialization tag for auto-start is 'enabled'. We've re-purposed the now
+ * transient enabled property for the auto-start persisted value to maintain backward compatibility.
+ *
+ * @return true if this channel should be auto-started on application startup
+ */
+ @JacksonXmlProperty(isAttribute = true, localName = "enabled")
+ public boolean getAutoStart()
+ {
+ return mAutoStart;
+ }
+
+ /**
+ * Indicates if this channel is set for auto-start
+ */
+ @JsonIgnore
+ public boolean isAutoStart()
+ {
+ return mAutoStart;
+ }
+
+ /**
+ * Sets the auto-start status for this channel.
+ *
+ * @param autoStart
+ */
+ public void setAutoStart(boolean autoStart)
+ {
+ mAutoStart = autoStart;
+ }
+
+ /**
+ * Auto-start order. Specifies an optional ordering for channels so that when the application starts up,
+ * the channels can be started in a user-specified order.
+ *
+ * @return auto-start order or null
+ */
+ @JacksonXmlProperty(isAttribute = true, localName = "order")
+ public Integer getAutoStartOrder()
+ {
+ return mAutoStartOrder;
+ }
+
+ /**
+ * Sets the auto-start order for this channel
+ *
+ * @param order
+ */
+ public void setAutoStartOrder(Integer order)
+ {
+ mAutoStartOrder = order;
+ }
+
+ /**
+ * Indicates if this channel has an auto-start order specified (ie non-null).
+ *
+ * @return true if this channel has an auto-start order
+ */
+ @JsonIgnore
+ public boolean hasAutoStartOrder()
+ {
+ return mAutoStartOrder != null;
}
/**
diff --git a/src/main/java/io/github/dsheirer/controller/channel/ChannelAutoStartFrame.java b/src/main/java/io/github/dsheirer/controller/channel/ChannelAutoStartFrame.java
new file mode 100644
index 00000000..92dbc945
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/controller/channel/ChannelAutoStartFrame.java
@@ -0,0 +1,246 @@
+/*******************************************************************************
+ * sdr-trunk
+ * Copyright (C) 2014-2018 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with this program.
+ * If not, see
+ *
+ ******************************************************************************/
+package io.github.dsheirer.controller.channel;
+
+import io.github.dsheirer.util.ThreadPool;
+import net.miginfocom.swing.MigLayout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.swing.JButton;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+import java.awt.Dimension;
+import java.awt.EventQueue;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.util.List;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class ChannelAutoStartFrame extends JFrame
+{
+ private final static Logger mLog = LoggerFactory.getLogger(ChannelAutoStartFrame.class);
+
+ private static final int COUNTDOWN_SECONDS = 10;
+ private ChannelEventListener mChannelEventListener;
+ private List mChannels;
+
+ private JLabel mCountdownLabel;
+ private JButton mStartButton;
+ private JButton mCancelButton;
+ private JTable mChannelTable;
+ private AtomicBoolean mChannelsStarted = new AtomicBoolean();
+
+ private ScheduledFuture> mTimerFuture;
+
+ /**
+ * Creates and displays a channel auto-start gui for presenting the user with a list of channels that
+ * will be automatically started once the countdown timer reaches zero, or the user chooses to start
+ * now or cancel.
+ *
+ * @param listener to receive channel start/enable request(s)
+ * @param channels to auto-start
+ */
+ public ChannelAutoStartFrame(ChannelEventListener listener, List channels)
+ {
+ mChannelEventListener = listener;
+ mChannels = channels;
+ init();
+
+ EventQueue.invokeLater(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ setVisible(true);
+ startTimer();
+ }
+ });
+ }
+
+ private void init()
+ {
+ setTitle("Auto-Start Channels");
+ setSize(new Dimension(400,300));
+ setLocationRelativeTo(null);
+ addWindowListener(new WindowAdapter()
+ {
+ @Override
+ public void windowClosing(WindowEvent event)
+ {
+ mLog.info("Channel auto-start canceled by user - window closed");
+ stopTimer();
+ super.windowClosing(event);
+ }
+ });
+
+ JPanel panel = new JPanel();
+ panel.setLayout(new MigLayout("","[grow,fill][grow,fill]",
+ "[][][grow,fill][]"));
+ panel.add(new JLabel("The following channels will be automatically"), "span");
+
+ mCountdownLabel = new JLabel(getCountdownText(COUNTDOWN_SECONDS));
+ panel.add(mCountdownLabel, "span");
+
+ panel.add(new JScrollPane(getChannelTable()), "span");
+
+ mStartButton = new JButton("Start Now");
+ mStartButton.addActionListener(new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ mLog.info("Starting [" + mChannels.size() + "] channels now - user invoked");
+ startChannels();
+ stopTimer();
+ }
+ });
+ panel.add(mStartButton);
+
+ mCancelButton = new JButton("Cancel");
+ mCancelButton.addActionListener(new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ mLog.info("Channel auto-start canceled by user");
+ stopTimer();
+ }
+ });
+ panel.add(mCancelButton);
+ setContentPane(panel);
+ }
+
+ private String getCountdownText(int value)
+ {
+ return "started in: " + value + " seconds.";
+ }
+
+ /**
+ * Updates the countdown text label on the swing event thread
+ * @param value for the countdown
+ */
+ private void updateCountdownText(final int value)
+ {
+ EventQueue.invokeLater(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ mCountdownLabel.setText(getCountdownText(value));
+ }
+ });
+ }
+
+ /**
+ * Creates (once) a table of auto-start channels
+ */
+ private JTable getChannelTable()
+ {
+ if(mChannelTable == null)
+ {
+ AutoStartChannelModel model = new AutoStartChannelModel(mChannels);
+ mChannelTable = new JTable(model);
+ }
+
+ return mChannelTable;
+ }
+
+ /**
+ * Sends an enable (ie start) request for each of the auto start channels.
+ *
+ * This method is thread-safe and will only be executed once.
+ *
+ * Although the redundant channel-start request would be ignored, this once-only will prevent the chance
+ * that both the timer and the user would attempt to start the channels at the same time.
+ */
+ private void startChannels()
+ {
+ if(mChannelsStarted.compareAndSet(false, true))
+ {
+ if(mChannelEventListener != null)
+ {
+ for(Channel channel: mChannels)
+ {
+ mChannelEventListener.channelChanged(new ChannelEvent(channel, ChannelEvent.Event.REQUEST_ENABLE));
+ }
+ }
+ }
+ }
+
+ /**
+ * Starts the countdown timer to fire once a second.
+ */
+ private void startTimer()
+ {
+ if(mTimerFuture == null)
+ {
+ mTimerFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new CountdownTimer(), 0, 1, TimeUnit.SECONDS);
+ }
+ }
+
+ /**
+ * Stops the countdown timer and disposes this frame
+ */
+ private void stopTimer()
+ {
+ if(mTimerFuture != null)
+ {
+ mTimerFuture.cancel(true);
+ mTimerFuture = null;
+ }
+
+ EventQueue.invokeLater(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ dispose();
+ }
+ });
+ }
+
+ /**
+ * Updates the countdown text and auto-starts the channels once the timer falls below zero.
+ */
+ public class CountdownTimer implements Runnable
+ {
+ private int mCount = COUNTDOWN_SECONDS;
+
+ @Override
+ public void run()
+ {
+ if(mCount >= 0)
+ {
+ updateCountdownText(mCount);
+ mCount--;
+ }
+ else
+ {
+ mLog.info("Starting [" + mChannels.size() + "] now - timer invoked");
+ startChannels();
+ stopTimer();
+ }
+ }
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/controller/channel/ChannelController.java b/src/main/java/io/github/dsheirer/controller/channel/ChannelController.java
index 24ae4915..de1b7cde 100644
--- a/src/main/java/io/github/dsheirer/controller/channel/ChannelController.java
+++ b/src/main/java/io/github/dsheirer/controller/channel/ChannelController.java
@@ -8,11 +8,19 @@ import net.coderazzi.filters.gui.AutoChoices;
import net.coderazzi.filters.gui.TableFilterHeader;
import net.miginfocom.swing.MigLayout;
-import javax.swing.*;
+import javax.swing.JButton;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+import javax.swing.ListSelectionModel;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.DefaultTableCellRenderer;
-import java.awt.*;
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
@@ -236,7 +244,7 @@ public class ChannelController extends JPanel
Channel channel = model.getChannelAtIndex( index );
- boolean enabled = channel.getEnabled();
+ boolean enabled = channel.isProcessing();
if( isSelected )
{
diff --git a/src/main/java/io/github/dsheirer/controller/channel/ChannelEditor.java b/src/main/java/io/github/dsheirer/controller/channel/ChannelEditor.java
index 9f28bf69..65d2cade 100644
--- a/src/main/java/io/github/dsheirer/controller/channel/ChannelEditor.java
+++ b/src/main/java/io/github/dsheirer/controller/channel/ChannelEditor.java
@@ -1,17 +1,17 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014-2016 Dennis Sheirer
- *
+ *
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
- *
+ *
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
- *
+ *
* You should have received a copy of the GNU General Public License
* along with this program. If not, see
******************************************************************************/
@@ -30,221 +30,227 @@ import io.github.dsheirer.source.SourceConfigurationEditor;
import io.github.dsheirer.source.SourceManager;
import net.miginfocom.swing.MigLayout;
-import javax.swing.*;
-import java.awt.*;
+import javax.swing.JButton;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class ChannelEditor extends Editor implements ActionListener, ChannelEventListener
{
- private static final long serialVersionUID = 1L;
+ private static final long serialVersionUID = 1L;
+ private static final String ACTION_START = "Start";
+ private static final String ACTION_STOP = "Stop";
+ private static final String ACTION_SAVE = "Save";
+ private static final String ACTION_RESET = "Reset";
- private NameConfigurationEditor mNameConfigurationEditor;
+ private NameConfigurationEditor mNameConfigurationEditor;
private SourceConfigurationEditor mSourceConfigurationEditor;
private DecodeConfigurationEditor mDecodeConfigurationEditor;
private AuxDecodeConfigurationEditor mAuxDecodeConfigurationEditor;
private EventLogConfigurationEditor mEventLogConfigurationEditor;
private RecordConfigurationEditor mRecordConfigurationEditor;
-
- private JButton mEnableButton = new JButton( "Enable" );
- private JLabel mChannelName = new JLabel( "Channel:" );
-
- private ChannelModel mChannelModel;
- private ChannelMapModel mChannelMapModel;
+
+ private JButton mProcessingStateButton = new JButton(ACTION_START);
+ private JLabel mChannelName = new JLabel("Channel:");
+
+ private ChannelModel mChannelModel;
+ private ChannelMapModel mChannelMapModel;
private SourceManager mSourceManager;
-
- private boolean mChannelEnableRequested = false;
-
- public ChannelEditor( ChannelModel channelModel,
- ChannelMapModel channelMapModel,
- SourceManager sourceManager,
- AliasModel aliasModel )
- {
- mChannelModel = channelModel;
- mChannelMapModel = channelMapModel;
- mSourceManager = sourceManager;
- mNameConfigurationEditor = new NameConfigurationEditor( aliasModel, mChannelModel );
-
- init();
- }
-
- private void init()
- {
- setLayout( new MigLayout( "fill,wrap 3", "[grow,fill][grow,fill][grow,fill]", "[grow,fill][]" ) );
-
- JideTabbedPane tabs = new JideTabbedPane();
- tabs.setFont( this.getFont() );
- tabs.setForeground( Color.BLACK );
- tabs.setTabTrailingComponent( mChannelName );
-
- mNameConfigurationEditor.setSaveRequestListener( this );
- tabs.addTab( "Name/Alias", mNameConfigurationEditor );
-
- mSourceConfigurationEditor = new SourceConfigurationEditor( mSourceManager );
- mSourceConfigurationEditor.setSaveRequestListener( this );
- tabs.addTab( "Source", mSourceConfigurationEditor );
-
- mDecodeConfigurationEditor = new DecodeConfigurationEditor( mChannelMapModel );
- mDecodeConfigurationEditor.setSaveRequestListener( this );
- tabs.addTab( "Decoder", mDecodeConfigurationEditor );
+ private boolean mChannelStartRequested = false;
- mAuxDecodeConfigurationEditor = new AuxDecodeConfigurationEditor();
- mAuxDecodeConfigurationEditor.setSaveRequestListener( this );
- tabs.addTab( "Aux Decoders", mAuxDecodeConfigurationEditor );
-
- mEventLogConfigurationEditor = new EventLogConfigurationEditor();
- mEventLogConfigurationEditor.setSaveRequestListener( this );
- tabs.addTab( "Logging", mEventLogConfigurationEditor );
-
- mRecordConfigurationEditor = new RecordConfigurationEditor();
- mRecordConfigurationEditor.setSaveRequestListener( this );
- tabs.addTab( "Recording", mRecordConfigurationEditor );
-
- add( tabs, "span" );
-
- mEnableButton.addActionListener( this );
- mEnableButton.setEnabled( false );
- mEnableButton.setToolTipText( "Start the currently selected channel running/decoding" );
- add( mEnableButton );
-
- JButton btnSave = new JButton( "Save" );
- btnSave.setToolTipText( "Save changes to the currently selected channel" );
- btnSave.addActionListener( ChannelEditor.this );
- add( btnSave );
-
- JButton btnReset = new JButton( "Reset" );
- btnReset.setToolTipText( "Reload the currently selected channel" );
- btnReset.addActionListener( ChannelEditor.this );
- add( btnReset );
- }
-
- @Override
- public void channelChanged( ChannelEvent event )
- {
- if( hasItem() && getItem() == event.getChannel() )
- {
- switch( event.getEvent() )
- {
- case NOTIFICATION_CONFIGURATION_CHANGE:
- case NOTIFICATION_PROCESSING_START:
- case NOTIFICATION_PROCESSING_STOP:
- setItem( getItem() );
- mChannelEnableRequested = false;
- break;
- case NOTIFICATION_DELETE:
- setItem( null );
- break;
- case NOTIFICATION_ENABLE_REJECTED:
- if( mChannelEnableRequested )
- {
- JOptionPane.showMessageDialog( ChannelEditor.this, "Channel could not be "
- + "enabled. This is likely because there are no tuner channels "
- + "available", "Couldn't enable channel", JOptionPane.INFORMATION_MESSAGE );
-
- mChannelEnableRequested = false;
- }
- break;
- default:
- break;
- }
- }
- }
-
- @Override
- public void actionPerformed( ActionEvent e )
+ public ChannelEditor(ChannelModel channelModel,
+ ChannelMapModel channelMapModel,
+ SourceManager sourceManager,
+ AliasModel aliasModel)
{
- String command = e.getActionCommand();
-
- if( command.contentEquals( "Enable" ) )
- {
- if( hasItem() )
- {
- save();
-
- mChannelEnableRequested = true;
- mChannelModel.broadcast( new ChannelEvent( getItem(), ChannelEvent.Event.REQUEST_ENABLE ) );
- }
- }
- else if( command.contentEquals( "Disable" ) )
- {
- if( hasItem() )
- {
- mChannelModel.broadcast( new ChannelEvent( getItem(), ChannelEvent.Event.REQUEST_DISABLE ) );
- }
- }
- else if( command.contentEquals( "Save" ) )
- {
- save();
- }
- else if( command.contentEquals( "Reset" ) )
- {
- setItem( getItem() );
- }
+ mChannelModel = channelModel;
+ mChannelMapModel = channelMapModel;
+ mSourceManager = sourceManager;
+ mNameConfigurationEditor = new NameConfigurationEditor(aliasModel, mChannelModel);
+
+ init();
+ }
+
+ private void init()
+ {
+ setLayout(new MigLayout("fill,wrap 3", "[grow,fill][grow,fill][grow,fill]", "[grow,fill][]"));
+
+ JideTabbedPane tabs = new JideTabbedPane();
+ tabs.setFont(this.getFont());
+ tabs.setForeground(Color.BLACK);
+
+ tabs.setTabTrailingComponent(mChannelName);
+
+ mNameConfigurationEditor.setSaveRequestListener(this);
+ tabs.addTab("Name/Alias", mNameConfigurationEditor);
+
+ mSourceConfigurationEditor = new SourceConfigurationEditor(mSourceManager);
+ mSourceConfigurationEditor.setSaveRequestListener(this);
+ tabs.addTab("Source", mSourceConfigurationEditor);
+
+ mDecodeConfigurationEditor = new DecodeConfigurationEditor(mChannelMapModel);
+ mDecodeConfigurationEditor.setSaveRequestListener(this);
+ tabs.addTab("Decoder", mDecodeConfigurationEditor);
+
+ mAuxDecodeConfigurationEditor = new AuxDecodeConfigurationEditor();
+ mAuxDecodeConfigurationEditor.setSaveRequestListener(this);
+ tabs.addTab("Aux Decoders", mAuxDecodeConfigurationEditor);
+
+ mEventLogConfigurationEditor = new EventLogConfigurationEditor();
+ mEventLogConfigurationEditor.setSaveRequestListener(this);
+ tabs.addTab("Logging", mEventLogConfigurationEditor);
+
+ mRecordConfigurationEditor = new RecordConfigurationEditor();
+ mRecordConfigurationEditor.setSaveRequestListener(this);
+ tabs.addTab("Recording", mRecordConfigurationEditor);
+
+ add(tabs, "span");
+
+ mProcessingStateButton.addActionListener(this);
+ mProcessingStateButton.setEnabled(false);
+ mProcessingStateButton.setToolTipText("Start/Stop the currently selected channel");
+ add(mProcessingStateButton);
+
+ JButton btnSave = new JButton(ACTION_SAVE);
+ btnSave.setToolTipText("Save changes to the currently selected channel");
+ btnSave.addActionListener(ChannelEditor.this);
+ add(btnSave);
+
+ JButton btnReset = new JButton(ACTION_RESET);
+ btnReset.setToolTipText("Reload the currently selected channel");
+ btnReset.addActionListener(ChannelEditor.this);
+ add(btnReset);
+ }
+
+ @Override
+ public void channelChanged(ChannelEvent event)
+ {
+ if(hasItem() && getItem() == event.getChannel())
+ {
+ switch(event.getEvent())
+ {
+ case NOTIFICATION_CONFIGURATION_CHANGE:
+ case NOTIFICATION_PROCESSING_START:
+ case NOTIFICATION_PROCESSING_STOP:
+ setItem(getItem());
+ mChannelStartRequested = false;
+ break;
+ case NOTIFICATION_DELETE:
+ setItem(null);
+ break;
+ case NOTIFICATION_START_PROCESSING_REJECTED:
+ if(mChannelStartRequested)
+ {
+ JOptionPane.showMessageDialog(ChannelEditor.this, "Channel could not be "
+ + "started. This is likely because there are no tuner channels "
+ + "available", "Couldn't start channel", JOptionPane.INFORMATION_MESSAGE);
+
+ mChannelStartRequested = false;
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ String command = e.getActionCommand();
+
+ if(command.contentEquals(ACTION_START))
+ {
+ if(hasItem())
+ {
+ save();
+
+ mChannelStartRequested = true;
+ mChannelModel.broadcast(new ChannelEvent(getItem(), ChannelEvent.Event.REQUEST_ENABLE));
+ }
+ }
+ else if(command.contentEquals(ACTION_STOP))
+ {
+ if(hasItem())
+ {
+ mChannelModel.broadcast(new ChannelEvent(getItem(), ChannelEvent.Event.REQUEST_DISABLE));
+ }
+ }
+ else if(command.contentEquals(ACTION_SAVE))
+ {
+ save();
+ }
+ else if(command.contentEquals(ACTION_RESET))
+ {
+ setItem(getItem());
+ }
+ }
+
+ /**
+ * Sets the channel configuration in each of the channel editor components
+ * Note: this method must be invoked on the swing event dispatch thread
+ */
+ public void setItem(final Channel channel)
+ {
+ super.setItem(channel);
+
+ mNameConfigurationEditor.setItem(channel);
+ mSourceConfigurationEditor.setItem(channel);
+ mDecodeConfigurationEditor.setItem(channel);
+ mAuxDecodeConfigurationEditor.setItem(channel);
+ mEventLogConfigurationEditor.setItem(channel);
+ mRecordConfigurationEditor.setItem(channel);
+
+ if(channel != null)
+ {
+ mChannelName.setText("Channel: " + channel.getName());
+ mProcessingStateButton.setText(channel.isProcessing() ? ACTION_STOP : ACTION_START);
+ mProcessingStateButton.setEnabled(true);
+ mProcessingStateButton.setBackground(channel.isProcessing() ? Color.GREEN : getBackground());
+ }
+ else
+ {
+ mChannelName.setText("Channel: ");
+ mProcessingStateButton.setText(ACTION_START);
+ mProcessingStateButton.setEnabled(false);
+ mProcessingStateButton.setBackground(getBackground());
+ }
}
- /**
- * Sets the channel configuration in each of the channel editor components
- * Note: this method must be invoked on the swing event dispatch thread
- */
- public void setItem( final Channel channel )
- {
- super.setItem( channel );
-
- mNameConfigurationEditor.setItem( channel );
- mSourceConfigurationEditor.setItem( channel );
- mDecodeConfigurationEditor.setItem( channel );
- mAuxDecodeConfigurationEditor.setItem( channel );
- mEventLogConfigurationEditor.setItem( channel );
- mRecordConfigurationEditor.setItem( channel );
-
- if( channel != null )
- {
- mChannelName.setText( "Channel: " + channel.getName() );
- mEnableButton.setText( channel.getEnabled() ? "Disable" : "Enable" );
- mEnableButton.setEnabled( true );
- mEnableButton.setBackground( channel.getEnabled() ? Color.GREEN : getBackground() );
- }
- else
- {
- mChannelName.setText( "Channel: " );
- mEnableButton.setText( "Enable" );
- mEnableButton.setEnabled( false );
- mEnableButton.setBackground( getBackground() );
- }
- }
-
public void save()
{
- if( hasItem() )
- {
- mNameConfigurationEditor.save();
- mSourceConfigurationEditor.save();
- mDecodeConfigurationEditor.save();
- mAuxDecodeConfigurationEditor.save();
- mEventLogConfigurationEditor.save();
- mRecordConfigurationEditor.save();
-
- try
- {
- mDecodeConfigurationEditor.validate( mSourceConfigurationEditor );
- mDecodeConfigurationEditor.validate( mAuxDecodeConfigurationEditor );
- }
- catch( EditorValidationException e )
- {
- e.getEditor().requestFocus();
-
- JOptionPane.showMessageDialog( e.getEditor(), e.getReason(),
- "Configuration Error", JOptionPane.ERROR_MESSAGE );
-
- return;
- }
-
+ if(hasItem())
+ {
+ mNameConfigurationEditor.save();
+ mSourceConfigurationEditor.save();
+ mDecodeConfigurationEditor.save();
+ mAuxDecodeConfigurationEditor.save();
+ mEventLogConfigurationEditor.save();
+ mRecordConfigurationEditor.save();
- mChannelModel.broadcast( new ChannelEvent( getItem(),
- ChannelEvent.Event.NOTIFICATION_CONFIGURATION_CHANGE ) );
- }
+ try
+ {
+ mDecodeConfigurationEditor.validate(mSourceConfigurationEditor);
+ mDecodeConfigurationEditor.validate(mAuxDecodeConfigurationEditor);
+ }
+ catch(EditorValidationException e)
+ {
+ e.getEditor().requestFocus();
- setModified( false );
+ JOptionPane.showMessageDialog(e.getEditor(), e.getReason(),
+ "Configuration Error", JOptionPane.ERROR_MESSAGE);
+
+ return;
+ }
+
+
+ mChannelModel.broadcast(new ChannelEvent(getItem(),
+ ChannelEvent.Event.NOTIFICATION_CONFIGURATION_CHANGE));
+ }
+
+ setModified(false);
}
}
diff --git a/src/main/java/io/github/dsheirer/controller/channel/ChannelEvent.java b/src/main/java/io/github/dsheirer/controller/channel/ChannelEvent.java
index fc778521..42679a38 100644
--- a/src/main/java/io/github/dsheirer/controller/channel/ChannelEvent.java
+++ b/src/main/java/io/github/dsheirer/controller/channel/ChannelEvent.java
@@ -1,6 +1,6 @@
/*******************************************************************************
* SDR Trunk
- * Copyright (C) 2014-2016 Dennis Sheirer
+ * Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -42,6 +42,11 @@ public class ChannelEvent
{
return mEvent;
}
+
+ public String toString()
+ {
+ return getEvent().name() + " Channel:" + getChannel().getName();
+ }
/**
* Channel events to describe the specific event
@@ -55,7 +60,7 @@ public class ChannelEvent
//Channel is deleted/removed
NOTIFICATION_DELETE,
//Channel enable request was rejected
- NOTIFICATION_ENABLE_REJECTED,
+ NOTIFICATION_START_PROCESSING_REJECTED,
//Channel has started processing/decoding
NOTIFICATION_PROCESSING_START,
//Channel has stopped processing/decoding
diff --git a/src/main/java/io/github/dsheirer/controller/channel/ChannelModel.java b/src/main/java/io/github/dsheirer/controller/channel/ChannelModel.java
index 8b0d2516..30939c9c 100644
--- a/src/main/java/io/github/dsheirer/controller/channel/ChannelModel.java
+++ b/src/main/java/io/github/dsheirer/controller/channel/ChannelModel.java
@@ -25,6 +25,7 @@ import io.github.dsheirer.module.decode.DecoderType;
import javax.swing.table.AbstractTableModel;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.Comparator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
@@ -42,9 +43,11 @@ public class ChannelModel extends AbstractTableModel implements IChannelEventBro
public static final int COLUMN_ALIAS_LIST = 4;
public static final int COLUMN_SOURCE = 5;
public static final int COLUMN_DECODER = 6;
+ public static final int COLUMN_AUTO_START = 7;
- private static final String[] COLUMN_NAMES = new String[] {"Enabled", "System", "Site", "Name", "Alias List",
- "Source", "Decoder"};
+ private static final String[] COLUMN_NAMES = new String[] {"Playing", "System", "Site", "Name", "Alias List",
+ "Source", "Decoder", "Auto-Start"};
+ private static final String VALUE_YES = "Yes";
private List mChannels = new CopyOnWriteArrayList<>();
private List mTrafficChannels = new CopyOnWriteArrayList<>();
@@ -209,11 +212,6 @@ public class ChannelModel extends AbstractTableModel implements IChannelEventBro
broadcast(new ChannelEvent(channel, Event.NOTIFICATION_ADD));
- if(channel.getEnabled())
- {
- broadcast(new ChannelEvent(channel, Event.REQUEST_ENABLE));
- }
-
return index;
}
@@ -247,6 +245,48 @@ public class ChannelModel extends AbstractTableModel implements IChannelEventBro
}
}
+ /**
+ * List of channels that have the auto-start flag set.
+ *
+ * @return auto-start channels sorted by the channel order value.
+ */
+ public List getAutoStartChannels()
+ {
+ List autoStartChannels = new ArrayList<>();
+
+ for(Channel channel: getChannels())
+ {
+ if(channel.isAutoStart())
+ {
+ autoStartChannels.add(channel);
+ }
+ }
+
+ Collections.sort(autoStartChannels, new Comparator()
+ {
+ @Override
+ public int compare(Channel channel1, Channel channel2)
+ {
+ if(channel1.hasAutoStartOrder() && channel2.hasAutoStartOrder())
+ {
+ return Integer.compare(channel1.getAutoStartOrder(), channel2.getAutoStartOrder());
+ }
+ else if(channel1.hasAutoStartOrder())
+ {
+ return -1;
+ }
+ else if(channel2.hasAutoStartOrder())
+ {
+ return 1;
+ }
+
+ return 0;
+ }
+ });
+
+ return autoStartChannels;
+ }
+
/**
* Returns a list of channels that fall within the frequency range
*
@@ -331,7 +371,7 @@ public class ChannelModel extends AbstractTableModel implements IChannelEventBro
switch(columnIndex)
{
case COLUMN_ENABLED:
- return channel.getEnabled() ? "Enabled" : null;
+ return channel.isProcessing() ? VALUE_YES : null;
case COLUMN_SYSTEM:
return channel.getSystem();
case COLUMN_SITE:
@@ -344,6 +384,19 @@ public class ChannelModel extends AbstractTableModel implements IChannelEventBro
return channel.getSourceConfiguration().getDescription();
case COLUMN_DECODER:
return channel.getDecodeConfiguration().getDecoderType().getShortDisplayString();
+ case COLUMN_AUTO_START:
+ if(channel.isAutoStart())
+ {
+ if(channel.hasAutoStartOrder())
+ {
+ return channel.getAutoStartOrder();
+ }
+ else
+ {
+ return VALUE_YES;
+ }
+ }
+ break;
}
return null;
diff --git a/src/main/java/io/github/dsheirer/controller/channel/ChannelProcessingManager.java b/src/main/java/io/github/dsheirer/controller/channel/ChannelProcessingManager.java
index ee4a232d..c456b8e6 100644
--- a/src/main/java/io/github/dsheirer/controller/channel/ChannelProcessingManager.java
+++ b/src/main/java/io/github/dsheirer/controller/channel/ChannelProcessingManager.java
@@ -159,7 +159,7 @@ public class ChannelProcessingManager implements ChannelEventListener
}
break;
case REQUEST_DISABLE:
- if(channel.getEnabled())
+ if(channel.isProcessing())
{
switch(channel.getChannelType())
{
@@ -176,7 +176,7 @@ public class ChannelProcessingManager implements ChannelEventListener
}
}
case NOTIFICATION_DELETE:
- if(channel.getEnabled())
+ if(channel.isProcessing())
{
stopProcessing(channel, true);
}
@@ -220,9 +220,9 @@ public class ChannelProcessingManager implements ChannelEventListener
if(source == null)
{
- channel.setEnabled(false);
+ channel.setProcessing(false);
- mChannelModel.broadcast(new ChannelEvent(channel, ChannelEvent.Event.NOTIFICATION_ENABLE_REJECTED));
+ mChannelModel.broadcast(new ChannelEvent(channel, ChannelEvent.Event.NOTIFICATION_START_PROCESSING_REJECTED));
return;
}
@@ -325,7 +325,7 @@ public class ChannelProcessingManager implements ChannelEventListener
getChannelMetadataModel().add(processingChain.getChannelState().getMutableMetadata(), channel);
- channel.setEnabled(true);
+ channel.setProcessing(true);
mProcessingChains.put(channel.getChannelID(), processingChain);
@@ -334,7 +334,7 @@ public class ChannelProcessingManager implements ChannelEventListener
private void stopProcessing(Channel channel, boolean remove)
{
- channel.setEnabled(false);
+ channel.setProcessing(false);
if(mProcessingChains.containsKey(channel.getChannelID()))
{
diff --git a/src/main/java/io/github/dsheirer/controller/channel/ChannelUtils.java b/src/main/java/io/github/dsheirer/controller/channel/ChannelUtils.java
index 657b65e4..69cff222 100644
--- a/src/main/java/io/github/dsheirer/controller/channel/ChannelUtils.java
+++ b/src/main/java/io/github/dsheirer/controller/channel/ChannelUtils.java
@@ -4,8 +4,11 @@ import io.github.dsheirer.channel.state.DecoderState;
import io.github.dsheirer.module.ProcessingChain;
import io.github.dsheirer.module.decode.event.ActivitySummaryFrame;
-import javax.swing.*;
-import java.awt.*;
+import javax.swing.JMenu;
+import javax.swing.JMenuItem;
+import javax.swing.JOptionPane;
+import javax.swing.JSeparator;
+import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
@@ -23,7 +26,7 @@ public class ChannelUtils
{
JMenu menu = new JMenu( "Channel: " + channel.getName() );
- if( channel.getEnabled() )
+ if( channel.isProcessing() )
{
JMenuItem disable = new JMenuItem( "Disable" );
disable.addActionListener( new ActionListener()
diff --git a/src/main/java/io/github/dsheirer/controller/channel/NameConfigurationEditor.java b/src/main/java/io/github/dsheirer/controller/channel/NameConfigurationEditor.java
index 9d07aa73..6699e177 100644
--- a/src/main/java/io/github/dsheirer/controller/channel/NameConfigurationEditor.java
+++ b/src/main/java/io/github/dsheirer/controller/channel/NameConfigurationEditor.java
@@ -23,9 +23,21 @@ import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import javax.swing.*;
+import javax.swing.ComboBoxModel;
+import javax.swing.DefaultComboBoxModel;
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSpinner;
+import javax.swing.JTextField;
+import javax.swing.SpinnerModel;
+import javax.swing.SpinnerNumberModel;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
+import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Collections;
@@ -48,6 +60,8 @@ public class NameConfigurationEditor extends Editor implements Document
private JComboBox mSystemNameCombo;
private JComboBox mSiteNameCombo;
private JComboBox mAliasListCombo;
+ private JCheckBox mAutoStartCheckBox;
+ private JSpinner mAutoStartOrder;
public NameConfigurationEditor(AliasModel aliasModel, ChannelModel model)
{
@@ -139,6 +153,40 @@ public class NameConfigurationEditor extends Editor implements Document
add(new JLabel("Site:"));
add(mSiteNameCombo);
+
+ JPanel autoStartPanel = new JPanel();
+
+ mAutoStartCheckBox = new JCheckBox("Auto-Start");
+ mAutoStartCheckBox.setToolTipText("Automatically start this channel each time the application runs.");
+ mAutoStartCheckBox.addActionListener(new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ mAutoStartOrder.setEnabled(mAutoStartCheckBox.isSelected());
+ setModified(true);
+ }
+ });
+ autoStartPanel.add(mAutoStartCheckBox);
+
+ SpinnerModel model = new SpinnerNumberModel(0,0,500,1);
+ mAutoStartOrder = new JSpinner(model);
+ mAutoStartOrder.setEnabled(false);
+ mAutoStartOrder.setPreferredSize(new Dimension(60, mAutoStartOrder.getPreferredSize().height));
+ mAutoStartOrder.setToolTipText("Auto-start order: 1 (high) <> 500 (low). 0 = no ordering");
+ mAutoStartOrder.addChangeListener(new ChangeListener()
+ {
+ @Override
+ public void stateChanged(ChangeEvent e)
+ {
+ setModified(true);
+ }
+ });
+
+ autoStartPanel.add(new JLabel("Order:"));
+ autoStartPanel.add(mAutoStartOrder);
+ add(new JLabel()); //Empty space holder
+ add(autoStartPanel);
}
@Override
@@ -156,6 +204,11 @@ public class NameConfigurationEditor extends Editor implements Document
Object aliasList = mAliasListCombo.getSelectedItem();
getItem().setAliasListName(aliasList == null ? null : (String)aliasList);
+
+ getItem().setAutoStart(mAutoStartCheckBox.isSelected());
+
+ int order = ((Number)mAutoStartOrder.getValue()).intValue();
+ getItem().setAutoStartOrder((order > 0 ? order : null));
}
setModified(false);
@@ -182,6 +235,16 @@ public class NameConfigurationEditor extends Editor implements Document
{
mSiteNameCombo.setEnabled(enabled);
}
+
+ if(mAutoStartCheckBox.isEnabled() != enabled)
+ {
+ mAutoStartCheckBox.setEnabled(enabled);
+ }
+
+ if(mAutoStartOrder.isEnabled() != enabled)
+ {
+ mAutoStartOrder.setEnabled(enabled);
+ }
}
@Override
@@ -237,6 +300,19 @@ public class NameConfigurationEditor extends Editor implements Document
{
mAliasListCombo.setSelectedItem(channel.getAliasListName());
}
+
+ if(channel.isAutoStart())
+ {
+ mAutoStartCheckBox.setSelected(channel.isAutoStart());
+ mAutoStartOrder.setEnabled(true);
+ }
+ else
+ {
+ mAutoStartCheckBox.setSelected(false);
+ mAutoStartOrder.setEnabled(false);
+ }
+
+ mAutoStartOrder.setValue(channel.hasAutoStartOrder() ? channel.getAutoStartOrder() : 0);
}
else
{
@@ -244,6 +320,7 @@ public class NameConfigurationEditor extends Editor implements Document
mChannelName.setText(null);
}
+
setModified(false);
}
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java b/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java
index 11e6955e..6ac9a164 100644
--- a/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java
+++ b/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java
@@ -18,6 +18,7 @@ package io.github.dsheirer.dsp.filter.fir.complex;
import io.github.dsheirer.dsp.filter.fir.FIRFilter;
import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter;
import io.github.dsheirer.sample.complex.Complex;
+import io.github.dsheirer.sample.complex.ComplexBuffer;
public class ComplexFIRFilter extends FIRFilter
{
@@ -37,83 +38,47 @@ public class ComplexFIRFilter extends FIRFilter
mQFilter = new RealFIRFilter(coefficients, gain);
}
- /**
- * Coefficients used in each of the I and Q filters
- */
public float[] getCoefficients()
{
return mIFilter.getCoefficients();
}
- /**
- * Filters the inphase sample independent of the quadrature side of the filter
- * @param sample to filter
- * @return filtered inphase sample
- */
public float filterInphase(float sample)
{
return mIFilter.filter(sample);
}
- /**
- * Filters the quadrature sample independent of the inphase side of the filter
- * @param sample to filter
- * @return filtered quadrature sample
- */
public float filterQuadrature(float sample)
{
return mQFilter.filter(sample);
}
- /**
- * Loads the complex sample and provides a filtered output
- * @param sample to load
- * @return filtered complex sample
- */
public Complex filter(Complex sample)
{
- return filter(sample.inphase(), sample.quadrature());
+ float i = filterInphase(sample.inphase());
+ float q = filterQuadrature(sample.quadrature());
+
+ return new Complex(i, q);
}
/**
- * Loads the complex sample and provides a filtered output
- * @param i inphase sample to load
- * @param q quadrature sample to load
- * @return filtered complex sample
- */
- public Complex filter(float i, float q)
- {
- float filteredI = filterInphase(i);
- float filteredQ = filterQuadrature(q);
-
- return new Complex(filteredI, filteredQ);
- }
-
- /**
- * Filters the complex sample array where the samples are loaded as i0,q0,i1,q1 ... iN-2,qN-1
+ * Filters the complex samples from the buffer and returns a new complex buffer with the filtered output
*
- * @param samples to filter.
+ * @param buffer with complex samples to filter
+ * @return new buffer containing filtered complex samples
*/
- public float[] filter(float[] 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)
{
- samples[x] = filterInphase(samples[x]);
- samples[x + 1] = filterInphase(samples[x + 1]);
+ filtered[x] = filterInphase(samples[x]);
+ filtered[x + 1] = filterQuadrature(samples[x + 1]);
}
- return samples;
- }
-
- /**
- * Loads the complex sample into the filter but does not calculate a filtered output sample
- * @param i inphase sample
- * @param q quadrature sample
- */
- public void load(float i, float q)
- {
- mIFilter.load(i);
- mQFilter.load(q);
+ return new ComplexBuffer(filtered);
}
@Override
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/interpolator/RealInterpolator.java b/src/main/java/io/github/dsheirer/dsp/filter/interpolator/RealInterpolator.java
index d0b99908..ff933ed1 100644
--- a/src/main/java/io/github/dsheirer/dsp/filter/interpolator/RealInterpolator.java
+++ b/src/main/java/io/github/dsheirer/dsp/filter/interpolator/RealInterpolator.java
@@ -5,54 +5,86 @@ import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.text.DecimalFormat;
+import java.util.Arrays;
+
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;
-
- public RealInterpolator( float gain )
- {
- mGain = gain;
- }
- /**
- * Calculates an interpolated value from the 8 samples beginning at the
- * offset index. Uses the mu value to determine which of 128 filter kernels
- * 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
- * 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);
+ private float mGain;
- /* Identify the filter bank that corresponds to mu */
- int index = (int)( NSTEPS * mu );
-
- 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 ] );
-
- return accumulator * mGain;
- }
-
- 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 );
- }
+ /**
+ * Provides an interpolated sample point along an eight-sample waveform representation using 128 filters to
+ * provide approximate interpolation with a resolution of 1/128th of a sample.
+ *
+ * @param gain to apply to the interpolated sample
+ */
+ public RealInterpolator(float gain)
+ {
+ mGain = gain;
+ }
+
+ /**
+ * 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);
+
+ /* Identify the filter bank that corresponds to mu */
+ int index = (int)(NSTEPS * mu);
+
+ 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]);
+
+ return accumulator * mGain;
+ }
+
+ 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)));
+ }
+
+ }
}
diff --git a/src/main/java/io/github/dsheirer/dsp/fm/FMDemodulator.java b/src/main/java/io/github/dsheirer/dsp/fm/FMDemodulator.java
index 30049836..6a53a8ff 100644
--- a/src/main/java/io/github/dsheirer/dsp/fm/FMDemodulator.java
+++ b/src/main/java/io/github/dsheirer/dsp/fm/FMDemodulator.java
@@ -1,78 +1,103 @@
/*******************************************************************************
* 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
******************************************************************************/
package io.github.dsheirer.dsp.fm;
+import io.github.dsheirer.sample.complex.Complex;
+
public abstract class FMDemodulator
{
- private float mPreviousI = 0.0f;
- private float mPreviousQ = 0.0f;
- protected float mGain;
+ private float mPreviousI = 0.0f;
+ private float mPreviousQ = 0.0f;
+ protected float mGain;
- public FMDemodulator( float gain )
- {
- mGain = gain;
- }
+ public FMDemodulator(float gain)
+ {
+ mGain = gain;
+ }
- public float demodulate( float currentI, float currentQ )
- {
- /**
- * Multiply the current sample against the complex conjugate of the
- * previous sample to derive the phase delta between the two samples
- *
- * Negating the previous sample quadrature produces the conjugate
- */
- double inphase = ( currentI * mPreviousI ) - ( currentQ * -mPreviousQ );
- double quadrature = ( currentQ * mPreviousI ) + ( currentI * -mPreviousQ );
-
- double angle = 0.0f;
-
- //Check for divide by zero
- if( inphase != 0 )
- {
- /**
- * 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 );
- }
-
- /**
- * Store the current sample to use during the next iteration
- */
- mPreviousI = currentI;
- mPreviousQ = currentQ;
-
- return (float)( angle * mGain );
- }
+ public float demodulate(float currentI, float currentQ)
+ {
+ /**
+ * Multiply the current sample against the complex conjugate of the
+ * previous sample to derive the phase delta between the two samples
+ *
+ * Negating the previous sample quadrature produces the conjugate
+ */
+ double inphase = (currentI * mPreviousI) - (currentQ * -mPreviousQ);
+ double quadrature = (currentQ * mPreviousI) + (currentI * -mPreviousQ);
- public abstract void dispose();
+ double angle = 0.0f;
- public void reset()
- {
- mPreviousI = 0.0f;
- mPreviousQ = 0.0f;
- }
+ //Check for divide by zero
+ if(inphase != 0)
+ {
+ /**
+ * 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);
+ }
- public void setGain( float gain )
- {
- mGain = gain;
- }
+ /**
+ * Store the current sample to use during the next iteration
+ */
+ mPreviousI = currentI;
+ mPreviousQ = currentQ;
+
+ return (float)(angle * mGain);
+ }
+
+ 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());
+
+ double angle = 0.0f;
+
+ //Check for divide by zero
+ if(inphase != 0)
+ {
+ /**
+ * 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;
+ }
}
\ No newline at end of file
diff --git a/src/main/java/io/github/dsheirer/dsp/fm/FMDemodulator_CB.java b/src/main/java/io/github/dsheirer/dsp/fm/FMDemodulator_CB.java
index a11620a2..b5cd4380 100644
--- a/src/main/java/io/github/dsheirer/dsp/fm/FMDemodulator_CB.java
+++ b/src/main/java/io/github/dsheirer/dsp/fm/FMDemodulator_CB.java
@@ -26,8 +26,7 @@ import org.slf4j.LoggerFactory;
public class FMDemodulator_CB extends FMDemodulator implements Listener
{
- private final static Logger mLog =
- LoggerFactory.getLogger( FMDemodulator_CB.class );
+ private final static Logger mLog = LoggerFactory.getLogger( FMDemodulator_CB.class );
private Listener mListener;
diff --git a/src/main/java/io/github/dsheirer/dsp/gain/ComplexFeedForwardGainControl.java b/src/main/java/io/github/dsheirer/dsp/gain/ComplexFeedForwardGainControl.java
index 4334e214..215d7b46 100644
--- a/src/main/java/io/github/dsheirer/dsp/gain/ComplexFeedForwardGainControl.java
+++ b/src/main/java/io/github/dsheirer/dsp/gain/ComplexFeedForwardGainControl.java
@@ -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
+ *
+ ******************************************************************************/
package io.github.dsheirer.dsp.gain;
-import io.github.dsheirer.buffer.RealCircularBuffer;
+import io.github.dsheirer.buffer.FloatCircularBuffer;
import io.github.dsheirer.sample.complex.Complex;
+import io.github.dsheirer.sample.complex.ComplexBuffer;
import io.github.dsheirer.sample.complex.ComplexSampleListener;
-/*******************************************************************************
- * SDR Trunk
- * Copyright (C) 2015 Dennis Sheirer
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see
- *
- * This class is modeled using concepts detailed in the GNURadio
- * implementation of feed forward AGC class located at:
- * https://github.com/gnuradio/gnuradio/blob/master/gr-analog/lib/
- * feedforward_agc_cc_impl.cc
- *
- ******************************************************************************/
public class ComplexFeedForwardGainControl implements ComplexSampleListener
{
- public static final float OBJECTIVE_ENVELOPE = 1.0f;
- public static final float MINIMUM_ENVELOPE = 0.0001f;
+ public static final float OBJECTIVE_ENVELOPE = 1.0f;
+ public static final float MINIMUM_ENVELOPE = 0.0001f;
- private ComplexSampleListener mListener;
+ private ComplexSampleListener mListener;
- private RealCircularBuffer mEnvelopeHistory;
-
- private float mMaxEnvelope = 0.0f;
- private float mGain = 1.0f;
-
- /**
- * Dynamic gain control for incoming sample stream to amplify or attenuate
- * all samples toward an objective unity)gain, using the maximum envelope
- * value detected in the stream history window.
- *
- * Uses the specified damping factor to limit gain swings. Damping factor
- * is applied against the delta between current gain value and a recalculated
- * gain value to limit how quickly the gain value will increase or decrease.
- *
- * @param window - history size to use in detecting maximum envelope value
- * @param damping - a value between 0 < damping <= 1.0;
- */
- public ComplexFeedForwardGainControl( int window )
- {
- mEnvelopeHistory = new RealCircularBuffer( window );
- }
-
- public void dispose()
- {
- mListener = null;
- }
+ private FloatCircularBuffer mEnvelopeHistory;
- @Override
- public void receive( float inphase, float quadrature )
- {
- float envelope = Complex.envelope( inphase, quadrature );
+ private float mMaxEnvelope = 0.0f;
+ private float mGain = 1.0f;
- if( envelope > mMaxEnvelope )
- {
- mMaxEnvelope = envelope;
-
- adjustGain();
- }
+ /**
+ * Dynamic gain control for incoming sample stream to amplify or attenuate
+ * all samples toward an objective unity)gain, using the maximum envelope
+ * value detected in the stream history window.
+ *
+ * Uses the specified damping factor to limit gain swings. Damping factor
+ * is applied against the delta between current gain value and a recalculated
+ * gain value to limit how quickly the gain value will increase or decrease.
+ *
+ * @param window - history size to use in detecting maximum envelope value
+ */
+ public ComplexFeedForwardGainControl(int window)
+ {
+ mEnvelopeHistory = new FloatCircularBuffer(window);
+ }
- /* Replace oldest envelope value with current envelope value */
- float oldestEnvelope = mEnvelopeHistory.putAndGet( envelope );
+ public void dispose()
+ {
+ mListener = null;
+ }
- /* If the oldest envelope value was the max envelope value, then we
- * have to rediscover the max value from the envelope history */
- if( mMaxEnvelope == oldestEnvelope && mMaxEnvelope != envelope )
- {
- mMaxEnvelope = MINIMUM_ENVELOPE;
-
- for( float value: mEnvelopeHistory.getBuffer() )
- {
- if( value > mMaxEnvelope )
- {
- mMaxEnvelope = value;
- }
- }
-
- adjustGain();
- }
-
- /* Apply current gain value to the sample and send to the listener */
- if( mListener != null )
- {
- mListener.receive( inphase *= mGain, quadrature *= mGain );
- }
- }
-
- private void adjustGain()
- {
- mGain = OBJECTIVE_ENVELOPE / mMaxEnvelope;
- }
+ @Override
+ public void receive(float inphase, float quadrature)
+ {
+ process(inphase, quadrature);
- public void setListener( ComplexSampleListener listener )
- {
- mListener = listener;
- }
+ /* Apply current gain value to the sample and send to the listener */
+ if(mListener != null)
+ {
+ mListener.receive(inphase *= mGain, quadrature *= mGain);
+ }
+ }
+
+ private void process(float inphase, float quadrature)
+ {
+ float envelope = Complex.envelope(inphase, quadrature);
+
+ if(envelope > mMaxEnvelope)
+ {
+ mMaxEnvelope = envelope;
+
+ adjustGain();
+ }
+
+ /* Replace oldest envelope value with current envelope value */
+ float oldestEnvelope = mEnvelopeHistory.get(envelope);
+
+ /* If the oldest envelope value was the max envelope value, then we
+ * have to rediscover the max value from the envelope history */
+ if(mMaxEnvelope == oldestEnvelope && mMaxEnvelope != envelope)
+ {
+ mMaxEnvelope = MINIMUM_ENVELOPE;
+
+ for(float value : mEnvelopeHistory.getBuffer())
+ {
+ if(value > mMaxEnvelope)
+ {
+ mMaxEnvelope = value;
+ }
+ }
+
+ adjustGain();
+ }
+ }
+
+ private void adjustGain()
+ {
+ mGain = OBJECTIVE_ENVELOPE / mMaxEnvelope;
+ }
+
+ public void setListener(ComplexSampleListener listener)
+ {
+ mListener = listener;
+ }
+
+ public ComplexBuffer filter(ComplexBuffer complexBuffer)
+ {
+ float[] samples = complexBuffer.getSamples();
+ float[] processed = new float[samples.length];
+
+ for(int x = 0; x < samples.length; x += 2)
+ {
+ process(samples[x], samples[x + 1]);
+
+ processed[x] = samples[x] * mGain;
+ processed[x + 1] = samples[x + 1] * mGain;
+ }
+
+ return new ComplexBuffer(processed);
+ }
}
diff --git a/src/main/java/io/github/dsheirer/dsp/gain/ComplexGain.java b/src/main/java/io/github/dsheirer/dsp/gain/ComplexGain.java
index f10b571b..70dcdd0e 100644
--- a/src/main/java/io/github/dsheirer/dsp/gain/ComplexGain.java
+++ b/src/main/java/io/github/dsheirer/dsp/gain/ComplexGain.java
@@ -5,27 +5,34 @@ import io.github.dsheirer.sample.complex.Complex;
public class ComplexGain implements Listener
{
- private float mGain;
- private Listener mListener;
+ private float mGain;
+ private Listener mListener;
- public ComplexGain( float gain )
- {
- mGain = gain;
- }
-
- @Override
- public void receive( Complex sample )
- {
- if( mListener != null )
- {
- sample.multiply( mGain );
-
- mListener.receive( sample );
- }
- }
+ public ComplexGain(float gain)
+ {
+ mGain = gain;
+ }
- public void setListener( Listener listener )
- {
- mListener = listener;
- }
+ public Complex apply(Complex sample)
+ {
+ sample.multiply(mGain);
+
+ return sample;
+ }
+
+ @Override
+ public void receive(Complex sample)
+ {
+ if(mListener != null)
+ {
+ apply(sample);
+
+ mListener.receive(sample);
+ }
+ }
+
+ public void setListener(Listener listener)
+ {
+ mListener = listener;
+ }
}
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulator.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulator.java
new file mode 100644
index 00000000..f8b6aabd
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulator.java
@@ -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
+ *
+ ******************************************************************************/
+package io.github.dsheirer.dsp.psk;
+
+import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop;
+import io.github.dsheirer.dsp.symbol.Dibit;
+import io.github.dsheirer.sample.Listener;
+import io.github.dsheirer.sample.complex.Complex;
+import io.github.dsheirer.sample.complex.ComplexBuffer;
+import io.github.dsheirer.sample.complex.ComplexSampleListener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class DQPSKDemodulator implements ComplexSampleListener
+{
+ private final static Logger mLog = LoggerFactory.getLogger(DQPSKDemodulator.class);
+ protected InterpolatingSampleBuffer mInterpolatingSampleBuffer;
+ protected IPhaseLockedLoop mPLL;
+ protected DQPSKSymbolEvaluator mSymbolEvaluator = new DQPSKSymbolEvaluator();
+ private Complex mReceivedSample = new Complex(0, 0);
+ private Complex mPreviousPrecedingSample = new Complex(0, 0);
+ private Complex mPreviousCurrentSample = new Complex(0, 0);
+ private Complex mPrecedingSample = new Complex(0, 0);
+ private Complex mCurrentSample = new Complex(0, 0);
+ private Complex mPrecedingSymbol = new Complex(0, 0);
+ protected Complex mCurrentSymbol = new Complex(0, 0);
+ private Listener mDibitListener;
+
+ /**
+ * Decoder for Differential Quaternary Phase Shift Keying (DQPSK). This decoder uses both a Costas Loop (PLL) and
+ * a custom DQPSK symbol timing error detector to automatically align to the incoming carrier frequency and to
+ * adjust for any changes in symbol timing.
+ *
+ * This detector is optimized for constant amplitude DQPSK symbols like C4FM.
+ *
+ * @param phaseLockedLoop for tracking carrier frequency error
+ * @param interpolatingSampleBuffer to hold samples for interpolating a symbol
+ */
+ public DQPSKDemodulator(IPhaseLockedLoop phaseLockedLoop, InterpolatingSampleBuffer interpolatingSampleBuffer)
+ {
+ mPLL = phaseLockedLoop;
+ mInterpolatingSampleBuffer = interpolatingSampleBuffer;
+ }
+
+ /**
+ * Processes a (filtered) buffer containing complex samples for decoding
+ * @param complexBuffer with complex samples
+ */
+ public void receive(ComplexBuffer complexBuffer)
+ {
+ float[] samples = complexBuffer.getSamples();
+
+ for(int x = 0; x < samples.length; x += 2)
+ {
+ receive(samples[x], samples[x + 1]);
+ }
+ }
+
+ /**
+ * Processes a complex sample for decoding. Once sufficient samples are buffered, a symbol decision is made.
+ * @param inphase value for the sample
+ * @param quadrature value for the sample
+ */
+ @Override
+ public void receive(float inphase, float quadrature)
+ {
+ //Update current sample with values
+ mReceivedSample.setValues(inphase, quadrature);
+
+ //Mix current sample with costas loop to remove any rotation that is present from a mis-tuned carrier frequency
+ mReceivedSample.multiply(mPLL.incrementAndGetCurrentVector());
+
+ //Store the sample in the interpolating buffer
+ mInterpolatingSampleBuffer.receive(mReceivedSample);
+
+ //Calculate the symbol once we've stored enough samples
+ if(mInterpolatingSampleBuffer.hasSymbol())
+ {
+ calculateSymbol();
+ }
+ }
+
+ /**
+ * Calculates a symbol from the interpolating buffer
+ */
+ protected void calculateSymbol()
+ {
+ //Get preceding sample and an interpolated current sample from the interpolating buffer
+ mPrecedingSample = mInterpolatingSampleBuffer.getPrecedingSample();
+ mCurrentSample = mInterpolatingSampleBuffer.getCurrentSample();
+
+ //Calculate preceding, current and following symbols as the delta rotation compared to the previous samples
+ mPrecedingSymbol.setInphase(Complex.multiplyInphase(mPrecedingSample.inphase(), mPrecedingSample.quadrature(),
+ mPreviousPrecedingSample.inphase(), -mPreviousPrecedingSample.quadrature()));
+ mPrecedingSymbol.setQuadrature(Complex.multiplyQuadrature(mPrecedingSample.inphase(), mPrecedingSample.quadrature(),
+ mPreviousPrecedingSample.inphase(), -mPreviousPrecedingSample.quadrature()));
+
+ mCurrentSymbol.setInphase(Complex.multiplyInphase(mCurrentSample.inphase(), mCurrentSample.quadrature(),
+ mPreviousCurrentSample.inphase(), -mPreviousCurrentSample.quadrature()));
+ mCurrentSymbol.setQuadrature(Complex.multiplyQuadrature(mCurrentSample.inphase(), mCurrentSample.quadrature(),
+ mPreviousCurrentSample.inphase(), -mPreviousCurrentSample.quadrature()));
+
+ //Set gain to unity before we calculate the error value
+ mPrecedingSymbol.normalize();
+ mCurrentSymbol.normalize();
+
+ mSymbolEvaluator.setSymbol(mPrecedingSymbol, mCurrentSymbol);
+
+ mInterpolatingSampleBuffer.resetAndAdjust(mSymbolEvaluator.getTimingError());
+
+ //Store current samples/symbols to use for the next period
+ mPreviousPrecedingSample.setValues(mPrecedingSample);
+ mPreviousCurrentSample.setValues(mCurrentSample);
+
+ //Calculate the phase error of the current symbol relative to the expected constellation and provide
+ //feedback to the PLL
+ mPLL.adjust(clip(mSymbolEvaluator.getPhaseError(), 0.5f));
+
+ if(mDibitListener != null)
+ {
+ mDibitListener.receive(mSymbolEvaluator.getSymbolDecision());
+ }
+ }
+
+ /**
+ * Registers the listener to receive dibit symbol decisions from this demodulator
+ */
+ public void setDibitListener(Listener listener)
+ {
+ mDibitListener = listener;
+ }
+
+ /**
+ * Constrains value to the range of ( -maximum <> maximum )
+ */
+ public static float clip(float value, float maximum)
+ {
+ if(value > maximum)
+ {
+ return maximum;
+ }
+ else if(value < -maximum)
+ {
+ return -maximum;
+ }
+
+ return value;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulatorInstrumented.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulatorInstrumented.java
new file mode 100644
index 00000000..cc90a742
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulatorInstrumented.java
@@ -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
+ *
+ ******************************************************************************/
+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 mSymbolDecisionDataListener;
+ private Listener mSamplesPerSymbolListener;
+ private Listener mSymbolListener;
+ private Listener mPLLErrorListener;
+ private Listener 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 listener)
+ {
+ mSymbolDecisionDataListener = listener;
+ }
+
+ /**
+ * Registers the listener to receive PLL error values
+ */
+ public void setSamplesPerSymbolListener(Listener listener)
+ {
+ mSamplesPerSymbolListener = listener;
+ }
+
+ /**
+ * Registers the listener to receive decoded QPSK symbols
+ */
+ public void setSymbolListener(Listener listener)
+ {
+ mSymbolListener = listener;
+ }
+
+ /**
+ * Registers the listener to receive PLL error values
+ */
+ public void setPLLErrorListener(Listener listener)
+ {
+ mPLLErrorListener = listener;
+ }
+
+ /**
+ * Registers the listener to receive PLL error values
+ */
+ public void setPLLFrequencyListener(Listener listener)
+ {
+ mPLLFrequencyListener = listener;
+ }
+
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolEvaluator.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolEvaluator.java
new file mode 100644
index 00000000..cab0fd22
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolEvaluator.java
@@ -0,0 +1,137 @@
+/*******************************************************************************
+ * sdr-trunk
+ * Copyright (C) 2014-2018 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with this program.
+ * If not, see
+ *
+ ******************************************************************************/
+package io.github.dsheirer.dsp.psk;
+
+import io.github.dsheirer.dsp.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;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/GardnerDetector.java b/src/main/java/io/github/dsheirer/dsp/psk/GardnerDetector.java
new file mode 100644
index 00000000..9b338e97
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/GardnerDetector.java
@@ -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
+ *
+ ******************************************************************************/
+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;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/IDQPSKSymbolEvaluator.java b/src/main/java/io/github/dsheirer/dsp/psk/IDQPSKSymbolEvaluator.java
new file mode 100644
index 00000000..d0213a0d
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/IDQPSKSymbolEvaluator.java
@@ -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
+ *
+ ******************************************************************************/
+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();
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/IQPSKSymbolDecoder.java b/src/main/java/io/github/dsheirer/dsp/psk/IQPSKSymbolDecoder.java
new file mode 100644
index 00000000..0befad75
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/IQPSKSymbolDecoder.java
@@ -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
+ *
+ ******************************************************************************/
+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);
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/ISymbolPhaseErrorCalculator.java b/src/main/java/io/github/dsheirer/dsp/psk/ISymbolPhaseErrorCalculator.java
new file mode 100644
index 00000000..4e413d88
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/ISymbolPhaseErrorCalculator.java
@@ -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
+ *
+ ******************************************************************************/
+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);
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBuffer.java b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBuffer.java
new file mode 100644
index 00000000..15adbd9a
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBuffer.java
@@ -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
+ *
+ ******************************************************************************/
+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);
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBufferInstrumented.java b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBufferInstrumented.java
new file mode 100644
index 00000000..6c71fc25
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBufferInstrumented.java
@@ -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
+ *
+ ******************************************************************************/
+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;
+ }
+
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/LSMConstellationDecoder.java b/src/main/java/io/github/dsheirer/dsp/psk/LSMConstellationDecoder.java
new file mode 100644
index 00000000..7c500838
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/LSMConstellationDecoder.java
@@ -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
+ *
+ ******************************************************************************/
+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;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/LSMDemodulator.java b/src/main/java/io/github/dsheirer/dsp/psk/LSMDemodulator.java
index 8eac3fd7..0536f7f2 100644
--- a/src/main/java/io/github/dsheirer/dsp/psk/LSMDemodulator.java
+++ b/src/main/java/io/github/dsheirer/dsp/psk/LSMDemodulator.java
@@ -2,36 +2,37 @@ package io.github.dsheirer.dsp.psk;
/*******************************************************************************
* SDR Trunk
- * Copyright (C) 2014, 2015 Dennis Sheirer
- *
+ * Copyright (C) 2014 - 2018 Dennis Sheirer
+ *
* Gardner detector, Costas loop and Interpolator derived from:
- *
+ *
* OP25 - gardner_costas_cc_impl.cc
* Copyright 2010, 2011, 2012, 2013 KA1RBI (gardner_costas_cc_impl.cc)
- *
+ *
* OP25 - scope.py
* Copyright 2008-2011 Steve Glass
* Copyright 2011, 2012, 2013 KA1RBI
- *
+ *
* GNURadio - control_loop and mpsk_receiver_cc
* Copyright 2011,2013 Free Software Foundation, Inc.
- *
+ *
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
- *
+ *
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
- *
+ *
* You should have received a copy of the GNU General Public License
* along with this program. If not, see
******************************************************************************/
import io.github.dsheirer.buffer.FloatAveragingBuffer;
import io.github.dsheirer.dsp.filter.interpolator.RealInterpolator;
+import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.instrument.Instrumentable;
import io.github.dsheirer.instrument.tap.Tap;
@@ -48,19 +49,19 @@ import java.util.Arrays;
import java.util.List;
/**
- * Implements a LSM (Pi/4) demodulator using a Gardner Detector to determine
- * optimal sampling timing and a Costas Loop as a phase locked loop synchronized
+ * Implements a LSM (Pi/4) demodulator using a Gardner Detector to determine
+ * optimal sampling timing and a Costas Loop as a phase locked loop synchronized
* with the incoming signal carrier frequency.
- *
+ *
* Sample Rate: 48000
* Symbol Rate: 4800
*/
public class LSMDemodulator implements Instrumentable, ComplexSampleListener
{
- private List mAvailableTaps;
-
- /* 45 degree rotation angle */
- public static final float THETA = (float)( Math.PI / 4.0d );
+ private List mAvailableTaps;
+
+ /* 45 degree rotation angle */
+ public static final float THETA = (float)(Math.PI / 4.0d);
/* 45 degree point */
public static final Complex POINT_45_DEGREES =
@@ -73,46 +74,46 @@ public class LSMDemodulator implements Instrumentable, ComplexSampleListener
private CostasLoop mCostasLoop = new CostasLoop();
private ISourceEventListener mFrequencyChangeListener;
-
+
private EyeDiagramDataTap mEyeDiagramDataTap;
-
+
public LSMDemodulator()
{
}
-
+
public void dispose()
{
mSymbolListener = null;
mFrequencyChangeListener = null;
}
-
+
public void setSymbolListener( Listener listener )
{
mSymbolListener = listener;
}
-
+
public void removeSymbolListener()
{
mSymbolListener = null;
}
-
+
@Override
public void receive( float inphase, float quadrature )
{
mGardnerDetector.receive( inphase, quadrature );
}
-
+
/**
* Applies a phase correction value to the costas loop to correct when a
* phase lock error is detected in the binary output stream.
- *
+ *
* @param correction - value in radians
*/
public void correctPhaseError( double correction )
{
mCostasLoop.correctPhaseError( correction );
}
-
+
/**
* Constrains value to the range of ( -maximum <> maximum )
*/
@@ -126,10 +127,10 @@ public class LSMDemodulator implements Instrumentable, ComplexSampleListener
{
return -maximum;
}
-
+
return value;
}
-
+
/**
* Constrains timing error to +/- the maximum value and corrects any
* floating point invalid numbers
@@ -145,37 +146,37 @@ public class LSMDemodulator implements Instrumentable, ComplexSampleListener
return clip( error, maximum );
}
}
-
+
public void addListener( ISourceEventListener listener )
{
mFrequencyChangeListener = listener;
}
-
+
public void removeListener( ISourceEventListener listener )
{
mFrequencyChangeListener = null;
}
- /**
- * Processes the current loop frequency of the costas loop and broadcasts
- * tuner frequency offset adjustments 10 times a second as needed to keep
- * the frequency offset below half of the maximum frequency
- */
- public class FrequencyControl
- {
- /* Set the trigger threshold at half of the maximum costas loop control
- * frequency */
- private static final float CORRECTION_THRESHOLD_FREQUENCY =
- ( 2.0f * (float)Math.PI * 600.0f ) / 48000.0f;
+ /**
+ * Processes the current loop frequency of the costas loop and broadcasts
+ * tuner frequency offset adjustments 10 times a second as needed to keep
+ * the frequency offset below half of the maximum frequency
+ */
+ public class FrequencyControl
+ {
+ /* Set the trigger threshold at half of the maximum costas loop control
+ * frequency */
+ private static final float CORRECTION_THRESHOLD_FREQUENCY =
+ (2.0f * (float)Math.PI * 600.0f) / 48000.0f;
- private FloatAveragingBuffer mBuffer = new FloatAveragingBuffer( 40 );
-
- private long mFrequencyError = 0;
-
- private int mCounter = 0;
-
- public void receive( float frequency )
- {
+ private FloatAveragingBuffer mBuffer = new FloatAveragingBuffer(40);
+
+ private long mFrequencyError = 0;
+
+ private int mCounter = 0;
+
+ public void receive(float frequency)
+ {
// mCounter++;
//
// if( mCounter >= 480 )
@@ -212,377 +213,402 @@ public class LSMDemodulator implements Instrumentable, ComplexSampleListener
// {
// mBuffer.get( frequency );
// }
- }
- }
+ }
+ }
- /**
- * Gardner symbol timing detector
- */
- public class GardnerSymbolTiming implements ComplexSampleListener
- {
- public static final int HALF_SAMPLES_PER_SYMBOL = 5;
- public static final int SAMPLES_PER_SYMBOL = 10;
- public static final int TWICE_SAMPLES_PER_SYMBOL = 20;
+ /**
+ * Gardner symbol timing detector
+ */
+ public class GardnerSymbolTiming implements ComplexSampleListener
+ {
+ public static final int HALF_SAMPLES_PER_SYMBOL = 5;
+ public static final int SAMPLES_PER_SYMBOL = 10;
+ public static final int TWICE_SAMPLES_PER_SYMBOL = 20;
- private float[] mDelayLineInphase = new float[ 2 * TWICE_SAMPLES_PER_SYMBOL ];
- private float[] mDelayLineQuadrature = new float[ 2 * TWICE_SAMPLES_PER_SYMBOL ];
+ private float[] mDelayLineInphase = new float[2 * TWICE_SAMPLES_PER_SYMBOL];
+ private float[] mDelayLineQuadrature = new float[2 * TWICE_SAMPLES_PER_SYMBOL];
- private int mDelayLinePointer = 0;
+ private int mDelayLinePointer = 0;
- /* Sampling point */
- private float mMu = 10.0f;
- private float mGainMu = 0.05f;
-
- /* Samples per symbol */
- private float mOmega = 10.0f;
- private float mGainOmega = 0.1f * mGainMu * mGainMu;
- private float mOmegaRel = 0.005f;
- private float mOmegaMid = 10.0f;
+ /* Sampling point */
+ private float mMu = 10.0f;
+ private float mGainMu = 0.05f;
- private RealInterpolator mInterpolator = new RealInterpolator( 1.0f );
-
- private Complex mPreviousSample = new Complex( 0, 0 );
- private Complex mPreviousMiddleSample = new Complex( 0, 0 );
- private Complex mPreviousSymbol = new Complex( 0, 0 );
-
- /**
- * Provides symbol sampling timing control
- */
- public GardnerSymbolTiming()
- {
- }
+ /* Samples per symbol */
+ private float mOmega = 10.0f;
+ private float mGainOmega = 0.1f * mGainMu * mGainMu;
+ private float mOmegaRel = 0.005f;
+ private float mOmegaMid = 10.0f;
- @Override
- public void receive( float inphase, float quadrature )
- {
- /* Count down samples per symbol until we calculate the symbol */
- mMu--;
+ private RealInterpolator mInterpolator = new RealInterpolator(1.0f);
- mCostasLoop.increment();
-
- /* Mix incoming sample with costas loop to remove any rotation
- * that is present from a mis-tuned carrier frequency */
-
- Complex costas = mCostasLoop.getCurrentVector();
-
- float derotatedInphase = Complex.multiplyInphase( inphase, quadrature, costas.inphase(), costas.quadrature() );
- float derotatedQuadrature = Complex.multiplyQuadrature( inphase, quadrature, costas.inphase(), costas.quadrature() );
-
- /* Fill up the delay line to use with the interpolator */
- mDelayLineInphase[ mDelayLinePointer ] = derotatedInphase;
- mDelayLineInphase[ mDelayLinePointer + TWICE_SAMPLES_PER_SYMBOL ] = derotatedInphase;
+ private Complex mPreviousSample = new Complex(0, 0);
+ private Complex mPreviousMiddleSample = new Complex(0, 0);
+ private Complex mPreviousSymbol = new Complex(0, 0);
- mDelayLineQuadrature[ mDelayLinePointer ] = derotatedQuadrature;
- mDelayLineQuadrature[ mDelayLinePointer + TWICE_SAMPLES_PER_SYMBOL ] = derotatedQuadrature;
+ /**
+ * Provides symbol sampling timing control
+ */
+ public GardnerSymbolTiming()
+ {
+ }
- /* Increment pointer and keep pointer in bounds */
- mDelayLinePointer = ( mDelayLinePointer + 1 ) % TWICE_SAMPLES_PER_SYMBOL;
-
- /* Calculate the symbol once we've stored enough samples */
- if( mMu <= 1.0f )
- {
- float half_omega = mOmega / 2.0f;
- int half_sps = (int)Math.floor( half_omega );
- float half_mu = mMu + half_omega - (float)half_sps;
-
- if( half_mu > 1.0 )
- {
- half_mu -= 1.0;
- half_sps += 1;
- }
+ @Override
+ public void receive(float inphase, float quadrature)
+ {
+ /* Count down samples per symbol until we calculate the symbol */
+ mMu--;
- /* Calculate interpolated middle sample and current sample */
- float middleSampleInphase = mInterpolator.filter(
- mDelayLineInphase, mDelayLinePointer, mMu );
- float middleSampleQuadrature = mInterpolator.filter(
- mDelayLineQuadrature, mDelayLinePointer, mMu );
+ mCostasLoop.increment();
- int index = mDelayLinePointer + half_sps;
-
- float currentSampleInphase = mInterpolator.filter(
- mDelayLineInphase, index, half_mu );
- float currentSampleQuadrature = mInterpolator.filter(
- mDelayLineQuadrature, index, half_mu );
+ /* Mix incoming sample with costas loop to remove any rotation
+ * that is present from a mis-tuned carrier frequency */
- /* Multiply current sample and conjugate (negated quadrature) of
- * previous sample to get symbols to use for gardner error feedback */
- Complex middleSymbol = Complex.multiply( middleSampleInphase,
- middleSampleQuadrature, mPreviousMiddleSample.conjugate() );
- Complex currentSymbol = Complex.multiply( currentSampleInphase,
- currentSampleQuadrature, mPreviousSample.conjugate() );
+ Complex costas = mCostasLoop.getCurrentVector();
- /* Set gain to unity */
- middleSymbol.normalize();
- currentSymbol.normalize();
+ float derotatedInphase = Complex.multiplyInphase(inphase, quadrature, costas.inphase(), costas.quadrature());
+ float derotatedQuadrature = Complex.multiplyQuadrature(inphase, quadrature, costas.inphase(), costas.quadrature());
- /* Gardner timing error calculations */
- float errorInphase = ( mPreviousSymbol.inphase() -
- currentSymbol.inphase() ) * middleSymbol.inphase();
+ /* Fill up the delay line to use with the interpolator */
+ mDelayLineInphase[mDelayLinePointer] = derotatedInphase;
+ mDelayLineInphase[mDelayLinePointer + TWICE_SAMPLES_PER_SYMBOL] = derotatedInphase;
- float errorQuadrature = ( mPreviousSymbol.quadrature() -
- currentSymbol.quadrature() ) * middleSymbol.quadrature();
+ mDelayLineQuadrature[mDelayLinePointer] = derotatedQuadrature;
+ mDelayLineQuadrature[mDelayLinePointer + TWICE_SAMPLES_PER_SYMBOL] = derotatedQuadrature;
- float gardnerError = normalize( errorInphase + errorQuadrature, 1.0f );
-
- if( mEyeDiagramDataTap != null )
- {
- mEyeDiagramDataTap.receive(
- new EyeDiagramData(
- Arrays.copyOfRange( mDelayLineInphase, 0, 20 ),
- Arrays.copyOfRange( mDelayLineQuadrature, 0, 20 ),
- mMu, (float)half_sps + half_mu, gardnerError ) );
- }
-
- /* mOmega is samples per symbol and is constrained to floating
- * between +/- .005 of the nominal 10.0 samples per symbol */
- mOmega = mOmega + mGainOmega * gardnerError;
- mOmega = mOmegaMid + clip( mOmega - mOmegaMid, mOmegaRel );
+ /* Increment pointer and keep pointer in bounds */
+ mDelayLinePointer = (mDelayLinePointer + 1) % TWICE_SAMPLES_PER_SYMBOL;
- /* Adjust sample timing based on error of current sample */
- mMu += mOmega + ( mGainMu * gardnerError );
+ /* Calculate the symbol once we've stored enough samples */
+ if(mMu <= 1.0f)
+ {
+ float half_omega = mOmega / 2.0f;
+ int half_sps = (int)Math.floor(half_omega);
+ float half_mu = mMu + half_omega - (float)half_sps;
- /* Store current samples/symbols to use for the next period */
- mPreviousSample.setValues( currentSampleInphase, currentSampleQuadrature );
- mPreviousMiddleSample.setValues( middleSampleInphase, middleSampleQuadrature );
- mPreviousSymbol = currentSymbol;
+ if(half_mu > 1.0)
+ {
+ half_mu -= 1.0;
+ half_sps += 1;
+ }
- /* Update costas loop using phase error present in current
- * symbol. The symbol is rotated from star orientation to polar
- * orientation to simplify error calculation */
- mCostasLoop.receive( currentSymbol );
+ /* Calculate interpolated middle sample and current sample */
+ float middleSampleInphase = mInterpolator.filter(
+ mDelayLineInphase, mDelayLinePointer, mMu);
+ float middleSampleQuadrature = mInterpolator.filter(
+ mDelayLineQuadrature, mDelayLinePointer, mMu);
+
+ int index = mDelayLinePointer + half_sps;
+
+ float currentSampleInphase = mInterpolator.filter(
+ mDelayLineInphase, index, half_mu);
+ float currentSampleQuadrature = mInterpolator.filter(
+ mDelayLineQuadrature, index, half_mu);
+
+ /* Multiply current sample and conjugate (negated quadrature) of
+ * previous sample to get symbols to use for gardner error feedback */
+ Complex middleSymbol = Complex.multiply(middleSampleInphase,
+ middleSampleQuadrature, mPreviousMiddleSample.conjugate());
+ Complex currentSymbol = Complex.multiply(currentSampleInphase,
+ currentSampleQuadrature, mPreviousSample.conjugate());
+
+ /* Set gain to unity */
+ middleSymbol.normalize();
+ currentSymbol.normalize();
+
+ /* Gardner timing error calculations */
+ float errorInphase = (mPreviousSymbol.inphase() -
+ currentSymbol.inphase()) * middleSymbol.inphase();
+
+ float errorQuadrature = (mPreviousSymbol.quadrature() -
+ currentSymbol.quadrature()) * middleSymbol.quadrature();
+
+ float gardnerError = normalize(errorInphase + errorQuadrature, 1.0f);
+
+ if(mEyeDiagramDataTap != null)
+ {
+ mEyeDiagramDataTap.receive(
+ new EyeDiagramData(
+ Arrays.copyOfRange(mDelayLineInphase, 0, 20),
+ Arrays.copyOfRange(mDelayLineQuadrature, 0, 20),
+ mMu, (float)half_sps + half_mu, gardnerError));
+ }
+
+ /* mOmega is samples per symbol and is constrained to floating
+ * between +/- .005 of the nominal 10.0 samples per symbol */
+ mOmega = mOmega + mGainOmega * gardnerError;
+ mOmega = mOmegaMid + clip(mOmega - mOmegaMid, mOmegaRel);
+
+ /* Adjust sample timing based on error of current sample */
+ mMu += mOmega + (mGainMu * gardnerError);
+
+ /* Store current samples/symbols to use for the next period */
+ mPreviousSample.setValues(currentSampleInphase, currentSampleQuadrature);
+ mPreviousMiddleSample.setValues(middleSampleInphase, middleSampleQuadrature);
+ mPreviousSymbol = currentSymbol;
+
+ /* Update costas loop using phase error present in current
+ * symbol. The symbol is rotated from star orientation to polar
+ * orientation to simplify error calculation */
+ mCostasLoop.receive(currentSymbol);
// mCostasLoop.receive(
// ComplexSample.multiply( currentSymbol, POINT_45_DEGREES ) );
-
- /* Dispatch the differentiated symbol to the registered listener */
- if( mSymbolListener != null )
- {
- mSymbolListener.receive( currentSymbol );
- }
- }
- }
- }
-
- /**
- * Costas Loop - phase locked loop synchronized to the frequency offset of
- * the incoming signal to enable the signal to be mixed down to zero offset
- * for proper synchronization. The mFrequency value indicates the detected
- * frequency offset. We attempt to keep that value close to zero by issuing
- * frequency adjustments to the tuner channel source.
- *
- * Most of the costas loop code was ported from gnuradio/control_loop and
- * gnuradio/mpsk_receiver_cc using initialization values from KA1RBI's
- * OP25/cqpsk.py
- */
- public class CostasLoop implements Listener
- {
- public static final double TWO_PI = 2.0 * Math.PI;
-
- private static final float MAXIMUM_FREQUENCY =
- ( 2400.0f * (float)TWO_PI ) / 48000.0f;
-
- /* http://www.trondeau.com/blog/2011/8/13/control-loop-gain-values.html */
- private float mDamping = (float)Math.sqrt( 2.0 ) / 2.0f;
-
- /* Use denominator between 100 (most) and 400 (least) to adjust costas
- * loop control level */
- private float mLoopBandwidth = (float)( TWO_PI / 400.0d );
-
- private float mAlphaGain = ( 4.0f * mDamping * mLoopBandwidth ) /
- ( 1.0f + ( 2.0f * mDamping * mLoopBandwidth ) +
- ( mLoopBandwidth * mLoopBandwidth ) );
- private float mBetaGain = ( 4.0f * mLoopBandwidth * mLoopBandwidth ) /
- ( 1.0f + ( 2.0f * mDamping * mLoopBandwidth ) +
- ( mLoopBandwidth * mLoopBandwidth ) );
+ /* Dispatch the differentiated symbol to the registered listener */
+ if(mSymbolListener != null)
+ {
+ mSymbolListener.receive(currentSymbol);
+ }
+ }
+ }
+ }
- private float mLoopPhase = 0.0f;
-
- private float mLoopFrequency = 0.0f;
-
- private FrequencyControl mFrequencyControl = new FrequencyControl();
-
- public CostasLoop()
- {
- }
-
- /**
- * Applies a phase correction value to the current phasor. Use this
- * method to apply correction when a +/- 90, or 180 degree phase error
- * is detected in the binary output stream.
- *
- * If the supplied correction value places the loop frequency outside
- * of the max frequency, then the frequency will be corrected 360
- * degrees in the opposite direction to maintain within the max
- * frequency bounds.
- *
- * @param correction - correction value in radians
- */
- public void correctPhaseError( double correction )
- {
- mLoopFrequency += correction;
-
- if( mLoopFrequency > MAXIMUM_FREQUENCY )
- {
- mLoopFrequency -= 2.0d * MAXIMUM_FREQUENCY;
- }
-
- if( mLoopFrequency < -MAXIMUM_FREQUENCY )
- {
- mLoopFrequency += 2.0d * MAXIMUM_FREQUENCY;
- }
- }
-
- /**
- * Increments the phase of the loop
- */
- public void increment()
- {
- mLoopPhase += mLoopFrequency;
+ /**
+ * Costas Loop - phase locked loop synchronized to the frequency offset of
+ * the incoming signal to enable the signal to be mixed down to zero offset
+ * for proper synchronization. The mFrequency value indicates the detected
+ * frequency offset. We attempt to keep that value close to zero by issuing
+ * frequency adjustments to the tuner channel source.
+ *
+ * Most of the costas loop code was ported from gnuradio/control_loop and
+ * gnuradio/mpsk_receiver_cc using initialization values from KA1RBI's
+ * OP25/cqpsk.py
+ */
+ public class CostasLoop implements Listener, IPhaseLockedLoop
+ {
+ public static final double TWO_PI = 2.0 * Math.PI;
- /* Keep the loop phase in bounds */
- unwrapPhase();
- }
+ private static final float MAXIMUM_FREQUENCY =
+ (2400.0f * (float)TWO_PI) / 48000.0f;
- private void unwrapPhase()
- {
- while( mLoopPhase > TWO_PI )
- {
- mLoopPhase -= TWO_PI;
- }
-
- while( mLoopPhase < -TWO_PI )
- {
- mLoopPhase += TWO_PI;
- }
- }
-
- /**
- * Current vector of the loop rotated by THETA degrees.
- */
- public Complex getCurrentVector()
- {
+ /* http://www.trondeau.com/blog/2011/8/13/control-loop-gain-values.html */
+ private float mDamping = (float)Math.sqrt(2.0) / 2.0f;
+
+ /* Use denominator between 100 (most) and 400 (least) to adjust costas
+ * loop control level */
+ private float mLoopBandwidth = (float)(TWO_PI / 400.0d);
+
+ private float mAlphaGain = (4.0f * mDamping * mLoopBandwidth) /
+ (1.0f + (2.0f * mDamping * mLoopBandwidth) +
+ (mLoopBandwidth * mLoopBandwidth));
+
+ private float mBetaGain = (4.0f * mLoopBandwidth * mLoopBandwidth) /
+ (1.0f + (2.0f * mDamping * mLoopBandwidth) +
+ (mLoopBandwidth * mLoopBandwidth));
+
+ private float mLoopPhase = 0.0f;
+
+ private float mLoopFrequency = 0.0f;
+
+ private FrequencyControl mFrequencyControl = new FrequencyControl();
+
+ public CostasLoop()
+ {
+ }
+
+ /**
+ * Applies a phase correction value to the current phasor. Use this
+ * method to apply correction when a +/- 90, or 180 degree phase error
+ * is detected in the binary output stream.
+ *
+ * If the supplied correction value places the loop frequency outside
+ * of the max frequency, then the frequency will be corrected 360
+ * degrees in the opposite direction to maintain within the max
+ * frequency bounds.
+ *
+ * @param correction - correction value in radians
+ */
+ public void correctPhaseError(double correction)
+ {
+ mLoopFrequency += correction;
+
+ if(mLoopFrequency > MAXIMUM_FREQUENCY)
+ {
+ mLoopFrequency -= 2.0d * MAXIMUM_FREQUENCY;
+ }
+
+ if(mLoopFrequency < -MAXIMUM_FREQUENCY)
+ {
+ mLoopFrequency += 2.0d * MAXIMUM_FREQUENCY;
+ }
+ }
+
+ /**
+ * Increments the phase of the loop
+ */
+ public void increment()
+ {
+ mLoopPhase += mLoopFrequency;
+
+ /* Keep the loop phase in bounds */
+ unwrapPhase();
+ }
+
+ @Override
+ public void reset()
+ {
+ mLoopPhase = 0.0f;
+ mLoopFrequency = 0.0f;
+ }
+
+ @Override
+ public void adjust(double error)
+ {
+ adjust(error);
+ }
+
+ @Override
+ public void correctInversion(double correction)
+ {
+ correctPhaseError(correction);
+ }
+
+ private void unwrapPhase()
+ {
+ while(mLoopPhase > TWO_PI)
+ {
+ mLoopPhase -= TWO_PI;
+ }
+
+ while(mLoopPhase < -TWO_PI)
+ {
+ mLoopPhase += TWO_PI;
+ }
+ }
+
+ /**
+ * Current vector of the loop rotated by THETA degrees.
+ */
+ public Complex getCurrentVector()
+ {
// return ComplexSample.fromAngle( mLoopPhase + THETA );
- return Complex.fromAngle( mLoopPhase );
- }
-
- @Override
- public void receive( Complex complex )
- {
- /* Calculate phase error */
- float phaseError = getPhaseError( complex );
-
- Dibit d = QPSKPolarSlicer.decide( complex );
+ return Complex.fromAngle(mLoopPhase);
+ }
+
+ @Override
+ public Complex incrementAndGetCurrentVector()
+ {
+ return null;
+ }
+
+ @Override
+ public void receive(Complex complex)
+ {
+ /* Calculate phase error */
+ float phaseError = getPhaseError(complex);
+
+ Dibit d = mQPSKSymbolDecoder.decode(complex);
// mLog.debug( "Dibit: " + d.name() + " Error: " + phaseError );
- /* Adjust for phase error */
- adjust( phaseError );
- }
+ /* Adjust for phase error */
+ adjust(phaseError);
+ }
- /**
- * Updates the costas loop frequency and phase to adjust for the phase
- * error value
- *
- * @param phase_error - (-)= late and (+)= early
- */
- private void adjust( float phase_error )
- {
- mLoopFrequency += mBetaGain * phase_error;
- mLoopPhase += mLoopFrequency + mAlphaGain * phase_error;
+ /**
+ * Updates the costas loop frequency and phase to adjust for the phase
+ * error value
+ *
+ * @param phase_error - (-)= late and (+)= early
+ */
+ private void adjust(float phase_error)
+ {
+ mLoopFrequency += mBetaGain * phase_error;
+ mLoopPhase += mLoopFrequency + mAlphaGain * phase_error;
- /* Maintain phase between +/- 2 * PI */
- unwrapPhase();
+ /* Maintain phase between +/- 2 * PI */
+ unwrapPhase();
- /* Limit frequency to +/- maximum loop frequency */
- limitFrequency();
- }
+ /* Limit frequency to +/- maximum loop frequency */
+ limitFrequency();
+ }
- /**
- * Constrains the frequency within the bounds of +/- loop frequency
- */
- private void limitFrequency()
- {
- /* Check for and issue tuner offset correction */
- mFrequencyControl.receive( mLoopFrequency );
-
- if( mLoopFrequency > MAXIMUM_FREQUENCY )
- {
- mLoopFrequency = MAXIMUM_FREQUENCY;
- }
-
- if( mLoopFrequency < -MAXIMUM_FREQUENCY )
- {
- mLoopFrequency = -MAXIMUM_FREQUENCY;
- }
- }
+ /**
+ * Constrains the frequency within the bounds of +/- loop frequency
+ */
+ private void limitFrequency()
+ {
+ /* Check for and issue tuner offset correction */
+ mFrequencyControl.receive(mLoopFrequency);
- /**
- * Detects rotational error present when the costas loop is not aligned
- * with the carrier frequency. Provides error feedback to adjust
- * mixer frequency.
- */
- public float getPhaseError( Complex complex )
- {
- float phase_error = 0;
-
- if( Math.abs( complex.inphase() ) > Math.abs( complex.quadrature() ) )
- {
- if( complex.inphase() > 0 )
- {
- phase_error = -complex.quadrature();
- }
- else
- {
- phase_error = complex.quadrature();
- }
- }
- else
- {
- if( complex.quadrature() > 0 )
- {
- phase_error = complex.inphase();
- }
- else
- {
- phase_error = -complex.inphase();
- }
- }
-
- return phase_error;
- }
- }
+ if(mLoopFrequency > MAXIMUM_FREQUENCY)
+ {
+ mLoopFrequency = MAXIMUM_FREQUENCY;
+ }
- @Override
- public List getTapGroups()
- {
- if( mAvailableTaps == null )
- {
- mAvailableTaps = new ArrayList<>();
-
- TapGroup group = new TapGroup( "LSM Demodulator" );
-
- group.add( new EyeDiagramDataTap( "Eye Diagram", 0, 4800 ) );
+ if(mLoopFrequency < -MAXIMUM_FREQUENCY)
+ {
+ mLoopFrequency = -MAXIMUM_FREQUENCY;
+ }
+ }
- mAvailableTaps.add( group );
- }
-
- return mAvailableTaps;
- }
+ /**
+ * Detects rotational error present when the costas loop is not aligned
+ * with the carrier frequency. Provides error feedback to adjust
+ * mixer frequency.
+ */
+ public float getPhaseError(Complex complex)
+ {
+ float phase_error = 0;
- @Override
- public void registerTap( Tap tap )
- {
- if( tap instanceof EyeDiagramDataTap )
- {
- mEyeDiagramDataTap = (EyeDiagramDataTap)tap;
- }
- }
+ if(Math.abs(complex.inphase()) > Math.abs(complex.quadrature()))
+ {
+ if(complex.inphase() > 0)
+ {
+ phase_error = -complex.quadrature();
+ }
+ else
+ {
+ phase_error = complex.quadrature();
+ }
+ }
+ else
+ {
+ if(complex.quadrature() > 0)
+ {
+ phase_error = complex.inphase();
+ }
+ else
+ {
+ phase_error = -complex.inphase();
+ }
+ }
- @Override
- public void unregisterTap( Tap tap )
- {
- if( tap instanceof EyeDiagramDataTap )
- {
- mEyeDiagramDataTap = null;
- }
- }
+ return phase_error;
+ }
+ }
+
+ @Override
+ public List getTapGroups()
+ {
+ if(mAvailableTaps == null)
+ {
+ mAvailableTaps = new ArrayList<>();
+
+ TapGroup group = new TapGroup("LSM Demodulator");
+
+ group.add(new EyeDiagramDataTap("Eye Diagram", 0, 4800));
+
+ mAvailableTaps.add(group);
+ }
+
+ return mAvailableTaps;
+ }
+
+ @Override
+ public void registerTap(Tap tap)
+ {
+ if(tap instanceof EyeDiagramDataTap)
+ {
+ mEyeDiagramDataTap = (EyeDiagramDataTap)tap;
+ }
+ }
+
+ @Override
+ public void unregisterTap(Tap tap)
+ {
+ if(tap instanceof EyeDiagramDataTap)
+ {
+ mEyeDiagramDataTap = null;
+ }
+ }
}
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/QPSKStarSlicer.java b/src/main/java/io/github/dsheirer/dsp/psk/QPSKStarSlicer.java
index f6b23861..b8295145 100644
--- a/src/main/java/io/github/dsheirer/dsp/psk/QPSKStarSlicer.java
+++ b/src/main/java/io/github/dsheirer/dsp/psk/QPSKStarSlicer.java
@@ -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
+ *
+ ******************************************************************************/
package io.github.dsheirer.dsp.psk;
import io.github.dsheirer.dsp.symbol.Dibit;
@@ -5,54 +20,54 @@ import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.Complex;
-public class QPSKStarSlicer implements Listener
+public class QPSKStarSlicer implements IQPSKSymbolDecoder, Listener
{
- private Broadcaster mBroadcaster = new Broadcaster();
+ private Broadcaster mBroadcaster = new Broadcaster();
- /**
- * Slices a ComplexSample representing a phase shifted symbol according to
- * the following constellation pattern:
- *
- * 00 | 01
- * ---|---
- * 10 | 11
- */
- public QPSKStarSlicer()
- {
- }
-
- public void dispose()
- {
- mBroadcaster.dispose();
- mBroadcaster = null;
- }
+ /**
+ * Slices a Complex sample representing a phase shifted symbol according to the following constellation pattern:
+ *
+ * 00 +3 | 01 +1
+ * ------|------
+ * 10 -1 | 11 -1
+ */
+ public QPSKStarSlicer()
+ {
+ }
- public void addListener( Listener listener )
- {
- mBroadcaster.addListener( listener );
- }
+ public void dispose()
+ {
+ mBroadcaster.dispose();
+ mBroadcaster = null;
+ }
- public void removeListener( Listener listener )
- {
- mBroadcaster.removeListener( listener );
- }
+ public void addListener(Listener listener)
+ {
+ mBroadcaster.addListener(listener);
+ }
+
+ public void removeListener(Listener listener)
+ {
+ mBroadcaster.removeListener(listener);
+ }
+
+ @Override
+ public void receive(Complex sample)
+ {
+ mBroadcaster.broadcast(decode(sample));
+ }
+
+ @Override
+ public Dibit decode(Complex sample)
+ {
+ if(sample.inphase() > 0)
+ {
+ return sample.quadrature() > 0 ? Dibit.D00_PLUS_1 : Dibit.D10_MINUS_1;
+ }
+ else
+ {
+ return sample.quadrature() > 0 ? Dibit.D01_PLUS_3 : Dibit.D11_MINUS_3;
+ }
+ }
- @Override
- public void receive( Complex sample )
- {
- mBroadcaster.broadcast( decide( sample ) );
- }
-
- public static Dibit decide( Complex sample )
- {
- if( sample.inphase() > 0 )
- {
- return sample.quadrature() > 0 ? Dibit.D00_PLUS_1 : Dibit.D10_MINUS_1;
- }
- else
- {
- return sample.quadrature() > 0 ? Dibit.D01_PLUS_3 : Dibit.D11_MINUS_3;
- }
- }
-
}
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/QPSKPolarSlicer.java b/src/main/java/io/github/dsheirer/dsp/psk/QPSKSymbolDecoder.java
similarity index 59%
rename from src/main/java/io/github/dsheirer/dsp/psk/QPSKPolarSlicer.java
rename to src/main/java/io/github/dsheirer/dsp/psk/QPSKSymbolDecoder.java
index 3ff0c708..42a97b6c 100644
--- a/src/main/java/io/github/dsheirer/dsp/psk/QPSKPolarSlicer.java
+++ b/src/main/java/io/github/dsheirer/dsp/psk/QPSKSymbolDecoder.java
@@ -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
+ *
+ ******************************************************************************/
package io.github.dsheirer.dsp.psk;
import io.github.dsheirer.dsp.symbol.Dibit;
@@ -5,24 +20,7 @@ import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.Complex;
-/*******************************************************************************
- * SDR Trunk
- * Copyright (C) 2014,2015 Dennis Sheirer
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see
- ******************************************************************************/
-public class QPSKPolarSlicer implements Listener
+public class QPSKSymbolDecoder implements Listener, IQPSKSymbolDecoder
{
private Broadcaster mBroadcaster = new Broadcaster();
@@ -37,7 +35,7 @@ public class QPSKPolarSlicer implements Listener
* / \
* / 11 \
*/
- public QPSKPolarSlicer()
+ public QPSKSymbolDecoder()
{
}
@@ -60,10 +58,10 @@ public class QPSKPolarSlicer implements Listener
@Override
public void receive( Complex complex )
{
- mBroadcaster.broadcast( decide( complex ) );
+ mBroadcaster.broadcast( decode( complex ) );
}
- public static Dibit decide( Complex complex )
+ public Dibit decode(Complex complex )
{
if( Math.abs( complex.inphase() ) > Math.abs( complex.quadrature() ) )
{
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/QPSKSymbolPhaseErrorCalculator.java b/src/main/java/io/github/dsheirer/dsp/psk/QPSKSymbolPhaseErrorCalculator.java
new file mode 100644
index 00000000..fed580ce
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/QPSKSymbolPhaseErrorCalculator.java
@@ -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
+ *
+ ******************************************************************************/
+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;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/SymbolDecisionData.java b/src/main/java/io/github/dsheirer/dsp/psk/SymbolDecisionData.java
new file mode 100644
index 00000000..e06f3d22
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/SymbolDecisionData.java
@@ -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
+ *
+ ******************************************************************************/
+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;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/SymbolInversion.java b/src/main/java/io/github/dsheirer/dsp/psk/SymbolInversion.java
new file mode 100644
index 00000000..0b7431c9
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/SymbolInversion.java
@@ -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
+ *
+ ******************************************************************************/
+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;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/pll/CostasLoop.java b/src/main/java/io/github/dsheirer/dsp/psk/pll/CostasLoop.java
new file mode 100644
index 00000000..38b8d101
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/pll/CostasLoop.java
@@ -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
+ *
+ ******************************************************************************/
+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;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/pll/IPhaseLockedLoop.java b/src/main/java/io/github/dsheirer/dsp/psk/pll/IPhaseLockedLoop.java
new file mode 100644
index 00000000..631a772c
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/pll/IPhaseLockedLoop.java
@@ -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
+ *
+ ******************************************************************************/
+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();
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/pll/Tracking.java b/src/main/java/io/github/dsheirer/dsp/psk/pll/Tracking.java
new file mode 100644
index 00000000..a4611d20
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/pll/Tracking.java
@@ -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
+ *
+ ******************************************************************************/
+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;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/gui/SDRTrunk.java b/src/main/java/io/github/dsheirer/gui/SDRTrunk.java
index 79cedda4..b6d6b6d8 100644
--- a/src/main/java/io/github/dsheirer/gui/SDRTrunk.java
+++ b/src/main/java/io/github/dsheirer/gui/SDRTrunk.java
@@ -25,6 +25,8 @@ import io.github.dsheirer.audio.AudioManager;
import io.github.dsheirer.audio.broadcast.BroadcastModel;
import io.github.dsheirer.audio.broadcast.BroadcastStatusPanel;
import io.github.dsheirer.controller.ControllerPanel;
+import io.github.dsheirer.controller.channel.Channel;
+import io.github.dsheirer.controller.channel.ChannelAutoStartFrame;
import io.github.dsheirer.controller.channel.ChannelModel;
import io.github.dsheirer.controller.channel.ChannelProcessingManager;
import io.github.dsheirer.controller.channel.ChannelSelectionManager;
@@ -51,8 +53,19 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.imageio.ImageIO;
-import javax.swing.*;
-import java.awt.*;
+import javax.swing.JCheckBoxMenuItem;
+import javax.swing.JFrame;
+import javax.swing.JMenu;
+import javax.swing.JMenuBar;
+import javax.swing.JMenuItem;
+import javax.swing.JOptionPane;
+import javax.swing.JSeparator;
+import javax.swing.KeyStroke;
+import java.awt.AWTException;
+import java.awt.Desktop;
+import java.awt.Dimension;
+import java.awt.EventQueue;
+import java.awt.Robot;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
@@ -61,6 +74,7 @@ import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.List;
public class SDRTrunk implements Listener
{
@@ -72,6 +86,8 @@ public class SDRTrunk implements Listener
private BroadcastStatusPanel mBroadcastStatusPanel;
private BroadcastModel mBroadcastModel;
private ControllerPanel mControllerPanel;
+ private ChannelModel mChannelModel;
+ private ChannelProcessingManager mChannelProcessingManager;
private SettingsManager mSettingsManager;
private SpectralDisplayPanel mSpectralPanel;
private JFrame mMainGui = new JFrame();
@@ -117,7 +133,7 @@ public class SDRTrunk implements Listener
AliasModel aliasModel = new AliasModel();
- ChannelModel channelModel = new ChannelModel();
+ mChannelModel = new ChannelModel();
ChannelMapModel channelMapModel = new ChannelMapModel();
@@ -127,43 +143,42 @@ public class SDRTrunk implements Listener
SourceManager sourceManager = new SourceManager(tunerModel, mSettingsManager);
- ChannelProcessingManager channelProcessingManager = new ChannelProcessingManager(
- channelModel, channelMapModel, aliasModel, eventLogManager, recorderManager, sourceManager);
- channelProcessingManager.addAudioPacketListener(recorderManager);
+ mChannelProcessingManager = new ChannelProcessingManager(mChannelModel, channelMapModel, aliasModel,
+ eventLogManager, recorderManager, sourceManager);
+ mChannelProcessingManager.addAudioPacketListener(recorderManager);
- channelModel.addListener(channelProcessingManager);
+ mChannelModel.addListener(mChannelProcessingManager);
ChannelSelectionManager channelSelectionManager =
- new ChannelSelectionManager(channelModel);
- channelModel.addListener(channelSelectionManager);
+ new ChannelSelectionManager(mChannelModel);
+ mChannelModel.addListener(channelSelectionManager);
AliasActionManager aliasActionManager = new AliasActionManager();
- channelProcessingManager.addMessageListener(aliasActionManager);
+ mChannelProcessingManager.addMessageListener(aliasActionManager);
AudioManager audioManager = new AudioManager(sourceManager.getMixerManager());
- channelProcessingManager.addAudioPacketListener(audioManager);
+ mChannelProcessingManager.addAudioPacketListener(audioManager);
mBroadcastModel = new BroadcastModel(mIconManager);
- channelProcessingManager.addAudioPacketListener(mBroadcastModel);
+ mChannelProcessingManager.addAudioPacketListener(mBroadcastModel);
MapService mapService = new MapService(mIconManager);
- channelProcessingManager.addMessageListener(mapService);
+ mChannelProcessingManager.addMessageListener(mapService);
mControllerPanel = new ControllerPanel(audioManager, aliasModel, mBroadcastModel,
- channelModel, channelMapModel, channelProcessingManager, mIconManager,
+ mChannelModel, channelMapModel, mChannelProcessingManager, mIconManager,
mapService, mSettingsManager, sourceManager, tunerModel);
- mSpectralPanel = new SpectralDisplayPanel(channelModel,
- channelProcessingManager, mSettingsManager);
+ mSpectralPanel = new SpectralDisplayPanel(mChannelModel,
+ mChannelProcessingManager, mSettingsManager);
- TunerSpectralDisplayManager tunerSpectralDisplayManager =
- new TunerSpectralDisplayManager(mSpectralPanel,
- channelModel, channelProcessingManager, mSettingsManager);
+ TunerSpectralDisplayManager tunerSpectralDisplayManager = new TunerSpectralDisplayManager(mSpectralPanel,
+ mChannelModel, mChannelProcessingManager, mSettingsManager);
tunerModel.addListener(tunerSpectralDisplayManager);
tunerModel.addListener(this);
- PlaylistManager playlistManager = new PlaylistManager(aliasModel, mBroadcastModel, channelModel,
+ PlaylistManager playlistManager = new PlaylistManager(aliasModel, mBroadcastModel, mChannelModel,
channelMapModel);
playlistManager.init();
@@ -183,6 +198,7 @@ public class SDRTrunk implements Listener
try
{
mMainGui.setVisible(true);
+ autoStartChannels();
}
catch(Exception e)
{
@@ -192,6 +208,21 @@ public class SDRTrunk implements Listener
});
}
+ /**
+ * Shows a dialog that lists the channels that have been designated for auto-start, sorted by auto-start order and
+ * allows the user to start now, cancel, or allow the timer to expire and then start the channels. The dialog will
+ * only show if there are one ore more channels designated for auto-start.
+ */
+ private void autoStartChannels()
+ {
+ List channels = mChannelModel.getAutoStartChannels();
+
+ if(channels.size() > 0)
+ {
+ ChannelAutoStartFrame autoStartFrame = new ChannelAutoStartFrame(mChannelProcessingManager, channels);
+ }
+ }
+
/**
* Launch the application.
*/
@@ -212,7 +243,8 @@ public class SDRTrunk implements Listener
*/
mTitle = SystemProperties.getInstance().getApplicationName();
mMainGui.setTitle(mTitle);
- mMainGui.setBounds(100, 100, 1280, 800);
+ mMainGui.setSize(new Dimension(1280,800));
+ mMainGui.setLocationRelativeTo(null);
mMainGui.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
//Set preferred sizes to influence the split
diff --git a/src/main/java/io/github/dsheirer/instrument/gui/DecoderSelectionFrame.java b/src/main/java/io/github/dsheirer/instrument/gui/DecoderSelectionFrame.java
index 00df6945..bc3f28d4 100644
--- a/src/main/java/io/github/dsheirer/instrument/gui/DecoderSelectionFrame.java
+++ b/src/main/java/io/github/dsheirer/instrument/gui/DecoderSelectionFrame.java
@@ -2,15 +2,19 @@ package io.github.dsheirer.instrument.gui;
import io.github.dsheirer.alias.AliasModel;
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.map.ChannelMapModel;
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.source.IControllableFileSource;
import net.miginfocom.swing.MigLayout;
-import javax.swing.*;
-import java.awt.*;
+import javax.swing.JButton;
+import javax.swing.JDesktopPane;
+import javax.swing.JInternalFrame;
+import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
@@ -18,14 +22,14 @@ public class DecoderSelectionFrame extends JInternalFrame
{
private static final long serialVersionUID = 1L;
+ private Channel mChannel = new Channel();
private DecodeConfigurationEditor mDecodeEditor = new DecodeConfigurationEditor(null);
private PlaylistManager mPlaylistManager;
private IControllableFileSource mSource;
private JDesktopPane mDesktop;
- public DecoderSelectionFrame(JDesktopPane desktop,
- IControllableFileSource source)
+ public DecoderSelectionFrame(JDesktopPane desktop, IControllableFileSource source)
{
AliasModel aliasModel = new AliasModel();
BroadcastModel broadcastModel = new BroadcastModel(null);
@@ -37,6 +41,7 @@ public class DecoderSelectionFrame extends JInternalFrame
mDesktop = desktop;
mSource = source;
+ mDecodeEditor.setItem(mChannel);
initGUI();
}
@@ -72,17 +77,23 @@ public class DecoderSelectionFrame extends JInternalFrame
@Override
public void actionPerformed(ActionEvent arg0)
{
-// DecodeConfiguration config = mDecodeEditor.getDecodeConfig();
-//
-// if( config != null )
-// {
-// DecoderViewFrame decoderFrame = new DecoderViewFrame(
-// mPlaylistManager, null, mSource );
-//
-// decoderFrame.setVisible( true );
-//
-// mDesktop.add( decoderFrame );
-// }
+ mDecodeEditor.save();
+
+ Channel channel = mDecodeEditor.getItem();
+
+ if(channel != null)
+ {
+ DecodeConfiguration config = channel.getDecodeConfiguration();
+
+ if( config != null )
+ {
+ DecoderViewFrame decoderFrame = new DecoderViewFrame(mPlaylistManager, channel, mSource );
+
+ decoderFrame.setVisible( true );
+
+ mDesktop.add( decoderFrame );
+ }
+ }
}
});
}
diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/DemodulatorViewerFX.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/DemodulatorViewerFX.java
new file mode 100644
index 00000000..5e9745a6
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/DemodulatorViewerFX.java
@@ -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
+ *
+ ******************************************************************************/
+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()
+ {
+ @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()
+ {
+ @Override
+ public void handle(ActionEvent event)
+ {
+ getViewerDesktop().close();
+ }
+ });
+
+ MenuItem exitMenuItem = new MenuItem("Exit");
+ exitMenuItem.setOnAction(new EventHandler()
+ {
+ @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()
+ {
+ @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);
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/PlaybackController.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/PlaybackController.java
new file mode 100644
index 00000000..1ea7a687
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/PlaybackController.java
@@ -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
+ *
+ ******************************************************************************/
+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
+{
+ 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 mPlayEventHandler;
+ private DecoderPane mSampleRateListener;
+
+ private IControllableFileSource mControllableFileSource;
+ private Broadcaster 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)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 listener)
+ {
+ mComplexBufferBroadcaster.addListener(listener);
+ }
+
+ /**
+ * Removes the listener from receiving complex buffers from this playback
+ */
+ public void removeListener(Listener 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 getPlaybackEventHandler()
+ {
+ if(mPlayEventHandler == null)
+ {
+ mPlayEventHandler = new EventHandler()
+ {
+ @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);
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/RecentFilesMenu.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/RecentFilesMenu.java
new file mode 100644
index 00000000..873babf7
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/RecentFilesMenu.java
@@ -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
+{
+ 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()
+ {
+ @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);
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/ViewerDesktop.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/ViewerDesktop.java
new file mode 100644
index 00000000..bdd01f5d
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/ViewerDesktop.java
@@ -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
+ *
+ ******************************************************************************/
+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;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexPhaseLineChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexPhaseLineChart.java
new file mode 100644
index 00000000..68d86efb
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexPhaseLineChart.java
@@ -0,0 +1,121 @@
+/*******************************************************************************
+ * sdr-trunk
+ * Copyright (C) 2014-2018 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with this program.
+ * If not, see
+ *
+ ******************************************************************************/
+package io.github.dsheirer.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
+{
+ 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> 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 phaseSeries = new Series<>("Phase", mPhaseValues);
+ ObservableList> observableList = FXCollections.observableArrayList(phaseSeries);
+
+ setData(observableList);
+ init(length);
+ }
+
+ private void init(int length)
+ {
+ mLengthProperty.addListener(new ChangeListener()
+ {
+ @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());
+ }
+ }
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexSampleLineChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexSampleLineChart.java
new file mode 100644
index 00000000..d2b25b4b
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexSampleLineChart.java
@@ -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
+ *
+ ******************************************************************************/
+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
+{
+ private final static Logger mLog = LoggerFactory.getLogger(ComplexSampleLineChart.class);
+
+ private ComplexCircularBuffer mComplexCircularBuffer;
+ private ComplexCircularBuffer mDemodulationCircularBuffer;
+ private ComplexGain mComplexGain = new ComplexGain(500.0f);
+ private ObservableList> mISamples = FXCollections.observableArrayList();
+ private ObservableList> 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 iSampleSeries = new LineChart.Series<>("Inphase", mISamples);
+ LineChart.Series qSampleSeries = new LineChart.Series<>("Quadrature", mQSamples);
+ ObservableList> 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()
+ {
+ @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());
+ }
+ }
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/DoubleLineChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/DoubleLineChart.java
new file mode 100644
index 00000000..54811999
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/DoubleLineChart.java
@@ -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
+ *
+ ******************************************************************************/
+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
+{
+ private final static Logger mLog = LoggerFactory.getLogger(DoubleLineChart.class);
+
+ private DoubleCircularBuffer mDoubleCircularBuffer;
+ private ObservableList> 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 phaseSeries = new Series<>(seriesLabel, mPhaseValues);
+ ObservableList> 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]);
+ }
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/EyeDiagramChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/EyeDiagramChart.java
new file mode 100644
index 00000000..5764cc8e
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/EyeDiagramChart.java
@@ -0,0 +1,121 @@
+/*******************************************************************************
+ * sdr-trunk
+ * Copyright (C) 2014-2018 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with this program.
+ * If not, see
+ *
+ ******************************************************************************/
+package io.github.dsheirer.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
+{
+ private final static Logger mLog = LoggerFactory.getLogger(EyeDiagramChart.class);
+
+ private ObservableList> 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> series = FXCollections.observableArrayList();
+ mData.add(new Series<>(series));
+ }
+
+ setData(mData);
+ }
+
+ private ObservableList> getSeries(int series, int length)
+ {
+ ObservableList> seriesData = mData.get(series).getData();
+
+ while(seriesData.size() > length)
+ {
+ seriesData.remove(0);
+ }
+
+ while(seriesData.size() < length)
+ {
+ seriesData.add(new Data(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> iSeries = getSeries(mSeriesPointer++, length + 1);
+
+ if(mSeriesPointer >= mSeriesCount)
+ {
+ mSeriesPointer = 0;
+ }
+
+ ObservableList> qSeries = getSeries(mSeriesPointer++, length + 1);
+
+ if(mSeriesPointer >= mSeriesCount)
+ {
+ mSeriesPointer = 0;
+ }
+
+ for(int x = 0; x < length; x++)
+ {
+ demodulated[x].normalize();
+ Data iDataPoint = iSeries.get(x);
+ iDataPoint.setXValue((double)x + 1 - symbolDecisionData.getSamplingPoint());
+ double iValue = demodulated[x].inphase();
+ iDataPoint.setYValue(iValue);
+
+ Data qDataPoint = qSeries.get(x);
+ qDataPoint.setXValue((double)x + 1 - symbolDecisionData.getSamplingPoint());
+ double qValue = demodulated[x].quadrature();
+ qDataPoint.setYValue(qValue);
+ }
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/FMDemodulatedLineChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/FMDemodulatedLineChart.java
new file mode 100644
index 00000000..3ccf8854
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/FMDemodulatedLineChart.java
@@ -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
+ *
+ ******************************************************************************/
+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
+{
+ private final static Logger mLog = LoggerFactory.getLogger(FMDemodulatedLineChart.class);
+
+ private DoubleCircularBuffer mDoubleCircularBuffer;
+ private int mSamplesPerSymbol;
+ private int mTwiceSamplesPerSymbol;
+ private int mSeriesCount;
+ private ObservableList> 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> series = FXCollections.observableArrayList();
+
+ for(int y = 0; y < seriesLength; y++)
+ {
+ series.add(new Data(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 series = mData.get(mSeriesPointer);
+ Data data = series.getData().get(mSamplePointer++);
+ data.setYValue(value);
+
+ if(mSamplePointer >= mTwiceSamplesPerSymbol)
+ {
+ mSamplePointer = 0;
+ mSeriesPointer++;
+
+ if(mSeriesPointer >= mSeriesCount)
+ {
+ mSeriesPointer = 0;
+ }
+ }
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/PhaseLineChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/PhaseLineChart.java
new file mode 100644
index 00000000..df7ff76a
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/PhaseLineChart.java
@@ -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
+ *
+ ******************************************************************************/
+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
+{
+ private final static Logger mLog = LoggerFactory.getLogger(PhaseLineChart.class);
+
+ private DoubleCircularBuffer mDoubleCircularBuffer;
+ private ObservableList> mPhaseValues = FXCollections.observableArrayList();
+
+ public PhaseLineChart(int length)
+ {
+ super(new NumberAxis("Time", 1, length, 2),
+ new NumberAxis("Phase", -0.8, 0.8, 0.1));
+
+ Series phaseSeries = new Series<>("PLL Phase Error", mPhaseValues);
+ ObservableList> 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]);
+ }
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/SymbolChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/SymbolChart.java
new file mode 100644
index 00000000..6f99f042
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/SymbolChart.java
@@ -0,0 +1,90 @@
+/*******************************************************************************
+ * sdr-trunk
+ * Copyright (C) 2014-2018 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with this program.
+ * If not, see
+ *
+ ******************************************************************************/
+package io.github.dsheirer.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
+{
+ private final static Logger mLog = LoggerFactory.getLogger(SymbolChart.class);
+
+ private ObservableList mCurrentPoints = FXCollections.observableArrayList();
+// private ObservableList mPreviousPoints = FXCollections.observableArrayList();
+// private ObservableList 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 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;
+ }
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPane.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPane.java
new file mode 100644
index 00000000..70c39c2e
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPane.java
@@ -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
+ *
+ ******************************************************************************/
+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
+{
+ private Broadcaster 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 listener)
+ {
+ mComplexBufferBroadcaster.addListener(listener);
+ }
+
+ public void removeListener(Listener listener)
+ {
+ mComplexBufferBroadcaster.removeListener(listener);
+ }
+
+ public void setSampleRate(double sampleRate)
+ {
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPaneFactory.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPaneFactory.java
new file mode 100644
index 00000000..b069204e
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPaneFactory.java
@@ -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
+ *
+ ******************************************************************************/
+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 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);
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1Pane.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1Pane.java
new file mode 100644
index 00000000..526d3e7f
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1Pane.java
@@ -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
+ *
+ ******************************************************************************/
+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 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()
+ {
+ @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;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/module/decode/Decoder.java b/src/main/java/io/github/dsheirer/module/decode/Decoder.java
index dedac6df..f8f4953b 100644
--- a/src/main/java/io/github/dsheirer/module/decode/Decoder.java
+++ b/src/main/java/io/github/dsheirer/module/decode/Decoder.java
@@ -1,17 +1,17 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014-2016 Dennis Sheirer
- *
+ *
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
- *
+ *
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
- *
+ *
* You should have received a copy of the GNU General Public License
* along with this program. If not, see
******************************************************************************/
@@ -22,38 +22,39 @@ import io.github.dsheirer.message.Message;
import io.github.dsheirer.module.Module;
import io.github.dsheirer.sample.Listener;
-public abstract class Decoder extends Module implements IMessageProvider, Listener
+public abstract class Decoder extends Module implements IMessageProvider
{
- /* This has to be a broadcaster in order for references to persist */
- protected Listener mMessageListener;
+ /* This has to be a broadcaster in order for references to persist */
+ private Listener mMessageDistributor = new MessageDistributor();
+ protected Listener mMessageListener;
- /**
- * Decoder - parent class for all decoders, demodulators and components.
- */
- public Decoder()
- {
- }
+ /**
+ * Decoder - parent class for all decoders, demodulators and components.
+ */
+ public Decoder()
+ {
+ }
- @Override
- public void dispose()
- {
- mMessageListener = null;
- }
+ @Override
+ public void dispose()
+ {
+ mMessageListener = null;
+ }
- /**
- * Identifies the decoder type (ie protocol)
- */
- public abstract DecoderType getDecoderType();
+ /**
+ * Identifies the decoder type (ie protocol)
+ */
+ public abstract DecoderType getDecoderType();
/**
* Adds a listener for receiving decoded messages from this decoder
*/
@Override
- public void setMessageListener( Listener listener )
+ public void setMessageListener(Listener listener)
{
- mMessageListener = listener;
- }
+ mMessageListener = listener;
+ }
/**
* Removes the listener from receiving decoded messages from all attached
@@ -62,38 +63,29 @@ public abstract class Decoder extends Module implements IMessageProvider, Listen
@Override
public void removeMessageListener()
{
- mMessageListener = null;
+ mMessageListener = null;
}
/**
- * Broadcasts the message to the registered message listener
+ * Message listener for receiving the output from this decoder
*/
- public void broadcast( Message message )
+ protected Listener getMessageListener()
{
- if( mMessageListener != null )
- {
- mMessageListener.receive( message );
- }
+ return mMessageDistributor;
}
- @Override
- public void receive( Message message )
- {
- broadcast( message );
- }
-
- @Override
- public void reset()
- {
- }
-
- @Override
- public void start()
- {
- }
-
- @Override
- public void stop()
- {
- }
+ /**
+ * Distributes/forwards messages from sub-class decoder implementations to the registered message listener.
+ */
+ public class MessageDistributor implements Listener
+ {
+ @Override
+ public void receive(Message message)
+ {
+ if(mMessageListener != null)
+ {
+ mMessageListener.receive(message);
+ }
+ }
+ }
}
diff --git a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java
index dac8df21..0c19e47c 100644
--- a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java
+++ b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java
@@ -247,10 +247,8 @@ public class DecoderFactory
switch(modulation)
{
case C4FM:
-// modules.add(new FMDemodulatorModule(P25_C4FM_IQ_FILTER));
- modules.add(new DemodulatedAudioFilterModule(P25_C4FM_DEMOD_FILTER, 1.0f));
- modules.add(new P25_C4FMDecoder(aliasList, decodeConfig.getAFCMaximumCorrection()));
- modules.add(new P25DecoderState(aliasList, channelType, Modulation.C4FM,
+ modules.add(new P25_C4FMDecoder(aliasList));
+ modules.add(new P25DecoderState(aliasList, channelType, P25Decoder.Modulation.C4FM,
p25Config.getIgnoreDataCalls()));
break;
case CQPSK:
diff --git a/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2Decoder.java b/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2Decoder.java
index 4a75ab70..bbd992cd 100644
--- a/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2Decoder.java
+++ b/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2Decoder.java
@@ -89,7 +89,7 @@ public class Fleetsync2Decoder extends Decoder implements IFilteredRealBufferLis
mMessageProcessor = new Fleetsync2MessageProcessor( aliasList );
mMessageFramer.addMessageListener( mMessageProcessor );
- mMessageProcessor.setMessageListener( this );
+ mMessageProcessor.setMessageListener( getMessageListener() );
}
public void dispose()
diff --git a/src/main/java/io/github/dsheirer/module/decode/lj1200/LJ1200Decoder.java b/src/main/java/io/github/dsheirer/module/decode/lj1200/LJ1200Decoder.java
index 2556e207..8e82ffdf 100644
--- a/src/main/java/io/github/dsheirer/module/decode/lj1200/LJ1200Decoder.java
+++ b/src/main/java/io/github/dsheirer/module/decode/lj1200/LJ1200Decoder.java
@@ -99,7 +99,7 @@ public class LJ1200Decoder extends Decoder implements IFilteredRealBufferListene
mMessageProcessor = new LJ1200MessageProcessor( aliasList );
mTowerMessageFramer.addMessageListener( mMessageProcessor );
mTransponderMessageFramer.addMessageListener( mMessageProcessor );
- mMessageProcessor.setMessageListener( this );
+ mMessageProcessor.setMessageListener( getMessageListener() );
}
@Override
diff --git a/src/main/java/io/github/dsheirer/module/decode/ltrnet/LTRNetDecoder.java b/src/main/java/io/github/dsheirer/module/decode/ltrnet/LTRNetDecoder.java
index 78d3db90..6f5f6d06 100644
--- a/src/main/java/io/github/dsheirer/module/decode/ltrnet/LTRNetDecoder.java
+++ b/src/main/java/io/github/dsheirer/module/decode/ltrnet/LTRNetDecoder.java
@@ -73,7 +73,7 @@ public class LTRNetDecoder extends Decoder
mLTRMessageFramer.addMessageListener( mLTRMessageProcessor );
- mLTRMessageProcessor.setMessageListener( this );
+ mLTRMessageProcessor.setMessageListener( getMessageListener() );
}
@Override
diff --git a/src/main/java/io/github/dsheirer/module/decode/ltrstandard/LTRStandardDecoder.java b/src/main/java/io/github/dsheirer/module/decode/ltrstandard/LTRStandardDecoder.java
index 8a6355a9..47bbd171 100644
--- a/src/main/java/io/github/dsheirer/module/decode/ltrstandard/LTRStandardDecoder.java
+++ b/src/main/java/io/github/dsheirer/module/decode/ltrstandard/LTRStandardDecoder.java
@@ -62,7 +62,7 @@ public class LTRStandardDecoder extends Decoder implements IUnFilteredRealBuffer
mLTRMessageProcessor = new LTRStandardMessageProcessor( direction, aliasList );
mLTRMessageFramer.addMessageListener( mLTRMessageProcessor );
- mLTRMessageProcessor.setMessageListener( this );
+ mLTRMessageProcessor.setMessageListener( getMessageListener() );
}
@Override
diff --git a/src/main/java/io/github/dsheirer/module/decode/mdc1200/MDCDecoder.java b/src/main/java/io/github/dsheirer/module/decode/mdc1200/MDCDecoder.java
index 1764c5db..735871fa 100644
--- a/src/main/java/io/github/dsheirer/module/decode/mdc1200/MDCDecoder.java
+++ b/src/main/java/io/github/dsheirer/module/decode/mdc1200/MDCDecoder.java
@@ -101,7 +101,7 @@ public class MDCDecoder extends Decoder implements IFilteredRealBufferListener,
/* Message Processor */
mMessageProcessor = new MDCMessageProcessor( aliasList );
mMessageFramer.addMessageListener( mMessageProcessor );
- mMessageProcessor.addMessageListener( this );
+ mMessageProcessor.addMessageListener(getMessageListener());
}
public void dispose()
diff --git a/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327Decoder.java b/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327Decoder.java
index c9455d95..6dc1fc84 100644
--- a/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327Decoder.java
+++ b/src/main/java/io/github/dsheirer/module/decode/mpt1327/MPT1327Decoder.java
@@ -147,7 +147,7 @@ public class MPT1327Decoder extends Decoder implements IFilteredRealBufferListen
/* Fully decoded and framed messages processor */
mMessageProcessor = new MPT1327MessageProcessor(aliasList);
- mMessageProcessor.setMessageListener(this);
+ mMessageProcessor.setMessageListener(getMessageListener());
mControlMessageFramer.addMessageListener(mMessageProcessor);
mTrafficMessageFramer.addMessageListener(mMessageProcessor);
}
diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25Decoder.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25Decoder.java
index 2f08eb4f..e85dec3c 100644
--- a/src/main/java/io/github/dsheirer/module/decode/p25/P25Decoder.java
+++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25Decoder.java
@@ -1,7 +1,21 @@
+/*******************************************************************************
+ * sdr-trunk
+ * Copyright (C) 2014-2018 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with this program.
+ * If not, see
+ *
+ ******************************************************************************/
package io.github.dsheirer.module.decode.p25;
import io.github.dsheirer.alias.AliasList;
-import io.github.dsheirer.instrument.Instrumentable;
import io.github.dsheirer.module.decode.Decoder;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.source.ISourceEventListener;
@@ -9,62 +23,69 @@ import io.github.dsheirer.source.ISourceEventProvider;
public abstract class P25Decoder extends Decoder implements ISourceEventListener, ISourceEventProvider, Instrumentable
{
- private P25MessageProcessor mMessageProcessor;
-
- public P25Decoder( AliasList aliasList )
- {
- mMessageProcessor = new P25MessageProcessor( aliasList );
- mMessageProcessor.setMessageListener( this );
- }
-
- public void dispose()
- {
- super.dispose();
-
- mMessageProcessor.dispose();
- mMessageProcessor = null;
- }
+ private P25MessageProcessor mMessageProcessor;
+ private AliasList mAliasList;
- @Override
- public DecoderType getDecoderType()
+ public P25Decoder(AliasList aliasList)
{
- return DecoderType.P25_PHASE1;
+ mAliasList = aliasList;
+ mMessageProcessor = new P25MessageProcessor(mAliasList);
+ mMessageProcessor.setMessageListener(getMessageListener());
}
- protected P25MessageProcessor getMessageProcessor()
- {
- return mMessageProcessor;
- }
-
- public abstract Modulation getModulation();
-
- public enum Modulation
- {
- CQPSK( "Simulcast (LSM)", "LSM" ),
- C4FM( "Normal (C4FM)", "C4FM" );
-
- private String mLabel;
- private String mShortLabel;
-
- private Modulation( String label, String shortLabel )
- {
- mLabel = label;
- mShortLabel = shortLabel;
- }
-
- public String getLabel()
- {
- return mLabel;
- }
-
- public String getShortLabel()
- {
- return mShortLabel;
- }
-
- public String toString()
- {
- return getLabel();
- }
- }
+ protected AliasList getAliasList()
+ {
+ return mAliasList;
+ }
+
+ public void dispose()
+ {
+ super.dispose();
+
+ mMessageProcessor.dispose();
+ mMessageProcessor = null;
+ }
+
+ @Override
+ public DecoderType getDecoderType()
+ {
+ return DecoderType.P25_PHASE1;
+ }
+
+ protected P25MessageProcessor getMessageProcessor()
+ {
+ return mMessageProcessor;
+ }
+
+ public abstract Modulation getModulation();
+
+ public enum Modulation
+ {
+ CQPSK("Simulcast (LSM)", "LSM"),
+ C4FM("Normal (C4FM)", "C4FM");
+
+ private String mLabel;
+ private String mShortLabel;
+
+ private Modulation(String label, String shortLabel)
+ {
+ mLabel = label;
+ mShortLabel = shortLabel;
+ }
+
+ public String getLabel()
+ {
+ return mLabel;
+ }
+
+ public String getShortLabel()
+ {
+ return mShortLabel;
+ }
+
+ public String toString()
+ {
+ return getLabel();
+ }
+ }
}
diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25MessageFramer.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25MessageFramer.java
index 8a0d4397..54047262 100644
--- a/src/main/java/io/github/dsheirer/module/decode/p25/P25MessageFramer.java
+++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25MessageFramer.java
@@ -7,7 +7,7 @@ import io.github.dsheirer.bits.ISyncDetectListener;
import io.github.dsheirer.bits.MultiSyncPatternMatcher;
import io.github.dsheirer.bits.SoftSyncDetector;
import io.github.dsheirer.bits.SyncDetector;
-import io.github.dsheirer.dsp.psk.LSMDemodulator;
+import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.dsp.symbol.FrameSync;
import io.github.dsheirer.edac.BCH_63_16_11;
@@ -39,619 +39,619 @@ import java.util.ArrayList;
public class P25MessageFramer implements Listener
{
- private final static Logger mLog =
- LoggerFactory.getLogger( P25MessageFramer.class );
+ private final static Logger mLog =
+ LoggerFactory.getLogger(P25MessageFramer.class);
- /* Determines the threshold for sync pattern soft matching */
- private static final int SYNC_MATCH_THRESHOLD = 2;
- private static final int SYNC_IN_CALL_THRESHOLD = 4;
+ /* Determines the threshold for sync pattern soft matching */
+ private static final int SYNC_MATCH_THRESHOLD = 2;
+ private static final int SYNC_IN_CALL_THRESHOLD = 4;
- /* Costas Loop phase lock error correction values. A phase lock error of
- * 90 degrees requires a correction of 1/4 of the symbol rate (1200Hz). An
- * error of 180 degrees requires a correction of 1/2 of the symbol rate */
- public static final double SYMBOL_FREQUENCY = 2.0d * Math.PI * 4800.0d / 48000.0d;
- public static final double PHASE_CORRECTION_90_DEGREES = SYMBOL_FREQUENCY / 4.0d;
- public static final double PHASE_CORRECTION_180_DEGREES = SYMBOL_FREQUENCY / 2.0d;
- public static final int TSBK_BEGIN = 64;
- public static final int TSBK_CRC_START = 144;
- public static final int TSBK_END = 260;
- public static final int TSBK_DECODED_END = 160;
+ /* Costas Loop phase lock error correction values. A phase lock error of
+ * 90 degrees requires a correction of 1/4 of the symbol rate (1200Hz). An
+ * error of 180 degrees requires a correction of 1/2 of the symbol rate */
+ public static final double SYMBOL_FREQUENCY = 2.0d * Math.PI * 4800.0d / 48000.0d;
+ public static final double PHASE_CORRECTION_90_DEGREES = SYMBOL_FREQUENCY / 4.0d;
+ public static final double PHASE_CORRECTION_180_DEGREES = SYMBOL_FREQUENCY / 2.0d;
+ public static final int TSBK_BEGIN = 64;
+ public static final int TSBK_CRC_START = 144;
+ public static final int TSBK_END = 260;
+ public static final int TSBK_DECODED_END = 160;
- public static final int PDU0_BEGIN = 64;
- public static final int PDU0_CRC_BEGIN = 144;
- public static final int PDU0_END = 260;
- public static final int PDU1_BEGIN = 160;
- public static final int PDU1_END = 356;
- public static final int PDU2_BEGIN = 256;
- public static final int PDU2_END = 452;
- public static final int PDU3_BEGIN = 352;
- public static final int PDU3_END = 548;
- public static final int PDU3_DECODED_END = 448;
-
- private SoftSyncDetector mPrimarySyncDetector = new SoftSyncDetector(
- FrameSync.P25_PHASE1_NORMAL.getSync(), SYNC_MATCH_THRESHOLD );
+ public static final int PDU0_BEGIN = 64;
+ public static final int PDU0_CRC_BEGIN = 144;
+ public static final int PDU0_END = 260;
+ public static final int PDU1_BEGIN = 160;
+ public static final int PDU1_END = 356;
+ public static final int PDU2_BEGIN = 256;
+ public static final int PDU2_END = 452;
+ public static final int PDU3_BEGIN = 352;
+ public static final int PDU3_END = 548;
+ public static final int PDU3_DECODED_END = 448;
- private MultiSyncPatternMatcher mMatcher = new MultiSyncPatternMatcher( 48 );
- private ArrayList mAssemblers =
- new ArrayList();
+ private SoftSyncDetector mPrimarySyncDetector = new SoftSyncDetector(
+ FrameSync.P25_PHASE1_NORMAL.getSync(), SYNC_MATCH_THRESHOLD);
- private Listener mListener;
- private AliasList mAliasList;
+ private MultiSyncPatternMatcher mMatcher = new MultiSyncPatternMatcher(48);
+ private ArrayList mAssemblers =
+ new ArrayList();
- private Trellis_1_2_Rate mHalfRate = new Trellis_1_2_Rate();
- private Trellis_3_4_Rate mThreeQuarterRate = new Trellis_3_4_Rate();
- private BCH_63_16_11 mNIDDecoder = new BCH_63_16_11();
-
- /**
- * Constructs a P25 message framer to receive a stream of symbols and
- * detect the sync pattern then capture the following stream of symbols up
- * to the message length, and then broadcast that bit buffer to the registered
- * listener.
- */
- public P25MessageFramer( AliasList aliasList )
- {
- mAliasList = aliasList;
-
- mPrimarySyncDetector.setListener( new ISyncDetectListener()
- {
- @Override
- public void syncDetected()
- {
- for( P25MessageAssembler assembler: mAssemblers )
- {
- if( !assembler.isActive() )
- {
- assembler.setActive( true );
- break;
- }
- }
- }
- } );
-
- mMatcher.add( mPrimarySyncDetector );
+ private Listener mListener;
+ private AliasList mAliasList;
- /**
- * We use two message assemblers to catch any sync detections, so that
- * if we have a false trigger then we still have a chance to catch the
- * valid message. If we have two false triggers before the arrival of
- * the actual message sync, then the third or subsequent real or false
- * sync pattern will produce a debug message indicating no assemblers
- * are available..
- */
- mAssemblers.add( new P25MessageAssembler() );
- mAssemblers.add( new P25MessageAssembler() );
- }
-
- public P25MessageFramer( AliasList aliasList, LSMDemodulator demodulator )
- {
- this( aliasList );
+ private Trellis_1_2_Rate mHalfRate = new Trellis_1_2_Rate();
+ private Trellis_3_4_Rate mThreeQuarterRate = new Trellis_3_4_Rate();
+ private BCH_63_16_11 mNIDDecoder = new BCH_63_16_11();
- if( demodulator != null )
- {
- /* For CQPSK, we include 3 additional sync detectors to watch for and
- * correct +/-90 and 180 degree costas loop phase lock errors */
- mMatcher.add( new CostasPhaseErrorDetector( FrameSync.P25_PHASE1_ERROR_90_CCW,
- demodulator, PHASE_CORRECTION_90_DEGREES ) );
-
- mMatcher.add( new CostasPhaseErrorDetector( FrameSync.P25_PHASE1_ERROR_90_CW,
- demodulator, -PHASE_CORRECTION_90_DEGREES ) );
-
- mMatcher.add( new CostasPhaseErrorDetector( FrameSync.P25_PHASE1_ERROR_180,
- demodulator, PHASE_CORRECTION_180_DEGREES ) );
- }
- }
-
- public void dispose()
- {
- for( P25MessageAssembler a: mAssemblers )
- {
- a.dispose();
- }
-
- mAssemblers.clear();
-
- mListener = null;
- mAliasList = null;
-
- mMatcher.dispose();
- mMatcher = null;
-
- mPrimarySyncDetector.dispose();
- mPrimarySyncDetector = null;
-
- }
-
- private void dispatch( Message message )
- {
- if( mListener != null )
- {
- mListener.receive( message );
- }
- }
-
- @Override
- public void receive( Dibit symbol )
+ /**
+ * Constructs a P25 message framer to receive a stream of symbols and
+ * detect the sync pattern then capture the following stream of symbols up
+ * to the message length, and then broadcast that bit buffer to the registered
+ * listener.
+ */
+ public P25MessageFramer(AliasList aliasList)
{
- for( P25MessageAssembler assembler: mAssemblers )
- {
- if( assembler.isActive() )
- {
- assembler.receive( symbol );
-
- if( assembler.complete() )
- {
- assembler.reset();
- }
- }
- }
-
- mMatcher.receive( symbol.getBit1(), symbol.getBit2() );
+ mAliasList = aliasList;
+
+ mPrimarySyncDetector.setListener(new ISyncDetectListener()
+ {
+ @Override
+ public void syncDetected()
+ {
+ for(P25MessageAssembler assembler : mAssemblers)
+ {
+ if(!assembler.isActive())
+ {
+ assembler.setActive(true);
+ break;
+ }
+ }
+ }
+ });
+
+ mMatcher.add(mPrimarySyncDetector);
+
+ /**
+ * We use two message assemblers to catch any sync detections, so that
+ * if we have a false trigger then we still have a chance to catch the
+ * valid message. If we have two false triggers before the arrival of
+ * the actual message sync, then the third or subsequent real or false
+ * sync pattern will produce a debug message indicating no assemblers
+ * are available..
+ */
+ mAssemblers.add(new P25MessageAssembler());
+ mAssemblers.add(new P25MessageAssembler());
}
- public void setListener( Listener listener )
+ public P25MessageFramer(AliasList aliasList, IPhaseLockedLoop phaseLockedLoop)
{
- mListener = listener;
+ this(aliasList);
+
+ if(phaseLockedLoop != null)
+ {
+ //Add additional sync pattern detectors to detect when we get 90/180 degree out of phase sync pattern
+ //detections so that we can apply correction to the phase locked loop
+ mMatcher.add(new PLLPhaseInversionDetector(FrameSync.P25_PHASE1_ERROR_90_CCW,
+ phaseLockedLoop, PHASE_CORRECTION_90_DEGREES));
+
+ mMatcher.add(new PLLPhaseInversionDetector(FrameSync.P25_PHASE1_ERROR_90_CW,
+ phaseLockedLoop, -PHASE_CORRECTION_90_DEGREES));
+
+ mMatcher.add(new PLLPhaseInversionDetector(FrameSync.P25_PHASE1_ERROR_180,
+ phaseLockedLoop, PHASE_CORRECTION_180_DEGREES));
+ }
}
- public void removeListener( Listener listener )
+ public void dispose()
{
- mListener = null;
+ for(P25MessageAssembler a : mAssemblers)
+ {
+ a.dispose();
+ }
+
+ mAssemblers.clear();
+
+ mListener = null;
+ mAliasList = null;
+
+ mMatcher.dispose();
+ mMatcher = null;
+
+ mPrimarySyncDetector.dispose();
+ mPrimarySyncDetector = null;
+
}
-
+
+ private void dispatch(Message message)
+ {
+ if(mListener != null)
+ {
+ mListener.receive(message);
+ }
+ }
+
+ @Override
+ public void receive(Dibit symbol)
+ {
+ for(P25MessageAssembler assembler : mAssemblers)
+ {
+ if(assembler.isActive())
+ {
+ assembler.receive(symbol);
+
+ if(assembler.complete())
+ {
+ assembler.reset();
+ }
+ }
+ }
+
+ mMatcher.receive(symbol.getBit1(), symbol.getBit2());
+ }
+
+ public void setListener(Listener listener)
+ {
+ mListener = listener;
+ }
+
+ public void removeListener(Listener listener)
+ {
+ mListener = null;
+ }
+
private class P25MessageAssembler
{
- /* Starting position of the status symbol counter is 24 symbols to
- * account for the 48-bit sync pattern which is not included in message */
- private int mStatusSymbolPointer = 24;
- private BinaryMessage mMessage;
+ /* Starting position of the status symbol counter is 24 symbols to
+ * account for the 48-bit sync pattern which is not included in message */
+ private int mStatusSymbolPointer = 24;
+ private BinaryMessage mMessage;
private int mMessageLength;
private boolean mComplete = false;
private boolean mActive = false;
private DataUnitID mDUID = DataUnitID.NID;
-
+
public P25MessageAssembler()
{
- mMessageLength = mDUID.getMessageLength();
- mMessage = new BinaryMessage( mMessageLength );
- reset();
+ mMessageLength = mDUID.getMessageLength();
+ mMessage = new BinaryMessage(mMessageLength);
+ reset();
}
-
- public void receive( Dibit dibit )
+
+ public void receive(Dibit dibit)
{
- if( mActive )
- {
- if( mStatusSymbolPointer == 35 )
- {
- mStatusSymbolPointer = 0;
- }
- else
- {
- mStatusSymbolPointer++;
+ if(mActive)
+ {
+ if(mStatusSymbolPointer == 35)
+ {
+ mStatusSymbolPointer = 0;
+ }
+ else
+ {
+ mStatusSymbolPointer++;
try
{
- mMessage.add( dibit.getBit1() );
- mMessage.add( dibit.getBit2() );
+ mMessage.add(dibit.getBit1());
+ mMessage.add(dibit.getBit2());
}
- catch( BitSetFullException e )
+ catch(BitSetFullException e)
{
- mComplete = true;
+ mComplete = true;
}
-
+
/* Check the message for complete */
- if( mMessage.isFull() )
+ if(mMessage.isFull())
{
- checkComplete();
+ checkComplete();
}
- }
- }
+ }
+ }
}
public void reset()
{
- mDUID = DataUnitID.NID;
- mMessage.setSize( mDUID.getMessageLength() );
- mMessage.clear();
- mStatusSymbolPointer = 24;
+ mDUID = DataUnitID.NID;
+ mMessage.setSize(mDUID.getMessageLength());
+ mMessage.clear();
+ mStatusSymbolPointer = 24;
mComplete = false;
mActive = false;
}
-
- public void setActive( boolean active )
+
+ public void setActive(boolean active)
{
- mActive = active;
+ mActive = active;
}
- private void setDUID( DataUnitID id )
+ private void setDUID(DataUnitID id)
{
- mDUID = id;
- mMessageLength = id.getMessageLength();
- mMessage.setSize( mMessageLength );
+ mDUID = id;
+ mMessageLength = id.getMessageLength();
+ mMessage.setSize(mMessageLength);
}
private void checkComplete()
{
- switch( mDUID )
- {
- case NID:
- mMessage = mNIDDecoder.correctNID( mMessage );
-
- if( mMessage.getCRC() != CRC.FAILED_CRC )
- {
- int value = mMessage.getInt( P25Message.DUID );
-
- DataUnitID duid = DataUnitID.fromValue( value );
-
- if( duid != DataUnitID.UNKN )
- {
- setDUID( duid );
- }
- else
- {
- mComplete = true;
- dispatch( new P25Message( mMessage.copy(), mDUID, mAliasList ) );
- }
- }
- else
- {
- mComplete = true;
- }
- break;
- case HDU:
- mComplete = true;
- dispatch( new HDUMessage( mMessage.copy(), mDUID, mAliasList ) );
-
- /* We're in a call now, lower the sync match threshold */
- mPrimarySyncDetector.setThreshold( SYNC_IN_CALL_THRESHOLD );
- break;
- case LDU1:
- mComplete = true;
-
- LDU1Message ldu1 = new LDU1Message( mMessage.copy(),
- mDUID, mAliasList );
+ switch(mDUID)
+ {
+ case NID:
+ mMessage = mNIDDecoder.correctNID(mMessage);
- /* Convert the LDU1 message into a link control LDU1 message */
- dispatch( LDULCMessageFactory.getMessage( ldu1 ) );
+ if(mMessage.getCRC() != CRC.FAILED_CRC)
+ {
+ int value = mMessage.getInt(P25Message.DUID);
+
+ DataUnitID duid = DataUnitID.fromValue(value);
+
+ if(duid != DataUnitID.UNKN)
+ {
+ setDUID(duid);
+ }
+ else
+ {
+ mComplete = true;
+ dispatch(new P25Message(mMessage.copy(), mDUID, mAliasList));
+ }
+ }
+ else
+ {
+ mComplete = true;
+ }
+ break;
+ case HDU:
+ mComplete = true;
+ dispatch(new HDUMessage(mMessage.copy(), mDUID, mAliasList));
/* We're in a call now, lower the sync match threshold */
- mPrimarySyncDetector.setThreshold( SYNC_IN_CALL_THRESHOLD );
- break;
- case LDU2:
- mComplete = true;
- dispatch( new LDU2Message( mMessage.copy(), mDUID, mAliasList ) );
+ mPrimarySyncDetector.setThreshold(SYNC_IN_CALL_THRESHOLD);
+ break;
+ case LDU1:
+ mComplete = true;
+
+ LDU1Message ldu1 = new LDU1Message(mMessage.copy(),
+ mDUID, mAliasList);
+
+ /* Convert the LDU1 message into a link control LDU1 message */
+ dispatch(LDULCMessageFactory.getMessage(ldu1));
/* We're in a call now, lower the sync match threshold */
- mPrimarySyncDetector.setThreshold( SYNC_IN_CALL_THRESHOLD );
- break;
- case PDU0:
+ mPrimarySyncDetector.setThreshold(SYNC_IN_CALL_THRESHOLD);
+ break;
+ case LDU2:
+ mComplete = true;
+ dispatch(new LDU2Message(mMessage.copy(), mDUID, mAliasList));
- /* Remove interleaving */
- P25Interleave.deinterleaveData( mMessage, PDU0_BEGIN, PDU0_END );
-
- /* Remove trellis encoding - abort processing if we have an
- * unsuccessful decode due to excessive errors */
- if( mHalfRate.decode( mMessage, PDU0_BEGIN, PDU0_END ) )
- {
- mMessage = CRCP25.correctCCITT80( mMessage,
- PDU0_BEGIN, PDU0_CRC_BEGIN );
-
+ /* We're in a call now, lower the sync match threshold */
+ mPrimarySyncDetector.setThreshold(SYNC_IN_CALL_THRESHOLD);
+ break;
+ case PDU0:
+
+ /* Remove interleaving */
+ P25Interleave.deinterleaveData(mMessage, PDU0_BEGIN, PDU0_END);
+
+ /* Remove trellis encoding - abort processing if we have an
+ * unsuccessful decode due to excessive errors */
+ if(mHalfRate.decode(mMessage, PDU0_BEGIN, PDU0_END))
+ {
+ mMessage = CRCP25.correctCCITT80(mMessage,
+ PDU0_BEGIN, PDU0_CRC_BEGIN);
+
+
+ if(mMessage.getCRC() != CRC.FAILED_CRC)
+ {
+ boolean confirmed = mMessage.get(
+ PDUMessage.CONFIRMATION_REQUIRED_INDICATOR);
+
+ if(confirmed)
+ {
+ setDUID(DataUnitID.PDUC);
+ }
+ else
+ {
+ setDUID(DataUnitID.PDU1);
+ }
+
+ mMessage.setPointer(PDU1_BEGIN);
+ }
+ else
+ {
+ mComplete = true;
+ }
+ }
+ else
+ {
+ mComplete = true;
+ }
- if( mMessage.getCRC() != CRC.FAILED_CRC )
- {
- boolean confirmed = mMessage.get(
- PDUMessage.CONFIRMATION_REQUIRED_INDICATOR );
-
- if( confirmed )
- {
- setDUID( DataUnitID.PDUC );
- }
- else
- {
- setDUID( DataUnitID.PDU1 );
- }
-
- mMessage.setPointer( PDU1_BEGIN );
- }
- else
- {
- mComplete = true;
- }
- }
- else
- {
- mComplete = true;
- }
-
/* Set sync match threshold to normal */
- mPrimarySyncDetector.setThreshold( SYNC_MATCH_THRESHOLD );
-
- break;
- case PDU1:
- /* Remove interleaving */
- P25Interleave.deinterleaveData( mMessage, PDU1_BEGIN, PDU1_END );
-
- /* Remove trellis encoding - abort processing if we have an
- * unsuccessful decode due to excessive errors */
- if( mHalfRate.decode( mMessage, PDU1_BEGIN, PDU1_END ) )
- {
- if( mMessage.getInt( PDUMessage.BLOCKS_TO_FOLLOW ) == 1 )
- {
- mMessage.setSize( PDU2_BEGIN );
-
- PDUMessage pduMessage1 = PDUMessageFactory.getMessage(
- mMessage.copy(), DataUnitID.PDU1, mAliasList );
+ mPrimarySyncDetector.setThreshold(SYNC_MATCH_THRESHOLD);
- dispatch( pduMessage1 );
-
- mComplete = true;
- }
- else
- {
- setDUID( DataUnitID.PDU2 );
-
- mMessage.setPointer( PDU2_BEGIN );
- }
- }
- else
- {
- mComplete = true;
- }
- break;
- case PDU2:
- /* Remove interleaving */
- P25Interleave.deinterleaveData( mMessage, PDU2_BEGIN, PDU2_END );
-
- /* Remove trellis encoding - abort processing if we have an
- * unsuccessful decode due to excessive errors */
- if( mHalfRate.decode( mMessage, PDU2_BEGIN, PDU2_END ) )
- {
- if( mMessage.getInt( PDUMessage.BLOCKS_TO_FOLLOW ) == 2 )
- {
- mMessage.setSize( PDU3_BEGIN );
-
- PDUMessage pduMessage2 = PDUMessageFactory.getMessage(
- mMessage.copy(), DataUnitID.PDU2, mAliasList );
+ break;
+ case PDU1:
+ /* Remove interleaving */
+ P25Interleave.deinterleaveData(mMessage, PDU1_BEGIN, PDU1_END);
- dispatch( pduMessage2 );
-
- mComplete = true;
- }
- else
- {
- setDUID( DataUnitID.PDU3 );
-
- mMessage.setPointer( PDU3_BEGIN );
- }
- }
- else
- {
- mComplete = true;
- }
+ /* Remove trellis encoding - abort processing if we have an
+ * unsuccessful decode due to excessive errors */
+ if(mHalfRate.decode(mMessage, PDU1_BEGIN, PDU1_END))
+ {
+ if(mMessage.getInt(PDUMessage.BLOCKS_TO_FOLLOW) == 1)
+ {
+ mMessage.setSize(PDU2_BEGIN);
- /* Set sync match threshold to normal */
- mPrimarySyncDetector.setThreshold( SYNC_MATCH_THRESHOLD );
- break;
- case PDU3:
- /* Remove interleaving */
- P25Interleave.deinterleaveData( mMessage, PDU3_BEGIN, PDU3_END );
-
- /* Remove trellis encoding - abort processing if we have an
- * unsuccessful decode due to excessive errors */
- if( mHalfRate.decode( mMessage, PDU3_BEGIN, PDU3_END ) )
- {
- mMessage.setSize( PDU3_DECODED_END );
-
- PDUMessage pduMessage3 = PDUMessageFactory.getMessage(mMessage.copy(), DataUnitID.PDU3, mAliasList );
+ PDUMessage pduMessage1 = PDUMessageFactory.getMessage(
+ mMessage.copy(), DataUnitID.PDU1, mAliasList);
- dispatch( pduMessage3 );
- }
-
- mComplete = true;
- break;
- case PDUC:
- /* De-interleave the latest block*/
- P25Interleave.deinterleaveData( mMessage,
- mMessage.size() - 196, mMessage.size() );
+ dispatch(pduMessage1);
- /* Decode 3/4 rate convolutional encoding from latest block */
- if( mThreeQuarterRate.decode( mMessage,
- mMessage.size() - 196, mMessage.size() ) )
- {
- /* Resize the message and adjust the message pointer
- * to account for removing 48 + 4 parity bits */
- mMessage.setSize( mMessage.size() - 52 );
- mMessage.adjustPointer( -52 );
-
- int blocks = mMessage.getInt( PDUMessage.BLOCKS_TO_FOLLOW );
-
- int current = ( mMessage.size() - 160 ) / 144;
-
- if( current < blocks )
- {
- mMessage.setSize( mMessage.size() + 196 );
- mMessageLength = mMessage.size();
- }
- else
- {
- PDUConfirmedMessage pducm =
- new PDUConfirmedMessage( mMessage.copy(),
- mAliasList );
+ mComplete = true;
+ }
+ else
+ {
+ setDUID(DataUnitID.PDU2);
- /* Translate into correct subclass */
- dispatch( PDUConfirmedMessageFactory.getMessage( pducm ) );
-
- mComplete = true;
- }
- break;
- }
- else
- {
- PDUConfirmedMessage pducm =
- new PDUConfirmedMessage( mMessage.copy(),
- mAliasList );
+ mMessage.setPointer(PDU2_BEGIN);
+ }
+ }
+ else
+ {
+ mComplete = true;
+ }
+ break;
+ case PDU2:
+ /* Remove interleaving */
+ P25Interleave.deinterleaveData(mMessage, PDU2_BEGIN, PDU2_END);
+
+ /* Remove trellis encoding - abort processing if we have an
+ * unsuccessful decode due to excessive errors */
+ if(mHalfRate.decode(mMessage, PDU2_BEGIN, PDU2_END))
+ {
+ if(mMessage.getInt(PDUMessage.BLOCKS_TO_FOLLOW) == 2)
+ {
+ mMessage.setSize(PDU3_BEGIN);
+
+ PDUMessage pduMessage2 = PDUMessageFactory.getMessage(
+ mMessage.copy(), DataUnitID.PDU2, mAliasList);
+
+ dispatch(pduMessage2);
+
+ mComplete = true;
+ }
+ else
+ {
+ setDUID(DataUnitID.PDU3);
+
+ mMessage.setPointer(PDU3_BEGIN);
+ }
+ }
+ else
+ {
+ mComplete = true;
+ }
- /* Translate into correct subclass */
- dispatch( PDUConfirmedMessageFactory.getMessage( pducm ) );
-
- mComplete = true;
- }
-
/* Set sync match threshold to normal */
- mPrimarySyncDetector.setThreshold( SYNC_MATCH_THRESHOLD );
- break;
- case TDU:
- dispatch( new TDUMessage( mMessage.copy(), mDUID, mAliasList ) );
- mComplete = true;
+ mPrimarySyncDetector.setThreshold(SYNC_MATCH_THRESHOLD);
+ break;
+ case PDU3:
+ /* Remove interleaving */
+ P25Interleave.deinterleaveData(mMessage, PDU3_BEGIN, PDU3_END);
- /* Set sync match threshold to normal */
- mPrimarySyncDetector.setThreshold( SYNC_MATCH_THRESHOLD );
- break;
- case TDULC:
- TDULinkControlMessage tdulc = new TDULinkControlMessage(
- mMessage.copy(), mDUID, mAliasList );
+ /* Remove trellis encoding - abort processing if we have an
+ * unsuccessful decode due to excessive errors */
+ if(mHalfRate.decode(mMessage, PDU3_BEGIN, PDU3_END))
+ {
+ mMessage.setSize(PDU3_DECODED_END);
- /* Convert to an appropriate link control message */
- tdulc = TDULCMessageFactory.getMessage( tdulc );
+ PDUMessage pduMessage3 = PDUMessageFactory.getMessage(mMessage.copy(), DataUnitID.PDU3, mAliasList);
- dispatch( tdulc );
- mComplete = true;
+ dispatch(pduMessage3);
+ }
- /* Set sync match threshold to normal */
- mPrimarySyncDetector.setThreshold( SYNC_MATCH_THRESHOLD );
- break;
- case TSBK1:
- /* Remove interleaving */
- P25Interleave.deinterleaveData( mMessage, TSBK_BEGIN, TSBK_END );
-
- /* Remove trellis encoding - abort processing if we have an
- * unsuccessful decode due to excessive errors */
-
- if( mHalfRate.decode( mMessage, TSBK_BEGIN, TSBK_END ) )
- {
- mMessage = CRCP25.correctCCITT80( mMessage,
- TSBK_BEGIN, TSBK_CRC_START );
+ mComplete = true;
+ break;
+ case PDUC:
+ /* De-interleave the latest block*/
+ P25Interleave.deinterleaveData(mMessage,
+ mMessage.size() - 196, mMessage.size());
- if( mMessage.getCRC() != CRC.FAILED_CRC )
- {
- BinaryMessage tsbkBuffer1 = mMessage.copy();
- tsbkBuffer1.setSize( TSBK_DECODED_END );
-
- TSBKMessage tsbkMessage1 = TSBKMessageFactory.getMessage(
- tsbkBuffer1, DataUnitID.TSBK1, mAliasList );
+ /* Decode 3/4 rate convolutional encoding from latest block */
+ if(mThreeQuarterRate.decode(mMessage,
+ mMessage.size() - 196, mMessage.size()))
+ {
+ /* Resize the message and adjust the message pointer
+ * to account for removing 48 + 4 parity bits */
+ mMessage.setSize(mMessage.size() - 52);
+ mMessage.adjustPointer(-52);
- if( tsbkMessage1.isLastBlock() )
- {
- mComplete = true;
- }
- else
- {
- setDUID( DataUnitID.TSBK2 );
- mMessage.setPointer( TSBK_BEGIN );
- }
-
- dispatch( tsbkMessage1 );
- }
- else
- {
- mComplete = true;
- }
- }
- else
- {
- mComplete = true;
- }
+ int blocks = mMessage.getInt(PDUMessage.BLOCKS_TO_FOLLOW);
- /* Set sync match threshold to normal */
- mPrimarySyncDetector.setThreshold( SYNC_MATCH_THRESHOLD );
- break;
- case TSBK2:
- /* Remove interleaving */
- P25Interleave.deinterleaveData( mMessage, TSBK_BEGIN, TSBK_END );
+ int current = (mMessage.size() - 160) / 144;
- /* Remove trellis encoding - abort processing if we have an
- * unsuccessful decode due to excessive errors */
- if( mHalfRate.decode( mMessage, TSBK_BEGIN, TSBK_END ) )
- {
- mMessage = CRCP25.correctCCITT80( mMessage,
- TSBK_BEGIN, TSBK_CRC_START );
-
- if( mMessage.getCRC() != CRC.FAILED_CRC )
- {
- BinaryMessage tsbkBuffer2 = mMessage.copy();
- tsbkBuffer2.setSize( TSBK_DECODED_END );
-
- TSBKMessage tsbkMessage2 = TSBKMessageFactory.getMessage(
- tsbkBuffer2, DataUnitID.TSBK2, mAliasList );
+ if(current < blocks)
+ {
+ mMessage.setSize(mMessage.size() + 196);
+ mMessageLength = mMessage.size();
+ }
+ else
+ {
+ PDUConfirmedMessage pducm =
+ new PDUConfirmedMessage(mMessage.copy(),
+ mAliasList);
- if( tsbkMessage2.isLastBlock() )
- {
- mComplete = true;
- }
- else
- {
- setDUID( DataUnitID.TSBK3 );
- mMessage.setPointer( TSBK_BEGIN );
- }
-
- dispatch( tsbkMessage2 );
- }
- else
- {
- mComplete = true;
- }
- }
- else
- {
- mComplete = true;
- }
- break;
- case TSBK3:
- /* Remove interleaving */
- P25Interleave.deinterleaveData( mMessage, TSBK_BEGIN, TSBK_END );
-
- /* Remove trellis encoding - abort processing if we have an
- * unsuccessful decode due to excessive errors */
- if( mHalfRate.decode( mMessage, TSBK_BEGIN, TSBK_END ) )
- {
- mMessage = CRCP25.correctCCITT80( mMessage,
- TSBK_BEGIN, TSBK_CRC_START );
-
- if( mMessage.getCRC() != CRC.FAILED_CRC )
- {
- BinaryMessage tsbkBuffer3 = mMessage.copy();
- tsbkBuffer3.setSize( TSBK_DECODED_END );
-
- TSBKMessage tsbkMessage3 = TSBKMessageFactory.getMessage(
- tsbkBuffer3, DataUnitID.TSBK3, mAliasList );
+ /* Translate into correct subclass */
+ dispatch(PDUConfirmedMessageFactory.getMessage(pducm));
- dispatch( tsbkMessage3 );
- }
- }
+ mComplete = true;
+ }
+ break;
+ }
+ else
+ {
+ PDUConfirmedMessage pducm =
+ new PDUConfirmedMessage(mMessage.copy(),
+ mAliasList);
- mComplete = true;
- break;
- case VSELP1:
- mComplete = true;
- dispatch( new VSELP1Message( mMessage.copy(), mDUID, mAliasList ) );
- break;
- case VSELP2:
- mComplete = true;
- dispatch( new VSELP2Message( mMessage.copy(), mDUID, mAliasList ) );
- break;
- case UNKN:
- mComplete = true;
- dispatch( new P25Message( mMessage.copy(), mDUID, mAliasList ) );
- break;
- default:
- mComplete = true;
- break;
- }
+ /* Translate into correct subclass */
+ dispatch(PDUConfirmedMessageFactory.getMessage(pducm));
+
+ mComplete = true;
+ }
+
+ /* Set sync match threshold to normal */
+ mPrimarySyncDetector.setThreshold(SYNC_MATCH_THRESHOLD);
+ break;
+ case TDU:
+ dispatch(new TDUMessage(mMessage.copy(), mDUID, mAliasList));
+ mComplete = true;
+
+ /* Set sync match threshold to normal */
+ mPrimarySyncDetector.setThreshold(SYNC_MATCH_THRESHOLD);
+ break;
+ case TDULC:
+ TDULinkControlMessage tdulc = new TDULinkControlMessage(
+ mMessage.copy(), mDUID, mAliasList);
+
+ /* Convert to an appropriate link control message */
+ tdulc = TDULCMessageFactory.getMessage(tdulc);
+
+ dispatch(tdulc);
+ mComplete = true;
+
+ /* Set sync match threshold to normal */
+ mPrimarySyncDetector.setThreshold(SYNC_MATCH_THRESHOLD);
+ break;
+ case TSBK1:
+ /* Remove interleaving */
+ P25Interleave.deinterleaveData(mMessage, TSBK_BEGIN, TSBK_END);
+
+ /* Remove trellis encoding - abort processing if we have an
+ * unsuccessful decode due to excessive errors */
+
+ if(mHalfRate.decode(mMessage, TSBK_BEGIN, TSBK_END))
+ {
+ mMessage = CRCP25.correctCCITT80(mMessage,
+ TSBK_BEGIN, TSBK_CRC_START);
+
+ if(mMessage.getCRC() != CRC.FAILED_CRC)
+ {
+ BinaryMessage tsbkBuffer1 = mMessage.copy();
+ tsbkBuffer1.setSize(TSBK_DECODED_END);
+
+ TSBKMessage tsbkMessage1 = TSBKMessageFactory.getMessage(
+ tsbkBuffer1, DataUnitID.TSBK1, mAliasList);
+
+ if(tsbkMessage1.isLastBlock())
+ {
+ mComplete = true;
+ }
+ else
+ {
+ setDUID(DataUnitID.TSBK2);
+ mMessage.setPointer(TSBK_BEGIN);
+ }
+
+ dispatch(tsbkMessage1);
+ }
+ else
+ {
+ mComplete = true;
+ }
+ }
+ else
+ {
+ mComplete = true;
+ }
+
+ /* Set sync match threshold to normal */
+ mPrimarySyncDetector.setThreshold(SYNC_MATCH_THRESHOLD);
+ break;
+ case TSBK2:
+ /* Remove interleaving */
+ P25Interleave.deinterleaveData(mMessage, TSBK_BEGIN, TSBK_END);
+
+ /* Remove trellis encoding - abort processing if we have an
+ * unsuccessful decode due to excessive errors */
+ if(mHalfRate.decode(mMessage, TSBK_BEGIN, TSBK_END))
+ {
+ mMessage = CRCP25.correctCCITT80(mMessage,
+ TSBK_BEGIN, TSBK_CRC_START);
+
+ if(mMessage.getCRC() != CRC.FAILED_CRC)
+ {
+ BinaryMessage tsbkBuffer2 = mMessage.copy();
+ tsbkBuffer2.setSize(TSBK_DECODED_END);
+
+ TSBKMessage tsbkMessage2 = TSBKMessageFactory.getMessage(
+ tsbkBuffer2, DataUnitID.TSBK2, mAliasList);
+
+ if(tsbkMessage2.isLastBlock())
+ {
+ mComplete = true;
+ }
+ else
+ {
+ setDUID(DataUnitID.TSBK3);
+ mMessage.setPointer(TSBK_BEGIN);
+ }
+
+ dispatch(tsbkMessage2);
+ }
+ else
+ {
+ mComplete = true;
+ }
+ }
+ else
+ {
+ mComplete = true;
+ }
+ break;
+ case TSBK3:
+ /* Remove interleaving */
+ P25Interleave.deinterleaveData(mMessage, TSBK_BEGIN, TSBK_END);
+
+ /* Remove trellis encoding - abort processing if we have an
+ * unsuccessful decode due to excessive errors */
+ if(mHalfRate.decode(mMessage, TSBK_BEGIN, TSBK_END))
+ {
+ mMessage = CRCP25.correctCCITT80(mMessage,
+ TSBK_BEGIN, TSBK_CRC_START);
+
+ if(mMessage.getCRC() != CRC.FAILED_CRC)
+ {
+ BinaryMessage tsbkBuffer3 = mMessage.copy();
+ tsbkBuffer3.setSize(TSBK_DECODED_END);
+
+ TSBKMessage tsbkMessage3 = TSBKMessageFactory.getMessage(
+ tsbkBuffer3, DataUnitID.TSBK3, mAliasList);
+
+ dispatch(tsbkMessage3);
+ }
+ }
+
+ mComplete = true;
+ break;
+ case VSELP1:
+ mComplete = true;
+ dispatch(new VSELP1Message(mMessage.copy(), mDUID, mAliasList));
+ break;
+ case VSELP2:
+ mComplete = true;
+ dispatch(new VSELP2Message(mMessage.copy(), mDUID, mAliasList));
+ break;
+ case UNKN:
+ mComplete = true;
+ dispatch(new P25Message(mMessage.copy(), mDUID, mAliasList));
+ break;
+ default:
+ mComplete = true;
+ break;
+ }
}
-
+
public void dispose()
{
- mMessage = null;
- mHalfRate.dispose();
+ mMessage = null;
+ mHalfRate.dispose();
}
/**
@@ -663,55 +663,54 @@ public class P25MessageFramer implements Listener
{
return mComplete;
}
-
+
public boolean isActive()
{
- return mActive;
+ return mActive;
}
}
-
+
/**
- * Sync pattern detector to listen for costas loop phase lock errors and
- * apply a phase correction to the costas loop so that we don't miss any
+ * Sync pattern detector to listen for costas loop phase lock errors and
+ * apply a phase correction to the costas loop so that we don't miss any
* messages.
- *
- * When the costas loop locks with a +/- 90 degree or 180 degree phase
+ *
+ * When the costas loop locks with a +/- 90 degree or 180 degree phase
* error, the slicer will incorrectly apply the symbol pattern rotated left
* or right by the phase error. However, we can detect these rotated sync
* patterns and apply immediate phase correction so that message processing
- * can continue.
+ * can continue.
*/
- public class CostasPhaseErrorDetector extends SyncDetector
+ public class PLLPhaseInversionDetector extends SyncDetector
{
- private LSMDemodulator mDemodulator;
- private double mCorrection;
-
- public CostasPhaseErrorDetector( FrameSync frameSync, LSMDemodulator demodulator, double correction )
- {
- super( frameSync.getSync() );
+ private IPhaseLockedLoop mPhaseLockedLoop;
+ private double mCorrection;
- mDemodulator = demodulator;
-
- mCorrection = correction;
+ public PLLPhaseInversionDetector(FrameSync frameSync, IPhaseLockedLoop phaseLockedLoop, double correction)
+ {
+ super(frameSync.getSync());
- setListener( new ISyncDetectListener()
- {
- @Override
- public void syncDetected()
- {
- mDemodulator.correctPhaseError( mCorrection );
+ mPhaseLockedLoop = phaseLockedLoop;
+ mCorrection = correction;
- /* Since we detected a sync pattern, start a message assembler */
- for( P25MessageAssembler assembler: mAssemblers )
- {
- if( !assembler.isActive() )
- {
- assembler.setActive( true );
- break;
- }
- }
- }
- } );
- }
+ setListener(new ISyncDetectListener()
+ {
+ @Override
+ public void syncDetected()
+ {
+ mPhaseLockedLoop.correctInversion(mCorrection);
+
+ /* Since we detected a sync pattern, start a message assembler */
+ for(P25MessageAssembler assembler : mAssemblers)
+ {
+ if(!assembler.isActive())
+ {
+ assembler.setActive(true);
+ break;
+ }
+ }
+ }
+ });
+ }
}
}
diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder.java
index c4d714e2..6923bc37 100644
--- a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder.java
+++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder.java
@@ -1,222 +1,200 @@
/*******************************************************************************
- * sdrtrunk
- * Copyright (C) 2014-2017 Dennis Sheirer
+ * 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 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.
+ * 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
+ * You should have received a copy of the GNU General Public License along with this program.
+ * If not, see
*
******************************************************************************/
package io.github.dsheirer.module.decode.p25;
import io.github.dsheirer.alias.AliasList;
-import io.github.dsheirer.instrument.tap.Tap;
-import io.github.dsheirer.instrument.tap.TapGroup;
-import io.github.dsheirer.instrument.tap.stream.DibitTap;
-import io.github.dsheirer.instrument.tap.stream.FloatTap;
+import io.github.dsheirer.dsp.filter.FilterFactory;
+import io.github.dsheirer.dsp.filter.design.FilterDesignException;
+import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
+import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter;
+import io.github.dsheirer.dsp.psk.DQPSKDemodulator;
+import io.github.dsheirer.dsp.psk.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.real.IFilteredRealBufferListener;
-import io.github.dsheirer.sample.real.RealBuffer;
-import io.github.dsheirer.source.SourceEvent;
+import io.github.dsheirer.sample.complex.ComplexBuffer;
+import io.github.dsheirer.sample.complex.IComplexBufferListener;
+import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.util.ArrayList;
-import java.util.List;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ScheduledExecutorService;
-public class P25_C4FMDecoder extends P25Decoder implements IFilteredRealBufferListener
+public class P25_C4FMDecoder extends P25Decoder implements IComplexBufferListener, Listener
{
- private final static Logger mLog = LoggerFactory.getLogger( P25_C4FMDecoder.class );
-
- /* Instrumentation Taps */
- 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 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 mBasebandFilters = new HashMap<>();
+ private ComplexFIRFilter mBasebandFilter;
- private List 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 getFilteredRealBufferListener()
- {
- return mSymbolFilter;
- }
-
- /**
- * Instrumentation taps for monitoring internal processing
- */
- @Override
- public List getTapGroups()
+ public P25_C4FMDecoder(AliasList aliasList)
{
- if( mAvailableTaps == null )
- {
- mAvailableTaps = new ArrayList<>();
-
- TapGroup group = new TapGroup( "P25 C4FM Decoder" );
-
- group.add( new FloatTap( INSTRUMENT_C4FM_SYMBOL_FILTER_OUTPUT, 0, 0.1f ) );
- group.add( new DibitTap( INSTRUMENT_C4FM_SLICER_OUTPUT, 0, 0.1f ) );
-
- mAvailableTaps.add( group );
-
- if( mSymbolFilter != null )
- {
- mAvailableTaps.addAll( mSymbolFilter.getTapGroups() );
- }
- }
-
- return mAvailableTaps;
+ super(aliasList);
+ setSampleRate(48000.0);
}
- /**
- * Adds an instrumentation tap to monitor internal processing.
- */
- @Override
- public void registerTap( Tap tap )
+ public void setSampleRate(double sampleRate)
{
- if( mSymbolFilter != null )
- {
- mSymbolFilter.registerTap( tap );
- }
-
- switch( tap.getName() )
- {
- case INSTRUMENT_C4FM_SYMBOL_FILTER_OUTPUT:
- FloatTap symbolTap = (FloatTap)tap;
- mSymbolFilter.setListener( symbolTap );
- symbolTap.setListener( mC4FMSlicer );
- break;
- case INSTRUMENT_C4FM_SLICER_OUTPUT:
- DibitTap slicerTap = (DibitTap)tap;
-
- if( mC4FMSlicer != null )
- {
- mC4FMSlicer.addListener( slicerTap );
- }
- break;
- default:
- break;
- }
+ if(sampleRate <= SYMBOL_RATE * 2)
+ {
+ throw new IllegalArgumentException("Sample rate must be at least twice the symbol rate [4800]");
+ }
+
+ mSampleRate = sampleRate;
+ mBasebandFilter = new ComplexFIRFilter(getBasebandFilter(), 1.0f);
+
+ mCostasLoop = new CostasLoop(mSampleRate, SYMBOL_RATE);
+
+ mInterpolatingSampleBuffer = new InterpolatingSampleBufferInstrumented((float)(sampleRate / SYMBOL_RATE));
+ mQPSKDemodulator = new DQPSKDemodulator(mCostasLoop, mInterpolatingSampleBuffer);
+
+ //Message framer can trigger a symbol-inversion correction to the PLL when detected
+ mMessageFramer = new P25MessageFramer(getAliasList(), mCostasLoop);
+ mMessageFramer.setListener(getMessageProcessor());
+ mQPSKDemodulator.setDibitListener(mMessageFramer);
}
- /**
- * Removes the instrumentation tap.
- */
- @Override
- public void unregisterTap( Tap tap )
+ @Override
+ public void receive(ComplexBuffer complexBuffer)
{
- if( mSymbolFilter != null )
- {
- mSymbolFilter.unregisterTap( tap );
- }
-
- switch( tap.getName() )
- {
- case INSTRUMENT_C4FM_SYMBOL_FILTER_OUTPUT:
- mSymbolFilter.setListener( mC4FMSlicer );
- break;
- case INSTRUMENT_C4FM_SLICER_OUTPUT:
- DibitTap slicerTap = (DibitTap)tap;
-
- if( mC4FMSlicer != null )
- {
- mC4FMSlicer.removeListener( slicerTap );
- }
- break;
- default:
- throw new IllegalArgumentException( "Unrecognized tap: " +
- tap.getName() );
- }
+ ComplexBuffer filtered = filter(complexBuffer);
+ mQPSKDemodulator.receive(filtered);
}
- @Override
- public void setSourceEventListener(Listener listener )
- {
- if( mSymbolFilter != null )
- {
- mSymbolFilter.setFrequencyChangeListener( listener );
- }
- }
+ protected ComplexBuffer filter(ComplexBuffer complexBuffer)
+ {
+ return mBasebandFilter.filter(complexBuffer);
+ }
- @Override
- public void removeSourceEventListener()
- {
- if( mSymbolFilter != null )
- {
- mSymbolFilter.setFrequencyChangeListener( null );
- }
- }
+ private double getSampleRate()
+ {
+ return mSampleRate;
+ }
- @Override
- public Listener getSourceEventListener()
- {
- if( mSymbolFilter != null )
- {
- return mSymbolFilter.getSourceEventListener();
- }
-
- return null;
- }
+ private float[] getBasebandFilter()
+ {
+ float[] filter = mBasebandFilters.get(getSampleRate());
+
+ 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();
+
+ try
+ {
+ filter = FilterFactory.getTaps(specification);
+
+ }
+ catch(FilterDesignException fde)
+ {
+ mLog.error("Couldn't design low pass baseband filter for sample rate: " + getSampleRate());
+ }
+ if(filter != null)
+ {
+ mBasebandFilters.put(getSampleRate(), filter);
+ }
+ else
+ {
+ throw new IllegalStateException("Couldn't design a C4FM symbol filter for sample rate: " + mSampleRate);
+ }
+ }
+
+ return filter;
+ }
+
+ public void dispose()
+ {
+ super.dispose();
+
+ mBasebandFilter.dispose();
+ mBasebandFilter = null;
+
+ mMessageFramer.dispose();
+ mMessageFramer = null;
+ }
+
+ @Override
+ public void setFrequencyChangeListener(Listener listener)
+ {
+ }
+
+ @Override
+ public void removeFrequencyChangeListener()
+ {
+ }
+
+ @Override
+ public Listener getFrequencyChangeListener()
+ {
+ return new Listener()
+ {
+ @Override
+ public void receive(FrequencyChangeEvent frequencyChangeEvent)
+ {
+ switch(frequencyChangeEvent.getEvent())
+ {
+ case NOTIFICATION_FREQUENCY_CHANGE:
+ case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
+ case NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE:
+ case NOTIFICATION_SAMPLE_RATE_CHANGE:
+ mCostasLoop.reset();
+ break;
+ }
+ }
+ };
+ }
+
+ @Override
+ public Listener getComplexBufferListener()
+ {
+ return P25_C4FMDecoder.this;
+ }
+
+ public Modulation getModulation()
+ {
+ return Modulation.C4FM;
+ }
+
+ @Override
+ public void reset()
+ {
+ mCostasLoop.reset();
+ }
+
+ @Override
+ public void start(ScheduledExecutorService executor)
+ {
+ }
+
+ @Override
+ public void stop()
+ {
+ }
}
diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoderInstrumented.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoderInstrumented.java
new file mode 100644
index 00000000..903396a7
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoderInstrumented.java
@@ -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
+ *
+ ******************************************************************************/
+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 mPLLPhaseErrorListener;
+ private Listener mPLLFrequencyListener;
+ private Listener mSamplesPerSymbolListener;
+ private Listener mSymbolListener;
+ private Listener mFilteredSymbolListener;
+ private Listener 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 listener)
+ {
+ mSymbolListener = listener;
+ ((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setSymbolListener(listener);
+ }
+
+ public void setPLLPhaseErrorListener(Listener listener)
+ {
+ mPLLPhaseErrorListener = listener;
+ ((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setPLLErrorListener(listener);
+ }
+
+ public void setPLLFrequencyListener(Listener listener)
+ {
+ mPLLFrequencyListener = listener;
+ ((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setPLLFrequencyListener(listener);
+ }
+
+ public void setFilteredBufferListener(Listener listener)
+ {
+ mFilteredSymbolListener = listener;
+ }
+
+ public void setSymbolDecisionDataListener(Listener listener)
+ {
+ mSymbolDecisionDataListener = listener;
+ ((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setSymbolDecisionDataListener(listener);
+ }
+
+ public void setSamplesPerSymbolListener(Listener listener)
+ {
+ mSamplesPerSymbolListener = listener;
+ ((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setSamplesPerSymbolListener(listener);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25_LSMDecoder.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25_LSMDecoder.java
index f5bf2108..20460d76 100644
--- a/src/main/java/io/github/dsheirer/module/decode/p25/P25_LSMDecoder.java
+++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25_LSMDecoder.java
@@ -21,13 +21,8 @@ import io.github.dsheirer.dsp.filter.Window.WindowType;
import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter_CB_CB;
import io.github.dsheirer.dsp.gain.ComplexFeedForwardGainControl;
import io.github.dsheirer.dsp.psk.LSMDemodulator;
-import io.github.dsheirer.dsp.psk.QPSKPolarSlicer;
-import io.github.dsheirer.instrument.tap.Tap;
+import io.github.dsheirer.dsp.psk.QPSKSymbolDecoder;
import io.github.dsheirer.instrument.tap.TapGroup;
-import io.github.dsheirer.instrument.tap.stream.ComplexSampleTap;
-import io.github.dsheirer.instrument.tap.stream.ComplexTap;
-import io.github.dsheirer.instrument.tap.stream.DibitTap;
-import io.github.dsheirer.instrument.tap.stream.QPSKTap;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.ComplexBuffer;
import io.github.dsheirer.sample.complex.ComplexBufferToStreamConverter;
@@ -37,7 +32,6 @@ import io.github.dsheirer.source.SourceEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.util.ArrayList;
import java.util.List;
public class P25_LSMDecoder extends P25Decoder implements IReusableComplexBufferListener
diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/message/tsbk/osp/control/SyncBroadcast.java b/src/main/java/io/github/dsheirer/module/decode/p25/message/tsbk/osp/control/SyncBroadcast.java
index 2f8f1988..e7ed4a93 100644
--- a/src/main/java/io/github/dsheirer/module/decode/p25/message/tsbk/osp/control/SyncBroadcast.java
+++ b/src/main/java/io/github/dsheirer/module/decode/p25/message/tsbk/osp/control/SyncBroadcast.java
@@ -78,7 +78,7 @@ public class SyncBroadcast extends TSBKMessage
if( isMicroslotsLockedToMinuteRollover() )
{
- sb.append( " MICROSLOT-MINUTE ROLLOVER:LOCKED" );
+ sb.append( " MICROSLOT-MINUTE ROLLOVER:SLOW" );
}
else
{
diff --git a/src/main/java/io/github/dsheirer/module/decode/passport/PassportDecoder.java b/src/main/java/io/github/dsheirer/module/decode/passport/PassportDecoder.java
index 5311eb1d..42deaed1 100644
--- a/src/main/java/io/github/dsheirer/module/decode/passport/PassportDecoder.java
+++ b/src/main/java/io/github/dsheirer/module/decode/passport/PassportDecoder.java
@@ -62,7 +62,7 @@ public class PassportDecoder extends Decoder
mPassportMessageProcessor = new PassportMessageProcessor( aliasList );
mPassportMessageFramer.addMessageListener( mPassportMessageProcessor );
- mPassportMessageProcessor.setMessageListener( this );
+ mPassportMessageProcessor.setMessageListener( getMessageListener() );
}
@Override
diff --git a/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200Decoder.java b/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200Decoder.java
index 18174720..f268e52e 100644
--- a/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200Decoder.java
+++ b/src/main/java/io/github/dsheirer/module/decode/tait/Tait1200Decoder.java
@@ -103,8 +103,8 @@ public class Tait1200Decoder extends Decoder implements IFilteredRealBufferListe
mMessageFramerGPS.addMessageListener( mMessageAProcessor );
mMessageFramerANI.addMessageListener( mMessageBProcessor );
- mMessageAProcessor.setMessageListener( this );
- mMessageBProcessor.setMessageListener( this );
+ mMessageAProcessor.setMessageListener(getMessageListener());
+ mMessageBProcessor.setMessageListener(getMessageListener());
}
public void dispose()
diff --git a/src/main/java/io/github/dsheirer/record/RecorderManager.java b/src/main/java/io/github/dsheirer/record/RecorderManager.java
index f7c77f41..60dacef2 100644
--- a/src/main/java/io/github/dsheirer/record/RecorderManager.java
+++ b/src/main/java/io/github/dsheirer/record/RecorderManager.java
@@ -42,6 +42,7 @@ public class RecorderManager implements Listener
private static final Logger mLog = LoggerFactory.getLogger(RecorderManager.class);
public static final int AUDIO_SAMPLE_RATE = 8000;
+ public static final int BASEBAND_SAMPLE_RATE = 48000;
public static final long IDLE_RECORDER_REMOVAL_THRESHOLD = 6000; //6 seconds
private Map mRecorders = new HashMap<>();
@@ -92,6 +93,7 @@ public class RecorderManager implements Listener
/**
* Primary ingest point for audio packets from all decoding channels
+ *
* @param audioPacket to process
*/
@Override
@@ -114,7 +116,7 @@ public class RecorderManager implements Listener
while(!audioPackets.isEmpty())
{
- for(AudioPacket audioPacket: audioPackets)
+ for(AudioPacket audioPacket : audioPackets)
{
String identifier = audioPacket.getMetadata().getUniqueIdentifier();
@@ -229,7 +231,7 @@ public class RecorderManager implements Listener
sb.append(SystemProperties.getInstance().getApplicationFolder("recordings"));
sb.append(File.separator).append(channelName).append("_baseband");
- return new ComplexBufferWaveRecorder(AUDIO_SAMPLE_RATE, sb.toString());
+ return new ComplexBufferWaveRecorder(BASEBAND_SAMPLE_RATE, sb.toString());
}
/**
diff --git a/src/main/java/io/github/dsheirer/record/wave/AudioPacketWaveRecorder.java b/src/main/java/io/github/dsheirer/record/wave/AudioPacketWaveRecorder.java
index aa0fdad8..045a6085 100644
--- a/src/main/java/io/github/dsheirer/record/wave/AudioPacketWaveRecorder.java
+++ b/src/main/java/io/github/dsheirer/record/wave/AudioPacketWaveRecorder.java
@@ -104,7 +104,7 @@ public class AudioPacketWaveRecorder extends Module implements Listener
******************************************************************************/
@@ -27,407 +27,424 @@ import java.io.Serializable;
*/
public class Complex implements Serializable
{
- private final static Logger mLog =
- LoggerFactory.getLogger( Complex.class );
+ private final static Logger mLog = LoggerFactory.getLogger(Complex.class);
- private static final long serialVersionUID = 1L;
+ private static final long serialVersionUID = 1L;
private float mLeft;
- private float mRight;
-
- public Complex( float left, float right )
- {
- mLeft = left;
- mRight = right;
- }
-
- public Complex()
- {
- this( 0.0f, 0.0f );
- }
-
- public void setInphase( float inphase )
- {
- mLeft = inphase;
- }
-
- public void setQuadrature( float quadrature )
- {
- mRight = quadrature;
- }
-
- public void setValues( float inphase, float quadrature )
- {
- mLeft = inphase;
- mRight = quadrature;
- }
+ private float mRight;
- public Complex copy()
- {
- return new Complex( mLeft, mRight );
- }
-
- public String toString()
- {
- StringBuilder sb = new StringBuilder();
-
- sb.append( "I:" );
- sb.append( mLeft );
- sb.append( " Q:" );
- sb.append( mRight );
-
- return sb.toString();
- }
-
- /**
- * Returns a new sample representing the conjugate of this one
- */
- public Complex conjugate()
- {
- return new Complex( mLeft, -mRight );
- }
+ public Complex(float left, float right)
+ {
+ mLeft = left;
+ mRight = right;
+ }
- /**
- * Multiplies this sample by the scalor value
- */
- public void multiply( float scalor )
- {
- mLeft *= scalor;
- mRight *= scalor;
- }
-
- public static Complex multiply( Complex sample, float scalor )
- {
- return new Complex( sample.left() * scalor,
- sample.right() * scalor );
- }
-
- /**
- * Calculates the inphase component of multiplying two complex numbers, A and B.
- */
- public static float multiplyInphase( float inphaseA, float quadratureA, float inphaseB, float quadratureB )
- {
- return ( inphaseA * inphaseB ) - ( quadratureA * quadratureB );
- }
-
- /**
- * Calculates the quadrature component of multiplying two complex numbers, A and B.
- */
- public static float multiplyQuadrature( float inphaseA, float quadratureA, float inphaseB, float quadratureB )
- {
- return ( quadratureA * inphaseB ) + ( inphaseA * quadratureB );
- }
-
- /**
- * Multiplies this sample by the multiplier sample
- */
- public void multiply( Complex multiplier )
- {
- float inphase = multiplyInphase( inphase(), quadrature(),
- multiplier.inphase(), multiplier.quadrature() );
- float quadrature = multiplyQuadrature( inphase(), quadrature(),
- multiplier.inphase(), multiplier.quadrature() );
-
- mLeft = inphase;
- mRight = quadrature;
- }
-
- /**
- * Multiplies both samples returning a new sample with the results
- */
- public static Complex multiply( Complex sample1, Complex sample2 )
- {
- float inphase = multiplyInphase( sample1.inphase(), sample1.quadrature(),
- sample2.inphase(), sample2.quadrature() );
- float quadrature = multiplyQuadrature( sample1.inphase(), sample1.quadrature(),
- sample2.inphase(), sample2.quadrature() );
+ public Complex()
+ {
+ this(0.0f, 0.0f);
+ }
- return new Complex( inphase, quadrature );
- }
+ public void setInphase(float inphase)
+ {
+ mLeft = inphase;
+ }
- public static Complex multiply( Complex sample,
- Float inphase, Float quadrature )
- {
- float i = multiplyInphase( sample.inphase(), sample.quadrature(),
- inphase, quadrature );
- float q = multiplyQuadrature( sample.inphase(), sample.quadrature(),
- inphase, quadrature );
-
- return new Complex( i, q );
- }
-
- public static Complex multiply( Float inphase, Float quadrature, Complex sample )
- {
- float i = multiplyInphase( inphase, quadrature, sample.inphase(), sample.quadrature() );
- float q = multiplyQuadrature( inphase, quadrature, sample.inphase(), sample.quadrature() );
-
- return new Complex( i, q );
- }
+ public void setQuadrature(float quadrature)
+ {
+ mRight = quadrature;
+ }
- public static Complex multiply( float inphaseA, float quadratureA, float inphaseB, float quadratureB )
- {
- float i = multiplyInphase( inphaseA, quadratureA, inphaseB, quadratureB );
- float q = multiplyQuadrature( inphaseA, quadratureA, inphaseB, quadratureB );
-
- return new Complex( i, q );
- }
-
- /**
- * Adds the adder sample value to this sample
- */
- public void add( Complex adder )
- {
- mLeft += adder.left();
- mRight += adder.right();
- }
+ public void setValues(float inphase, float quadrature)
+ {
+ mLeft = inphase;
+ mRight = quadrature;
+ }
- /**
- * Adds the two complex samples returning a new complex sample with the result
- */
- public static Complex add( Complex first, Complex second )
- {
- return new Complex( first.left() + second.left(),
- first.right() + second.right() );
- }
+ public void setValues(Complex sampleToCopy)
+ {
+ mLeft = sampleToCopy.left();
+ mRight = sampleToCopy.right();
+ }
- /**
- * Adds the two complex samples returning a new complex sample with the result
- */
- public static Complex subtract( Complex first, Complex second )
- {
- return new Complex( first.left() - second.left(),
- first.right() - second.right() );
- }
+ public Complex copy()
+ {
+ return new Complex(mLeft, mRight);
+ }
- /**
- * Magnitude of this sample
- */
- public float magnitude()
- {
- return (float)Math.sqrt( magnitudeSquared() );
- }
-
- public static float magnitude( float inphase, float quadrature )
- {
- return (float)Math.sqrt( ( inphase * inphase ) + ( quadrature * quadrature ) );
- }
+ public String toString()
+ {
+ StringBuilder sb = new StringBuilder();
- /**
- * Magnitude squared of this sample
- */
- public float magnitudeSquared()
- {
- return norm();
- }
+ sb.append("I:");
+ sb.append(mLeft);
+ sb.append(" Q:");
+ sb.append(mRight);
- /**
- * Norm of this sample = ( i * i ) + ( q * q )
- */
- public float norm()
- {
- return (float)( ( inphase() * inphase() ) +
- ( quadrature() * quadrature() ) );
- }
-
- /**
- * Returns the vector length to 1 (unit circle)
- */
- public void normalize()
- {
- float magnitude = magnitude();
-
- if( magnitude != 0 )
- {
- multiply( (float)( 1.0f / magnitude() ) );
- }
- }
+ return sb.toString();
+ }
- /**
- * Returns the vector length to 1 (unit circle),
- * avoiding square root multiplication
- */
- public void fastNormalize()
- {
- multiply( (float)( 1.95f - magnitudeSquared() ) );
- }
-
- public float left()
- {
- return mLeft;
- }
-
- public float right()
- {
- return mRight;
- }
+ /**
+ * Returns a new sample representing the conjugate of this one
+ */
+ public Complex conjugate()
+ {
+ return new Complex(mLeft, -mRight);
+ }
- public float inphase()
- {
- return mLeft;
- }
-
- /**
- * Absolute value of in-phase component
- */
- public float inPhaseAbsolute()
- {
- return Math.abs( mLeft );
- }
+ /**
+ * Multiplies this sample by the scalor value
+ */
+ public void multiply(float scalor)
+ {
+ mLeft *= scalor;
+ mRight *= scalor;
+ }
- public float quadrature()
- {
- return mRight;
- }
-
- /**
- * Absolute value of quadrature component
- */
- public float quadratureAbsolute()
- {
- return Math.abs( mRight );
- }
+ public static Complex multiply(Complex sample, float scalor)
+ {
+ return new Complex(sample.left() * scalor,
+ sample.right() * scalor);
+ }
- public float x()
- {
- return mLeft;
- }
-
- public float y()
- {
- return mRight;
- }
-
- public float real()
- {
- return mLeft;
- }
+ /**
+ * Calculates the inphase component of multiplying two complex numbers, A and B.
+ */
+ public static float multiplyInphase(float inphaseA, float quadratureA, float inphaseB, float quadratureB)
+ {
+ return (inphaseA * inphaseB) - (quadratureA * quadratureB);
+ }
- /**
- * Absolute value of real component
- */
- public float realAbsolute()
- {
- return Math.abs( mLeft );
- }
+ /**
+ * Calculates the quadrature component of multiplying two complex numbers, A and B.
+ */
+ public static float multiplyQuadrature(float inphaseA, float quadratureA, float inphaseB, float quadratureB)
+ {
+ return (quadratureA * inphaseB) + (inphaseA * quadratureB);
+ }
- public float imaginary()
- {
- return mRight;
- }
+ /**
+ * Multiplies this sample by the multiplier sample
+ */
+ public void multiply(Complex multiplier)
+ {
+ float inphase = multiplyInphase(inphase(), quadrature(),
+ multiplier.inphase(), multiplier.quadrature());
+ float quadrature = multiplyQuadrature(inphase(), quadrature(),
+ multiplier.inphase(), multiplier.quadrature());
- /**
- * Absolute value of imaginary component
- */
- public float imaginaryAbsolute()
- {
- return Math.abs( mRight );
- }
+ mLeft = inphase;
+ mRight = quadrature;
+ }
- /**
- * Returns the greater absolute value between left and right values
- */
- public float maximumAbsolute()
- {
- if( Math.abs( mLeft ) > Math.abs( mRight ) )
- {
- return Math.abs( mLeft );
- }
- else
- {
- return Math.abs( mRight );
- }
- }
+ /**
+ * Multiplies both samples returning a new sample with the results
+ */
+ public static Complex multiply(Complex sample1, Complex sample2)
+ {
+ float inphase = multiplyInphase(sample1.inphase(), sample1.quadrature(),
+ sample2.inphase(), sample2.quadrature());
+ float quadrature = multiplyQuadrature(sample1.inphase(), sample1.quadrature(),
+ sample2.inphase(), sample2.quadrature());
- /**
- * Creates a new complex sample representing the angle with unit circle
- * magnitude
- * @param angle in radians
- * @return
- */
- public static Complex fromAngle( float angle )
- {
- return new Complex( (float)Math.cos( angle ),
- (float)Math.sin( angle ) );
- }
+ return new Complex(inphase, quadrature);
+ }
- /**
- * Angle of this sample in radians
- */
- public float angle()
- {
- return (float)Math.atan2( y(), x() );
- }
+ public static Complex multiply(Complex sample,
+ Float inphase, Float quadrature)
+ {
+ float i = multiplyInphase(sample.inphase(), sample.quadrature(),
+ inphase, quadrature);
+ float q = multiplyQuadrature(sample.inphase(), sample.quadrature(),
+ inphase, quadrature);
- /**
- * Constrains the i and q quantities to +/- value.
- *
- * @param value - maximum absolute value
- */
- public void clip( float value )
- {
- if( mLeft > value )
- {
- mLeft = value;
- }
- else if( mLeft < -value )
- {
- mLeft = -value;
- }
-
- if( mRight > value )
- {
- mRight = value;
- }
- else if( mRight < -value )
- {
- mRight = -value;
- }
- }
+ return new Complex(i, q);
+ }
- /**
- * Angle of this sample in degrees
- */
- public float polarAngle()
- {
- return (float)Math.toDegrees( angle() );
- }
+ 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());
- /**
- * Provides an approximate magnitude value for this sample.
- */
- public float envelope()
- {
- return envelope( mLeft, mRight );
- }
-
- public static float envelope( float inphase, float quadrature )
- {
- float inphaseAbsolute = Math.abs( inphase );
- float quadratureAbsolute = Math.abs( quadrature );
-
- if( inphaseAbsolute > quadratureAbsolute )
- {
- return inphaseAbsolute + ( 0.4f * quadratureAbsolute );
- }
- else
- {
- return quadratureAbsolute + ( 0.4f * inphaseAbsolute );
- }
- }
-
- public static void main( String[] args )
- {
- double angle = Math.PI; //180 degrees
-
- Complex s1 = new Complex( (float)Math.sin( angle ), (float)Math.cos( angle ) );
+ return new Complex(i, q);
+ }
- Complex tap = new Complex( 1.0f, -0.1f );
+ public static Complex multiply(float inphaseA, float quadratureA, float inphaseB, float quadratureB)
+ {
+ float i = multiplyInphase(inphaseA, quadratureA, inphaseB, quadratureB);
+ float q = multiplyQuadrature(inphaseA, quadratureA, inphaseB, quadratureB);
- Complex convolved = Complex.multiply( s1, tap );
-
- mLog.debug( "s: " + s1.toString() +
- " t: " + tap.toString() +
- " convolved: " + convolved.toString() );
-
- }
+ 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
+ */
+ public static Complex add(Complex first, Complex second)
+ {
+ return new Complex(first.left() + second.left(),
+ first.right() + second.right());
+ }
+
+ /**
+ * Adds the two complex samples returning a new complex sample with the result
+ */
+ public static Complex subtract(Complex first, Complex second)
+ {
+ return new Complex(first.left() - second.left(),
+ first.right() - second.right());
+ }
+
+ /**
+ * Magnitude of this sample
+ */
+ public float magnitude()
+ {
+ return (float)Math.sqrt(magnitudeSquared());
+ }
+
+ public static float magnitude(float inphase, float quadrature)
+ {
+ return (float)Math.sqrt((inphase * inphase) + (quadrature * quadrature));
+ }
+
+ /**
+ * Magnitude squared of this sample
+ */
+ public float magnitudeSquared()
+ {
+ return norm();
+ }
+
+ /**
+ * Norm of this sample = ( i * i ) + ( q * q )
+ */
+ public float norm()
+ {
+ return (float)((inphase() * inphase()) +
+ (quadrature() * quadrature()));
+ }
+
+ /**
+ * Returns the vector length to 1 (unit circle)
+ */
+ public void normalize()
+ {
+ float magnitude = magnitude();
+
+ if(magnitude != 0)
+ {
+ multiply((float)(1.0f / magnitude()));
+ }
+ }
+
+ /**
+ * Returns the vector length to 1 (unit circle),
+ * avoiding square root multiplication
+ */
+ public void fastNormalize()
+ {
+ multiply((float)(1.95f - magnitudeSquared()));
+ }
+
+ public float left()
+ {
+ return mLeft;
+ }
+
+ public float right()
+ {
+ return mRight;
+ }
+
+ public float inphase()
+ {
+ return mLeft;
+ }
+
+ /**
+ * Absolute value of in-phase component
+ */
+ public float inPhaseAbsolute()
+ {
+ return Math.abs(mLeft);
+ }
+
+ public float quadrature()
+ {
+ return mRight;
+ }
+
+ /**
+ * Absolute value of quadrature component
+ */
+ public float quadratureAbsolute()
+ {
+ return Math.abs(mRight);
+ }
+
+ public float x()
+ {
+ return mLeft;
+ }
+
+ public float y()
+ {
+ return mRight;
+ }
+
+ public float real()
+ {
+ return mLeft;
+ }
+
+ /**
+ * Absolute value of real component
+ */
+ public float realAbsolute()
+ {
+ return Math.abs(mLeft);
+ }
+
+ public float imaginary()
+ {
+ return mRight;
+ }
+
+ /**
+ * Absolute value of imaginary component
+ */
+ public float imaginaryAbsolute()
+ {
+ return Math.abs(mRight);
+ }
+
+ /**
+ * Returns the greater absolute value between left and right values
+ */
+ public float maximumAbsolute()
+ {
+ if(Math.abs(mLeft) > Math.abs(mRight))
+ {
+ return Math.abs(mLeft);
+ }
+ else
+ {
+ return Math.abs(mRight);
+ }
+ }
+
+ /**
+ * Creates a new complex sample representing the angle with unit circle
+ * magnitude
+ *
+ * @param angle in radians
+ * @return
+ */
+ public static Complex fromAngle(double angle)
+ {
+ return new Complex((float)Math.cos(angle), (float)Math.sin(angle));
+ }
+
+ /**
+ * Sets this complex instance with the specified angle in radians.
+ * @param angle in radians
+ */
+ public void setAngle(double angle)
+ {
+ setInphase((float)Math.cos(angle));
+ setQuadrature((float)Math.sin(angle));
+ }
+
+ /**
+ * Angle of this sample in radians
+ */
+ public float angle()
+ {
+ return (float)Math.atan2(y(), x());
+ }
+
+ public float angleDegrees()
+ {
+ return (float)Math.toDegrees(angle());
+ }
+
+ /**
+ * Constrains the i and q quantities to +/- value.
+ *
+ * @param value - maximum absolute value
+ */
+ public void clip(float value)
+ {
+ if(mLeft > value)
+ {
+ mLeft = value;
+ }
+ else if(mLeft < -value)
+ {
+ mLeft = -value;
+ }
+
+ if(mRight > value)
+ {
+ mRight = value;
+ }
+ else if(mRight < -value)
+ {
+ mRight = -value;
+ }
+ }
+
+ /**
+ * Angle of this sample in degrees
+ */
+ public float polarAngle()
+ {
+ return (float)Math.toDegrees(angle());
+ }
+
+ /**
+ * Provides an approximate magnitude value for this sample.
+ */
+ public float envelope()
+ {
+ return envelope(mLeft, mRight);
+ }
+
+ public static float envelope(float inphase, float quadrature)
+ {
+ float inphaseAbsolute = Math.abs(inphase);
+ float quadratureAbsolute = Math.abs(quadrature);
+
+ if(inphaseAbsolute > quadratureAbsolute)
+ {
+ return inphaseAbsolute + (0.4f * quadratureAbsolute);
+ }
+ else
+ {
+ return quadratureAbsolute + (0.4f * inphaseAbsolute);
+ }
+ }
+
+ public static void main(String[] args)
+ {
+ 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());
+
+ Complex symbol = new Complex(0.7665009f, -0.6422432f);
+ mLog.debug("SYM: " + symbol.toString());
+
+ symbol.multiply(ROTATE_FROM_MINUS_45);
+ mLog.debug("DER: " + symbol.toString());
+ }
}
diff --git a/src/main/java/io/github/dsheirer/source/IControllableFileSource.java b/src/main/java/io/github/dsheirer/source/IControllableFileSource.java
index 13270141..59868d66 100644
--- a/src/main/java/io/github/dsheirer/source/IControllableFileSource.java
+++ b/src/main/java/io/github/dsheirer/source/IControllableFileSource.java
@@ -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
+ *
+ ******************************************************************************/
package io.github.dsheirer.source;
import javax.sound.sampled.UnsupportedAudioFileException;
@@ -10,22 +25,24 @@ import java.io.IOException;
*/
public interface IControllableFileSource
{
- /**
- * Opens the file source
- */
- public void open() throws IOException, UnsupportedAudioFileException;
-
- public void close() throws IOException;
-
- public File getFile();
-
- public void next( int frames ) throws IOException;
-
- public void next( int frames, boolean broadcast ) throws IOException;
-
- public long getFrameCount() throws IOException;
-
- public void setListener( IFrameLocationListener listener );
-
- public void removeListener( IFrameLocationListener listener );
+ /**
+ * Opens the file source
+ */
+ public void open() throws IOException, UnsupportedAudioFileException;
+
+ public void close() throws IOException;
+
+ public File getFile();
+
+ public void next(int frames) throws IOException;
+
+ public void next(int frames, boolean broadcast) throws IOException;
+
+ public long getFrameCount() throws IOException;
+
+ public int getSampleRate();
+
+ public void setListener(IFrameLocationListener listener);
+
+ public void removeListener(IFrameLocationListener listener);
}
diff --git a/src/main/java/io/github/dsheirer/source/SourceConfigurationEditor.java b/src/main/java/io/github/dsheirer/source/SourceConfigurationEditor.java
index 4f404e2b..8a903f3e 100644
--- a/src/main/java/io/github/dsheirer/source/SourceConfigurationEditor.java
+++ b/src/main/java/io/github/dsheirer/source/SourceConfigurationEditor.java
@@ -25,7 +25,8 @@ import io.github.dsheirer.source.mixer.MixerSourceEditor;
import io.github.dsheirer.source.tuner.TunerSourceEditor;
import net.miginfocom.swing.MigLayout;
-import javax.swing.*;
+import javax.swing.DefaultComboBoxModel;
+import javax.swing.JComboBox;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
@@ -43,7 +44,7 @@ public class SourceConfigurationEditor extends Editor
mMixerSourceEditor = new MixerSourceEditor( sourceManager );
mMixerSourceEditor.setSaveRequestListener( this );
- mTunerSourceEditor = new TunerSourceEditor();
+ mTunerSourceEditor = new TunerSourceEditor(sourceManager.getTunerModel());
mTunerSourceEditor.setSaveRequestListener( this );
init();
diff --git a/src/main/java/io/github/dsheirer/source/SourceManager.java b/src/main/java/io/github/dsheirer/source/SourceManager.java
index 1a798b69..4af01bfd 100644
--- a/src/main/java/io/github/dsheirer/source/SourceManager.java
+++ b/src/main/java/io/github/dsheirer/source/SourceManager.java
@@ -59,6 +59,11 @@ public class SourceManager
return mTunerManager;
}
+ public TunerModel getTunerModel()
+ {
+ return mTunerModel;
+ }
+
public Source getSource(SourceConfiguration config, int bandwidth)
throws SourceException
{
diff --git a/src/main/java/io/github/dsheirer/source/config/SourceConfigTuner.java b/src/main/java/io/github/dsheirer/source/config/SourceConfigTuner.java
index 9e9db98e..3c467c6e 100644
--- a/src/main/java/io/github/dsheirer/source/config/SourceConfigTuner.java
+++ b/src/main/java/io/github/dsheirer/source/config/SourceConfigTuner.java
@@ -1,6 +1,6 @@
/*******************************************************************************
* SDR Trunk
- * Copyright (C) 2014-2016 Dennis Sheirer
+ * Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -32,6 +32,7 @@ public class SourceConfigTuner extends SourceConfiguration
private long mFrequency = 0;
private int mBandwidth = 12500;
+ private String mPreferredTuner;
public SourceConfigTuner()
{
@@ -57,6 +58,34 @@ public class SourceConfigTuner extends SourceConfiguration
mFrequency = frequency;
}
+ /**
+ * Preferred tuner to use for this configuration.
+ * @return tuner name or null
+ */
+ @JacksonXmlProperty(isAttribute = true, localName = "preferred_tuner")
+ public String getPreferredTuner()
+ {
+ return mPreferredTuner;
+ }
+
+ /**
+ * Indicates if this configuration has a specified preferred tuner
+ */
+ @JsonIgnore
+ public boolean hasPreferredTuner()
+ {
+ return mPreferredTuner != null;
+ }
+
+ /**
+ * Specifies the preferred tuner to use for this configuration
+ * @param preferredTuner to use, if available
+ */
+ public void setPreferredTuner(String preferredTuner)
+ {
+ mPreferredTuner = preferredTuner;
+ }
+
@JsonIgnore
@Override
public String getDescription()
diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerChannelSource.java b/src/main/java/io/github/dsheirer/source/tuner/TunerChannelSource.java
new file mode 100644
index 00000000..e69de29b
diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerModel.java b/src/main/java/io/github/dsheirer/source/tuner/TunerModel.java
index 9c00915e..a88db737 100644
--- a/src/main/java/io/github/dsheirer/source/tuner/TunerModel.java
+++ b/src/main/java/io/github/dsheirer/source/tuner/TunerModel.java
@@ -71,14 +71,36 @@ public class TunerModel extends AbstractTableModel implements Listener Tuners)
+ /**
+ * Find a tuner that matches the name argument
+ *
+ * @param name of the tuner
+ * @return named tuner or null
+ */
+ public Tuner getTuner(String name)
{
- for(Tuner tuner : Tuners)
+ if(name != null)
{
- addTuner(tuner);
+ for(Tuner tuner: mTuners)
+ {
+ if(tuner.getName().equalsIgnoreCase(name))
+ {
+ return tuner;
+ }
+ }
}
+
+ return null;
}
+ public void addTuners( List Tuners )
+ {
+ for( Tuner tuner: Tuners )
+ {
+ addTuner( tuner );
+ }
+ }
+
/**
* Adds the Tuner to this model
*/
@@ -262,27 +284,52 @@ public class TunerModel extends AbstractTableModel implements Listener it = mTuners.iterator();
+
+ Tuner tuner;
- TunerChannel tunerChannel = config.getTunerChannel();
-
- tunerChannel.setBandwidth(bandwidth);
-
- Iterator it = mTuners.iterator();
-
- Tuner tuner;
-
- while(it.hasNext() && retVal == null)
+ if(config.hasPreferredTuner())
{
- tuner = it.next();
+ tuner = getTuner(config.getPreferredTuner());
- try
+ if(tuner != null)
+ {
+ try
+ {
+ retVal = tuner.getChannel( tunerChannel );
+
+ if(retVal != null)
+ {
+ return retVal;
+ }
+ }
+ catch(Exception e)
+ {
+ //Fall through to logger below
+ }
+ }
+
+ mLog.info("Unable to source channel [" + config.getFrequency() + "] from preferred tuner [" +
+ config.getPreferredTuner() + "] - searching for another tuner");
+ }
+
+ while( it.hasNext() && retVal == null )
+ {
+ tuner = it.next();
+
+ try
{
retVal = tuner.getChannelSourceManager().getSource(tunerChannel);
}
catch(Exception e)
{
- mLog.error("Error obtaining channel source from tuner [" + tuner.getName() + "]", e);
+ mLog.error( "error obtaining channel from tuner [" + tuner.getName() + "]", e );
}
}
diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerSourceEditor.java b/src/main/java/io/github/dsheirer/source/tuner/TunerSourceEditor.java
index be958fdc..1c5392f3 100644
--- a/src/main/java/io/github/dsheirer/source/tuner/TunerSourceEditor.java
+++ b/src/main/java/io/github/dsheirer/source/tuner/TunerSourceEditor.java
@@ -1,17 +1,17 @@
/*******************************************************************************
* 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
* 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
******************************************************************************/
@@ -26,70 +26,164 @@ import io.github.dsheirer.source.config.SourceConfigTuner;
import io.github.dsheirer.source.config.SourceConfiguration;
import net.miginfocom.swing.MigLayout;
+import javax.swing.DefaultComboBoxModel;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
public class TunerSourceEditor extends Editor
{
- private static final long serialVersionUID = 1L;
- private JFrequencyControl mFrequencyControl;
+ private static final long serialVersionUID = 1L;
+ private static final String NO_PREFERRED_TUNER = "(none)";
+ private static final String UNAVAILABLE_TUNER = " (unavailable)";
+ private JFrequencyControl mFrequencyControl;
+ private JComboBox mTunerNameComboBox;
+ private TunerModel mTunerModel;
+ private List mCurrentTunerNames;
- public TunerSourceEditor()
- {
- init();
- }
+ public TunerSourceEditor(TunerModel tunerModel)
+ {
+ mTunerModel = tunerModel;
+ init();
+ }
- private void init()
- {
- setLayout( new MigLayout( "insets 0 0 0 0", "[left]", "" ) );
- mFrequencyControl = new JFrequencyControl();
- mFrequencyControl.setEnabled( false );
- mFrequencyControl.addListener( new ISourceEventProcessor()
- {
- @Override
- public void process(SourceEvent event )
- {
- setModified( true );
- }
- } );
- add( mFrequencyControl );
- }
+ private void init()
+ {
+ loadTunerNames();
- public void save()
- {
- if( hasItem() && isModified() )
- {
- SourceConfigTuner config = new SourceConfigTuner();
- config.setFrequency( mFrequencyControl.getFrequency() );
- getItem().setSourceConfiguration( config );
- }
+ setLayout(new MigLayout("insets 0 0 0 0", "[left]", ""));
+ mFrequencyControl = new JFrequencyControl();
+ mFrequencyControl.setEnabled(false);
+ mFrequencyControl.addListener(new IFrequencyChangeProcessor()
+ {
+ @Override
+ public void frequencyChanged(FrequencyChangeEvent event)
+ {
+ setModified(true);
+ }
+ });
+ add(mFrequencyControl);
- setModified( false );
- }
+ mTunerNameComboBox = new JComboBox<>(mCurrentTunerNames.toArray(new String[mCurrentTunerNames.size()]));
+ mTunerNameComboBox.addActionListener(new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ setModified(true);
+ }
+ });
+ add(new JLabel("Preferred Tuner:"));
+ add(mTunerNameComboBox);
+ }
- @Override
- public void setItem( Channel item )
- {
- super.setItem( item );
+ private void loadTunerNames()
+ {
+ mCurrentTunerNames = new ArrayList<>();
- if( hasItem() )
- {
- SourceConfiguration config = getItem().getSourceConfiguration();
+ for(Tuner tuner: mTunerModel.getTuners())
+ {
+ mCurrentTunerNames.add(tuner.getName());
+ }
- mFrequencyControl.setEnabled( true );
+ Collections.sort(mCurrentTunerNames);
- if( config instanceof SourceConfigTuner )
- {
- mFrequencyControl.setFrequency( ((SourceConfigTuner)config).getFrequency(), false );
- setModified( false );
- }
- else
- {
- mFrequencyControl.setFrequency( 101000000, false );
- setModified( true );
- }
- }
- else
- {
- mFrequencyControl.setEnabled( false );
- setModified( false );
- }
- }
-}
\ No newline at end of file
+ mCurrentTunerNames.add(0, NO_PREFERRED_TUNER);
+ }
+
+ private void updateTunerNameCombo(String preferredTuner)
+ {
+ if(preferredTuner != null && !mCurrentTunerNames.contains(preferredTuner))
+ {
+ List updatedTunerNameList = new ArrayList<>(mCurrentTunerNames);
+ String unavailableTuner = preferredTuner + UNAVAILABLE_TUNER;
+ updatedTunerNameList.add(unavailableTuner);
+ String[] tunerNameArray = updatedTunerNameList.toArray(new String[updatedTunerNameList.size()]);
+ mTunerNameComboBox.setModel(new DefaultComboBoxModel<>(tunerNameArray));
+ mTunerNameComboBox.setSelectedItem(unavailableTuner);
+ }
+ else
+ {
+ String[] tunerNameArray = mCurrentTunerNames.toArray(new String[mCurrentTunerNames.size()]);
+ mTunerNameComboBox.setModel(new DefaultComboBoxModel<>(tunerNameArray));
+
+ if(preferredTuner == null)
+ {
+ mTunerNameComboBox.setSelectedItem(NO_PREFERRED_TUNER);
+ }
+ else
+ {
+ mTunerNameComboBox.setSelectedItem(preferredTuner);
+ }
+ }
+ }
+
+ public void save()
+ {
+ if(hasItem() && isModified())
+ {
+ SourceConfigTuner config = new SourceConfigTuner();
+ config.setFrequency(mFrequencyControl.getFrequency());
+
+ String preferredTuner = (String)mTunerNameComboBox.getSelectedItem();
+
+ if(preferredTuner != null)
+ {
+ if(preferredTuner.equalsIgnoreCase(NO_PREFERRED_TUNER))
+ {
+ config.setPreferredTuner(null);
+ }
+ else if(preferredTuner.contains(UNAVAILABLE_TUNER))
+ {
+ //don't do anything - the config already contains this unavailable tuner name
+ }
+ else
+ {
+ config.setPreferredTuner(preferredTuner);
+ }
+ }
+
+ getItem().setSourceConfiguration(config);
+ }
+
+ setModified(false);
+ }
+
+ @Override
+ public void setItem(Channel item)
+ {
+ super.setItem(item);
+
+ if(hasItem())
+ {
+ SourceConfiguration config = getItem().getSourceConfiguration();
+
+ mFrequencyControl.setEnabled(true);
+
+ if(config instanceof SourceConfigTuner)
+ {
+ SourceConfigTuner tunerConfig = (SourceConfigTuner)config;
+
+ mFrequencyControl.setFrequency(tunerConfig.getFrequency(), false);
+
+ updateTunerNameCombo(tunerConfig.getPreferredTuner());
+
+ setModified(false);
+ }
+ else
+ {
+ mFrequencyControl.setFrequency(101000000, false);
+ setModified(true);
+ }
+ }
+ else
+ {
+ mFrequencyControl.setEnabled(false);
+ setModified(false);
+ }
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java b/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java
index 4ddb7704..63453767 100644
--- a/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java
+++ b/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java
@@ -147,13 +147,8 @@ public class ComplexWaveSource extends ComplexSource implements IControllableFil
if(mInputStream != null)
{
mInputStream.close();
+ mInputStream = null;
}
- else
- {
- throw new IOException("Can't close wave source - was not opened");
- }
-
- mInputStream = null;
}
/**
diff --git a/src/main/java/io/github/dsheirer/spectrum/ChannelSpectrumPanel.java b/src/main/java/io/github/dsheirer/spectrum/ChannelSpectrumPanel.java
index c646aba7..879a7963 100644
--- a/src/main/java/io/github/dsheirer/spectrum/ChannelSpectrumPanel.java
+++ b/src/main/java/io/github/dsheirer/spectrum/ChannelSpectrumPanel.java
@@ -26,15 +26,15 @@ import io.github.dsheirer.dsp.filter.smoothing.SmoothingFilter.SmoothingType;
import io.github.dsheirer.module.ProcessingChain;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.SampleType;
-import io.github.dsheirer.sample.complex.reusable.ReusableBufferQueue;
-import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer;
+import io.github.dsheirer.sample.complex.ComplexBuffer;
import io.github.dsheirer.sample.real.RealBuffer;
import io.github.dsheirer.settings.ColorSetting.ColorSettingName;
import io.github.dsheirer.settings.ColorSettingMenuItem;
import io.github.dsheirer.settings.Setting;
import io.github.dsheirer.settings.SettingChangeListener;
import io.github.dsheirer.settings.SettingsManager;
-import io.github.dsheirer.source.SourceEvent;
+import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent;
+import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent.Event;
import io.github.dsheirer.spectrum.converter.DFTResultsConverter;
import io.github.dsheirer.spectrum.converter.RealDecibelConverter;
import io.github.dsheirer.spectrum.menu.AveragingItem;
@@ -45,13 +45,8 @@ import io.github.dsheirer.spectrum.menu.SmoothingItem;
import io.github.dsheirer.spectrum.menu.SmoothingTypeItem;
import net.miginfocom.swing.MigLayout;
-import javax.swing.JLayeredPane;
-import javax.swing.JMenu;
-import javax.swing.JPanel;
-import javax.swing.JPopupMenu;
-import javax.swing.JSeparator;
-import javax.swing.SwingUtilities;
-import java.awt.Component;
+import javax.swing.*;
+import java.awt.*;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.MouseEvent;
@@ -59,459 +54,446 @@ import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.concurrent.atomic.AtomicBoolean;
-public class ChannelSpectrumPanel extends JPanel implements ChannelEventListener,
- Listener, SettingChangeListener, SpectralDisplayAdjuster
+public class ChannelSpectrumPanel extends JPanel
+ implements ChannelEventListener,
+ Listener,
+ SettingChangeListener,
+ SpectralDisplayAdjuster
{
- private static final long serialVersionUID = 1L;
+ private static final long serialVersionUID = 1L;
- private DFTProcessor mDFTProcessor = new DFTProcessor(SampleType.REAL);
+ private DFTProcessor mDFTProcessor = new DFTProcessor( SampleType.REAL );
private DFTResultsConverter mDFTConverter = new RealDecibelConverter();
private JLayeredPane mLayeredPane;
private SpectrumPanel mSpectrumPanel;
private ChannelOverlayPanel mOverlayPanel;
- private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue();
-
+
private Channel mCurrentChannel;
-
+
private int mSampleBufferSize = 2400;
- private HalfBandFilter_RB_RB mDecimatingFilter = new HalfBandFilter_RB_RB(
- Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO.getCoefficients(), 1.0f, true);
-
+ private HalfBandFilter_RB_RB mDecimatingFilter = new HalfBandFilter_RB_RB(
+ Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO.getCoefficients(), 1.0f, true );
+
private AtomicBoolean mEnabled = new AtomicBoolean();
-
+
private SettingsManager mSettingsManager;
private ChannelProcessingManager mChannelProcessingManager;
- public ChannelSpectrumPanel(SettingsManager settingsManager,
- ChannelProcessingManager channelProcessingManager)
+ public ChannelSpectrumPanel( SettingsManager settingsManager,
+ ChannelProcessingManager channelProcessingManager )
{
- mSettingsManager = settingsManager;
- mChannelProcessingManager = channelProcessingManager;
+ mSettingsManager = settingsManager;
+ mChannelProcessingManager = channelProcessingManager;
+
+ if( mSettingsManager != null )
+ {
+ mSettingsManager.addListener( this );
+ }
+
+ mSpectrumPanel = new SpectrumPanel( mSettingsManager );
+ mSpectrumPanel.setAveraging( 1 );
- if(mSettingsManager != null)
- {
- mSettingsManager.addListener(this);
- }
+ mOverlayPanel = new ChannelOverlayPanel( mSettingsManager );
+ mDFTProcessor.addConverter( mDFTConverter );
+ mDFTConverter.addListener( mSpectrumPanel );
- mSpectrumPanel = new SpectrumPanel(mSettingsManager);
- mSpectrumPanel.setAveraging(1);
-
- mOverlayPanel = new ChannelOverlayPanel(mSettingsManager);
- mDFTProcessor.addConverter(mDFTConverter);
- mDFTConverter.addListener(mSpectrumPanel);
-
- /* Set the DFTProcessor to the decimated 24kHz sample rate */
- mDFTProcessor.process(SourceEvent.sampleRateChange(24000.0));
-
- initGui();
+ /* Set the DFTProcessor to the decimated 24kHz sample rate */
+ mDFTProcessor.frequencyChanged(
+ new FrequencyChangeEvent( Event.NOTIFICATION_SAMPLE_RATE_CHANGE, 24000 ) );
+
+ initGui();
}
-
+
public void dispose()
{
- setEnabled(false);
+ setEnabled( false );
+
+ mDFTProcessor.dispose();
- mDFTProcessor.dispose();
-
- if(mSettingsManager != null)
- {
- mSettingsManager.removeListener(this);
- }
-
- mSettingsManager = null;
- mCurrentChannel = null;
- mDFTProcessor = null;
- mSpectrumPanel = null;
+ if( mSettingsManager != null )
+ {
+ mSettingsManager.removeListener( this );
+ }
+
+ mSettingsManager = null;
+ mCurrentChannel = null;
+ mDFTProcessor = null;
+ mSpectrumPanel = null;
}
-
- public void setFrameRate(int framesPerSecond)
+
+ public void setFrameRate( int framesPerSecond )
{
- mSampleBufferSize = (int)(48000 / framesPerSecond);
-
- mDFTProcessor.setFrameRate(framesPerSecond);
+ mSampleBufferSize = (int)( 48000 / framesPerSecond );
+
+ mDFTProcessor.setFrameRate( framesPerSecond );
}
-
+
private void initGui()
{
- setLayout(new MigLayout("insets 0 0 0 0 ",
- "[grow,fill]",
- "[grow,fill]"));
+ setLayout( new MigLayout( "insets 0 0 0 0 ",
+ "[grow,fill]",
+ "[grow,fill]") );
+
+ mLayeredPane = new JLayeredPane();
+ mLayeredPane.addComponentListener( new ResizeListener() );
+
+ MouseEventProcessor mouser = new MouseEventProcessor();
+
+ mOverlayPanel.addMouseListener( mouser );
+ mOverlayPanel.addMouseMotionListener( mouser );
+
+ mLayeredPane.add( mSpectrumPanel, new Integer( 0 ), 0 );
+ mLayeredPane.add( mOverlayPanel, new Integer( 1 ), 0 );
- mLayeredPane = new JLayeredPane();
- mLayeredPane.addComponentListener(new ResizeListener());
-
- MouseEventProcessor mouser = new MouseEventProcessor();
-
- mOverlayPanel.addMouseListener(mouser);
- mOverlayPanel.addMouseMotionListener(mouser);
-
- mLayeredPane.add(mSpectrumPanel, new Integer(0), 0);
- mLayeredPane.add(mOverlayPanel, new Integer(1), 0);
-
- add(mLayeredPane);
+ add( mLayeredPane );
}
-
- public void setEnabled(boolean enabled)
+
+ public void setEnabled( boolean enabled )
{
- if(enabled && mEnabled.compareAndSet(false, true))
- {
- start();
- }
- else if(!enabled && mEnabled.compareAndSet(true, false))
- {
- stop();
- }
+ if( enabled && mEnabled.compareAndSet( false, true ) )
+ {
+ start();
+ }
+ else if( !enabled && mEnabled.compareAndSet( true, false ) )
+ {
+ stop();
+ }
}
@Override
- @SuppressWarnings("incomplete-switch")
- public void channelChanged(ChannelEvent event)
+ @SuppressWarnings( "incomplete-switch" )
+ public void channelChanged( ChannelEvent event )
{
- switch(event.getEvent())
- {
- case NOTIFICATION_SELECTION_CHANGE:
- //ChannelSelectionManager ensures that only 1 channel can be
- //selected and any previously selected channel will be first
- //deselected before we get a new selection event
- if(event.getChannel().isSelected())
- {
- if(mCurrentChannel != null)
- {
- stop();
- mCurrentChannel = null;
- }
+ switch( event.getEvent() )
+ {
+ case NOTIFICATION_SELECTION_CHANGE:
+ //ChannelSelectionManager ensures that only 1 channel can be
+ //selected and any previously selected channel will be first
+ //deselected before we get a new selection event
+ if( event.getChannel().isSelected() )
+ {
+ if( mCurrentChannel != null )
+ {
+ stop();
+ mCurrentChannel = null;
+ }
+
+ mCurrentChannel = event.getChannel();
+
+ if( mEnabled.get() )
+ {
+ start();
+ }
+ }
+ else
+ {
+ stop();
+ mCurrentChannel = null;
+ }
+ break;
+ case NOTIFICATION_PROCESSING_STOP:
+ if( event.getChannel() == mCurrentChannel )
+ {
+ if( mEnabled.get() )
+ {
+ stop();
+ }
- mCurrentChannel = event.getChannel();
+ mCurrentChannel = null;
+ }
+ break;
+ }
+ }
+
+ private void start()
+ {
+ if( mEnabled.get() && mCurrentChannel != null && mCurrentChannel.isProcessing() )
+ {
+ ProcessingChain processingChain = mChannelProcessingManager
+ .getProcessingChain( mCurrentChannel );
- if(mEnabled.get())
- {
- start();
- }
- }
- else
- {
- stop();
- mCurrentChannel = null;
- }
- break;
- case NOTIFICATION_PROCESSING_STOP:
- if(event.getChannel() == mCurrentChannel)
- {
- if(mEnabled.get())
- {
- stop();
- }
+ if( processingChain != null )
+ {
+ processingChain.addRealBufferListener( this );
+
+ mDFTProcessor.start();
+ }
+ }
+ }
+
+ private void stop()
+ {
+ if( mCurrentChannel != null && mCurrentChannel.isProcessing() )
+ {
+ ProcessingChain processingChain = mChannelProcessingManager
+ .getProcessingChain( mCurrentChannel );
- mCurrentChannel = null;
- }
- break;
- }
+ if( processingChain != null )
+ {
+ processingChain.removeRealBufferListener( this );
+ }
+ }
+
+ mDFTProcessor.stop();
+
+ mSpectrumPanel.clearSpectrum();
+ }
+
+ @Override
+ public void settingChanged( Setting setting )
+ {
+ if( mSpectrumPanel != null )
+ {
+ mSpectrumPanel.settingChanged( setting );
+ }
+ if( mOverlayPanel != null )
+ {
+ mOverlayPanel.settingChanged( setting );
+ }
}
- private void start()
+ @Override
+ public void settingDeleted( Setting setting )
{
- if(mEnabled.get() && mCurrentChannel != null && mCurrentChannel.getEnabled())
- {
- ProcessingChain processingChain = mChannelProcessingManager
- .getProcessingChain(mCurrentChannel);
+ if( mSpectrumPanel != null )
+ {
+ mSpectrumPanel.settingDeleted( setting );
+ }
- if(processingChain != null)
- {
- processingChain.addRealBufferListener(this);
-
- mDFTProcessor.start();
- }
- }
+ if( mOverlayPanel != null )
+ {
+ mOverlayPanel.settingDeleted( setting );
+ }
}
- private void stop()
+ @Override
+ public void receive( RealBuffer buffer )
{
- if(mCurrentChannel != null && mCurrentChannel.getEnabled())
- {
- ProcessingChain processingChain = mChannelProcessingManager
- .getProcessingChain(mCurrentChannel);
+ RealBuffer decimated = mDecimatingFilter.filter( buffer );
- if(processingChain != null)
- {
- processingChain.removeRealBufferListener(this);
- }
- }
-
- mDFTProcessor.stop();
-
- mSpectrumPanel.clearSpectrum();
+ //Hack: we're placing real samples in a complex buffer that the DFT
+ //processor is expecting.
+ mDFTProcessor.receive( new ComplexBuffer( decimated.getSamples() ) );
}
- @Override
- public void settingChanged(Setting setting)
- {
- if(mSpectrumPanel != null)
+ /**
+ * Monitors the sizing of the layered pane and resizes the spectrum and
+ * channel panels whenever the layered pane is resized
+ */
+ public class ResizeListener implements ComponentListener
+ {
+ @Override
+ public void componentResized( ComponentEvent e )
{
- mSpectrumPanel.settingChanged(setting);
- }
- if(mOverlayPanel != null)
- {
- mOverlayPanel.settingChanged(setting);
- }
- }
-
- @Override
- public void settingDeleted(Setting setting)
- {
- if(mSpectrumPanel != null)
- {
- mSpectrumPanel.settingDeleted(setting);
+ Component c = e.getComponent();
+
+ mSpectrumPanel.setBounds( 0, 0, c.getWidth(), c.getHeight() );
+ mOverlayPanel.setBounds( 0, 0, c.getWidth(), c.getHeight() );
}
- if(mOverlayPanel != null)
+ @Override
+ public void componentHidden( ComponentEvent arg0 ) {}
+ @Override
+ public void componentMoved( ComponentEvent arg0 ) {}
+ @Override
+ public void componentShown( ComponentEvent arg0 ) {}
+ }
+
+ /**
+ * Mouse event handler for the channel panel.
+ */
+ public class MouseEventProcessor implements MouseMotionListener, MouseListener
+ {
+ @Override
+ public void mouseMoved( MouseEvent event )
{
- mOverlayPanel.settingDeleted(setting);
+ update( event );
}
- }
+ @Override
+ public void mouseDragged( MouseEvent event )
+ {
+ update( event );
+ }
+
+ private void update( MouseEvent event )
+ {
+ if( event.getComponent() == mOverlayPanel )
+ {
+ mOverlayPanel.setCursorLocation( event.getPoint() );
+ }
+ }
- @Override
- public void receive(RealBuffer buffer)
- {
- RealBuffer decimated = mDecimatingFilter.filter(buffer);
+ @Override
+ public void mouseEntered( MouseEvent e )
+ {
+ if( e.getComponent() == mOverlayPanel )
+ {
+ mOverlayPanel.setCursorVisible( true );
+ }
+ }
+
+ @Override
+ public void mouseExited( MouseEvent e )
+ {
+ mOverlayPanel.setCursorVisible( false );
+ }
- //Hack: we're placing real samples in a complex buffer that the DFT
- //processor is expecting.
- ReusableComplexBuffer reusableComplexBuffer = mReusableBufferQueue.getBuffer(decimated.getSamples().length);
- reusableComplexBuffer.reloadFrom(decimated.getSamples(), System.currentTimeMillis());
- mDFTProcessor.receive(reusableComplexBuffer.incrementUserCount());
- }
-
- /**
- * Monitors the sizing of the layered pane and resizes the spectrum and
- * channel panels whenever the layered pane is resized
- */
- public class ResizeListener implements ComponentListener
- {
- @Override
- public void componentResized(ComponentEvent e)
+ /**
+ * Displays the context menu.
+ */
+ @Override
+ public void mouseClicked( MouseEvent event )
{
- Component c = e.getComponent();
+ if( SwingUtilities.isRightMouseButton( event ) )
+ {
+ JPopupMenu contextMenu = new JPopupMenu();
+
+ /**
+ * Color Menus
+ */
+ JMenu colorMenu = new JMenu( "Color" );
- mSpectrumPanel.setBounds(0, 0, c.getWidth(), c.getHeight());
- mOverlayPanel.setBounds(0, 0, c.getWidth(), c.getHeight());
+ colorMenu.add( new ColorSettingMenuItem( mSettingsManager,
+ ColorSettingName.SPECTRUM_CURSOR ) );
+
+ colorMenu.add( new ColorSettingMenuItem( mSettingsManager,
+ ColorSettingName.SPECTRUM_LINE ) );
+
+ colorMenu.add( new ColorSettingMenuItem( mSettingsManager,
+ ColorSettingName.SPECTRUM_BACKGROUND ) );
+
+ colorMenu.add( new ColorSettingMenuItem( mSettingsManager,
+ ColorSettingName.SPECTRUM_GRADIENT_BOTTOM ) );
+
+ colorMenu.add( new ColorSettingMenuItem( mSettingsManager,
+ ColorSettingName.SPECTRUM_GRADIENT_TOP ) );
+
+ contextMenu.add( colorMenu );
+
+ /**
+ * Display items: fft and frame rate
+ */
+ JMenu displayMenu = new JMenu( "Display" );
+ contextMenu.add( displayMenu );
+
+ /**
+ * Averaging menu
+ */
+ JMenu averagingMenu = new JMenu( "Averaging" );
+ averagingMenu.add(
+ new AveragingItem( ChannelSpectrumPanel.this, 2 ) );
+ displayMenu.add( averagingMenu );
+
+ /**
+ * FFT width
+ */
+ JMenu fftWidthMenu = new JMenu( "FFT Width" );
+ displayMenu.add( fftWidthMenu );
+
+ for( DFTSize width: DFTSize.values() )
+ {
+ fftWidthMenu.add( new DFTSizeItem( mDFTProcessor, width ) );
+ }
+
+ /**
+ * DFT Processor Frame Rate
+ */
+ JMenu frameRateMenu = new JMenu( "Frame Rate" );
+ displayMenu.add( frameRateMenu );
+
+ frameRateMenu.add( new FrameRateItem( mDFTProcessor, 14 ) );
+ frameRateMenu.add( new FrameRateItem( mDFTProcessor, 16 ) );
+ frameRateMenu.add( new FrameRateItem( mDFTProcessor, 18 ) );
+ frameRateMenu.add( new FrameRateItem( mDFTProcessor, 20 ) );
+ frameRateMenu.add( new FrameRateItem( mDFTProcessor, 25 ) );
+ frameRateMenu.add( new FrameRateItem( mDFTProcessor, 30 ) );
+ frameRateMenu.add( new FrameRateItem( mDFTProcessor, 40 ) );
+ frameRateMenu.add( new FrameRateItem( mDFTProcessor, 50 ) );
+
+ /**
+ * FFT Window Type
+ */
+ JMenu fftWindowType = new JMenu( "Window Type" );
+ displayMenu.add( fftWindowType );
+
+ for( WindowType type: WindowType.values() )
+ {
+ fftWindowType.add(
+ new FFTWindowTypeItem( mDFTProcessor, type ) );
+ }
+
+ /**
+ * Smoothing menu
+ */
+ JMenu smoothingMenu = new JMenu( "Smoothing" );
+
+ if( mSpectrumPanel.getSmoothingType() != SmoothingType.NONE )
+ {
+ smoothingMenu.add( new SmoothingItem( ChannelSpectrumPanel.this, 5 ) );
+ smoothingMenu.add( new JSeparator() );
+ }
+
+ smoothingMenu.add( new SmoothingTypeItem( ChannelSpectrumPanel.this, SmoothingType.GAUSSIAN ) );
+ smoothingMenu.add( new SmoothingTypeItem( ChannelSpectrumPanel.this, SmoothingType.TRIANGLE ) );
+ smoothingMenu.add( new SmoothingTypeItem( ChannelSpectrumPanel.this, SmoothingType.RECTANGLE ) );
+ smoothingMenu.add( new SmoothingTypeItem( ChannelSpectrumPanel.this, SmoothingType.NONE ) );
+
+ displayMenu.add( smoothingMenu );
+
+ if( contextMenu != null )
+ {
+ contextMenu.show( mOverlayPanel,
+ event.getX(),
+ event.getY() );
+ }
+ }
}
- @Override
- public void componentHidden(ComponentEvent arg0)
- {
- }
+ @Override
+ public void mousePressed( MouseEvent e ) {}
+ @Override
+ public void mouseReleased( MouseEvent e ) {}
+ }
- @Override
- public void componentMoved(ComponentEvent arg0)
- {
- }
-
- @Override
- public void componentShown(ComponentEvent arg0)
- {
- }
- }
-
- /**
- * Mouse event handler for the channel panel.
- */
- public class MouseEventProcessor implements MouseMotionListener, MouseListener
- {
- @Override
- public void mouseMoved(MouseEvent event)
- {
- update(event);
- }
-
- @Override
- public void mouseDragged(MouseEvent event)
- {
- update(event);
- }
-
- private void update(MouseEvent event)
- {
- if(event.getComponent() == mOverlayPanel)
- {
- mOverlayPanel.setCursorLocation(event.getPoint());
- }
- }
-
- @Override
- public void mouseEntered(MouseEvent e)
- {
- if(e.getComponent() == mOverlayPanel)
- {
- mOverlayPanel.setCursorVisible(true);
- }
- }
-
- @Override
- public void mouseExited(MouseEvent e)
- {
- mOverlayPanel.setCursorVisible(false);
- }
-
- /**
- * Displays the context menu.
- */
- @Override
- public void mouseClicked(MouseEvent event)
- {
- if(SwingUtilities.isRightMouseButton(event))
- {
- JPopupMenu contextMenu = new JPopupMenu();
-
- /**
- * Color Menus
- */
- JMenu colorMenu = new JMenu("Color");
-
- colorMenu.add(new ColorSettingMenuItem(mSettingsManager,
- ColorSettingName.SPECTRUM_CURSOR));
-
- colorMenu.add(new ColorSettingMenuItem(mSettingsManager,
- ColorSettingName.SPECTRUM_LINE));
-
- colorMenu.add(new ColorSettingMenuItem(mSettingsManager,
- ColorSettingName.SPECTRUM_BACKGROUND));
-
- colorMenu.add(new ColorSettingMenuItem(mSettingsManager,
- ColorSettingName.SPECTRUM_GRADIENT_BOTTOM));
-
- colorMenu.add(new ColorSettingMenuItem(mSettingsManager,
- ColorSettingName.SPECTRUM_GRADIENT_TOP));
-
- contextMenu.add(colorMenu);
-
- /**
- * Display items: fft and frame rate
- */
- JMenu displayMenu = new JMenu("Display");
- contextMenu.add(displayMenu);
-
- /**
- * Averaging menu
- */
- JMenu averagingMenu = new JMenu("Averaging");
- averagingMenu.add(
- new AveragingItem(ChannelSpectrumPanel.this, 2));
- displayMenu.add(averagingMenu);
-
- /**
- * FFT width
- */
- JMenu fftWidthMenu = new JMenu("FFT Width");
- displayMenu.add(fftWidthMenu);
-
- for(DFTSize width : DFTSize.values())
- {
- fftWidthMenu.add(new DFTSizeItem(mDFTProcessor, width));
- }
-
- /**
- * DFT Processor Frame Rate
- */
- JMenu frameRateMenu = new JMenu("Frame Rate");
- displayMenu.add(frameRateMenu);
-
- frameRateMenu.add(new FrameRateItem(mDFTProcessor, 14));
- frameRateMenu.add(new FrameRateItem(mDFTProcessor, 16));
- frameRateMenu.add(new FrameRateItem(mDFTProcessor, 18));
- frameRateMenu.add(new FrameRateItem(mDFTProcessor, 20));
- frameRateMenu.add(new FrameRateItem(mDFTProcessor, 25));
- frameRateMenu.add(new FrameRateItem(mDFTProcessor, 30));
- frameRateMenu.add(new FrameRateItem(mDFTProcessor, 40));
- frameRateMenu.add(new FrameRateItem(mDFTProcessor, 50));
-
- /**
- * FFT Window Type
- */
- JMenu fftWindowType = new JMenu("Window Type");
- displayMenu.add(fftWindowType);
-
- for(WindowType type : WindowType.values())
- {
- fftWindowType.add(
- new FFTWindowTypeItem(mDFTProcessor, type));
- }
-
- /**
- * Smoothing menu
- */
- JMenu smoothingMenu = new JMenu("Smoothing");
-
- if(mSpectrumPanel.getSmoothingType() != SmoothingType.NONE)
- {
- smoothingMenu.add(new SmoothingItem(ChannelSpectrumPanel.this, 5));
- smoothingMenu.add(new JSeparator());
- }
-
- smoothingMenu.add(new SmoothingTypeItem(ChannelSpectrumPanel.this, SmoothingType.GAUSSIAN));
- smoothingMenu.add(new SmoothingTypeItem(ChannelSpectrumPanel.this, SmoothingType.TRIANGLE));
- smoothingMenu.add(new SmoothingTypeItem(ChannelSpectrumPanel.this, SmoothingType.RECTANGLE));
- smoothingMenu.add(new SmoothingTypeItem(ChannelSpectrumPanel.this, SmoothingType.NONE));
-
- displayMenu.add(smoothingMenu);
-
- if(contextMenu != null)
- {
- contextMenu.show(mOverlayPanel,
- event.getX(),
- event.getY());
- }
- }
- }
-
- @Override
- public void mousePressed(MouseEvent e)
- {
- }
-
- @Override
- public void mouseReleased(MouseEvent e)
- {
- }
- }
-
- @Override
+ @Override
public int getAveraging()
{
- return mSpectrumPanel.getAveraging();
+ return mSpectrumPanel.getAveraging();
}
- @Override
- public void setAveraging(int averaging)
+ @Override
+ public void setAveraging( int averaging )
{
- mSpectrumPanel.setAveraging(averaging);
+ mSpectrumPanel.setAveraging( averaging );
}
- public void setSampleSize(double sampleSize)
- {
- mSpectrumPanel.setSampleSize(sampleSize);
- }
+ public void setSampleSize( double sampleSize )
+ {
+ mSpectrumPanel.setSampleSize( sampleSize );
+ }
- @Override
- public int getSmoothing()
- {
- return mSpectrumPanel.getSmoothing();
- }
+ @Override
+ public int getSmoothing()
+ {
+ return mSpectrumPanel.getSmoothing();
+ }
- @Override
- public void setSmoothing(int smoothing)
- {
- mSpectrumPanel.setSmoothing(smoothing);
- }
+ @Override
+ public void setSmoothing( int smoothing )
+ {
+ mSpectrumPanel.setSmoothing( smoothing );
+ }
- @Override
- public SmoothingType getSmoothingType()
- {
- return mSpectrumPanel.getSmoothingType();
- }
+ @Override
+ public SmoothingType getSmoothingType()
+ {
+ return mSpectrumPanel.getSmoothingType();
+ }
- @Override
- public void setSmoothingType(SmoothingType type)
- {
- mSpectrumPanel.setSmoothingType(type);
- }
+ @Override
+ public void setSmoothingType( SmoothingType type )
+ {
+ mSpectrumPanel.setSmoothingType( type );
+ }
}
diff --git a/src/main/java/io/github/dsheirer/spectrum/OverlayPanel.java b/src/main/java/io/github/dsheirer/spectrum/OverlayPanel.java
index 37b607df..2f621334 100644
--- a/src/main/java/io/github/dsheirer/spectrum/OverlayPanel.java
+++ b/src/main/java/io/github/dsheirer/spectrum/OverlayPanel.java
@@ -525,14 +525,14 @@ public class OverlayPanel extends JPanel implements ChannelEventListener, ISourc
for(Channel channel : mVisibleChannels)
{
if(mChannelDisplay == ChannelDisplay.ALL ||
- (mChannelDisplay == ChannelDisplay.ENABLED && channel.getEnabled()))
+ (mChannelDisplay == ChannelDisplay.ENABLED && channel.isProcessing()))
{
//Choose the correct background color to use
if(channel.isSelected())
{
graphics.setColor(mColorChannelConfigSelected);
}
- else if(channel.getEnabled())
+ else if(channel.isProcessing())
{
graphics.setColor(mColorChannelConfigProcessing);
}