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