From 1c06c4382f223b36e3bf87e0f7e7a62d115dc3cb Mon Sep 17 00:00:00 2001 From: Denny Date: Fri, 9 Feb 2018 05:55:29 -0500 Subject: [PATCH 01/23] Adds preferred tuner to tuner source configuration. --- .../source/config/SourceConfigTuner.java | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) 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 a542e8c6..36686771 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 @@ -33,6 +33,7 @@ public class SourceConfigTuner extends SourceConfiguration private long mFrequency = 0; private int mBandwidth = 12500; + private String mPreferredTuner; public SourceConfigTuner() { @@ -58,6 +59,25 @@ 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; + } + + /** + * 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() From c2fcc9e85de98d78f2b94f8efc8b6071e5e20608 Mon Sep 17 00:00:00 2001 From: Denny Date: Fri, 9 Feb 2018 07:11:38 -0500 Subject: [PATCH 02/23] 321 - adds a preferred tuner option to the channel's source configuration so that on startup, the application attempts to source the channel from the preferred tuner. If the tuner is currently not available, or if the tuner cannot source the channel, then the next available tuner is selected. --- sdr-trunk.ipr | 104 +++++++++ .../source/SourceConfigurationEditor.java | 5 +- .../github/dsheirer/source/SourceManager.java | 5 + .../source/config/SourceConfigTuner.java | 9 + .../dsheirer/source/tuner/TunerModel.java | 51 ++++- .../source/tuner/TunerSourceEditor.java | 216 +++++++++++++----- 6 files changed, 324 insertions(+), 66 deletions(-) create mode 100644 sdr-trunk.ipr 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/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 36686771..2b926be9 100644 --- a/src/main/java/io/github/dsheirer/source/config/SourceConfigTuner.java +++ b/src/main/java/io/github/dsheirer/source/config/SourceConfigTuner.java @@ -69,6 +69,15 @@ public class SourceConfigTuner extends SourceConfiguration 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 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 3b7aa297..e5e52ee0 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/TunerModel.java +++ b/src/main/java/io/github/dsheirer/source/tuner/TunerModel.java @@ -70,6 +70,28 @@ public class TunerModel extends AbstractTableModel implements Listener Tuners ) { for( Tuner tuner: Tuners ) @@ -270,6 +292,31 @@ public class TunerModel extends AbstractTableModel implements Listener it = mTuners.iterator(); Tuner tuner; + + if(config.hasPreferredTuner()) + { + tuner = getTuner(config.getPreferredTuner()); + + 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 ) { @@ -285,12 +332,10 @@ public class TunerModel extends AbstractTableModel implements Listener ******************************************************************************/ @@ -26,70 +26,164 @@ import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent; import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeProcessor; 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 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 IFrequencyChangeProcessor() - { - @Override - public void frequencyChanged( FrequencyChangeEvent 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 ); - } - - @Override - public void setItem( Channel item ) - { - super.setItem( item ); - - if( hasItem() ) - { - SourceConfiguration config = getItem().getSourceConfiguration(); - - mFrequencyControl.setEnabled( true ); + 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); + } - 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 ); - } - } + private void loadTunerNames() + { + mCurrentTunerNames = new ArrayList<>(); + + for(Tuner tuner: mTunerModel.getTuners()) + { + mCurrentTunerNames.add(tuner.getName()); + } + + Collections.sort(mCurrentTunerNames); + + 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); + } + } } From 030fc1617c517cba81e3ef8af9f5ee17331a4942 Mon Sep 17 00:00:00 2001 From: Denny Date: Sat, 10 Feb 2018 08:09:43 -0500 Subject: [PATCH 03/23] 328 Channel Auto-Start capability. Adds auto-start and auto-start ordering to each channel configuration to provide the user with greater control over initial application startup configuration. Adds new dialog window that displays on startup that allows the user to invoke/cancel auto-start, or simply allow the timer to expire whereby the channels will be auto-started. Updates channel table model to include an auto-start column and relabels the 'Enabled' column to read 'Playing'. Updates channel editor to change the 'Enable/Disable' button to read 'Start/Stop'. Changes the 'enabled' property of the channel configuration to indicate the current 'processing' status - isProcessing(). Channel's XML configuration re-purposes the 'enabled' property to indicate auto-start and adds a new (optional) auto-start 'order' attribute. --- .../traffic/TrafficChannelManager.java | 4 +- .../channel/AutoStartChannelModel.java | 105 +++++ .../dsheirer/controller/channel/Channel.java | 107 ++++- .../channel/ChannelAutoStartFrame.java | 286 +++++++++++++ .../controller/channel/ChannelController.java | 14 +- .../controller/channel/ChannelEditor.java | 402 +++++++++--------- .../controller/channel/ChannelEvent.java | 9 +- .../controller/channel/ChannelModel.java | 69 ++- .../channel/ChannelProcessingManager.java | 12 +- .../controller/channel/ChannelUtils.java | 9 +- .../channel/NameConfigurationEditor.java | 78 +++- .../java/io/github/dsheirer/gui/SDRTrunk.java | 74 +++- .../spectrum/ChannelSpectrumPanel.java | 13 +- .../dsheirer/spectrum/OverlayPanel.java | 16 +- 14 files changed, 926 insertions(+), 272 deletions(-) create mode 100644 src/main/java/io/github/dsheirer/controller/channel/AutoStartChannelModel.java create mode 100644 src/main/java/io/github/dsheirer/controller/channel/ChannelAutoStartFrame.java 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 dcd12ce5..efd9e52c 100644 --- a/src/main/java/io/github/dsheirer/channel/traffic/TrafficChannelManager.java +++ b/src/main/java/io/github/dsheirer/channel/traffic/TrafficChannelManager.java @@ -137,7 +137,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider, { for(Channel configuredChannel : mTrafficChannelPool) { - if(!configuredChannel.getEnabled()) + if(!configuredChannel.isProcessing()) { channel = configuredChannel; break; @@ -207,7 +207,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 9c0cee7f..4927f68d 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 @@ -45,18 +45,18 @@ import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent.Event; 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; private DecodeConfiguration mDecodeConfiguration = DecoderFactory.getDefaultDecodeConfiguration(); - private AuxDecodeConfiguration mAuxDecodeConfiguration = - new AuxDecodeConfiguration(); - private SourceConfiguration mSourceConfiguration = - SourceConfigFactory.getDefaultSourceConfiguration(); - private EventLogConfiguration mEventLogConfiguration = - new EventLogConfiguration(); + private AuxDecodeConfiguration mAuxDecodeConfiguration = new AuxDecodeConfiguration(); + private SourceConfiguration mSourceConfiguration = SourceConfigFactory.getDefaultSourceConfiguration(); + private EventLogConfiguration mEventLogConfiguration = new EventLogConfiguration(); private RecordConfiguration mRecordConfiguration = new RecordConfiguration(); private String mAliasListName; @@ -64,7 +64,9 @@ public class Channel extends Configuration implements Listener + * + ******************************************************************************/ +package io.github.dsheirer.controller.channel; + +import io.github.dsheirer.module.decode.p25.DecodeConfigP25Phase1; +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.ArrayList; +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(); + } + } + } + + public static void main(String[] args) + { + List channels = new ArrayList<>(); + Channel channel1 = new Channel(); + channel1.setSystem("A System"); + channel1.setSite("My Site"); + channel1.setName("Channel 1 Name"); + channel1.setDecodeConfiguration(new DecodeConfigP25Phase1()); + channels.add(channel1); + + Channel channel2 = new Channel(); + channel2.setSystem("B System"); + channel2.setSite("My Site2"); + channel2.setName("Channel 2 Name"); + channel2.setDecodeConfiguration(new DecodeConfigP25Phase1()); + channel2.setAutoStartOrder(2); + channels.add(channel2); + + Channel channel3 = new Channel(); + channel3.setSystem("C System"); + channel3.setSite("My Site3"); + channel3.setName("Channel 3 Name"); + channel3.setDecodeConfiguration(new DecodeConfigP25Phase1()); + channel3.setAutoStartOrder(1); + channels.add(channel3); + + ChannelEventListener listener = new ChannelEventListener() + { + @Override + public void channelChanged(ChannelEvent event) + { + mLog.debug("Channel Event: " + event); + } + }; + + ChannelAutoStartFrame frame = new ChannelAutoStartFrame(listener, channels); + } +} 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..94e0090d 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 @@ -244,6 +307,19 @@ public class NameConfigurationEditor extends Editor implements Document mChannelName.setText(null); } + if(channel.isAutoStart()) + { + mAutoStartCheckBox.setSelected(channel.isAutoStart()); + mAutoStartOrder.setEnabled(true); + } + else + { + mAutoStartCheckBox.setSelected(false); + mAutoStartOrder.setEnabled(false); + } + + mAutoStartOrder.setValue(channel.hasAutoStartOrder() ? channel.getAutoStartOrder() : 0); + setModified(false); } 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/spectrum/ChannelSpectrumPanel.java b/src/main/java/io/github/dsheirer/spectrum/ChannelSpectrumPanel.java index 7b7bf9d0..ff383ac1 100644 --- a/src/main/java/io/github/dsheirer/spectrum/ChannelSpectrumPanel.java +++ b/src/main/java/io/github/dsheirer/spectrum/ChannelSpectrumPanel.java @@ -30,8 +30,13 @@ import io.github.dsheirer.spectrum.menu.SmoothingItem; import io.github.dsheirer.spectrum.menu.SmoothingTypeItem; import net.miginfocom.swing.MigLayout; -import javax.swing.*; -import java.awt.*; +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 java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.event.MouseEvent; @@ -193,7 +198,7 @@ public class ChannelSpectrumPanel extends JPanel private void start() { - if( mEnabled.get() && mCurrentChannel != null && mCurrentChannel.getEnabled() ) + if( mEnabled.get() && mCurrentChannel != null && mCurrentChannel.isProcessing() ) { ProcessingChain processingChain = mChannelProcessingManager .getProcessingChain( mCurrentChannel ); @@ -209,7 +214,7 @@ public class ChannelSpectrumPanel extends JPanel private void stop() { - if( mCurrentChannel != null && mCurrentChannel.getEnabled() ) + if( mCurrentChannel != null && mCurrentChannel.isProcessing() ) { ProcessingChain processingChain = mChannelProcessingManager .getProcessingChain( mCurrentChannel ); diff --git a/src/main/java/io/github/dsheirer/spectrum/OverlayPanel.java b/src/main/java/io/github/dsheirer/spectrum/OverlayPanel.java index d1317e11..3647a047 100644 --- a/src/main/java/io/github/dsheirer/spectrum/OverlayPanel.java +++ b/src/main/java/io/github/dsheirer/spectrum/OverlayPanel.java @@ -34,8 +34,16 @@ import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeProcessor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.swing.*; -import java.awt.*; +import javax.swing.JPanel; +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.RenderingHints; +import java.awt.Stroke; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.geom.Line2D; @@ -517,14 +525,14 @@ public class OverlayPanel extends JPanel implements ChannelEventListener, IFrequ 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); } From 05eea44f7e1ddd50e61c0a0b38920a283f96638a Mon Sep 17 00:00:00 2001 From: Denny Date: Sat, 10 Feb 2018 08:14:00 -0500 Subject: [PATCH 04/23] Cleanup --- .../channel/ChannelAutoStartFrame.java | 40 ------------------- 1 file changed, 40 deletions(-) diff --git a/src/main/java/io/github/dsheirer/controller/channel/ChannelAutoStartFrame.java b/src/main/java/io/github/dsheirer/controller/channel/ChannelAutoStartFrame.java index 0fe0481c..92dbc945 100644 --- a/src/main/java/io/github/dsheirer/controller/channel/ChannelAutoStartFrame.java +++ b/src/main/java/io/github/dsheirer/controller/channel/ChannelAutoStartFrame.java @@ -15,7 +15,6 @@ ******************************************************************************/ package io.github.dsheirer.controller.channel; -import io.github.dsheirer.module.decode.p25.DecodeConfigP25Phase1; import io.github.dsheirer.util.ThreadPool; import net.miginfocom.swing.MigLayout; import org.slf4j.Logger; @@ -33,7 +32,6 @@ import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; -import java.util.ArrayList; import java.util.List; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -245,42 +243,4 @@ public class ChannelAutoStartFrame extends JFrame } } } - - public static void main(String[] args) - { - List channels = new ArrayList<>(); - Channel channel1 = new Channel(); - channel1.setSystem("A System"); - channel1.setSite("My Site"); - channel1.setName("Channel 1 Name"); - channel1.setDecodeConfiguration(new DecodeConfigP25Phase1()); - channels.add(channel1); - - Channel channel2 = new Channel(); - channel2.setSystem("B System"); - channel2.setSite("My Site2"); - channel2.setName("Channel 2 Name"); - channel2.setDecodeConfiguration(new DecodeConfigP25Phase1()); - channel2.setAutoStartOrder(2); - channels.add(channel2); - - Channel channel3 = new Channel(); - channel3.setSystem("C System"); - channel3.setSite("My Site3"); - channel3.setName("Channel 3 Name"); - channel3.setDecodeConfiguration(new DecodeConfigP25Phase1()); - channel3.setAutoStartOrder(1); - channels.add(channel3); - - ChannelEventListener listener = new ChannelEventListener() - { - @Override - public void channelChanged(ChannelEvent event) - { - mLog.debug("Channel Event: " + event); - } - }; - - ChannelAutoStartFrame frame = new ChannelAutoStartFrame(listener, channels); - } } From ec30cd6305dff0411c897d88a20209f74b0cf9fa Mon Sep 17 00:00:00 2001 From: Denny Date: Sat, 10 Feb 2018 08:28:40 -0500 Subject: [PATCH 05/23] Updates changelog --- CHANGELOG | 5 +++++ 1 file changed, 5 insertions(+) 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 From bb078de0a9cd84ac8a0d7a8f6f4f14465f9cb12e Mon Sep 17 00:00:00 2001 From: Denny Date: Sat, 10 Feb 2018 10:23:28 -0500 Subject: [PATCH 06/23] Cleanup from last PR. --- .../channel/NameConfigurationEditor.java | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) 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 94e0090d..6699e177 100644 --- a/src/main/java/io/github/dsheirer/controller/channel/NameConfigurationEditor.java +++ b/src/main/java/io/github/dsheirer/controller/channel/NameConfigurationEditor.java @@ -300,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 { @@ -307,18 +320,6 @@ public class NameConfigurationEditor extends Editor implements Document mChannelName.setText(null); } - if(channel.isAutoStart()) - { - mAutoStartCheckBox.setSelected(channel.isAutoStart()); - mAutoStartOrder.setEnabled(true); - } - else - { - mAutoStartCheckBox.setSelected(false); - mAutoStartOrder.setEnabled(false); - } - - mAutoStartOrder.setValue(channel.hasAutoStartOrder() ? channel.getAutoStartOrder() : 0); setModified(false); } From 54227703a25c0837567675a353daa928f275acbd Mon Sep 17 00:00:00 2001 From: Denny Date: Sun, 11 Feb 2018 06:12:57 -0500 Subject: [PATCH 07/23] Cleanup from last PR. --- .../instrument/gui/DecoderSelectionFrame.java | 41 +++--- .../gui/viewer/DemodulatorViewerFX.java | 120 ++++++++++++++++++ .../gui/viewer/PlaybackController.java | 81 ++++++++++++ .../instrument/gui/viewer/ViewerDesktop.java | 50 ++++++++ 4 files changed, 277 insertions(+), 15 deletions(-) create mode 100644 src/main/java/io/github/dsheirer/instrument/gui/viewer/DemodulatorViewerFX.java create mode 100644 src/main/java/io/github/dsheirer/instrument/gui/viewer/PlaybackController.java create mode 100644 src/main/java/io/github/dsheirer/instrument/gui/viewer/ViewerDesktop.java 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..a0747fb9 --- /dev/null +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/DemodulatorViewerFX.java @@ -0,0 +1,120 @@ +/******************************************************************************* + * 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 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 java.io.File; + +/** + * Application for viewing and working with demodulators and recording files. + */ +public class DemodulatorViewerFX extends Application +{ + private Stage mStage; + private MenuBar mMenuBar; + private ViewerDesktop mViewerDesktop; + + @Override + public void start(Stage primaryStage) throws Exception + { + mStage = primaryStage; + mStage.setTitle("Demodulator Viewer FX"); + + BorderPane borderPane = new BorderPane(); + borderPane.setTop(getMenuBar()); + borderPane.setCenter(getViewerDesktop()); + + Scene scene = new Scene(borderPane, 1200, 800); + + mStage.setScene(scene); + mStage.show(); + } + + private ViewerDesktop getViewerDesktop() + { + if(mViewerDesktop == null) + { + mViewerDesktop = new ViewerDesktop(); + } + + return mViewerDesktop; + } + + private MenuBar getMenuBar() + { + if(mMenuBar == null) + { + mMenuBar = new MenuBar(); + + Menu menuFile = new Menu("File"); + MenuItem menuItemOpen = new MenuItem("Open ..."); + menuItemOpen.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); + } + }); + + MenuItem menuItemClose = new MenuItem("Close"); + menuItemClose.setOnAction(new EventHandler() + { + @Override + public void handle(ActionEvent event) + { + getViewerDesktop().close(); + } + }); + + MenuItem menuItemExit = new MenuItem("Exit"); + menuItemExit.setOnAction(new EventHandler() + { + @Override + public void handle(ActionEvent event) + { + Platform.exit(); + } + }); + + menuFile.getItems().addAll(menuItemOpen, menuItemClose, new SeparatorMenuItem(), menuItemExit); + + mMenuBar.getMenus().addAll(menuFile); + } + + return mMenuBar; + } + + 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..6d09b92b --- /dev/null +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/PlaybackController.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 javafx.scene.control.Button; +import javafx.scene.layout.HBox; + +import java.nio.file.Path; + +public class PlaybackController extends HBox +{ + private Button mPlay1Button; + private Button mPlay10Button; + private Button mPlay100Button; + private Button mPlay1000Button; + + public PlaybackController() + { + getChildren().addAll(getPlay1Button(), getPlay10Button(), getPlay100Button(), getPlay1000Button()); + + } + + public void load(Path path) + { + + } + + public Button getPlay1Button() + { + if(mPlay1Button == null) + { + mPlay1Button = new Button("> 1"); + } + + return mPlay1Button; + } + + public Button getPlay10Button() + { + if(mPlay10Button == null) + { + mPlay10Button = new Button(">> 10"); + } + + return mPlay10Button; + } + + public Button getPlay100Button() + { + if(mPlay100Button == null) + { + mPlay100Button = new Button(">> 100"); + } + + return mPlay100Button; + } + + public Button getPlay1000Button() + { + if(mPlay1000Button == null) + { + mPlay1000Button = new Button(">> 1000"); + } + + return mPlay1000Button; + } + +} 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..85b774cd --- /dev/null +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/ViewerDesktop.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.instrument.gui.viewer; + +import javafx.scene.layout.BorderPane; + +import java.io.File; + +public class ViewerDesktop extends BorderPane +{ + private PlaybackController mPlaybackController; + + public ViewerDesktop() + { + setBottom(getPlaybackController()); + } + + public void load(File file) + { + + } + + public void close() + { + + } + + private PlaybackController getPlaybackController() + { + if(mPlaybackController == null) + { + mPlaybackController = new PlaybackController(); + } + + return mPlaybackController; + } +} From 28f351287cf7f44238429144c1865d085a5300d7 Mon Sep 17 00:00:00 2001 From: Denny Date: Sat, 10 Feb 2018 10:23:28 -0500 Subject: [PATCH 08/23] 328 - cleanup issue with NPE --- .../channel/NameConfigurationEditor.java | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) 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 94e0090d..6699e177 100644 --- a/src/main/java/io/github/dsheirer/controller/channel/NameConfigurationEditor.java +++ b/src/main/java/io/github/dsheirer/controller/channel/NameConfigurationEditor.java @@ -300,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 { @@ -307,18 +320,6 @@ public class NameConfigurationEditor extends Editor implements Document mChannelName.setText(null); } - if(channel.isAutoStart()) - { - mAutoStartCheckBox.setSelected(channel.isAutoStart()); - mAutoStartOrder.setEnabled(true); - } - else - { - mAutoStartCheckBox.setSelected(false); - mAutoStartOrder.setEnabled(false); - } - - mAutoStartOrder.setValue(channel.hasAutoStartOrder() ? channel.getAutoStartOrder() : 0); setModified(false); } From 1406d5576af0614b03ad43076832466566dcd961 Mon Sep 17 00:00:00 2001 From: Denny Date: Sun, 11 Feb 2018 10:16:50 -0500 Subject: [PATCH 09/23] 330 - Adds new JavaFX implementation of the demodulator viewer applicaiton with initial support for playback control and I/Q sample axis viewing. --- build.gradle | 2 + .../buffer/ComplexCircularBuffer.java | 131 ++++++----- .../github/dsheirer/dsp/gain/ComplexGain.java | 49 ++-- .../gui/viewer/ComplexSampleLineChart.java | 119 ++++++++++ .../gui/viewer/PlaybackController.java | 220 +++++++++++++++++- .../instrument/gui/viewer/ViewerDesktop.java | 16 +- .../dsheirer/sample/ConversionUtils.java | 5 +- .../source/wave/ComplexWaveSource.java | 3 +- 8 files changed, 449 insertions(+), 96 deletions(-) create mode 100644 src/main/java/io/github/dsheirer/instrument/gui/viewer/ComplexSampleLineChart.java diff --git a/build.gradle b/build.gradle index 5039a823..b89f9ef5 100644 --- a/build.gradle +++ b/build.gradle @@ -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/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/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/instrument/gui/viewer/ComplexSampleLineChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/ComplexSampleLineChart.java new file mode 100644 index 00000000..c141a3d3 --- /dev/null +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/ComplexSampleLineChart.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; + +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 ComplexGain mComplexGain = new ComplexGain(1000.0f); + private ObservableList> mISamples = FXCollections.observableArrayList(); + private ObservableList> mQSamples = FXCollections.observableArrayList(); + private IntegerProperty mLengthProperty = new SimpleIntegerProperty(40); + + public ComplexSampleLineChart(int length) + { + super(new NumberAxis("Sample Number", 1, length, 1), + new NumberAxis("Value", -1.0, 1.0, 0.1)); + + 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); + } + + 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 = 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; + + for(int x = 0; x < samples.length; x += 2) + { + sample = new Complex(samples[x], samples[x + 1]); + mComplexGain.apply(sample); + mComplexCircularBuffer.put(sample); + } + + updateChart(); + } + + private void updateChart() + { + Complex[] samples = mComplexCircularBuffer.getAll(); + + for(int x = 0; x < samples.length; x++) + { + Complex sample = samples[x]; + + if(sample != null) + { + mISamples.get(x).setYValue(sample.inphase()); + mQSamples.get(x).setYValue(sample.quadrature()); + } + } + } +} 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 index 6d09b92b..16f5f055 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/PlaybackController.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/PlaybackController.java @@ -15,67 +15,263 @@ ******************************************************************************/ package io.github.dsheirer.instrument.gui.viewer; +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 java.nio.file.Path; +import javax.sound.sampled.UnsupportedAudioFileException; +import java.io.File; +import java.io.IOException; -public class PlaybackController extends HBox +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 IControllableFileSource mControllableFileSource; + private Broadcaster mComplexBufferBroadcaster = new Broadcaster<>(); public PlaybackController() { - getChildren().addAll(getPlay1Button(), getPlay10Button(), getPlay100Button(), getPlay1000Button()); + super(10); + getChildren().addAll(getControlsBox(), getFileLabel()); + + setControlsEnabled(false); } - public void load(Path path) + 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(); + } + 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") + "]"); + + 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 Button getPlay1Button() + /** + * 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 = new Button("1"); + mPlay1Button.setUserData(1); + mPlay1Button.setOnAction(getPlaybackEventHandler()); + mPlay1Button.setGraphic(new FontIcon(FontAwesome.PLAY)); } return mPlay1Button; } - public Button getPlay10Button() + private Button getPlay10Button() { if(mPlay10Button == null) { - mPlay10Button = new Button(">> 10"); + mPlay10Button = new Button("10"); + mPlay10Button.setUserData(10); + mPlay10Button.setOnAction(getPlaybackEventHandler()); + mPlay10Button.setGraphic(new FontIcon(FontAwesome.FAST_FORWARD)); } return mPlay10Button; } - public Button getPlay100Button() + private Button getPlay100Button() { if(mPlay100Button == null) { - mPlay100Button = new Button(">> 100"); + mPlay100Button = new Button("100"); + mPlay100Button.setUserData(100); + mPlay100Button.setOnAction(getPlaybackEventHandler()); + mPlay100Button.setGraphic(new FontIcon(FontAwesome.FAST_FORWARD)); } return mPlay100Button; } - public Button getPlay1000Button() + private Button getPlay1000Button() { if(mPlay1000Button == null) { - mPlay1000Button = new Button(">> 1000"); + 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/ViewerDesktop.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/ViewerDesktop.java index 85b774cd..2c9449bf 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/ViewerDesktop.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/ViewerDesktop.java @@ -22,15 +22,18 @@ import java.io.File; public class ViewerDesktop extends BorderPane { private PlaybackController mPlaybackController; + private ComplexSampleLineChart mSampleLineChart; public ViewerDesktop() { setBottom(getPlaybackController()); + getPlaybackController().addListener(getSampleLineChart()); + setCenter(getSampleLineChart()); } public void load(File file) { - + getPlaybackController().load(file); } public void close() @@ -47,4 +50,15 @@ public class ViewerDesktop extends BorderPane return mPlaybackController; } + + + private ComplexSampleLineChart getSampleLineChart() + { + if(mSampleLineChart == null) + { + mSampleLineChart = new ComplexSampleLineChart(40); + } + + return mSampleLineChart; + } } diff --git a/src/main/java/io/github/dsheirer/sample/ConversionUtils.java b/src/main/java/io/github/dsheirer/sample/ConversionUtils.java index 95d72bcb..7ad5c27c 100644 --- a/src/main/java/io/github/dsheirer/sample/ConversionUtils.java +++ b/src/main/java/io/github/dsheirer/sample/ConversionUtils.java @@ -21,14 +21,13 @@ public class ConversionUtils */ public static float[] convertFromSigned16BitSamples( ByteBuffer buffer ) { - ShortBuffer byteBuffer = - buffer.order( ByteOrder.LITTLE_ENDIAN ).asShortBuffer(); + ShortBuffer byteBuffer = buffer.order( ByteOrder.LITTLE_ENDIAN ).asShortBuffer(); float[] samples = new float[ buffer.limit() / 2 ]; for( int x = 0; x < samples.length; x++ ) { - samples[ x ] = (float)byteBuffer.get() / 32767.0f; + samples[ x ] = (float)byteBuffer.get() / (float)Short.MAX_VALUE; } return samples; 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 8ece68b6..d227a199 100644 --- a/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java +++ b/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java @@ -239,8 +239,7 @@ public class ComplexWaveSource extends ComplexSource implements IControllableFil buffer = Arrays.copyOf(buffer, samplesRead); } - float[] samples = ConversionUtils - .convertFromSigned16BitSamples(buffer); + float[] samples = ConversionUtils.convertFromSigned16BitSamples(buffer); mListener.receive(new ComplexBuffer(samples)); } From 956bf2d76aedc74a8814de53c21e3201c28bda68 Mon Sep 17 00:00:00 2001 From: Denny Date: Mon, 12 Feb 2018 12:34:42 +0000 Subject: [PATCH 10/23] Adds phase viewer and recent files menu item to viewer desktop. --- .../gui/viewer/ComplexPhaseLineChart.java | 122 ++++++++++++++++++ .../gui/viewer/ComplexSampleLineChart.java | 8 +- .../gui/viewer/DemodulatorViewerFX.java | 25 +++- .../gui/viewer/PlaybackController.java | 15 +++ .../gui/viewer/RecentFilesMenuItem.java | 106 +++++++++++++++ .../instrument/gui/viewer/ViewerDesktop.java | 30 ++++- .../dsheirer/sample/complex/Complex.java | 5 + 7 files changed, 305 insertions(+), 6 deletions(-) create mode 100644 src/main/java/io/github/dsheirer/instrument/gui/viewer/ComplexPhaseLineChart.java create mode 100644 src/main/java/io/github/dsheirer/instrument/gui/viewer/RecentFilesMenuItem.java diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/ComplexPhaseLineChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/ComplexPhaseLineChart.java new file mode 100644 index 00000000..08189c93 --- /dev/null +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/ComplexPhaseLineChart.java @@ -0,0 +1,122 @@ +/******************************************************************************* + * 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.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.apache.commons.math3.complex.ComplexUtils; +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((float)(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, 1), + new NumberAxis("Phase", -Math.PI, Math.PI, 1.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 - 10).setYValue(sample.angle()); + } + } + } +} diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/ComplexSampleLineChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/ComplexSampleLineChart.java index c141a3d3..7044bdac 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/ComplexSampleLineChart.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/ComplexSampleLineChart.java @@ -37,7 +37,7 @@ public class ComplexSampleLineChart extends LineChart implements Listener> mISamples = FXCollections.observableArrayList(); private ObservableList> mQSamples = FXCollections.observableArrayList(); private IntegerProperty mLengthProperty = new SimpleIntegerProperty(40); @@ -94,8 +94,9 @@ public class ComplexSampleLineChart extends LineChart implements Listener +{ + private final static Logger mLog = LoggerFactory.getLogger(RecentFilesMenuItem.class); + + private Preferences mPreferences = Preferences.userNodeForPackage(RecentFilesMenuItem.class); + private IFileSelectionListener mFileSelectionListener; + private String mIdentifier; + private int mSize; + + public RecentFilesMenuItem(String identifier, int size) + { + super("Recent"); + mIdentifier = identifier; + mSize = size; + + load(); + } + + public void setFileSelectionListener(IFileSelectionListener listener) + { + mFileSelectionListener = listener; + } + + public void add(File file) + { + if(file != null) + { + int index = mSize - 1; + + 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(); + } + + private String getKey(int index) + { + return mIdentifier + ".recent." + index; + } + + 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); + } + } + } + } + + @Override + public void handle(ActionEvent event) + { + if(mFileSelectionListener != null) + { + Object fileObject = ((MenuItem)event.getSource()).getUserData(); + + if(fileObject instanceof File) + { + mFileSelectionListener.fileSelected((File)fileObject); + } + } + } + + 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 index 2c9449bf..38e84acc 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/ViewerDesktop.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/ViewerDesktop.java @@ -16,29 +16,35 @@ package io.github.dsheirer.instrument.gui.viewer; import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; import java.io.File; public class ViewerDesktop extends BorderPane { private PlaybackController mPlaybackController; + private VBox mChartBox; private ComplexSampleLineChart mSampleLineChart; + private ComplexPhaseLineChart mPhaseLineChart; public ViewerDesktop() { setBottom(getPlaybackController()); getPlaybackController().addListener(getSampleLineChart()); - setCenter(getSampleLineChart()); + getPlaybackController().addListener(getPhaseLineChart()); + setCenter(getChartBox()); } public void load(File file) { + close(); getPlaybackController().load(file); } public void close() { - + getPlaybackController().close(); } private PlaybackController getPlaybackController() @@ -51,6 +57,16 @@ public class ViewerDesktop extends BorderPane return mPlaybackController; } + private VBox getChartBox() + { + if(mChartBox == null) + { + mChartBox = new VBox(); + mChartBox.getChildren().addAll(getSampleLineChart(), getPhaseLineChart()); + } + + return mChartBox; + } private ComplexSampleLineChart getSampleLineChart() { @@ -61,4 +77,14 @@ public class ViewerDesktop extends BorderPane return mSampleLineChart; } + + private ComplexPhaseLineChart getPhaseLineChart() + { + if(mPhaseLineChart == null) + { + mPhaseLineChart = new ComplexPhaseLineChart(50); + } + + return mPhaseLineChart; + } } diff --git a/src/main/java/io/github/dsheirer/sample/complex/Complex.java b/src/main/java/io/github/dsheirer/sample/complex/Complex.java index e9a7fdc0..afeaee9f 100644 --- a/src/main/java/io/github/dsheirer/sample/complex/Complex.java +++ b/src/main/java/io/github/dsheirer/sample/complex/Complex.java @@ -358,6 +358,11 @@ public class Complex implements Serializable return (float)Math.atan2( y(), x() ); } + public float angleDegrees() + { + return (float)Math.toDegrees(angle()); + } + /** * Constrains the i and q quantities to +/- value. * From 80c5ad1bc89b000634e6aba634482f2da8bfc30c Mon Sep 17 00:00:00 2001 From: Denny Date: Tue, 13 Feb 2018 12:31:50 +0000 Subject: [PATCH 11/23] WIP - updates to the demodulator viewer app and the recently used files menu. --- .../gui/viewer/ComplexPhaseLineChart.java | 2 +- .../gui/viewer/DemodulatorViewerFX.java | 18 +- .../gui/viewer/PlaybackController.java | 3 + .../gui/viewer/RecentFilesMenu.java | 192 ++++++++++++++++++ .../gui/viewer/RecentFilesMenuItem.java | 106 ---------- 5 files changed, 204 insertions(+), 117 deletions(-) create mode 100644 src/main/java/io/github/dsheirer/instrument/gui/viewer/RecentFilesMenu.java delete mode 100644 src/main/java/io/github/dsheirer/instrument/gui/viewer/RecentFilesMenuItem.java diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/ComplexPhaseLineChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/ComplexPhaseLineChart.java index 08189c93..9ec6d77f 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/ComplexPhaseLineChart.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/ComplexPhaseLineChart.java @@ -115,7 +115,7 @@ public class ComplexPhaseLineChart extends LineChart implements Listener +{ + 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/RecentFilesMenuItem.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/RecentFilesMenuItem.java deleted file mode 100644 index 8efb0899..00000000 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/RecentFilesMenuItem.java +++ /dev/null @@ -1,106 +0,0 @@ -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 org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.File; -import java.util.prefs.Preferences; - -public class RecentFilesMenuItem extends Menu implements EventHandler -{ - private final static Logger mLog = LoggerFactory.getLogger(RecentFilesMenuItem.class); - - private Preferences mPreferences = Preferences.userNodeForPackage(RecentFilesMenuItem.class); - private IFileSelectionListener mFileSelectionListener; - private String mIdentifier; - private int mSize; - - public RecentFilesMenuItem(String identifier, int size) - { - super("Recent"); - mIdentifier = identifier; - mSize = size; - - load(); - } - - public void setFileSelectionListener(IFileSelectionListener listener) - { - mFileSelectionListener = listener; - } - - public void add(File file) - { - if(file != null) - { - int index = mSize - 1; - - 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(); - } - - private String getKey(int index) - { - return mIdentifier + ".recent." + index; - } - - 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); - } - } - } - } - - @Override - public void handle(ActionEvent event) - { - if(mFileSelectionListener != null) - { - Object fileObject = ((MenuItem)event.getSource()).getUserData(); - - if(fileObject instanceof File) - { - mFileSelectionListener.fileSelected((File)fileObject); - } - } - } - - public interface IFileSelectionListener - { - void fileSelected(File file); - } -} From eaf7c7d11ee9394f2f9f0237da5ca429ae92837b Mon Sep 17 00:00:00 2001 From: Denny Date: Sat, 17 Feb 2018 13:41:36 -0500 Subject: [PATCH 12/23] 330 WIP on redesigning the P25 C4FM decoder. Creates a new decoder based on the P25 LSM decoder using a costas loop and a gardner detector. Updates the Demodulator Viewer FX application to add in a symbol chart and a phase error chart. The decoder won't lock on yet ... still a wip. --- .../dsheirer/buffer/DoubleCircularBuffer.java | 196 ++- .../dsp/filter/design/FilterViewer.java | 21 +- .../filter/fir/complex/ComplexFIRFilter.java | 126 +- .../fir/complex/ComplexFIRFilter_CB_CB.java | 3 +- .../gain/ComplexFeedForwardGainControl.java | 205 +-- .../github/dsheirer/dsp/mixer/Oscillator.java | 3 +- .../psk/DQPSKSymbolPhaseErrorCalculator.java | 30 + .../dsheirer/dsp/psk/GardnerDetector.java | 82 ++ .../dsheirer/dsp/psk/IQPSKSymbolDecoder.java | 29 + .../dsp/psk/ISymbolPhaseErrorCalculator.java | 35 + .../dsp/psk/InterpolatingSymbolBuffer.java | 187 +++ .../dsp/psk/LSMConstellationDecoder.java | 28 + .../dsheirer/dsp/psk/LSMDemodulator.java | 950 +++++++------- .../dsheirer/dsp/psk/QPSKDemodulator.java | 182 +++ ...olarSlicer.java => QPSKSymbolDecoder.java} | 40 +- .../psk/QPSKSymbolPhaseErrorCalculator.java | 63 + .../dsheirer/dsp/psk/SymbolInversion.java | 35 + .../dsheirer/dsp/psk/pll/CostasLoop.java | 337 +++++ .../dsp/psk/pll/IPhaseLockedLoop.java | 56 + .../github/dsheirer/dsp/psk/pll/Tracking.java | 48 + .../gui/viewer/DemodulatorViewerFX.java | 60 +- .../gui/viewer/PlaybackController.java | 17 + .../instrument/gui/viewer/ViewerDesktop.java | 69 +- .../{ => chart}/ComplexPhaseLineChart.java | 9 +- .../{ => chart}/ComplexSampleLineChart.java | 6 +- .../gui/viewer/chart/PhaseLineChart.java | 73 ++ .../gui/viewer/chart/SymbolChart.java | 90 ++ .../gui/viewer/decoder/DecoderPane.java | 62 + .../viewer/decoder/DecoderPaneFactory.java | 55 + .../gui/viewer/decoder/P25Phase1Pane.java | 141 ++ .../dsheirer/module/decode/Decoder.java | 80 +- .../decode/fleetsync2/Fleetsync2Decoder.java | 2 +- .../module/decode/lj1200/LJ1200Decoder.java | 2 +- .../module/decode/ltrnet/LTRNetDecoder.java | 2 +- .../ltrstandard/LTRStandardDecoder.java | 2 +- .../module/decode/mdc1200/MDCDecoder.java | 2 +- .../module/decode/mpt1327/MPT1327Decoder.java | 2 +- .../module/decode/p25/P25Decoder.java | 133 +- .../module/decode/p25/P25MessageFramer.java | 1139 ++++++++--------- .../module/decode/p25/P25_C4FMDecoder.java | 91 -- .../module/decode/p25/P25_C4FMDecoder2.java | 223 ++++ .../module/decode/p25/P25_LSMDecoder.java | 262 ++-- .../decode/passport/PassportDecoder.java | 2 +- .../module/decode/tait/Tait1200Decoder.java | 4 +- .../dsheirer/record/RecorderManager.java | 3 +- .../dsheirer/sample/complex/Complex.java | 779 +++++------ .../source/IControllableFileSource.java | 53 +- 47 files changed, 3926 insertions(+), 2093 deletions(-) create mode 100644 src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolPhaseErrorCalculator.java create mode 100644 src/main/java/io/github/dsheirer/dsp/psk/GardnerDetector.java create mode 100644 src/main/java/io/github/dsheirer/dsp/psk/IQPSKSymbolDecoder.java create mode 100644 src/main/java/io/github/dsheirer/dsp/psk/ISymbolPhaseErrorCalculator.java create mode 100644 src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer.java create mode 100644 src/main/java/io/github/dsheirer/dsp/psk/LSMConstellationDecoder.java create mode 100644 src/main/java/io/github/dsheirer/dsp/psk/QPSKDemodulator.java rename src/main/java/io/github/dsheirer/dsp/psk/{QPSKPolarSlicer.java => QPSKSymbolDecoder.java} (59%) create mode 100644 src/main/java/io/github/dsheirer/dsp/psk/QPSKSymbolPhaseErrorCalculator.java create mode 100644 src/main/java/io/github/dsheirer/dsp/psk/SymbolInversion.java create mode 100644 src/main/java/io/github/dsheirer/dsp/psk/pll/CostasLoop.java create mode 100644 src/main/java/io/github/dsheirer/dsp/psk/pll/IPhaseLockedLoop.java create mode 100644 src/main/java/io/github/dsheirer/dsp/psk/pll/Tracking.java rename src/main/java/io/github/dsheirer/instrument/gui/viewer/{ => chart}/ComplexPhaseLineChart.java (94%) rename src/main/java/io/github/dsheirer/instrument/gui/viewer/{ => chart}/ComplexSampleLineChart.java (96%) create mode 100644 src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/PhaseLineChart.java create mode 100644 src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/SymbolChart.java create mode 100644 src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPane.java create mode 100644 src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/DecoderPaneFactory.java create mode 100644 src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1Pane.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2.java diff --git a/src/main/java/io/github/dsheirer/buffer/DoubleCircularBuffer.java b/src/main/java/io/github/dsheirer/buffer/DoubleCircularBuffer.java index 0c9172fe..af5bd86d 100644 --- a/src/main/java/io/github/dsheirer/buffer/DoubleCircularBuffer.java +++ b/src/main/java/io/github/dsheirer/buffer/DoubleCircularBuffer.java @@ -1,80 +1,168 @@ /******************************************************************************* * SDR Trunk * Copyright (C) 2014 Dennis Sheirer - * + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with this program. If not, see ******************************************************************************/ package io.github.dsheirer.buffer; +import org.apache.commons.math3.stat.descriptive.moment.Mean; +import org.apache.commons.math3.stat.descriptive.moment.Variance; +import org.apache.commons.math3.stat.descriptive.rank.Max; +import org.apache.commons.math3.stat.descriptive.rank.Min; + import java.util.Arrays; /** - * Circular sample buffer - allocates a buffer and stores samples in a circular - * fashion, overwriting older samples with newly arrived samples. Initially - * fills buffer with 0-valued samples - * - * Can be used as a delay-type buffer, to delay samples by the 'size' amount + * Circular sample buffer - allocates a buffer and stores samples in a circular fashion, overwriting older samples with + * newly arrived samples. Initially fills buffer with 0-valued (default) samples or with a specified value. */ public class DoubleCircularBuffer { - double[] mBuffer; - int mBufferPointer = 0; - - public DoubleCircularBuffer( int size ) - { - mBuffer = new double[ size ]; - - Arrays.fill( mBuffer, 0.0 ); - } + private Variance mVariance; + private Max mMax; + private Mean mMean; + private Min mMin; + private double[] mBuffer; + private int mBufferPointer = 0; - /** - * Puts the new value into the buffer and returns the oldest buffer value - * that it replaced - * @param newValue - * @return - */ - public double get( double newValue ) - { - double oldestSample = mBuffer[ mBufferPointer ]; + /** + * Creates a circular double buffer of the specified size and all entries filled with the specified initial value. + * @param size of the buffer + * @param initialFillValue to fill the buffer entries + */ + public DoubleCircularBuffer(int size, double initialFillValue) + { + mBuffer = new double[size]; + Arrays.fill(mBuffer, initialFillValue); + } - mBuffer[ mBufferPointer ] = newValue; - - mBufferPointer++; - - if( mBufferPointer >= mBuffer.length ) - { - mBufferPointer = 0; - } - - return oldestSample; - } + /** + * Creates a circular double buffer of the specified size and all entries filled with an initial value of zero. + * @param size of the buffer + */ + public DoubleCircularBuffer(int size) + { + this(size, 0.0); + } - /** - * Returns the maximum from the values currently in the buffer - */ - public double max() - { - double max = 0.0d; - - for( int x = 0; x < mBuffer.length; x++ ) - { - if( mBuffer[ x ] > max ) - { - max = mBuffer[ x ]; - } - } - - return max; - } + /** + * Puts the new value into the buffer and returns the oldest buffer value that it replaced + * + * @param newValue to add to the buffer + * @return oldest value that was overwritten by the new value + */ + public double get(double newValue) + { + double oldestSample = mBuffer[mBufferPointer]; + + mBuffer[mBufferPointer] = newValue; + + mBufferPointer++; + + if(mBufferPointer >= mBuffer.length) + { + mBufferPointer = 0; + } + + return oldestSample; + } + + public double[] getAll() + { + int valuePointer = 0; + double[] values = new double[mBuffer.length]; + int bufferPointer = mBufferPointer; + + while(valuePointer < values.length) + { + values[valuePointer++] = mBuffer[bufferPointer++]; + + if(bufferPointer >= mBuffer.length) + { + bufferPointer = 0; + } + } + + return values; + } + + /** + * Places the new value into the buffer, overwriting the oldest value + * @param newValue to add to the buffer + */ + public void put(double newValue) + { + mBuffer[mBufferPointer] = newValue; + + mBufferPointer++; + + if(mBufferPointer >= mBuffer.length) + { + mBufferPointer = 0; + } + } + + /** + * Returns the maximum from the values currently in the buffer + */ + public double max() + { + if(mMax == null) + { + mMax = new Max(); + } + + return mMax.evaluate(mBuffer, 0, mBuffer.length); + } + + /** + * Returns the minimum from the values currently in the buffer + */ + public double min() + { + if(mMin == null) + { + mMin = new Min(); + } + + return mMin.evaluate(mBuffer, 0, mBuffer.length); + } + + /** + * Calculates the variance of the values contained in this buffer + */ + public double variance() + { + if(mVariance == null) + { + mVariance = new Variance(); + } + + return mVariance.evaluate(mBuffer); + } + + /** + * Calculates the average/mean of the values contained in this buffer + */ + public double mean() + { + if(mMean == null) + { + mMean = new Mean(); + } + + return mMean.evaluate(mBuffer, 0, mBuffer.length); + } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java index e2e7d9ca..c129a26d 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java @@ -39,15 +39,24 @@ public class FilterViewer extends Application */ private float[] getFilter() { +// FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() +// .sampleRate(48000) +// .gridDensity(16) +// .passBandCutoff(2500) +// .passBandAmplitude(1.0) +// .passBandRipple(0.01) +// .stopBandStart(4000) +// .stopBandAmplitude(0.0) +// .stopBandRipple(0.008) +// .build(); FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() - .sampleRate(48000) - .gridDensity(16) - .passBandCutoff(2500) + .sampleRate((int)48000.0) + .passBandCutoff(6000) .passBandAmplitude(1.0) - .passBandRipple(0.01) - .stopBandStart(4000) + .passBandRipple(0.1) .stopBandAmplitude(0.0) - .stopBandRipple(0.008) + .stopBandStart(6250) + .stopBandRipple(0.01) .build(); // FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder() 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 8190b9a1..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 @@ -1,54 +1,90 @@ +/******************************************************************************* + * sdr-trunk + * Copyright (C) 2014-2018 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. + * If not, see + * + ******************************************************************************/ package io.github.dsheirer.dsp.filter.fir.complex; import io.github.dsheirer.dsp.filter.fir.FIRFilter; import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter; import io.github.dsheirer.sample.complex.Complex; +import io.github.dsheirer.sample.complex.ComplexBuffer; public class ComplexFIRFilter extends FIRFilter { - private RealFIRFilter mIFilter; - private RealFIRFilter mQFilter; - - /** - * Complex FIR Filter for processing complex sample pairs. Wraps two real - * FIR filters for processing each of the inphase and quadrature samples. - * - * @param coefficients - filter taps - * @param gain - gain to apply to filtered outputs - use 1.0f for no gain - */ - public ComplexFIRFilter( float[] coefficients, float gain ) - { - mIFilter = new RealFIRFilter( coefficients, gain ); - mQFilter = new RealFIRFilter( coefficients, gain ); - } - - public float[] getCoefficients() - { - return mIFilter.getCoefficients(); - } - - public float filterInphase( float sample ) - { - return mIFilter.filter( sample ); - } - - public float filterQuadrature( float sample ) - { - return mQFilter.filter( sample ); - } - - public Complex filter( Complex sample ) - { - float i = filterInphase( sample.inphase() ); - float q = filterQuadrature( sample.quadrature() ); - - return new Complex( i, q ); - } - - @Override - public void dispose() - { - mIFilter.dispose(); - mQFilter.dispose(); - } + private RealFIRFilter mIFilter; + private RealFIRFilter mQFilter; + + /** + * Complex FIR Filter for processing complex sample pairs. Wraps two real + * FIR filters for processing each of the inphase and quadrature samples. + * + * @param coefficients - filter taps + * @param gain - gain to apply to filtered outputs - use 1.0f for no gain + */ + public ComplexFIRFilter(float[] coefficients, float gain) + { + mIFilter = new RealFIRFilter(coefficients, gain); + mQFilter = new RealFIRFilter(coefficients, gain); + } + + public float[] getCoefficients() + { + return mIFilter.getCoefficients(); + } + + public float filterInphase(float sample) + { + return mIFilter.filter(sample); + } + + public float filterQuadrature(float sample) + { + return mQFilter.filter(sample); + } + + public Complex filter(Complex sample) + { + float i = filterInphase(sample.inphase()); + float q = filterQuadrature(sample.quadrature()); + + return new Complex(i, q); + } + + /** + * Filters the complex samples from the buffer and returns a new complex buffer with the filtered output + * + * @param buffer with complex samples to filter + * @return new buffer containing filtered complex samples + */ + public ComplexBuffer filter(ComplexBuffer buffer) + { + float[] samples = buffer.getSamples(); + float[] filtered = new float[samples.length]; + + for(int x = 0; x < samples.length; x += 2) + { + filtered[x] = filterInphase(samples[x]); + filtered[x + 1] = filterQuadrature(samples[x + 1]); + } + + return new ComplexBuffer(filtered); + } + + @Override + public void dispose() + { + mIFilter.dispose(); + mQFilter.dispose(); + } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter_CB_CB.java b/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter_CB_CB.java index 33cd4862..d864d2e2 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter_CB_CB.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter_CB_CB.java @@ -3,8 +3,7 @@ package io.github.dsheirer.dsp.filter.fir.complex; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.complex.ComplexBuffer; -public class ComplexFIRFilter_CB_CB extends ComplexFIRFilter - implements Listener +public class ComplexFIRFilter_CB_CB extends ComplexFIRFilter implements Listener { 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 c939b952..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.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 FloatCircularBuffer mEnvelopeHistory; - - private float mMaxEnvelope = 0.0f; - private float mGain = 1.0f; - - /** - * Dynamic gain control for incoming sample stream to amplify or attenuate - * all samples toward an objective unity)gain, using the maximum envelope - * value detected in the stream history window. - * - * Uses the specified damping factor to limit gain swings. Damping factor - * is applied against the delta between current gain value and a recalculated - * gain value to limit how quickly the gain value will increase or decrease. - * - * @param window - history size to use in detecting maximum envelope value - * @param damping - a value between 0 < damping <= 1.0; - */ - public ComplexFeedForwardGainControl( int window ) - { - mEnvelopeHistory = new FloatCircularBuffer( window ); - } - - public void dispose() - { - mListener = null; - } + private FloatCircularBuffer mEnvelopeHistory; - @Override - public void receive( float inphase, float quadrature ) - { - float envelope = Complex.envelope( inphase, quadrature ); + private float mMaxEnvelope = 0.0f; + private float mGain = 1.0f; - if( envelope > mMaxEnvelope ) - { - mMaxEnvelope = envelope; - - adjustGain(); - } + /** + * Dynamic gain control for incoming sample stream to amplify or attenuate + * all samples toward an objective unity)gain, using the maximum envelope + * value detected in the stream history window. + * + * Uses the specified damping factor to limit gain swings. Damping factor + * is applied against the delta between current gain value and a recalculated + * gain value to limit how quickly the gain value will increase or decrease. + * + * @param window - history size to use in detecting maximum envelope value + */ + public ComplexFeedForwardGainControl(int window) + { + mEnvelopeHistory = new FloatCircularBuffer(window); + } - /* Replace oldest envelope value with current envelope value */ - float oldestEnvelope = mEnvelopeHistory.get( envelope ); + public void dispose() + { + mListener = null; + } - /* If the oldest envelope value was the max envelope value, then we - * have to rediscover the max value from the envelope history */ - if( mMaxEnvelope == oldestEnvelope && mMaxEnvelope != envelope ) - { - mMaxEnvelope = MINIMUM_ENVELOPE; - - for( float value: mEnvelopeHistory.getBuffer() ) - { - if( value > mMaxEnvelope ) - { - mMaxEnvelope = value; - } - } - - adjustGain(); - } - - /* Apply current gain value to the sample and send to the listener */ - if( mListener != null ) - { - mListener.receive( inphase *= mGain, quadrature *= mGain ); - } - } - - private void adjustGain() - { - mGain = OBJECTIVE_ENVELOPE / mMaxEnvelope; - } + @Override + public void receive(float inphase, float quadrature) + { + process(inphase, quadrature); - public void setListener( ComplexSampleListener listener ) - { - mListener = listener; - } + /* Apply current gain value to the sample and send to the listener */ + if(mListener != null) + { + mListener.receive(inphase *= mGain, quadrature *= mGain); + } + } + + private void process(float inphase, float quadrature) + { + float envelope = Complex.envelope(inphase, quadrature); + + if(envelope > mMaxEnvelope) + { + mMaxEnvelope = envelope; + + adjustGain(); + } + + /* Replace oldest envelope value with current envelope value */ + float oldestEnvelope = mEnvelopeHistory.get(envelope); + + /* If the oldest envelope value was the max envelope value, then we + * have to rediscover the max value from the envelope history */ + if(mMaxEnvelope == oldestEnvelope && mMaxEnvelope != envelope) + { + mMaxEnvelope = MINIMUM_ENVELOPE; + + for(float value : mEnvelopeHistory.getBuffer()) + { + if(value > mMaxEnvelope) + { + mMaxEnvelope = value; + } + } + + adjustGain(); + } + } + + private void adjustGain() + { + mGain = OBJECTIVE_ENVELOPE / mMaxEnvelope; + } + + public void setListener(ComplexSampleListener listener) + { + mListener = listener; + } + + public ComplexBuffer filter(ComplexBuffer complexBuffer) + { + float[] samples = complexBuffer.getSamples(); + float[] processed = new float[samples.length]; + + for(int x = 0; x < samples.length; x += 2) + { + process(samples[x], samples[x + 1]); + + processed[x] = samples[x] * mGain; + processed[x + 1] = samples[x + 1] * mGain; + } + + return new ComplexBuffer(processed); + } } diff --git a/src/main/java/io/github/dsheirer/dsp/mixer/Oscillator.java b/src/main/java/io/github/dsheirer/dsp/mixer/Oscillator.java index 50511208..9825dfbb 100644 --- a/src/main/java/io/github/dsheirer/dsp/mixer/Oscillator.java +++ b/src/main/java/io/github/dsheirer/dsp/mixer/Oscillator.java @@ -51,8 +51,7 @@ public class Oscillator */ private void update() { - float anglePerSample = - (float)( 2.0d * Math.PI * mFrequency / mSampleRate ); + float anglePerSample = (float)( 2.0d * Math.PI * mFrequency / mSampleRate ); mAnglePerSample = Complex.fromAngle( anglePerSample ); } diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolPhaseErrorCalculator.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolPhaseErrorCalculator.java new file mode 100644 index 00000000..c299d122 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolPhaseErrorCalculator.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * sdr-trunk + * Copyright (C) 2014-2018 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. + * If not, see + * + ******************************************************************************/ +package io.github.dsheirer.dsp.psk; + +import io.github.dsheirer.sample.complex.Complex; + +public class DQPSKSymbolPhaseErrorCalculator extends QPSKSymbolPhaseErrorCalculator +{ + //Negative 45 degrees rotation to offset for differential additive 45 degrees of rotation + public static final Complex DIFFERENTIAL_OFFSET = Complex.fromAngle(Math.PI / -4.0d); + + @Override + public void adjust(Complex symbol) + { + symbol.multiply(DIFFERENTIAL_OFFSET); + } +} 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..023b3e87 --- /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 = (previous.inphase() - current.inphase()) * middle.inphase(); + mErrorQuadrature = (previous.quadrature() - current.quadrature()) * middle.quadrature(); + mError = normalize(mErrorInphase + mErrorQuadrature, 1.0f); + + return mError; + } + + /** + * Constrains timing error to +/- the maximum value and corrects any floating point invalid numbers + */ + public static float normalize(float error, float maximum) + { + if(Float.isNaN(error)) + { + return 0.0f; + } + else + { + return clip(error, maximum); + } + } + + /** + * Constrains value to the range of ( -maximum <> maximum ) + */ + public static float clip(float value, float maximum) + { + if(value > maximum) + { + return maximum; + } + else if(value < -maximum) + { + return -maximum; + } + + return value; + } +} 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..5b5df5a9 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/ISymbolPhaseErrorCalculator.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; + +import io.github.dsheirer.sample.complex.Complex; + +public interface ISymbolPhaseErrorCalculator +{ + /** + * Adjusts the calculated symbol as necessary before calculating any phase error. This might be useful for + * differential encoding to remove a 45 degree rotational spin. + * @param symbol to adjust + */ + void adjust(Complex symbol); + + /** + * Calculates the phase error of the symbol relative to an alignment for the constellation + * @param symbol to calculate error + * @return error value + */ + float getPhaseError(Complex symbol); +} diff --git a/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer.java b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer.java new file mode 100644 index 00000000..c67a2b9a --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer.java @@ -0,0 +1,187 @@ +/******************************************************************************* + * sdr-trunk + * Copyright (C) 2014-2018 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. + * If not, see + * + ******************************************************************************/ +package io.github.dsheirer.dsp.psk; + +import io.github.dsheirer.dsp.filter.interpolator.RealInterpolator; +import io.github.dsheirer.sample.complex.Complex; + +public class InterpolatingSymbolBuffer +{ + private Complex mMiddleSample = new Complex(0,0); + private Complex mCurrentSample = new Complex(0,0); + + private float[] mDelayLineInphase; + private float[] mDelayLineQuadrature; + private float mMu; //Sample Counter + private float mGainMu = 0.05f; //Sample Counter Gain + private float mOmega; + private float mGainOmega = 0.1f * mGainMu * mGainMu; + private float mOmegaRel = 0.005f; + private float mOmegaMid; + + private int mDelayLinePointer = 0; + private int mTwiceSamplesPerSymbol; + private RealInterpolator mInterpolator = new RealInterpolator(1.0f); + + public InterpolatingSymbolBuffer(float samplesPerSymbol) + { + mMu = samplesPerSymbol; + mTwiceSamplesPerSymbol = (int)Math.floor(2.0 * samplesPerSymbol); + mDelayLineInphase = new float[2 * mTwiceSamplesPerSymbol]; + mDelayLineQuadrature = new float[2 * mTwiceSamplesPerSymbol]; + mOmega = samplesPerSymbol; + mOmegaMid = samplesPerSymbol; + + } + + public void receive(Complex currentSample) + { + mMu--; + + //Fill up the delay line to use with the interpolator + mDelayLineInphase[mDelayLinePointer] = currentSample.inphase(); + mDelayLineInphase[mDelayLinePointer + mTwiceSamplesPerSymbol] = currentSample.inphase(); + mDelayLineQuadrature[mDelayLinePointer] = currentSample.quadrature(); + mDelayLineQuadrature[mDelayLinePointer + mTwiceSamplesPerSymbol] = currentSample.quadrature(); + + //Increment pointer and keep pointer in bounds + mDelayLinePointer = (mDelayLinePointer + 1) % mTwiceSamplesPerSymbol; + } + + /** + * Updates the internal sample counter with additional samples to add to the symbol sample counter. + * @param samplesToAdd to the sample counter + */ + public void increaseSampleCounter(float samplesToAdd) + { + mMu += samplesToAdd; + } + + /** + * Adjusts samples per symbol and symbol timing counters and increments the sample counter to collect another + * symbol. + * + * @param symbolTimingError from a symbol timing error detector + */ + public void resetAndAdjust(float symbolTimingError) + { + //mOmega is samples per symbol and is constrained to floating between +/- .005 of the nominal samples per + //symbol + mOmega = mOmega + mGainOmega * symbolTimingError; + mOmega = mOmegaMid + clip(mOmega - mOmegaMid, mOmegaRel); + + //Add another symbol's worth of samples to the counter and adjust timing based on gardner error + increaseSampleCounter((mOmega + (mGainMu * symbolTimingError))); + } + + /** + * Indicates if this buffer has accumulated a enough samples to represent a full symbol + */ + public boolean hasSymbol() + { + return mMu < 1.0f; + } + + /** + * Interpolated middle sample for the symbol. Note: this method should only be invoked after testing for a + * complete symbol with the hasSymbol() method. + */ + public Complex getMiddleSample() + { + /* Calculate interpolated middle sample and current sample */ + mMiddleSample.setValues(getInphase(mMu), getQuadrature(mMu)); + + return mMiddleSample; + } + + /** + * Interpolated current sample for the symbol. This sample represents the ideal symbol sampling point. + * Note: this method should only be invoked after testing for a complete symbol with the hasSymbol() method. + */ + public Complex getCurrentSample() + { + float half_omega = mOmega / 2.0f; + int half_sps = (int)Math.floor(half_omega); + float half_mu = mMu + half_omega - half_sps; + + if(half_mu > 1.0) + { + half_mu -= 1.0; + half_sps += 1; + } + + mCurrentSample.setValues(getInphase(half_mu, half_sps), getQuadrature(half_mu, half_sps)); + + return mCurrentSample; + } + + /** + * Returns the interpolated inphase value for the specified offset + * @param interpolation into the buffer to calculate the interpolated sample + * @return inphase value of the interpolated sample + */ + public float getInphase(float interpolation) + { + return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer, interpolation); + } + + /** + * Returns the interpolated inphase value for the specified offset + * @param interpolation into the buffer to calculate the interpolated sample + * @return inphase value of the interpolated sample + */ + public float getInphase(float interpolation, int indexOffet) + { + return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer + indexOffet, interpolation); + } + + /** + * Returns the interpolated quadrature value for the specified offset + * @param interpolation into the buffer to calculate the interpolated sample + * @return quadrature value of the interpolated sample + */ + public float getQuadrature(float interpolation) + { + return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer, interpolation); + } + + /** + * Returns the interpolated quadrature value for the specified offset + * @param interpolation into the buffer to calculate the interpolated sample + * @return quadrature value of the interpolated sample + */ + public float getQuadrature(float interpolation, int indexOffset) + { + return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer + indexOffset, interpolation); + } + + /** + * Constrains value to the range of ( -maximum <> maximum ) + */ + public static float clip(float value, float maximum) + { + if(value > maximum) + { + return maximum; + } + else if(value < -maximum) + { + return -maximum; + } + + return value; + } +} 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 030ef0d8..aba3a907 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,134 +49,139 @@ 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 point */ - public static final Complex POINT_45_DEGREES = - new Complex( (float)Math.sin( THETA ), (float)Math.cos( THETA ) ); - - private Listener mSymbolListener; - - private GardnerSymbolTiming mGardnerDetector = new GardnerSymbolTiming(); - - private CostasLoop mCostasLoop = new CostasLoop(); - - private IFrequencyChangeListener 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 ) - */ - public static float clip( float value, float maximum ) - { - if( value > maximum ) - { - return maximum; - } - else if( value < -maximum ) - { - return -maximum; - } - - return value; - } - - /** - * Constrains timing error to +/- the maximum value and corrects any - * floating point invalid numbers - */ - private float normalize( float error, float maximum ) - { - if( Float.isNaN( error ) ) - { - return 0.0f; - } - else - { - return clip( error, maximum ); - } - } - - public void addListener( IFrequencyChangeListener listener ) - { - mFrequencyChangeListener = listener; - } - - public void removeListener( IFrequencyChangeListener listener ) - { - mFrequencyChangeListener = null; - } + /* 45 degree rotation angle */ + public static final float THETA = (float)(Math.PI / 4.0d); - /** - * 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; + /* 45 degree point */ + public static final Complex POINT_45_DEGREES = new Complex((float)Math.sin(THETA), (float)Math.cos(THETA)); - private FloatAveragingBuffer mBuffer = new FloatAveragingBuffer( 40 ); - - private long mFrequencyError = 0; - - private int mCounter = 0; - - public void receive( float frequency ) - { + private Listener mSymbolListener; + + private GardnerSymbolTiming mGardnerDetector = new GardnerSymbolTiming(); + + private CostasLoop mCostasLoop = new CostasLoop(); + private QPSKSymbolDecoder mQPSKSymbolDecoder = new QPSKSymbolDecoder(); + + private IFrequencyChangeListener mFrequencyChangeListener; + + private EyeDiagramDataTap mEyeDiagramDataTap; + + public LSMDemodulator() + { + } + + public CostasLoop getCostasLoop() + { + return mCostasLoop; + } + + 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 ) + */ + public static float clip(float value, float maximum) + { + if(value > maximum) + { + return maximum; + } + else if(value < -maximum) + { + return -maximum; + } + + return value; + } + + /** + * Constrains timing error to +/- the maximum value and corrects any + * floating point invalid numbers + */ + private float normalize(float error, float maximum) + { + if(Float.isNaN(error)) + { + return 0.0f; + } + else + { + return clip(error, maximum); + } + } + + public void addListener(IFrequencyChangeListener listener) + { + mFrequencyChangeListener = listener; + } + + public void removeListener(IFrequencyChangeListener 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; + + 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 +218,395 @@ 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 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/QPSKDemodulator.java b/src/main/java/io/github/dsheirer/dsp/psk/QPSKDemodulator.java new file mode 100644 index 00000000..91d1ce76 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/QPSKDemodulator.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; + +import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop; +import io.github.dsheirer.dsp.symbol.Dibit; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.complex.Complex; +import io.github.dsheirer.sample.complex.ComplexBuffer; +import io.github.dsheirer.sample.complex.ComplexSampleListener; + +public class QPSKDemodulator implements ComplexSampleListener +{ + private InterpolatingSymbolBuffer mInterpolatingSymbolBuffer; + private IPhaseLockedLoop mPLL; + private ISymbolPhaseErrorCalculator mSymbolPhaseErrorCalculator; + private IQPSKSymbolDecoder mQPSKSymbolDecoder; + private GardnerDetector mGardnerDetector = new GardnerDetector(); + private Listener mSymbolListener; + private Listener mPLLErrorListener; + private Listener mDibitListener; + + private Complex mPreviousSample = new Complex(0, 0); + private Complex mPreviousMiddleSample = new Complex(0, 0); + private Complex mCurrentSample = new Complex(0, 0); + private Complex mMiddleSample = new Complex(0, 0); + private Complex mPreviousSymbol = new Complex(0, 0); + private Complex mMiddleSymbol = new Complex(0, 0); + private Complex mCurrentSymbol = new Complex(0, 0); + private float mPhaseError; + private float mSymbolTimingError; + + /** + * Decoder for Quaternary Phase Shift Keying (QPSK) and Differential QPSK (DQPSK). This decoder uses both a + * phase-locked loop and a gardner symbol timing error detector to automatically align to the incoming carrier + * frequency and to adjust for any changes in symbol timing. + * + * @param phaseLockedLoop for tracking carrier frequency error + * @param symbolPhaseErrorCalculator to calculate symbol phase errors and optionally adjust for differential encoding + * @param symbolDecoder to decode demodulated symbols into dibits + * @param sampleRate of the incoming complex sample stream + * @param symbolRate of the decoded QPSK symbols. + */ + public QPSKDemodulator(IPhaseLockedLoop phaseLockedLoop, ISymbolPhaseErrorCalculator symbolPhaseErrorCalculator, + IQPSKSymbolDecoder symbolDecoder, double sampleRate, double symbolRate) + { + if(sampleRate < (symbolRate * 2)) + { + throw new IllegalArgumentException("Sample rate [" + sampleRate + + "] must be at least 2 x symbol rate [" + symbolRate + "]"); + } + + mPLL = phaseLockedLoop; + mSymbolPhaseErrorCalculator = symbolPhaseErrorCalculator; + mQPSKSymbolDecoder = symbolDecoder; + + float samplesPerSymbol = (float)(sampleRate / symbolRate); + mInterpolatingSymbolBuffer = new InterpolatingSymbolBuffer(samplesPerSymbol); + } + + /** + * Submits a buffer containing complex samples for decoding + * @param complexBuffer with complex samples + */ + public void receive(ComplexBuffer complexBuffer) + { + float[] samples = complexBuffer.getSamples(); + + for(int x = 0; x < samples.length; x += 2) + { + receive(samples[x], samples[x + 1]); + } + } + + /** + * Submits a complex sample for decoding. + * @param inphase value for the sample + * @param quadrature value for the sample + */ + @Override + public void receive(float inphase, float quadrature) + { + //Update current sample with values + mCurrentSample.setValues(inphase, quadrature); + + //Mix current sample with costas loop to remove any rotation that is present from a mis-tuned carrier frequency + mCurrentSample.multiply(mPLL.incrementAndGetCurrentVector()); + + mInterpolatingSymbolBuffer.receive(mCurrentSample); + + //Calculate the symbol once we've stored enough samples + if(mInterpolatingSymbolBuffer.hasSymbol()) + { + //Get middle and current samples from the interpolating buffer + mMiddleSample = mInterpolatingSymbolBuffer.getMiddleSample(); + mCurrentSample = mInterpolatingSymbolBuffer.getCurrentSample(); + + //Calculate middle and current symbols as the delta between the previous and current samples + mMiddleSymbol.setInphase(Complex.multiplyInphase(mMiddleSample.inphase(), mMiddleSample.quadrature(), mPreviousMiddleSample.inphase(), -mPreviousMiddleSample.quadrature())); + mMiddleSymbol.setQuadrature(Complex.multiplyQuadrature(mMiddleSample.inphase(), mMiddleSample.quadrature(), mPreviousMiddleSample.inphase(), -mPreviousMiddleSample.quadrature())); + mCurrentSymbol.setInphase(Complex.multiplyInphase(mCurrentSample.inphase(), mCurrentSample.quadrature(), mPreviousSample.inphase(), -mPreviousSample.quadrature())); + mCurrentSymbol.setQuadrature(Complex.multiplyQuadrature(mCurrentSample.inphase(), mCurrentSample.quadrature(), mPreviousSample.inphase(), -mPreviousSample.quadrature())); + + //Set gain to unity before we calculate the error value + mMiddleSymbol.normalize(); + mCurrentSymbol.normalize(); + + //Symbol timing error calculations + mSymbolTimingError = mGardnerDetector.getError(mPreviousSymbol, mMiddleSymbol, mCurrentSymbol); + mInterpolatingSymbolBuffer.resetAndAdjust(mSymbolTimingError); + + //Store current samples/symbols to use for the next period + mPreviousSample.setValues(mCurrentSample); + mPreviousMiddleSample.setValues(mMiddleSample); + mPreviousSymbol.setValues(mCurrentSymbol); + + //Adjust the current symbol as necessary (e.g. for differential encoding) + mSymbolPhaseErrorCalculator.adjust(mCurrentSymbol); + + //Calculate the phase error of the current symbol relative to the expected constellation and provide + //feedback to the PLL + mPhaseError = mSymbolPhaseErrorCalculator.getPhaseError(mCurrentSymbol); + mPLL.adjust(mPhaseError); + + if(mPLLErrorListener != null) + { + mPLLErrorListener.receive((double)mPhaseError); + } + + //Decode the dibit from the symbol and send to the listener + if(mDibitListener != null) + { + mDibitListener.receive(mQPSKSymbolDecoder.decode(mCurrentSymbol)); + } + + //Send to an external constellation symbol listener when registered + if(mSymbolListener != null) + { + mSymbolListener.receive(mCurrentSymbol); + } + + //TODO: Assemble dibits here and broadcast + } + } + + /** + * Registers the listener to receive decoded QPSK symbols + */ + public void setSymbolListener(Listener listener) + { + mSymbolListener = listener; + } + + /** + * Registers the listener to receive decoded QPSK dibits + */ + public void setDibitListener(Listener listener) + { + mDibitListener = listener; + } + + /** + * Registers the listener to receive PLL error values + */ + public void setPLLErrorListener(Listener listener) + { + mPLLErrorListener = listener; + } +} 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..d7c94323 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/QPSKSymbolPhaseErrorCalculator.java @@ -0,0 +1,63 @@ +/******************************************************************************* + * sdr-trunk + * Copyright (C) 2014-2018 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. + * If not, see + * + ******************************************************************************/ +package io.github.dsheirer.dsp.psk; + +import io.github.dsheirer.sample.complex.Complex; + +/** + * Calculates the phase error for a QPSK polar-orientation constellation + */ +public class QPSKSymbolPhaseErrorCalculator implements ISymbolPhaseErrorCalculator +{ + private float mPhaseError; + + @Override + public void adjust(Complex symbol) + { + //No-op - since there is no differential, we don't have to adjust + } + + @Override + public float getPhaseError(Complex symbol) + { + mPhaseError = 0.0f; + + if(Math.abs(symbol.inphase()) > Math.abs(symbol.quadrature())) + { + if(symbol.inphase() > 0) + { + mPhaseError = -symbol.quadrature(); + } + else + { + mPhaseError = symbol.quadrature(); + } + } + else + { + if(symbol.quadrature() > 0) + { + mPhaseError = symbol.inphase(); + } + else + { + mPhaseError = -symbol.inphase(); + } + } + + return mPhaseError; + } +} 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..71d07392 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/pll/CostasLoop.java @@ -0,0 +1,337 @@ +/******************************************************************************* + * sdr-trunk + * Copyright (C) 2014-2018 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. + * If not, see + * + ******************************************************************************/ +package io.github.dsheirer.dsp.psk.pll; + +import io.github.dsheirer.buffer.DoubleCircularBuffer; +import io.github.dsheirer.dsp.mixer.Oscillator; +import io.github.dsheirer.sample.complex.Complex; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.text.DecimalFormat; + +/** + * Costas Loop - phase locked loop designed to automatically synchronize to the incoming carrier frequency in order to + * zeroize any frequency offset inherent in the signal due to either mis-tuning or carrier frequency drift. + * + * Costas loop structure was modeled from KA1RBI's OP25/cqpsk.py + * + * Loop bandwidth and alpha/beta gain formulas derived from: + * http://www.trondeau.com/blog/2011/8/13/control-loop-gain-values.html + */ +public class CostasLoop implements IPhaseLockedLoop +{ + private final static Logger mLog = LoggerFactory.getLogger(CostasLoop.class); + public static final double TWO_PI = 2.0 * Math.PI; + private DoubleCircularBuffer mErrorBuffer = new DoubleCircularBuffer(10); + private double mDamping = Math.sqrt(2.0) / 2.0; + private double mAlphaGain; + private double mBetaGain; + private double mLoopPhase = 0.0; + private double mLoopFrequency = 0.0; + private double mMaximumLoopFrequency; + private Complex mCurrentVector = new Complex(0, 0); + + private Tracking mTracking = Tracking.SEARCHING; + private ITrackingStateListener mTrackingStateListener; + private boolean mAutomaticTrackingEnabled = true; + + /** + * Costas Loop for tracking and correcting frequency error in a received carrier signal. + * + * @param sampleRate of the incoming samples + * @param symbolRate of the digital signal + */ + public CostasLoop(double sampleRate, double symbolRate) + { + mMaximumLoopFrequency = TWO_PI * (symbolRate / 2.0) / sampleRate; + updateLoopBandwidth(); + } + + /** + * Corrects the current phase tracker when an inverted output is detected. The inversion will take the form of + * +/- 90 degrees or +/- 180 degrees, although the latter correction is equal regardless of the sign. + * + * If the supplied correction value places the loop frequency outside of the max frequency, then the frequency will + * be corrected 360 degrees in the opposite direction to maintain within the max frequency bounds. + * + * @param correction as measured in radians + */ + public void correctInversion(double correction) + { + mLoopFrequency += correction; + + if(mLoopFrequency > mMaximumLoopFrequency) + { + mLoopFrequency -= 2.0 * mMaximumLoopFrequency; + } + + if(mLoopFrequency < -mMaximumLoopFrequency) + { + mLoopFrequency += 2.0 * mMaximumLoopFrequency; + } + } + + /** + * Updates the loop bandwidth and alpha/beta gains according to the current loop synchronization state. + */ + private void updateLoopBandwidth() + { + double bandwidth = TWO_PI / mTracking.getLoopBandwidth(); + + mAlphaGain = (4.0 * mDamping * bandwidth) / (1.0 + (2.0 * mDamping * bandwidth) + (bandwidth * bandwidth)); + mBetaGain = (4.0 * bandwidth * bandwidth) / (1.0 + (2.0 * mDamping * bandwidth) + (bandwidth * bandwidth)); + } + + /** + * Sets the tracking state for this costas loop. The tracking state affects the aggressiveness of the alpha/beta + * gain values in synchronizing with the signal carrier. + * + * @param tracking + */ + public void setTracking(Tracking tracking) + { + mTracking = tracking; + updateLoopBandwidth(); + + mLog.debug("Tracking: " + tracking.name()); + + if(mTrackingStateListener != null) + { + mTrackingStateListener.trackingStateChanged(tracking); + } + } + + /** + * Sets the listener to receive notifications when the tracking state changes + */ + public void setTrackingStateListener(ITrackingStateListener listener) + { + mTrackingStateListener = listener; + } + + /** + * Enables or disables automatic tracking state updates. When enabled, the loop will automatically increase or + * decrease the loop alpha/beta gain values based on the variance of the error tracking values submitted to the + * loop control. As the variance decreases, so will the gain values, and vice-versa. Setting this to disabled + * will force the tracker to retain the gain values associated with either the default or user-specified tracking + * value. + * + * @param enabled true (default) to enable automatic gain tracking + */ + public void setAutomaticTracking(boolean enabled) + { + mAutomaticTrackingEnabled = enabled; + } + + /** + * Increments the phase of the loop for each sample received at the sample rate. + */ + public void increment() + { + mLoopPhase += mLoopFrequency; + + /* Keep the loop phase in bounds */ + normalizePhase(); + } + + /** + * Current vector of the loop. Note: this value is updated for the current angle in radians each time this method + * is invoked. + */ + public Complex getCurrentVector() + { + mCurrentVector.setAngle(mLoopPhase); + return mCurrentVector; + } + + @Override + public Complex incrementAndGetCurrentVector() + { + increment(); + return getCurrentVector(); + } + + public double getLoopFrequency() + { + return mLoopFrequency; + } + + public double getMaximumLoopFrequency() + { + return mMaximumLoopFrequency; + } + + /** + * Updates the costas loop frequency and phase to adjust for the phase + * error value + * + * @param phaseError - (-)= late and (+)= early + */ + public void adjust(double phaseError) + { + mLoopFrequency += (mBetaGain * phaseError); + mLoopPhase += mLoopFrequency + (mAlphaGain * phaseError); + + /* Maintain phase between +/- 2 * PI */ + normalizePhase(); + + /* Limit frequency to +/- maximum loop frequency */ + limitFrequency(); + + mErrorBuffer.put(phaseError); + + double variance = mErrorBuffer.variance(); + + switch(mTracking) + { + case SEARCHING: + if(Tracking.COARSE.meetsThreshold(variance)) + { + setTracking(Tracking.COARSE); + } + break; + case COARSE: + if(Tracking.COARSE.meetsThreshold(variance)) + { + if(Tracking.FINE.meetsThreshold(variance)) + { + setTracking(Tracking.FINE); + } + } + else + { + setTracking(Tracking.SEARCHING); + } + break; + case FINE: + if(Tracking.FINE.meetsThreshold(variance)) + { + if(Tracking.LOCKED.meetsThreshold(variance)) + { + setTracking(Tracking.LOCKED); + } + } + else + { + setTracking(Tracking.COARSE); + } + break; + case LOCKED: + if(!Tracking.LOCKED.meetsThreshold(variance)) + { + setTracking(Tracking.FINE); + } + break; + } + } + + public double getErrorMean() + { + return mErrorBuffer.mean(); + } + + public double getErrorVariance() + { + return mErrorBuffer.variance(); + } + + /** + * Normalizes the phase tracker to maintain within +/- 2 Pi + */ + private void normalizePhase() + { + while(mLoopPhase > TWO_PI) + { + mLoopPhase -= TWO_PI; + } + + while(mLoopPhase < -TWO_PI) + { + mLoopPhase += TWO_PI; + } + } + + /** + * Constrains the frequency within the bounds of +/- loop frequency + */ + private void limitFrequency() + { + if(mLoopFrequency > mMaximumLoopFrequency) + { + mLoopFrequency = mMaximumLoopFrequency; + } + + if(mLoopFrequency < -mMaximumLoopFrequency) + { + mLoopFrequency = -mMaximumLoopFrequency; + } + } + + /** + * Tracking state listener interface for receiving updates when the tracking state of the costas loop changes + */ + public interface ITrackingStateListener + { + void trackingStateChanged(Tracking tracking); + } + + public static void main(String[] arguments) + { + DecimalFormat decimalFormat = new DecimalFormat("0.000000"); + double sampleRate = 25000.0; + int errorFrequency = 411; + double loopBandwidth = 50.0; + + Oscillator oscillator = new Oscillator(errorFrequency, (int)sampleRate); + CostasLoop costasLoop = new CostasLoop(sampleRate, 4800.0); + costasLoop.setTrackingStateListener(new ITrackingStateListener() + { + @Override + public void trackingStateChanged(Tracking tracking) + { + mLog.debug("Tracking State Changed: " + tracking.name()); + } + }); + + for(int x = 0; x < 1000; x++) + { + Complex complex = oscillator.getComplex(); + + oscillator.rotate(); + costasLoop.increment(); + + if(x % 5 == 0) + { + complex.multiply(costasLoop.getCurrentVector().conjugate()); + + double phaseError = complex.angle(); + costasLoop.adjust(phaseError); + + double loopFrequency = costasLoop.getLoopFrequency() * sampleRate / TWO_PI; + double maxFrequency = costasLoop.getMaximumLoopFrequency() * sampleRate / TWO_PI; + + mLog.debug(x + " Frequency: " + errorFrequency + + " Loop:" + decimalFormat.format(loopFrequency) + + " Max:" + maxFrequency + + " Detected Error:" + decimalFormat.format(phaseError) + + " Avg:" + decimalFormat.format(costasLoop.getErrorMean()) + + " Var:" + decimalFormat.format(costasLoop.getErrorVariance())); + } + } + } + +} 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..1f4f2a52 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/pll/IPhaseLockedLoop.java @@ -0,0 +1,56 @@ +/******************************************************************************* + * sdr-trunk + * Copyright (C) 2014-2018 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. + * If not, see + * + ******************************************************************************/ +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(); + + +} 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..5cabc7ab --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/pll/Tracking.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.pll; + +public enum Tracking +{ + SEARCHING(50.0, Math.PI), + COARSE(100.0, 0.01), + FINE(150.0, 0.001), + LOCKED(200.0, 0.0001); + + private double mLoopBandwidth; + private double mVarianceThreshold; + + Tracking(double loopBandwidth, double varianceThreshold) + { + mLoopBandwidth = loopBandwidth; + mVarianceThreshold = varianceThreshold; + } + + public double getLoopBandwidth() + { + return mLoopBandwidth; + } + + public double getVarianceThreshold() + { + return mVarianceThreshold; + } + + public boolean meetsThreshold(double variance) + { + return variance < getVarianceThreshold(); + } +} 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 index d46e5463..0046606f 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/DemodulatorViewerFX.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/DemodulatorViewerFX.java @@ -15,6 +15,8 @@ ******************************************************************************/ package io.github.dsheirer.instrument.gui.viewer; +import io.github.dsheirer.instrument.gui.viewer.decoder.DecoderPaneFactory; +import io.github.dsheirer.module.decode.DecoderType; import javafx.application.Application; import javafx.application.Platform; import javafx.event.ActionEvent; @@ -44,7 +46,7 @@ public class DemodulatorViewerFX extends Application public void start(Stage primaryStage) throws Exception { mStage = primaryStage; - mStage.setTitle("Demodulator Viewer FX"); + setTitle(null); BorderPane borderPane = new BorderPane(); borderPane.setTop(getMenuBar()); @@ -56,6 +58,18 @@ public class DemodulatorViewerFX extends Application mStage.show(); } + private void setTitle(DecoderType decoderType) + { + if(decoderType != null) + { + mStage.setTitle("Demodulator Viewer FX - " + decoderType.getDisplayString()); + } + else + { + mStage.setTitle("Demodulator Viewer FX"); + } + } + private ViewerDesktop getViewerDesktop() { if(mViewerDesktop == null) @@ -72,9 +86,9 @@ public class DemodulatorViewerFX extends Application { mMenuBar = new MenuBar(); - Menu menuFile = new Menu("File"); - MenuItem menuItemOpen = new MenuItem("Open ..."); - menuItemOpen.setOnAction(new EventHandler() + Menu fileMenu = new Menu("File"); + MenuItem openMenuItem = new MenuItem("Open ..."); + openMenuItem.setOnAction(new EventHandler() { @Override public void handle(ActionEvent event) @@ -87,8 +101,8 @@ public class DemodulatorViewerFX extends Application } }); - MenuItem menuItemClose = new MenuItem("Close"); - menuItemClose.setOnAction(new EventHandler() + MenuItem closeMenuItem = new MenuItem("Close"); + closeMenuItem.setOnAction(new EventHandler() { @Override public void handle(ActionEvent event) @@ -97,8 +111,8 @@ public class DemodulatorViewerFX extends Application } }); - MenuItem menuItemExit = new MenuItem("Exit"); - menuItemExit.setOnAction(new EventHandler() + MenuItem exitMenuItem = new MenuItem("Exit"); + exitMenuItem.setOnAction(new EventHandler() { @Override public void handle(ActionEvent event) @@ -107,10 +121,34 @@ public class DemodulatorViewerFX extends Application } }); - menuFile.getItems().addAll(menuItemOpen, getRecentFilesMenu(), menuItemClose, new SeparatorMenuItem(), - menuItemExit); + fileMenu.getItems().addAll(openMenuItem, getRecentFilesMenu(), closeMenuItem, new SeparatorMenuItem(), + exitMenuItem); - mMenuBar.getMenus().addAll(menuFile); + mMenuBar.getMenus().add(fileMenu); + + Menu decoderMenu = new Menu("Decoder"); + + for(DecoderType decoderType: DecoderType.values()) + { + if(DecoderPaneFactory.isSupported(decoderType)) + { + MenuItem decoderMenuItem = new MenuItem(decoderType.getShortDisplayString()); + + decoderMenuItem.setOnAction(new EventHandler() + { + @Override + public void handle(ActionEvent event) + { + getViewerDesktop().setDecoder(decoderType); + setTitle(decoderType); + } + }); + + decoderMenu.getItems().add(decoderMenuItem); + } + } + + mMenuBar.getMenus().add(decoderMenu); } return mMenuBar; 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 index 94a958f2..646f41cf 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/PlaybackController.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/PlaybackController.java @@ -15,6 +15,7 @@ ******************************************************************************/ package io.github.dsheirer.instrument.gui.viewer; +import io.github.dsheirer.instrument.gui.viewer.decoder.DecoderPane; import io.github.dsheirer.sample.Broadcaster; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.complex.ComplexBuffer; @@ -52,6 +53,7 @@ public class PlaybackController extends HBox implements IFrameLocationListener, private HBox mControlsBox; private Label mFileLabel; private EventHandler mPlayEventHandler; + private DecoderPane mSampleRateListener; private IControllableFileSource mControllableFileSource; private Broadcaster mComplexBufferBroadcaster = new Broadcaster<>(); @@ -65,6 +67,16 @@ public class PlaybackController extends HBox implements IFrameLocationListener, setControlsEnabled(false); } + public void setSampleRateListener(DecoderPane decoderPane) + { + mSampleRateListener = decoderPane; + + if(mSampleRateListener != null && mControllableFileSource != null) + { + mSampleRateListener.setSampleRate(mControllableFileSource.getSampleRate()); + } + } + public void load(File file) { mLog.debug("loading:" + file.getAbsolutePath()); @@ -76,6 +88,11 @@ public class PlaybackController extends HBox implements IFrameLocationListener, mControllableFileSource.setListener(this); ((ComplexWaveSource)mControllableFileSource).setListener((Listener)this); mControllableFileSource.open(); + + if(mSampleRateListener != null) + { + mSampleRateListener.setSampleRate(mControllableFileSource.getSampleRate()); + } } catch(UnsupportedAudioFileException e) { 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 index 38e84acc..bdd01f5d 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/ViewerDesktop.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/ViewerDesktop.java @@ -15,25 +15,22 @@ ******************************************************************************/ package io.github.dsheirer.instrument.gui.viewer; +import io.github.dsheirer.instrument.gui.viewer.decoder.DecoderPane; +import io.github.dsheirer.instrument.gui.viewer.decoder.DecoderPaneFactory; +import io.github.dsheirer.module.decode.DecoderType; import javafx.scene.layout.BorderPane; -import javafx.scene.layout.HBox; -import javafx.scene.layout.VBox; import java.io.File; public class ViewerDesktop extends BorderPane { private PlaybackController mPlaybackController; - private VBox mChartBox; - private ComplexSampleLineChart mSampleLineChart; - private ComplexPhaseLineChart mPhaseLineChart; + private DecoderPane mDecoderPane = DecoderPaneFactory.getDefaultPane(); public ViewerDesktop() { setBottom(getPlaybackController()); - getPlaybackController().addListener(getSampleLineChart()); - getPlaybackController().addListener(getPhaseLineChart()); - setCenter(getChartBox()); + setCenter(getDecoderPane()); } public void load(File file) @@ -42,6 +39,31 @@ public class ViewerDesktop extends BorderPane getPlaybackController().load(file); } + public void setDecoder(DecoderType decoderType) + { + getPlaybackController().removeListener(getDecoderPane()); + getChildren().remove(getDecoderPane()); + + mDecoderPane = DecoderPaneFactory.getDecoderPane(decoderType); + setCenter(getDecoderPane()); + getPlaybackController().addListener(getDecoderPane()); + + getPlaybackController().setSampleRateListener(getDecoderPane()); + } + + /** + * Decoder pane + */ + public DecoderPane getDecoderPane() + { + if(mDecoderPane == null) + { + mDecoderPane = DecoderPaneFactory.getDefaultPane(); + } + + return mDecoderPane; + } + public void close() { getPlaybackController().close(); @@ -56,35 +78,4 @@ public class ViewerDesktop extends BorderPane return mPlaybackController; } - - private VBox getChartBox() - { - if(mChartBox == null) - { - mChartBox = new VBox(); - mChartBox.getChildren().addAll(getSampleLineChart(), getPhaseLineChart()); - } - - return mChartBox; - } - - private ComplexSampleLineChart getSampleLineChart() - { - if(mSampleLineChart == null) - { - mSampleLineChart = new ComplexSampleLineChart(40); - } - - return mSampleLineChart; - } - - private ComplexPhaseLineChart getPhaseLineChart() - { - if(mPhaseLineChart == null) - { - mPhaseLineChart = new ComplexPhaseLineChart(50); - } - - return mPhaseLineChart; - } } diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/ComplexPhaseLineChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexPhaseLineChart.java similarity index 94% rename from src/main/java/io/github/dsheirer/instrument/gui/viewer/ComplexPhaseLineChart.java rename to src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexPhaseLineChart.java index 9ec6d77f..68d86efb 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/ComplexPhaseLineChart.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexPhaseLineChart.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.gui.viewer; +package io.github.dsheirer.instrument.gui.viewer.chart; import io.github.dsheirer.buffer.ComplexCircularBuffer; import io.github.dsheirer.dsp.gain.ComplexGain; @@ -28,7 +28,6 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.scene.chart.LineChart; import javafx.scene.chart.NumberAxis; -import org.apache.commons.math3.complex.ComplexUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,7 +35,7 @@ public class ComplexPhaseLineChart extends LineChart implements Listener> mPhaseValues = FXCollections.observableArrayList(); @@ -44,8 +43,8 @@ public class ComplexPhaseLineChart extends LineChart implements Listener phaseSeries = new Series<>("Phase", mPhaseValues); ObservableList> observableList = FXCollections.observableArrayList(phaseSeries); diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/ComplexSampleLineChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexSampleLineChart.java similarity index 96% rename from src/main/java/io/github/dsheirer/instrument/gui/viewer/ComplexSampleLineChart.java rename to src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexSampleLineChart.java index 7044bdac..13f49d7d 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/ComplexSampleLineChart.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexSampleLineChart.java @@ -13,7 +13,7 @@ * If not, see * ******************************************************************************/ -package io.github.dsheirer.instrument.gui.viewer; +package io.github.dsheirer.instrument.gui.viewer.chart; import io.github.dsheirer.buffer.ComplexCircularBuffer; import io.github.dsheirer.dsp.gain.ComplexGain; @@ -44,8 +44,8 @@ public class ComplexSampleLineChart extends LineChart implements Listener iSampleSeries = new LineChart.Series<>("Inphase", mISamples); LineChart.Series qSampleSeries = new LineChart.Series<>("Quadrature", mQSamples); 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..3857026d --- /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", -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) + { + 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..5d67bb7d --- /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.0, 1.0, 0.2), + new NumberAxis("Q", -1.0, 1.0, 0.2)); + + mHistory = history; + + for(int x = 0; x < mHistory; x++) + { + mCurrentPoints.add(new XYChart.Data<>(0.0, 0.0)); + mPreviousPoints.add(new XYChart.Data<>(0.0, 0.0)); + mOldestPoints.add(new XYChart.Data<>(0.0, 0.0)); + } + + ObservableList data = FXCollections.observableArrayList(); + data.add(new ScatterChart.Series("Current", mCurrentPoints)); + data.add(new ScatterChart.Series("Previous", mPreviousPoints)); + data.add(new ScatterChart.Series("Oldest", mOldestPoints)); + + this.setData(data); + } + + @Override + public void receive(Complex complex) + { + //Transfer previous to oldest + XYChart.Data oldest = mOldestPoints.get(mDataPointer); + XYChart.Data previous = mPreviousPoints.get(mDataPointer); + XYChart.Data current = mCurrentPoints.get(mDataPointer); + + oldest.setXValue(previous.getXValue()); + oldest.setYValue(previous.getYValue()); + + previous.setXValue(current.getXValue()); + previous.setYValue(current.getYValue()); + + current.setXValue(complex.inphase()); + current.setYValue(complex.quadrature()); + + mDataPointer++; + + if(mDataPointer >= mHistory) + { + mDataPointer = 0; + } + } +} 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..62253028 --- /dev/null +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1Pane.java @@ -0,0 +1,141 @@ +/******************************************************************************* + * sdr-trunk + * Copyright (C) 2014-2018 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. + * If not, see + * + ******************************************************************************/ +package io.github.dsheirer.instrument.gui.viewer.decoder; + +import io.github.dsheirer.instrument.gui.viewer.chart.ComplexPhaseLineChart; +import io.github.dsheirer.instrument.gui.viewer.chart.ComplexSampleLineChart; +import io.github.dsheirer.instrument.gui.viewer.chart.PhaseLineChart; +import io.github.dsheirer.instrument.gui.viewer.chart.SymbolChart; +import io.github.dsheirer.module.decode.DecoderType; +import io.github.dsheirer.module.decode.p25.P25_C4FMDecoder2; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class P25Phase1Pane extends DecoderPane +{ + private final static Logger mLog = LoggerFactory.getLogger(P25Phase1Pane.class); + + private HBox mSampleChartBox; + private ComplexSampleLineChart mSampleLineChart; + private ComplexPhaseLineChart mPhaseLineChart; + private HBox mDecoderChartBox; + private SymbolChart mSymbolChart; + private PhaseLineChart mPLLPhaseErrorLineChart; + private P25_C4FMDecoder2 mDecoder = new P25_C4FMDecoder2(null); + + public P25Phase1Pane() + { + super(DecoderType.P25_PHASE1); + init(); + } + + private void init() + { + addListener(getSampleLineChart()); + addListener(getPhaseLineChart()); + addListener(getDecoder()); + + getDecoder().setSymbolListener(getSymbolChart()); + getDecoder().setPLLPhaseErrorListener(getPLLPhaseErrorLineChart()); + + getChildren().addAll(getSampleChartBox(), getDecoderChartBox()); + } + + @Override + public void setSampleRate(double sampleRate) + { + mLog.debug("Configuring for sample rate: " + sampleRate); + + mDecoder.setSampleRate(sampleRate); + } + + private P25_C4FMDecoder2 getDecoder() + { + return mDecoder; + } + + private SymbolChart getSymbolChart() + { + if(mSymbolChart == null) + { + mSymbolChart = new SymbolChart(20); + } + + return mSymbolChart; + } + + private HBox getDecoderChartBox() + { + if(mDecoderChartBox == null) + { + mDecoderChartBox = new HBox(); + getSymbolChart().setMaxWidth(Double.MAX_VALUE); + getPLLPhaseErrorLineChart().setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(getSymbolChart(), Priority.ALWAYS); + HBox.setHgrow(getPLLPhaseErrorLineChart(), Priority.ALWAYS); + mDecoderChartBox.getChildren().addAll(getSymbolChart(), getPLLPhaseErrorLineChart()); + } + + return mDecoderChartBox; + } + + private HBox getSampleChartBox() + { + if(mSampleChartBox == null) + { + mSampleChartBox = new HBox(); + getSampleLineChart().setMaxWidth(Double.MAX_VALUE); + getPhaseLineChart().setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(getSampleLineChart(), Priority.ALWAYS); + HBox.setHgrow(getPhaseLineChart(), Priority.ALWAYS); + mSampleChartBox.getChildren().addAll(getSampleLineChart(), getPhaseLineChart()); + } + + return mSampleChartBox; + } + + private ComplexSampleLineChart getSampleLineChart() + { + if(mSampleLineChart == null) + { + mSampleLineChart = new ComplexSampleLineChart(40); + } + + return mSampleLineChart; + } + + private ComplexPhaseLineChart getPhaseLineChart() + { + if(mPhaseLineChart == null) + { + mPhaseLineChart = new ComplexPhaseLineChart(50); + } + + return mPhaseLineChart; + } + + private PhaseLineChart getPLLPhaseErrorLineChart() + { + if(mPLLPhaseErrorLineChart == null) + { + mPLLPhaseErrorLineChart = new PhaseLineChart(80); + } + + return mPLLPhaseErrorLineChart; + } +} 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 9efeb0e7..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,39 +22,39 @@ import io.github.dsheirer.message.Message; import io.github.dsheirer.module.Module; import io.github.dsheirer.sample.Listener; -public abstract class Decoder extends Module - implements IMessageProvider, Listener +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 @@ -63,23 +63,29 @@ public abstract class Decoder extends Module @Override public void removeMessageListener() { - mMessageListener = null; + mMessageListener = null; } /** - * Broadcasts the message to the registered message listener + * Message listener for receiving the output from this decoder */ - public void broadcast( Message message ) + protected Listener getMessageListener() { - if( mMessageListener != null ) - { - mMessageListener.receive( message ); - } + return mMessageDistributor; } - @Override - public void receive( Message message ) - { - broadcast( message ); - } + /** + * Distributes/forwards messages from sub-class decoder implementations to the registered message listener. + */ + public class MessageDistributor implements Listener + { + @Override + public void receive(Message message) + { + if(mMessageListener != null) + { + mMessageListener.receive(message); + } + } + } } 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 072d28f4..629d5f5f 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 @@ -90,7 +90,7 @@ public class Fleetsync2Decoder extends Decoder implements IFilteredRealBufferLis mMessageProcessor = new Fleetsync2MessageProcessor( aliasList ); mMessageFramer.addMessageListener( mMessageProcessor ); - mMessageProcessor.setMessageListener( this ); + mMessageProcessor.setMessageListener( getMessageListener() ); } public void dispose() 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 fb19a8d2..a1ed1899 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 @@ -100,7 +100,7 @@ public class LJ1200Decoder extends Decoder implements IFilteredRealBufferListene mMessageProcessor = new LJ1200MessageProcessor( aliasList ); mTowerMessageFramer.addMessageListener( mMessageProcessor ); mTransponderMessageFramer.addMessageListener( mMessageProcessor ); - mMessageProcessor.setMessageListener( this ); + mMessageProcessor.setMessageListener( getMessageListener() ); } @Override 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 08041b72..f8e234b9 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 @@ -74,7 +74,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 0a96486b..6836aabf 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 @@ -64,7 +64,7 @@ public class LTRStandardDecoder extends Decoder implements IUnFilteredRealBuffer mLTRMessageProcessor = new LTRStandardMessageProcessor( direction, aliasList ); mLTRMessageFramer.addMessageListener( mLTRMessageProcessor ); - mLTRMessageProcessor.setMessageListener( this ); + mLTRMessageProcessor.setMessageListener( getMessageListener() ); } @Override 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 beaf51da..85cb9657 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 @@ -102,7 +102,7 @@ public class MDCDecoder extends Decoder implements IFilteredRealBufferListener, /* Message Processor */ mMessageProcessor = new MDCMessageProcessor( aliasList ); mMessageFramer.addMessageListener( mMessageProcessor ); - mMessageProcessor.addMessageListener( this ); + mMessageProcessor.addMessageListener(getMessageListener()); } public void dispose() 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 d78d3670..ecd67c24 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 @@ -148,7 +148,7 @@ public class MPT1327Decoder extends Decoder implements IFilteredRealBufferListen /* Fully decoded and framed messages processor */ mMessageProcessor = new MPT1327MessageProcessor(aliasList); - mMessageProcessor.setMessageListener(this); + mMessageProcessor.setMessageListener(getMessageListener()); mControlMessageFramer.addMessageListener(mMessageProcessor); mTrafficMessageFramer.addMessageListener(mMessageProcessor); } 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 38e3fdfc..7851230d 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,70 +1,91 @@ +/******************************************************************************* + * sdr-trunk + * Copyright (C) 2014-2018 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. + * If not, see + * + ******************************************************************************/ package io.github.dsheirer.module.decode.p25; import io.github.dsheirer.alias.AliasList; -import io.github.dsheirer.instrument.Instrumentable; import io.github.dsheirer.module.decode.Decoder; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeListener; import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeProvider; -public abstract class P25Decoder extends Decoder implements IFrequencyChangeListener, IFrequencyChangeProvider, Instrumentable +public abstract class P25Decoder extends Decoder implements IFrequencyChangeListener, IFrequencyChangeProvider { - private P25MessageProcessor mMessageProcessor; - - public P25Decoder( AliasList aliasList ) - { - mMessageProcessor = new P25MessageProcessor( aliasList ); - mMessageProcessor.setMessageListener( this ); - } - - public void dispose() - { - super.dispose(); - - mMessageProcessor.dispose(); - mMessageProcessor = null; - } + private P25MessageProcessor mMessageProcessor; + private AliasList mAliasList; - @Override - public DecoderType getDecoderType() + public P25Decoder(AliasList aliasList) { - return DecoderType.P25_PHASE1; + mAliasList = aliasList; + mMessageProcessor = new P25MessageProcessor(mAliasList); + mMessageProcessor.setMessageListener(getMessageListener()); } - protected P25MessageProcessor getMessageProcessor() - { - return mMessageProcessor; - } - - public abstract Modulation getModulation(); - - public enum Modulation - { - CQPSK( "Simulcast (LSM)", "LSM" ), - C4FM( "Normal (C4FM)", "C4FM" ); - - private String mLabel; - private String mShortLabel; - - private Modulation( String label, String shortLabel ) - { - mLabel = label; - mShortLabel = shortLabel; - } - - public String getLabel() - { - return mLabel; - } - - public String getShortLabel() - { - return mShortLabel; - } - - public String toString() - { - return getLabel(); - } - } + protected AliasList getAliasList() + { + return mAliasList; + } + + public void dispose() + { + super.dispose(); + + mMessageProcessor.dispose(); + mMessageProcessor = null; + } + + @Override + public DecoderType getDecoderType() + { + return DecoderType.P25_PHASE1; + } + + protected P25MessageProcessor getMessageProcessor() + { + return mMessageProcessor; + } + + public abstract Modulation getModulation(); + + public enum Modulation + { + CQPSK("Simulcast (LSM)", "LSM"), + C4FM("Normal (C4FM)", "C4FM"); + + private String mLabel; + private String mShortLabel; + + private Modulation(String label, String shortLabel) + { + mLabel = label; + mShortLabel = shortLabel; + } + + public String getLabel() + { + return mLabel; + } + + public String getShortLabel() + { + return mShortLabel; + } + + public String toString() + { + return getLabel(); + } + } } 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 13512400..c51a7adf 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 @@ -19,10 +19,7 @@ package io.github.dsheirer.module.decode.p25; import io.github.dsheirer.alias.AliasList; -import io.github.dsheirer.instrument.tap.Tap; import io.github.dsheirer.instrument.tap.TapGroup; -import io.github.dsheirer.instrument.tap.stream.DibitTap; -import io.github.dsheirer.instrument.tap.stream.FloatTap; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.real.IFilteredRealBufferListener; import io.github.dsheirer.sample.real.RealBuffer; @@ -30,7 +27,6 @@ import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayList; import java.util.List; import java.util.concurrent.ScheduledExecutorService; @@ -105,93 +101,6 @@ public class P25_C4FMDecoder extends P25Decoder implements IFilteredRealBufferLi return mSymbolFilter; } - /** - * Instrumentation taps for monitoring internal processing - */ - @Override - public List getTapGroups() - { - if( mAvailableTaps == null ) - { - mAvailableTaps = new ArrayList<>(); - - TapGroup group = new TapGroup( "P25 C4FM Decoder" ); - - group.add( new FloatTap( INSTRUMENT_C4FM_SYMBOL_FILTER_OUTPUT, 0, 0.1f ) ); - group.add( new DibitTap( INSTRUMENT_C4FM_SLICER_OUTPUT, 0, 0.1f ) ); - - mAvailableTaps.add( group ); - - if( mSymbolFilter != null ) - { - mAvailableTaps.addAll( mSymbolFilter.getTapGroups() ); - } - } - - return mAvailableTaps; - } - - /** - * Adds an instrumentation tap to monitor internal processing. - */ - @Override - public void registerTap( Tap tap ) - { - if( mSymbolFilter != null ) - { - mSymbolFilter.registerTap( tap ); - } - - switch( tap.getName() ) - { - case INSTRUMENT_C4FM_SYMBOL_FILTER_OUTPUT: - FloatTap symbolTap = (FloatTap)tap; - mSymbolFilter.setListener( symbolTap ); - symbolTap.setListener( mC4FMSlicer ); - break; - case INSTRUMENT_C4FM_SLICER_OUTPUT: - DibitTap slicerTap = (DibitTap)tap; - - if( mC4FMSlicer != null ) - { - mC4FMSlicer.addListener( slicerTap ); - } - break; - default: - break; - } - } - - /** - * Removes the instrumentation tap. - */ - @Override - public void unregisterTap( Tap tap ) - { - if( mSymbolFilter != null ) - { - mSymbolFilter.unregisterTap( tap ); - } - - switch( tap.getName() ) - { - case INSTRUMENT_C4FM_SYMBOL_FILTER_OUTPUT: - mSymbolFilter.setListener( mC4FMSlicer ); - break; - case INSTRUMENT_C4FM_SLICER_OUTPUT: - DibitTap slicerTap = (DibitTap)tap; - - if( mC4FMSlicer != null ) - { - mC4FMSlicer.removeListener( slicerTap ); - } - break; - default: - throw new IllegalArgumentException( "Unrecognized tap: " + - tap.getName() ); - } - } - @Override public void reset() { diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2.java new file mode 100644 index 00000000..5a992365 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2.java @@ -0,0 +1,223 @@ +/******************************************************************************* + * sdr-trunk + * Copyright (C) 2014-2018 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. + * If not, see + * + ******************************************************************************/ +package io.github.dsheirer.module.decode.p25; + +import io.github.dsheirer.alias.AliasList; +import io.github.dsheirer.dsp.filter.FilterFactory; +import io.github.dsheirer.dsp.filter.design.FilterDesignException; +import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification; +import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter; +import io.github.dsheirer.dsp.gain.ComplexFeedForwardGainControl; +import io.github.dsheirer.dsp.psk.DQPSKSymbolPhaseErrorCalculator; +import io.github.dsheirer.dsp.psk.IQPSKSymbolDecoder; +import io.github.dsheirer.dsp.psk.ISymbolPhaseErrorCalculator; +import io.github.dsheirer.dsp.psk.QPSKDemodulator; +import io.github.dsheirer.dsp.psk.QPSKSymbolDecoder; +import io.github.dsheirer.dsp.psk.pll.CostasLoop; +import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.complex.Complex; +import io.github.dsheirer.sample.complex.ComplexBuffer; +import io.github.dsheirer.sample.complex.ComplexBufferToStreamConverter; +import io.github.dsheirer.sample.complex.IComplexBufferListener; +import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ScheduledExecutorService; + +public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListener, Listener +{ + private final static Logger mLog = LoggerFactory.getLogger(P25_C4FMDecoder2.class); + private static final double SYMBOL_RATE = 4800; + private double mSampleRate; + + private Map mBasebandFilters = new HashMap<>(); + private ComplexFIRFilter mBasebandFilter; + private QPSKDemodulator mQPSKDemodulator; + private IPhaseLockedLoop mPhaseLockedLoop; + private ComplexBufferToStreamConverter mStreamConverter = new ComplexBufferToStreamConverter(); + private ComplexFeedForwardGainControl mAGC = new ComplexFeedForwardGainControl(32); + private QPSKSymbolDecoder mQPSKSlicer = new QPSKSymbolDecoder(); + private P25MessageFramer mMessageFramer; + private ISymbolPhaseErrorCalculator mSymbolPhaseErrorCalculator = new DQPSKSymbolPhaseErrorCalculator(); + private IQPSKSymbolDecoder mSymbolDecoder = new QPSKSymbolDecoder(); + private Listener mSymbolListener; + private Listener mPLLPhaseErrorListener; + + public P25_C4FMDecoder2(AliasList aliasList) + { + super(aliasList); + + setSampleRate(48000.0); + + mStreamConverter.setListener(mAGC); + } + + public void setSampleRate(double sampleRate) + { + if(sampleRate <= SYMBOL_RATE * 2) + { + throw new IllegalArgumentException("Sample rate must be at least twice the symbol rate [4800]"); + } + + mSampleRate = sampleRate; + mBasebandFilter = new ComplexFIRFilter(getBasebandFilter(), 1.0f); + + mPhaseLockedLoop = new CostasLoop(mSampleRate, SYMBOL_RATE); + mQPSKDemodulator = new QPSKDemodulator(mPhaseLockedLoop, mSymbolPhaseErrorCalculator, mSymbolDecoder, + mSampleRate, SYMBOL_RATE); + mQPSKDemodulator.setSymbolListener(mSymbolListener); + mQPSKDemodulator.setPLLErrorListener(mPLLPhaseErrorListener); + + //Message framer can trigger a symbol-inversion correction to the PLL when detected + mMessageFramer = new P25MessageFramer(getAliasList(), mPhaseLockedLoop); + mMessageFramer.setListener(getMessageProcessor()); + mQPSKDemodulator.setDibitListener(mMessageFramer); + } + + public void setSymbolListener(Listener listener) + { + mSymbolListener = listener; + mQPSKDemodulator.setSymbolListener(listener); + } + + public void setPLLPhaseErrorListener(Listener listener) + { + mPLLPhaseErrorListener = listener; + mQPSKDemodulator.setPLLErrorListener(listener); + } + + @Override + public void receive(ComplexBuffer complexBuffer) + { + ComplexBuffer filtered = mBasebandFilter.filter(complexBuffer); + ComplexBuffer amplified = mAGC.filter(filtered); + mQPSKDemodulator.receive(amplified); + } + + private double getSampleRate() + { + return mSampleRate; + } + + private float[] getBasebandFilter() + { + float[] filter = mBasebandFilters.get(getSampleRate()); + + if(filter == null) + { + FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() + .sampleRate((int)getSampleRate()) + .passBandCutoff(6000) + .passBandAmplitude(1.0) + .passBandRipple(0.1) + .stopBandAmplitude(0.0) + .stopBandStart(6250) + .stopBandRipple(0.01) + .build(); + + try + { + filter = FilterFactory.getTaps(specification); + mBasebandFilters.put(getSampleRate(), filter); + + } + catch(FilterDesignException fde) + { + mLog.error("Couldn't design low pass baseband filter for sample rate: " + getSampleRate()); + } + } + + return filter; + } + + public void dispose() + { + super.dispose(); + + mBasebandFilter.dispose(); + mBasebandFilter = null; + + mStreamConverter.dispose(); + mStreamConverter = null; + + mAGC.dispose(); + mAGC = null; + + mQPSKSlicer.dispose(); + mQPSKSlicer = null; + + mMessageFramer.dispose(); + mMessageFramer = null; + } + + @Override + public void setFrequencyChangeListener(Listener listener) + { + } + + @Override + public void removeFrequencyChangeListener() + { + } + + @Override + public Listener getFrequencyChangeListener() + { + return new Listener() + { + @Override + public void receive(FrequencyChangeEvent frequencyChangeEvent) + { + //Ignored + } + }; + } + + @Override + public Listener getComplexBufferListener() + { + return P25_C4FMDecoder2.this; + } + + public Modulation getModulation() + { + return Modulation.C4FM; + } + + @Override + public void reset() + { + // TODO Auto-generated method stub + + } + + @Override + public void start(ScheduledExecutorService executor) + { + // TODO Auto-generated method stub + + } + + @Override + public void stop() + { + // TODO Auto-generated method stub + } +} 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 806b20e6..00bb781e 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 @@ -1,17 +1,17 @@ /******************************************************************************* * SDR Trunk * Copyright (C) 2014,2015 Dennis Sheirer - * + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with this program. If not, see ******************************************************************************/ @@ -23,13 +23,8 @@ import io.github.dsheirer.dsp.filter.Window.WindowType; import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter_CB_CB; import io.github.dsheirer.dsp.gain.ComplexFeedForwardGainControl; import io.github.dsheirer.dsp.psk.LSMDemodulator; -import io.github.dsheirer.dsp.psk.QPSKPolarSlicer; -import io.github.dsheirer.instrument.tap.Tap; +import io.github.dsheirer.dsp.psk.QPSKSymbolDecoder; import io.github.dsheirer.instrument.tap.TapGroup; -import io.github.dsheirer.instrument.tap.stream.ComplexSampleTap; -import io.github.dsheirer.instrument.tap.stream.ComplexTap; -import io.github.dsheirer.instrument.tap.stream.DibitTap; -import io.github.dsheirer.instrument.tap.stream.QPSKTap; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.complex.ComplexBuffer; import io.github.dsheirer.sample.complex.ComplexBufferToStreamConverter; @@ -38,73 +33,72 @@ import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayList; import java.util.List; import java.util.concurrent.ScheduledExecutorService; public class P25_LSMDecoder extends P25Decoder implements IComplexBufferListener { - private final static Logger mLog = LoggerFactory.getLogger( P25_LSMDecoder.class ); - + private final static Logger mLog = LoggerFactory.getLogger(P25_LSMDecoder.class); + /* Instrumentation Taps */ - private static final String INSTRUMENT_BASEBAND_FILTER_OUTPUT = "Tap Point: Baseband Filter Output"; - private static final String INSTRUMENT_AGC_OUTPUT = "Tap Point: AGC Output"; - private static final String INSTRUMENT_LSM_DEMODULATOR_OUTPUT = "Tap Point: LSM Demodulator Output"; - private static final String INSTRUMENT_QPSK_SLICER_OUTPUT = "Tap Point: QPSK Slicer Output"; + private static final String INSTRUMENT_BASEBAND_FILTER_OUTPUT = "Tap Point: Baseband Filter Output"; + private static final String INSTRUMENT_AGC_OUTPUT = "Tap Point: AGC Output"; + private static final String INSTRUMENT_LSM_DEMODULATOR_OUTPUT = "Tap Point: LSM Demodulator Output"; + private static final String INSTRUMENT_QPSK_SLICER_OUTPUT = "Tap Point: QPSK Slicer Output"; - private List mAvailableTaps; - - private ComplexFIRFilter_CB_CB mBasebandFilter; - private ComplexBufferToStreamConverter mStreamConverter = new ComplexBufferToStreamConverter(); - private ComplexFeedForwardGainControl mAGC = - new ComplexFeedForwardGainControl( 32 ); - private LSMDemodulator mLSMDemodulator = new LSMDemodulator(); - private QPSKPolarSlicer mQPSKSlicer = new QPSKPolarSlicer(); - private P25MessageFramer mMessageFramer; - - public P25_LSMDecoder( AliasList aliasList ) - { - super( aliasList ); - - mBasebandFilter = new ComplexFIRFilter_CB_CB( FilterFactory.getLowPass( - 48000, 7250, 8000, 60, WindowType.HANNING, true ), 1.0f ); - - mBasebandFilter.setListener( mStreamConverter ); - - mStreamConverter.setListener( mAGC ); + private List mAvailableTaps; - mAGC.setListener( mLSMDemodulator ); - - mLSMDemodulator.setSymbolListener( mQPSKSlicer ); - - mMessageFramer = new P25MessageFramer( aliasList, mLSMDemodulator ); - mQPSKSlicer.addListener( mMessageFramer ); - - mMessageFramer.setListener( getMessageProcessor() ); - } - - public void dispose() - { - super.dispose(); - - mBasebandFilter.dispose(); - mBasebandFilter = null; - - mStreamConverter.dispose(); - mStreamConverter = null; - - mAGC.dispose(); - mAGC = null; + private ComplexFIRFilter_CB_CB mBasebandFilter; + private ComplexBufferToStreamConverter mStreamConverter = new ComplexBufferToStreamConverter(); + private ComplexFeedForwardGainControl mAGC = + new ComplexFeedForwardGainControl(32); + private LSMDemodulator mLSMDemodulator = new LSMDemodulator(); + private QPSKSymbolDecoder mQPSKSlicer = new QPSKSymbolDecoder(); + private P25MessageFramer mMessageFramer; - mLSMDemodulator.dispose(); - mLSMDemodulator = null; - - mQPSKSlicer.dispose(); - mQPSKSlicer = null; - - mMessageFramer.dispose(); - mMessageFramer = null; - } + public P25_LSMDecoder(AliasList aliasList) + { + super(aliasList); + + mBasebandFilter = new ComplexFIRFilter_CB_CB(FilterFactory.getLowPass( + 48000, 7250, 8000, 60, WindowType.HANNING, true), 1.0f); + + mBasebandFilter.setListener(mStreamConverter); + + mStreamConverter.setListener(mAGC); + + mAGC.setListener(mLSMDemodulator); + + mLSMDemodulator.setSymbolListener(mQPSKSlicer); + + mMessageFramer = new P25MessageFramer(aliasList, mLSMDemodulator.getCostasLoop()); + mQPSKSlicer.addListener(mMessageFramer); + + mMessageFramer.setListener(getMessageProcessor()); + } + + public void dispose() + { + super.dispose(); + + mBasebandFilter.dispose(); + mBasebandFilter = null; + + mStreamConverter.dispose(); + mStreamConverter = null; + + mAGC.dispose(); + mAGC = null; + + mLSMDemodulator.dispose(); + mLSMDemodulator = null; + + mQPSKSlicer.dispose(); + mQPSKSlicer = null; + + mMessageFramer.dispose(); + mMessageFramer = null; + } @Override public void setFrequencyChangeListener(Listener listener) @@ -130,130 +124,34 @@ public class P25_LSMDecoder extends P25Decoder implements IComplexBufferListener } @Override - public Listener getComplexBufferListener() - { - return mBasebandFilter; - } - - public Modulation getModulation() - { - return Modulation.CQPSK; - } - - /** - * Provides a list of instrumentation taps for monitoring internal processing - */ - @Override - public List getTapGroups() + public Listener getComplexBufferListener() { - if( mAvailableTaps == null ) - { - mAvailableTaps = new ArrayList<>(); - - TapGroup group = new TapGroup( "P25 LSM Decoder" ); - - group.add( new ComplexTap( INSTRUMENT_BASEBAND_FILTER_OUTPUT, 0, 1.0f ) ); - group.add( new ComplexTap( INSTRUMENT_AGC_OUTPUT, 0, 1.0f ) ); - group.add( new QPSKTap( INSTRUMENT_LSM_DEMODULATOR_OUTPUT, 0, 1.0f ) ); - group.add( new DibitTap( INSTRUMENT_QPSK_SLICER_OUTPUT, 0, 0.1f ) ); - - mAvailableTaps.add( group ); - - if( mLSMDemodulator != null ) - { - mAvailableTaps.addAll( mLSMDemodulator.getTapGroups() ); - } - } - - return mAvailableTaps; + return mBasebandFilter; } - /** - * Adds the instrumentation tap - */ - @Override - public void registerTap( Tap tap ) + public Modulation getModulation() { - if( mLSMDemodulator != null ) - { - mLSMDemodulator.registerTap( tap ); - } - - switch( tap.getName() ) - { - case INSTRUMENT_BASEBAND_FILTER_OUTPUT: - ComplexSampleTap baseband = (ComplexSampleTap)tap; - mStreamConverter.setListener( baseband ); - baseband.setListener( mAGC ); - break; - case INSTRUMENT_AGC_OUTPUT: - ComplexSampleTap agcSymbol = (ComplexSampleTap)tap; - mAGC.setListener( agcSymbol ); - agcSymbol.setListener( mLSMDemodulator ); - break; - case INSTRUMENT_LSM_DEMODULATOR_OUTPUT: - QPSKTap qpsk = (QPSKTap)tap; - mLSMDemodulator.setSymbolListener( qpsk ); - qpsk.setListener( mQPSKSlicer ); - break; - case INSTRUMENT_QPSK_SLICER_OUTPUT: - mQPSKSlicer.addListener( (DibitTap)tap ); - break; - default: - throw new IllegalArgumentException( "Unrecognized tap: " + - tap.getName() ); - } + return Modulation.CQPSK; } - /** - * Removes the instrumentation tap - */ - @Override - public void unregisterTap( Tap tap ) + @Override + public void reset() { - if( mLSMDemodulator != null ) - { - mLSMDemodulator.unregisterTap( tap ); - } - - switch( tap.getName() ) - { - case INSTRUMENT_BASEBAND_FILTER_OUTPUT: - mStreamConverter.setListener( mAGC ); - break; - case INSTRUMENT_AGC_OUTPUT: - mAGC.setListener( mLSMDemodulator ); - break; - case INSTRUMENT_LSM_DEMODULATOR_OUTPUT: - mLSMDemodulator.setSymbolListener( mQPSKSlicer ); - break; - case INSTRUMENT_QPSK_SLICER_OUTPUT: - mQPSKSlicer.removeListener( (DibitTap)tap ); - break; - default: - throw new IllegalArgumentException( "Unrecognized tap: " + - tap.getName() ); - } + // TODO Auto-generated method stub + } - @Override - public void reset() - { - // TODO Auto-generated method stub - - } + @Override + public void start(ScheduledExecutorService executor) + { + // TODO Auto-generated method stub - @Override - public void start( ScheduledExecutorService executor ) - { - // TODO Auto-generated method stub - - } + } - @Override - public void stop() - { - // TODO Auto-generated method stub - - } + @Override + public void stop() + { + // TODO Auto-generated method stub + + } } 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 72ba45a2..9f7980b2 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 @@ -63,7 +63,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 9ccae831..848bb85c 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 @@ -104,8 +104,8 @@ public class Tait1200Decoder extends Decoder implements IFilteredRealBufferListe mMessageFramerGPS.addMessageListener( mMessageAProcessor ); mMessageFramerANI.addMessageListener( mMessageBProcessor ); - mMessageAProcessor.setMessageListener( this ); - mMessageBProcessor.setMessageListener( this ); + mMessageAProcessor.setMessageListener(getMessageListener()); + mMessageBProcessor.setMessageListener(getMessageListener()); } public void dispose() diff --git a/src/main/java/io/github/dsheirer/record/RecorderManager.java b/src/main/java/io/github/dsheirer/record/RecorderManager.java index 738ffe3f..c5845cdc 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<>(); @@ -229,7 +230,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/sample/complex/Complex.java b/src/main/java/io/github/dsheirer/sample/complex/Complex.java index afeaee9f..8b98e9e8 100644 --- a/src/main/java/io/github/dsheirer/sample/complex/Complex.java +++ b/src/main/java/io/github/dsheirer/sample/complex/Complex.java @@ -1,17 +1,17 @@ /******************************************************************************* * SDR Trunk * Copyright (C) 2014 Dennis Sheirer - * + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with this program. If not, see ******************************************************************************/ @@ -27,412 +27,427 @@ import java.io.Serializable; */ public class Complex implements Serializable { - private final static Logger mLog = - LoggerFactory.getLogger( Complex.class ); + private final static Logger mLog = LoggerFactory.getLogger(Complex.class); - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; private float mLeft; - private float mRight; - - public Complex( float left, float right ) - { - mLeft = left; - mRight = right; - } - - public Complex() - { - this( 0.0f, 0.0f ); - } - - public void setInphase( float inphase ) - { - mLeft = inphase; - } - - public void setQuadrature( float quadrature ) - { - mRight = quadrature; - } - - public void setValues( float inphase, float quadrature ) - { - mLeft = inphase; - mRight = quadrature; - } + private float mRight; - public Complex copy() - { - return new Complex( mLeft, mRight ); - } - - public String toString() - { - StringBuilder sb = new StringBuilder(); - - sb.append( "I:" ); - sb.append( mLeft ); - sb.append( " Q:" ); - sb.append( mRight ); - - return sb.toString(); - } - - /** - * Returns a new sample representing the conjugate of this one - */ - public Complex conjugate() - { - return new Complex( mLeft, -mRight ); - } + public Complex(float left, float right) + { + mLeft = left; + mRight = right; + } - /** - * Multiplies this sample by the scalor value - */ - public void multiply( float scalor ) - { - mLeft *= scalor; - mRight *= scalor; - } - - public static Complex multiply( Complex sample, float scalor ) - { - return new Complex( sample.left() * scalor, - sample.right() * scalor ); - } - - /** - * Calculates the inphase component of multiplying two complex numbers, A and B. - */ - public static float multiplyInphase( float inphaseA, float quadratureA, float inphaseB, float quadratureB ) - { - return ( inphaseA * inphaseB ) - ( quadratureA * quadratureB ); - } - - /** - * Calculates the quadrature component of multiplying two complex numbers, A and B. - */ - public static float multiplyQuadrature( float inphaseA, float quadratureA, float inphaseB, float quadratureB ) - { - return ( quadratureA * inphaseB ) + ( inphaseA * quadratureB ); - } - - /** - * Multiplies this sample by the multiplier sample - */ - public void multiply( Complex multiplier ) - { - float inphase = multiplyInphase( inphase(), quadrature(), - multiplier.inphase(), multiplier.quadrature() ); - float quadrature = multiplyQuadrature( inphase(), quadrature(), - multiplier.inphase(), multiplier.quadrature() ); - - mLeft = inphase; - mRight = quadrature; - } - - /** - * Multiplies both samples returning a new sample with the results - */ - public static Complex multiply( Complex sample1, Complex sample2 ) - { - float inphase = multiplyInphase( sample1.inphase(), sample1.quadrature(), - sample2.inphase(), sample2.quadrature() ); - float quadrature = multiplyQuadrature( sample1.inphase(), sample1.quadrature(), - sample2.inphase(), sample2.quadrature() ); + public Complex() + { + this(0.0f, 0.0f); + } - return new Complex( inphase, quadrature ); - } + public void setInphase(float inphase) + { + mLeft = inphase; + } - public static Complex multiply( Complex sample, - Float inphase, Float quadrature ) - { - float i = multiplyInphase( sample.inphase(), sample.quadrature(), - inphase, quadrature ); - float q = multiplyQuadrature( sample.inphase(), sample.quadrature(), - inphase, quadrature ); - - return new Complex( i, q ); - } - - public static Complex multiply( Float inphase, Float quadrature, Complex sample ) - { - float i = multiplyInphase( inphase, quadrature, sample.inphase(), sample.quadrature() ); - float q = multiplyQuadrature( inphase, quadrature, sample.inphase(), sample.quadrature() ); - - return new Complex( i, q ); - } + public void setQuadrature(float quadrature) + { + mRight = quadrature; + } - public static Complex multiply( float inphaseA, float quadratureA, float inphaseB, float quadratureB ) - { - float i = multiplyInphase( inphaseA, quadratureA, inphaseB, quadratureB ); - float q = multiplyQuadrature( inphaseA, quadratureA, inphaseB, quadratureB ); - - return new Complex( i, q ); - } - - /** - * Adds the adder sample value to this sample - */ - public void add( Complex adder ) - { - mLeft += adder.left(); - mRight += adder.right(); - } + public void setValues(float inphase, float quadrature) + { + mLeft = inphase; + mRight = quadrature; + } - /** - * Adds the two complex samples returning a new complex sample with the result - */ - public static Complex add( Complex first, Complex second ) - { - return new Complex( first.left() + second.left(), - first.right() + second.right() ); - } + public void setValues(Complex sampleToCopy) + { + mLeft = sampleToCopy.left(); + mRight = sampleToCopy.right(); + } - /** - * Adds the two complex samples returning a new complex sample with the result - */ - public static Complex subtract( Complex first, Complex second ) - { - return new Complex( first.left() - second.left(), - first.right() - second.right() ); - } + public Complex copy() + { + return new Complex(mLeft, mRight); + } - /** - * Magnitude of this sample - */ - public float magnitude() - { - return (float)Math.sqrt( magnitudeSquared() ); - } - - public static float magnitude( float inphase, float quadrature ) - { - return (float)Math.sqrt( ( inphase * inphase ) + ( quadrature * quadrature ) ); - } + public String toString() + { + StringBuilder sb = new StringBuilder(); - /** - * Magnitude squared of this sample - */ - public float magnitudeSquared() - { - return norm(); - } + sb.append("I:"); + sb.append(mLeft); + sb.append(" Q:"); + sb.append(mRight); - /** - * Norm of this sample = ( i * i ) + ( q * q ) - */ - public float norm() - { - return (float)( ( inphase() * inphase() ) + - ( quadrature() * quadrature() ) ); - } - - /** - * Returns the vector length to 1 (unit circle) - */ - public void normalize() - { - float magnitude = magnitude(); - - if( magnitude != 0 ) - { - multiply( (float)( 1.0f / magnitude() ) ); - } - } + return sb.toString(); + } - /** - * Returns the vector length to 1 (unit circle), - * avoiding square root multiplication - */ - public void fastNormalize() - { - multiply( (float)( 1.95f - magnitudeSquared() ) ); - } - - public float left() - { - return mLeft; - } - - public float right() - { - return mRight; - } + /** + * Returns a new sample representing the conjugate of this one + */ + public Complex conjugate() + { + return new Complex(mLeft, -mRight); + } - public float inphase() - { - return mLeft; - } - - /** - * Absolute value of in-phase component - */ - public float inPhaseAbsolute() - { - return Math.abs( mLeft ); - } + /** + * Multiplies this sample by the scalor value + */ + public void multiply(float scalor) + { + mLeft *= scalor; + mRight *= scalor; + } - public float quadrature() - { - return mRight; - } - - /** - * Absolute value of quadrature component - */ - public float quadratureAbsolute() - { - return Math.abs( mRight ); - } + public static Complex multiply(Complex sample, float scalor) + { + return new Complex(sample.left() * scalor, + sample.right() * scalor); + } - public float x() - { - return mLeft; - } - - public float y() - { - return mRight; - } - - public float real() - { - return mLeft; - } + /** + * Calculates the inphase component of multiplying two complex numbers, A and B. + */ + public static float multiplyInphase(float inphaseA, float quadratureA, float inphaseB, float quadratureB) + { + return (inphaseA * inphaseB) - (quadratureA * quadratureB); + } - /** - * Absolute value of real component - */ - public float realAbsolute() - { - return Math.abs( mLeft ); - } + /** + * Calculates the quadrature component of multiplying two complex numbers, A and B. + */ + public static float multiplyQuadrature(float inphaseA, float quadratureA, float inphaseB, float quadratureB) + { + return (quadratureA * inphaseB) + (inphaseA * quadratureB); + } - public float imaginary() - { - return mRight; - } + /** + * Multiplies this sample by the multiplier sample + */ + public void multiply(Complex multiplier) + { + float inphase = multiplyInphase(inphase(), quadrature(), + multiplier.inphase(), multiplier.quadrature()); + float quadrature = multiplyQuadrature(inphase(), quadrature(), + multiplier.inphase(), multiplier.quadrature()); - /** - * Absolute value of imaginary component - */ - public float imaginaryAbsolute() - { - return Math.abs( mRight ); - } + mLeft = inphase; + mRight = quadrature; + } - /** - * Returns the greater absolute value between left and right values - */ - public float maximumAbsolute() - { - if( Math.abs( mLeft ) > Math.abs( mRight ) ) - { - return Math.abs( mLeft ); - } - else - { - return Math.abs( mRight ); - } - } + /** + * Multiplies both samples returning a new sample with the results + */ + public static Complex multiply(Complex sample1, Complex sample2) + { + float inphase = multiplyInphase(sample1.inphase(), sample1.quadrature(), + sample2.inphase(), sample2.quadrature()); + float quadrature = multiplyQuadrature(sample1.inphase(), sample1.quadrature(), + sample2.inphase(), sample2.quadrature()); - /** - * Creates a new complex sample representing the angle with unit circle - * magnitude - * @param angle in radians - * @return - */ - public static Complex fromAngle( float angle ) - { - return new Complex( (float)Math.cos( angle ), - (float)Math.sin( angle ) ); - } + return new Complex(inphase, quadrature); + } - /** - * Angle of this sample in radians - */ - public float angle() - { - return (float)Math.atan2( y(), x() ); - } + public static Complex multiply(Complex sample, + Float inphase, Float quadrature) + { + float i = multiplyInphase(sample.inphase(), sample.quadrature(), + inphase, quadrature); + float q = multiplyQuadrature(sample.inphase(), sample.quadrature(), + inphase, quadrature); - public float angleDegrees() - { - return (float)Math.toDegrees(angle()); - } + return new Complex(i, q); + } - /** - * Constrains the i and q quantities to +/- value. - * - * @param value - maximum absolute value - */ - public void clip( float value ) - { - if( mLeft > value ) - { - mLeft = value; - } - else if( mLeft < -value ) - { - mLeft = -value; - } - - if( mRight > value ) - { - mRight = value; - } - else if( mRight < -value ) - { - mRight = -value; - } - } + public static Complex multiply(Float inphase, Float quadrature, Complex sample) + { + float i = multiplyInphase(inphase, quadrature, sample.inphase(), sample.quadrature()); + float q = multiplyQuadrature(inphase, quadrature, sample.inphase(), sample.quadrature()); - /** - * Angle of this sample in degrees - */ - public float polarAngle() - { - return (float)Math.toDegrees( angle() ); - } + return new Complex(i, q); + } - /** - * Provides an approximate magnitude value for this sample. - */ - public float envelope() - { - return envelope( mLeft, mRight ); - } - - public static float envelope( float inphase, float quadrature ) - { - float inphaseAbsolute = Math.abs( inphase ); - float quadratureAbsolute = Math.abs( quadrature ); - - if( inphaseAbsolute > quadratureAbsolute ) - { - return inphaseAbsolute + ( 0.4f * quadratureAbsolute ); - } - else - { - return quadratureAbsolute + ( 0.4f * inphaseAbsolute ); - } - } - - public static void main( String[] args ) - { - double angle = Math.PI; //180 degrees - - Complex s1 = new Complex( (float)Math.sin( angle ), (float)Math.cos( angle ) ); + public static Complex multiply(float inphaseA, float quadratureA, float inphaseB, float quadratureB) + { + float i = multiplyInphase(inphaseA, quadratureA, inphaseB, quadratureB); + float q = multiplyQuadrature(inphaseA, quadratureA, inphaseB, quadratureB); - Complex tap = new Complex( 1.0f, -0.1f ); + return new Complex(i, q); + } - Complex convolved = Complex.multiply( s1, tap ); - - mLog.debug( "s: " + s1.toString() + - " t: " + tap.toString() + - " convolved: " + convolved.toString() ); - - } + /** + * Adds the adder sample value to this sample + */ + public void add(Complex adder) + { + mLeft += adder.left(); + mRight += adder.right(); + } + + /** + * Adds the two complex samples returning a new complex sample with the result + */ + public static Complex add(Complex first, Complex second) + { + return new Complex(first.left() + second.left(), + first.right() + second.right()); + } + + /** + * Adds the two complex samples returning a new complex sample with the result + */ + public static Complex subtract(Complex first, Complex second) + { + return new Complex(first.left() - second.left(), + first.right() - second.right()); + } + + /** + * Magnitude of this sample + */ + public float magnitude() + { + return (float)Math.sqrt(magnitudeSquared()); + } + + public static float magnitude(float inphase, float quadrature) + { + return (float)Math.sqrt((inphase * inphase) + (quadrature * quadrature)); + } + + /** + * Magnitude squared of this sample + */ + public float magnitudeSquared() + { + return norm(); + } + + /** + * Norm of this sample = ( i * i ) + ( q * q ) + */ + public float norm() + { + return (float)((inphase() * inphase()) + + (quadrature() * quadrature())); + } + + /** + * Returns the vector length to 1 (unit circle) + */ + public void normalize() + { + float magnitude = magnitude(); + + if(magnitude != 0) + { + multiply((float)(1.0f / magnitude())); + } + } + + /** + * Returns the vector length to 1 (unit circle), + * avoiding square root multiplication + */ + public void fastNormalize() + { + multiply((float)(1.95f - magnitudeSquared())); + } + + public float left() + { + return mLeft; + } + + public float right() + { + return mRight; + } + + public float inphase() + { + return mLeft; + } + + /** + * Absolute value of in-phase component + */ + public float inPhaseAbsolute() + { + return Math.abs(mLeft); + } + + public float quadrature() + { + return mRight; + } + + /** + * Absolute value of quadrature component + */ + public float quadratureAbsolute() + { + return Math.abs(mRight); + } + + public float x() + { + return mLeft; + } + + public float y() + { + return mRight; + } + + public float real() + { + return mLeft; + } + + /** + * Absolute value of real component + */ + public float realAbsolute() + { + return Math.abs(mLeft); + } + + public float imaginary() + { + return mRight; + } + + /** + * Absolute value of imaginary component + */ + public float imaginaryAbsolute() + { + return Math.abs(mRight); + } + + /** + * Returns the greater absolute value between left and right values + */ + public float maximumAbsolute() + { + if(Math.abs(mLeft) > Math.abs(mRight)) + { + return Math.abs(mLeft); + } + else + { + return Math.abs(mRight); + } + } + + /** + * Creates a new complex sample representing the angle with unit circle + * magnitude + * + * @param angle in radians + * @return + */ + public static Complex fromAngle(double angle) + { + return new Complex((float)Math.cos(angle), (float)Math.sin(angle)); + } + + /** + * Sets this complex instance with the specified angle in radians. + * @param angle in radians + */ + public void setAngle(double angle) + { + setInphase((float)Math.cos(angle)); + setQuadrature((float)Math.sin(angle)); + } + + /** + * Angle of this sample in radians + */ + public float angle() + { + return (float)Math.atan2(y(), x()); + } + + public float angleDegrees() + { + return (float)Math.toDegrees(angle()); + } + + /** + * Constrains the i and q quantities to +/- value. + * + * @param value - maximum absolute value + */ + public void clip(float value) + { + if(mLeft > value) + { + mLeft = value; + } + else if(mLeft < -value) + { + mLeft = -value; + } + + if(mRight > value) + { + mRight = value; + } + else if(mRight < -value) + { + mRight = -value; + } + } + + /** + * Angle of this sample in degrees + */ + public float polarAngle() + { + return (float)Math.toDegrees(angle()); + } + + /** + * Provides an approximate magnitude value for this sample. + */ + public float envelope() + { + return envelope(mLeft, mRight); + } + + public static float envelope(float inphase, float quadrature) + { + float inphaseAbsolute = Math.abs(inphase); + float quadratureAbsolute = Math.abs(quadrature); + + if(inphaseAbsolute > quadratureAbsolute) + { + return inphaseAbsolute + (0.4f * quadratureAbsolute); + } + else + { + return quadratureAbsolute + (0.4f * inphaseAbsolute); + } + } + + public static void main(String[] args) + { + double angle = Math.PI; //180 degrees + + Complex s1 = new Complex((float)Math.sin(angle), (float)Math.cos(angle)); + + Complex tap = new Complex(1.0f, -0.1f); + + Complex convolved = Complex.multiply(s1, tap); + + mLog.debug("s: " + s1.toString() + + " t: " + tap.toString() + + " convolved: " + convolved.toString()); + + } } 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); } From dda37a1198d58015516edb9d0fb6e50111b718d5 Mon Sep 17 00:00:00 2001 From: Denny Date: Sun, 18 Feb 2018 16:20:45 -0500 Subject: [PATCH 13/23] 330 WIP - updates the demodulator viewer with an eye diagram and symbol chart. Testing various configurations to see which yields the best PLL and symbol timing locks. --- .../dsheirer/buffer/DoubleCircularBuffer.java | 12 + .../dsp/filter/design/FilterView.java | 278 +++++++++--------- .../dsp/filter/design/FilterViewer.java | 9 +- .../github/dsheirer/dsp/fm/FMDemodulator.java | 133 +++++---- .../dsheirer/dsp/fm/FMDemodulator_CB.java | 3 +- .../dsp/psk/InterpolatingSymbolBuffer.java | 21 +- .../dsheirer/dsp/psk/QPSKDemodulator.java | 48 ++- .../dsheirer/dsp/psk/SymbolDecisionData.java | 70 +++++ .../dsheirer/dsp/psk/pll/CostasLoop.java | 16 +- .../github/dsheirer/dsp/psk/pll/Tracking.java | 45 ++- .../gui/viewer/PlaybackController.java | 3 +- .../gui/viewer/chart/EyeDiagramChart.java | 118 ++++++++ .../viewer/chart/FMDemodulatedLineChart.java | 119 ++++++++ .../gui/viewer/chart/FrequencyLineChart.java | 73 +++++ .../gui/viewer/chart/PhaseLineChart.java | 4 +- .../gui/viewer/chart/SymbolChart.java | 30 +- .../gui/viewer/decoder/P25Phase1Pane.java | 45 ++- .../module/decode/p25/P25MessageFramer.java | 3 +- .../module/decode/p25/P25_C4FMDecoder2.java | 86 ++++-- .../source/wave/ComplexWaveSource.java | 7 +- 20 files changed, 844 insertions(+), 279 deletions(-) create mode 100644 src/main/java/io/github/dsheirer/dsp/psk/SymbolDecisionData.java create mode 100644 src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/EyeDiagramChart.java create mode 100644 src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/FMDemodulatedLineChart.java create mode 100644 src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/FrequencyLineChart.java diff --git a/src/main/java/io/github/dsheirer/buffer/DoubleCircularBuffer.java b/src/main/java/io/github/dsheirer/buffer/DoubleCircularBuffer.java index af5bd86d..b65f2f67 100644 --- a/src/main/java/io/github/dsheirer/buffer/DoubleCircularBuffer.java +++ b/src/main/java/io/github/dsheirer/buffer/DoubleCircularBuffer.java @@ -18,6 +18,7 @@ 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; @@ -30,6 +31,7 @@ import java.util.Arrays; */ public class DoubleCircularBuffer { + private StandardDeviation mStandardDeviation; private Variance mVariance; private Max mMax; private Mean mMean; @@ -140,6 +142,16 @@ public class DoubleCircularBuffer 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 */ diff --git a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterView.java b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterView.java index edaa4289..591da465 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterView.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterView.java @@ -12,155 +12,169 @@ import org.jtransforms.fft.FloatFFT_1D; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.net.URL; + public class FilterView extends BorderPane { - private final static Logger mLog = LoggerFactory.getLogger( FilterView.class ); + private final static Logger mLog = LoggerFactory.getLogger(FilterView.class); - private CurveFittedAreaChart mImpulseChart; - private NumberAxis mImpulseXAxis; - private NumberAxis mImpulseYAxis; - private CurveFittedAreaChart mDFTChart; - private NumberAxis mDFTXAxis; - private NumberAxis mDFTYAxis; - private static final int FFT_SIZE = 4096; - private FloatFFT_1D mFFT = new FloatFFT_1D( FFT_SIZE ); - - public FilterView( float[] taps ) - { - init(); + private CurveFittedAreaChart mImpulseChart; + private NumberAxis mImpulseXAxis; + private NumberAxis mImpulseYAxis; + private CurveFittedAreaChart mDFTChart; + private NumberAxis mDFTXAxis; + private NumberAxis mDFTYAxis; + private static final int FFT_SIZE = 4096; + private FloatFFT_1D mFFT = new FloatFFT_1D(FFT_SIZE); - if( taps != null ) - { - setImpulseTaps( taps ); + public FilterView(float[] taps) + { + init(); - float[] dft = new float[ FFT_SIZE ]; - System.arraycopy( taps, 0, dft, 0, taps.length ); - - mFFT.realForward( dft ); - - setDFTTaps( convertDFTToDecibel( dft, taps.length ) ); - } - } - - private void init() - { - HBox hbox = new HBox(); - - mImpulseXAxis = new NumberAxis( 0.0, 0.5, 1.0 ); - mImpulseYAxis = new NumberAxis( -0.2, 1.2, .1 ); - - mImpulseChart = new CurveFittedAreaChart( mImpulseXAxis, mImpulseYAxis ); - mImpulseChart.setLegendVisible( false ); - mImpulseChart.setHorizontalGridLinesVisible( false ); - mImpulseChart.setVerticalGridLinesVisible(false); + if(taps != null) + { + setImpulseTaps(taps); + + float[] dft = new float[FFT_SIZE]; + System.arraycopy(taps, 0, dft, 0, taps.length); + + mFFT.realForward(dft); + + setDFTTaps(convertDFTToDecibel(dft, taps.length)); + } + } + + private void init() + { + HBox hbox = new HBox(); + + mImpulseXAxis = new NumberAxis(0.0, 0.5, 1.0); + mImpulseYAxis = new NumberAxis(-0.2, 1.2, .1); + + mImpulseChart = new CurveFittedAreaChart(mImpulseXAxis, mImpulseYAxis); + mImpulseChart.setLegendVisible(false); + mImpulseChart.setHorizontalGridLinesVisible(false); + mImpulseChart.setVerticalGridLinesVisible(false); mImpulseChart.setAlternativeColumnFillVisible(false); mImpulseChart.setAlternativeRowFillVisible(false); - - String css = FilterView.class.getResource( "FilterView.css" ).toExternalForm(); - mImpulseChart.getStylesheets().add( css ); - hbox.getChildren().add( mImpulseChart ); - HBox.setHgrow( mImpulseChart, Priority.ALWAYS ); - - mDFTXAxis = new NumberAxis( 0.0, FFT_SIZE / 2, FFT_SIZE / 8 ); - mDFTYAxis = new NumberAxis( -200.0, 10.0, 10.0 ); - - mDFTChart = new CurveFittedAreaChart( mDFTXAxis, mDFTYAxis ); - mDFTChart.setLegendVisible( false ); - mDFTChart.setHorizontalGridLinesVisible( false ); - mDFTChart.setVerticalGridLinesVisible(false); + URL cssURL = FilterView.class.getResource("FilterView.css"); + + String css = null; + + if(cssURL != null) + { + css = cssURL.toExternalForm(); + + } + + hbox.getChildren().add(mImpulseChart); + HBox.setHgrow(mImpulseChart, Priority.ALWAYS); + + mDFTXAxis = new NumberAxis(0.0, FFT_SIZE / 2, FFT_SIZE / 8); + mDFTYAxis = new NumberAxis(-200.0, 10.0, 10.0); + + mDFTChart = new CurveFittedAreaChart(mDFTXAxis, mDFTYAxis); + mDFTChart.setLegendVisible(false); + mDFTChart.setHorizontalGridLinesVisible(false); + mDFTChart.setVerticalGridLinesVisible(false); mDFTChart.setAlternativeColumnFillVisible(false); mDFTChart.setAlternativeRowFillVisible(false); - mDFTChart.getStylesheets().add( css ); - - hbox.getChildren().add( mDFTChart ); - HBox.setHgrow( mDFTChart, Priority.ALWAYS ); - setCenter( hbox ); - } - - public void setImpulseTaps( double[] taps ) - { - float[] convertedTaps = new float[ taps.length ]; - - for( int x = 0; x < taps.length; x++ ) - { - convertedTaps[ x ] = (float)taps[ x ]; - } - - setImpulseTaps( convertedTaps ); - } - - public void setImpulseTaps( float[] taps ) - { - double smallest = 0.0; - double largest = 0.0; - - final XYChart.Series series = new XYChart.Series<>(); - - for( int x = 0; x < taps.length; x++ ) + if(css != null) { - if( taps[ x ] < smallest ) - { - smallest = taps[ x ]; - } - - if( taps[ x ] > largest ) - { - largest = taps[ x ]; - } - - series.getData().add( new XYChart.Data( x, taps[ x ] ) ); + mImpulseChart.getStylesheets().add(css); + mDFTChart.getStylesheets().add(css); } - - mImpulseXAxis.setLowerBound( 0.0 ); - mImpulseXAxis.setUpperBound( taps.length - 1 ); - mImpulseXAxis.setTickUnit( 1.0 ); - - double padding = Math.abs( largest - smallest ) * .1; - - mImpulseYAxis.setLowerBound( smallest - padding ); - mImpulseYAxis.setUpperBound( largest + padding ); - mImpulseYAxis.setTickUnit( padding ); + + hbox.getChildren().add(mDFTChart); + HBox.setHgrow(mDFTChart, Priority.ALWAYS); + + setCenter(hbox); + } + + public void setImpulseTaps(double[] taps) + { + float[] convertedTaps = new float[taps.length]; + + for(int x = 0; x < taps.length; x++) + { + convertedTaps[x] = (float)taps[x]; + } + + setImpulseTaps(convertedTaps); + } + + public void setImpulseTaps(float[] taps) + { + double smallest = 0.0; + double largest = 0.0; + + final XYChart.Series series = new XYChart.Series<>(); + + for(int x = 0; x < taps.length; x++) + { + if(taps[x] < smallest) + { + smallest = taps[x]; + } + + if(taps[x] > largest) + { + largest = taps[x]; + } + + series.getData().add(new XYChart.Data(x, taps[x])); + } + + mImpulseXAxis.setLowerBound(0.0); + mImpulseXAxis.setUpperBound(taps.length - 1); + mImpulseXAxis.setTickUnit(1.0); + + double padding = Math.abs(largest - smallest) * .1; + + mImpulseYAxis.setLowerBound(smallest - padding); + mImpulseYAxis.setUpperBound(largest + padding); + mImpulseYAxis.setTickUnit(padding); ObservableList> list = mImpulseChart.getData(); - list.add( series ); - } - - public void setDFTTaps( float[] taps ) - { - double smallest = 0.0; - - final XYChart.Series series = new XYChart.Series<>(); - - for( int x = 0; x < taps.length; x++ ) - { - if( taps[ x ] < smallest ) - { - smallest = taps[ x ]; - } - - series.getData().add( new XYChart.Data( x, taps[ x ] ) ); - } - - ObservableList> list = mDFTChart.getData(); - list.add( series ); - } - - public float[] convertDFTToDecibel( float[] dft, int tapLength ) + list.add(series); + } + + public void setDFTTaps(float[] taps) { - float[] decibels = new float[ dft.length / 2 ]; + double smallest = 0.0; - int index = 0; - - for( int x = 0; x < decibels.length; x ++ ) - { - index = x * 2; - - decibels[ x ] = 20.0f * (float)Math.log10( ( ( dft[ index ] * dft[ index ] ) + - ( dft[ index + 1 ] * dft[ index + 1 ] ) ) ); - } + final XYChart.Series series = new XYChart.Series<>(); - return decibels; + for(int x = 0; x < taps.length; x++) + { + if(taps[x] < smallest) + { + smallest = taps[x]; + } + + series.getData().add(new XYChart.Data(x, taps[x])); + } + + ObservableList> list = mDFTChart.getData(); + list.add(series); + } + + public float[] convertDFTToDecibel(float[] dft, int tapLength) + { + float[] decibels = new float[dft.length / 2]; + + int index = 0; + + for(int x = 0; x < decibels.length; x++) + { + index = x * 2; + + decibels[x] = 20.0f * (float)Math.log10(((dft[index] * dft[index]) + + (dft[index + 1] * dft[index + 1]))); + } + + return decibels; } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java index c129a26d..ff42ad85 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java @@ -51,11 +51,11 @@ public class FilterViewer extends Application // .build(); FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() .sampleRate((int)48000.0) - .passBandCutoff(6000) + .passBandCutoff(2880) .passBandAmplitude(1.0) - .passBandRipple(0.1) + .passBandRipple(0.01) .stopBandAmplitude(0.0) - .stopBandStart(6250) + .stopBandStart(3600) .stopBandRipple(0.01) .build(); @@ -81,6 +81,9 @@ public class FilterViewer extends Application float[] taps = null; +// taps = FilterFactory.getSinc(6000, 1920, 47, Window.WindowType.BLACKMAN); + +// taps = FilterFactory.getRootRaisedCosine(10, 10, 0.2f); // taps = FilterFactory.getLowPass(48000, 6750, 7500, 60, // Window.WindowType.HAMMING, true); 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/psk/InterpolatingSymbolBuffer.java b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer.java index c67a2b9a..b949efe2 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer.java @@ -33,11 +33,13 @@ public class InterpolatingSymbolBuffer private float mOmegaMid; private int mDelayLinePointer = 0; + private float mSamplesPerSymbol; private int mTwiceSamplesPerSymbol; private RealInterpolator mInterpolator = new RealInterpolator(1.0f); public InterpolatingSymbolBuffer(float samplesPerSymbol) { + mSamplesPerSymbol = samplesPerSymbol; mMu = samplesPerSymbol; mTwiceSamplesPerSymbol = (int)Math.floor(2.0 * samplesPerSymbol); mDelayLineInphase = new float[2 * mTwiceSamplesPerSymbol]; @@ -58,7 +60,8 @@ public class InterpolatingSymbolBuffer mDelayLineQuadrature[mDelayLinePointer + mTwiceSamplesPerSymbol] = currentSample.quadrature(); //Increment pointer and keep pointer in bounds - mDelayLinePointer = (mDelayLinePointer + 1) % mTwiceSamplesPerSymbol; + mDelayLinePointer++; + mDelayLinePointer = mDelayLinePointer % mTwiceSamplesPerSymbol; } /** @@ -80,11 +83,11 @@ public class InterpolatingSymbolBuffer { //mOmega is samples per symbol and is constrained to floating between +/- .005 of the nominal samples per //symbol - mOmega = mOmega + mGainOmega * symbolTimingError; + mOmega = mOmega + mGainOmega * -symbolTimingError; mOmega = mOmegaMid + clip(mOmega - mOmegaMid, mOmegaRel); //Add another symbol's worth of samples to the counter and adjust timing based on gardner error - increaseSampleCounter((mOmega + (mGainMu * symbolTimingError))); + increaseSampleCounter((mOmega + (mGainMu * -symbolTimingError))); } /** @@ -128,6 +131,18 @@ public class InterpolatingSymbolBuffer return mCurrentSample; } + /** + * 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() + { + //TODO: I think that delay line pointer needs to have +4 added to it since the filter is 8 taps long + return new SymbolDecisionData(mDelayLineInphase, mDelayLineQuadrature, + mSamplesPerSymbol, mDelayLinePointer, mMu); + } + /** * Returns the interpolated inphase value for the specified offset * @param interpolation into the buffer to calculate the interpolated sample diff --git a/src/main/java/io/github/dsheirer/dsp/psk/QPSKDemodulator.java b/src/main/java/io/github/dsheirer/dsp/psk/QPSKDemodulator.java index 91d1ce76..38194543 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/QPSKDemodulator.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/QPSKDemodulator.java @@ -15,6 +15,7 @@ ******************************************************************************/ 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.dsp.symbol.Dibit; import io.github.dsheirer.sample.Listener; @@ -31,7 +32,9 @@ public class QPSKDemodulator implements ComplexSampleListener private GardnerDetector mGardnerDetector = new GardnerDetector(); private Listener mSymbolListener; private Listener mPLLErrorListener; + private Listener mPLLFrequencyListener; private Listener mDibitListener; + private Listener mSymbolDecisionDataListener; private Complex mPreviousSample = new Complex(0, 0); private Complex mPreviousMiddleSample = new Complex(0, 0); @@ -42,6 +45,7 @@ public class QPSKDemodulator implements ComplexSampleListener private Complex mCurrentSymbol = new Complex(0, 0); private float mPhaseError; private float mSymbolTimingError; + private double mSampleRate; /** * Decoder for Quaternary Phase Shift Keying (QPSK) and Differential QPSK (DQPSK). This decoder uses both a @@ -63,6 +67,7 @@ public class QPSKDemodulator implements ComplexSampleListener "] must be at least 2 x symbol rate [" + symbolRate + "]"); } + mSampleRate = sampleRate; mPLL = phaseLockedLoop; mSymbolPhaseErrorCalculator = symbolPhaseErrorCalculator; mQPSKSymbolDecoder = symbolDecoder; @@ -108,6 +113,12 @@ public class QPSKDemodulator implements ComplexSampleListener mMiddleSample = mInterpolatingSymbolBuffer.getMiddleSample(); mCurrentSample = mInterpolatingSymbolBuffer.getCurrentSample(); + //Eye diagram listener + if(mSymbolDecisionDataListener != null) + { + mSymbolDecisionDataListener.receive(mInterpolatingSymbolBuffer.getSymbolDecisionData()); + } + //Calculate middle and current symbols as the delta between the previous and current samples mMiddleSymbol.setInphase(Complex.multiplyInphase(mMiddleSample.inphase(), mMiddleSample.quadrature(), mPreviousMiddleSample.inphase(), -mPreviousMiddleSample.quadrature())); mMiddleSymbol.setQuadrature(Complex.multiplyQuadrature(mMiddleSample.inphase(), mMiddleSample.quadrature(), mPreviousMiddleSample.inphase(), -mPreviousMiddleSample.quadrature())); @@ -127,12 +138,20 @@ public class QPSKDemodulator implements ComplexSampleListener mPreviousMiddleSample.setValues(mMiddleSample); mPreviousSymbol.setValues(mCurrentSymbol); + //Send to an external constellation symbol listener when registered + if(mSymbolListener != null) + { + mSymbolListener.receive(mCurrentSymbol); + } + //Adjust the current symbol as necessary (e.g. for differential encoding) mSymbolPhaseErrorCalculator.adjust(mCurrentSymbol); //Calculate the phase error of the current symbol relative to the expected constellation and provide //feedback to the PLL mPhaseError = mSymbolPhaseErrorCalculator.getPhaseError(mCurrentSymbol); + + mPhaseError = GardnerDetector.clip(mPhaseError, 0.2f); mPLL.adjust(mPhaseError); if(mPLLErrorListener != null) @@ -140,18 +159,21 @@ public class QPSKDemodulator implements ComplexSampleListener mPLLErrorListener.receive((double)mPhaseError); } + if(mPLLFrequencyListener != null) + { + double loopFrequency = ((CostasLoop)mPLL).getLoopFrequency(); + + loopFrequency *= mSampleRate / (2.0 * Math.PI); + + mPLLFrequencyListener.receive(loopFrequency); + } + //Decode the dibit from the symbol and send to the listener if(mDibitListener != null) { mDibitListener.receive(mQPSKSymbolDecoder.decode(mCurrentSymbol)); } - //Send to an external constellation symbol listener when registered - if(mSymbolListener != null) - { - mSymbolListener.receive(mCurrentSymbol); - } - //TODO: Assemble dibits here and broadcast } } @@ -179,4 +201,18 @@ public class QPSKDemodulator implements ComplexSampleListener { mPLLErrorListener = listener; } + + /** + * Registers the listener to receive PLL error values + */ + public void setPLLFrequencyListener(Listener listener) + { + mPLLFrequencyListener = listener; + } + + public void setSymbolDecisionDataListener(Listener listener) + { + mSymbolDecisionDataListener = listener; + } + } 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..75a70332 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/SymbolDecisionData.java @@ -0,0 +1,70 @@ +/******************************************************************************* + * 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 class SymbolDecisionData +{ + private float[] mSamplesInphase; + private float[] mSamplesQuadrature; + private float mSamplesPerSymbol; + private int mSampleIndex; + private float mSampleIndexOffset; + + /** + * Symbol decision data - samples and timing decision for a single symbol decision instant. + * + * @param samplesInphase + * @param samplesQuadrature + * @param samplesPerSymbol + * @param sampleIndex + * @param sampleIndexOffset + */ + public SymbolDecisionData(float[] samplesInphase, float[] samplesQuadrature, float samplesPerSymbol, + int sampleIndex, float sampleIndexOffset) + { + mSamplesInphase = samplesInphase; + mSamplesQuadrature = samplesQuadrature; + mSamplesPerSymbol = samplesPerSymbol; + mSampleIndex = sampleIndex; + mSampleIndexOffset = sampleIndexOffset; + } + + public float[] getInphaseSamples() + { + return mSamplesInphase; + } + + public float[] getQuadratureSamples() + { + return mSamplesQuadrature; + } + + public float getSamplesPerSymbol() + { + return mSamplesPerSymbol; + } + + public int getSampleIndex() + { + return mSampleIndex; + } + + public float getSampleIndexOffset() + { + return mSampleIndexOffset; + } +} 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 index 71d07392..22726f09 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/pll/CostasLoop.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/pll/CostasLoop.java @@ -194,20 +194,21 @@ public class CostasLoop implements IPhaseLockedLoop mErrorBuffer.put(phaseError); - double variance = mErrorBuffer.variance(); + double standardDeviation = mErrorBuffer.standardDeviation(); switch(mTracking) { case SEARCHING: - if(Tracking.COARSE.meetsThreshold(variance)) + if(Tracking.COARSE.contains(standardDeviation)) { setTracking(Tracking.COARSE); } break; case COARSE: - if(Tracking.COARSE.meetsThreshold(variance)) + if(Tracking.COARSE.contains(standardDeviation)) { - if(Tracking.FINE.meetsThreshold(variance)) + //Promote if possible - otherwise remain + if(Tracking.FINE.contains(standardDeviation)) { setTracking(Tracking.FINE); } @@ -218,9 +219,10 @@ public class CostasLoop implements IPhaseLockedLoop } break; case FINE: - if(Tracking.FINE.meetsThreshold(variance)) + if(Tracking.FINE.contains(standardDeviation)) { - if(Tracking.LOCKED.meetsThreshold(variance)) + //Promote if possible - otherwise remain + if(Tracking.LOCKED.contains(standardDeviation)) { setTracking(Tracking.LOCKED); } @@ -231,7 +233,7 @@ public class CostasLoop implements IPhaseLockedLoop } break; case LOCKED: - if(!Tracking.LOCKED.meetsThreshold(variance)) + if(!Tracking.LOCKED.contains(standardDeviation)) { setTracking(Tracking.FINE); } 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 index 5cabc7ab..a9f07ad8 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/pll/Tracking.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/pll/Tracking.java @@ -17,32 +17,55 @@ package io.github.dsheirer.dsp.psk.pll; public enum Tracking { - SEARCHING(50.0, Math.PI), - COARSE(100.0, 0.01), - FINE(150.0, 0.001), - LOCKED(200.0, 0.0001); +// SEARCHING(50.0, 0.01, Math.PI), +// COARSE(100.0, 0.001, 0.01), +// FINE(150.0, 0.0001, 0.001), +// LOCKED(200.0, 0.0, 0.0001); + SEARCHING(100.0, 0.4, Math.PI), + COARSE(150.0, 0.2, 0.4), + FINE(175.0, 0.1, 0.2), + LOCKED(200.0, 0.0, 0.1); private double mLoopBandwidth; - private double mVarianceThreshold; + private double mThresholdMinimum; + private double mThresholdMaximum; - Tracking(double loopBandwidth, double varianceThreshold) + Tracking(double loopBandwidth, double thresholdMinimum, double thresholdMaximum) { mLoopBandwidth = loopBandwidth; - mVarianceThreshold = varianceThreshold; + mThresholdMinimum = thresholdMinimum; + mThresholdMaximum = thresholdMaximum; } + /** + * Loop bandwidth setting + */ public double getLoopBandwidth() { return mLoopBandwidth; } - public double getVarianceThreshold() + /** + * Minimum error value for this entry + */ + public double getThresholdMinimum() { - return mVarianceThreshold; + return mThresholdMinimum; } - public boolean meetsThreshold(double variance) + /** + * Maximum error value for this entry + */ + public double getThresholdMaximum() { - return variance < getVarianceThreshold(); + return mThresholdMaximum; + } + + /** + * Indicates if the error value is within the range for this entry + */ + public boolean contains(double standardDeviation) + { + return getThresholdMinimum() <= standardDeviation && standardDeviation < getThresholdMaximum(); } } 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 index 646f41cf..1ea7a687 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/PlaybackController.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/PlaybackController.java @@ -79,7 +79,6 @@ public class PlaybackController extends HBox implements IFrameLocationListener, public void load(File file) { - mLog.debug("loading:" + file.getAbsolutePath()); if(file != null && file.isFile()) { try @@ -104,7 +103,7 @@ public class PlaybackController extends HBox implements IFrameLocationListener, } catch(IOException ioe) { - mLog.error("Error opening file [" + (file != null ? file.getAbsolutePath() : "null") + "]"); + 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); 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..f1dde889 --- /dev/null +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/EyeDiagramChart.java @@ -0,0 +1,118 @@ +/******************************************************************************* + * 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.fm.FMDemodulator; +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", 1.0, 10.0, 1.0), + new NumberAxis("Value", -1.0, 1.0, 0.25)); + + mSeriesCount = seriesCount; + 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) + { + checkChartLength((int)symbolDecisionData.getSamplesPerSymbol()); + int length = (int)symbolDecisionData.getSamplesPerSymbol() + 1; + + + ObservableList> series = getSeries(mSeriesPointer++, length + 1); + + if(mSeriesPointer >= mSeriesCount) + { + mSeriesPointer = 0; + } + + int index = symbolDecisionData.getSampleIndex(); + + Complex previous = new Complex(symbolDecisionData.getInphaseSamples()[index], + symbolDecisionData.getQuadratureSamples()[index]); + + for(int x = 1; x <= length; x++) + { + Complex current = new Complex(symbolDecisionData.getInphaseSamples()[index + x], + symbolDecisionData.getQuadratureSamples()[index + x]); + + double demodulated = FMDemodulator.demodulate(previous, current); + demodulated *= 6; + + Data dataPoint = series.get(x); + dataPoint.setXValue((double)x - symbolDecisionData.getSampleIndexOffset()); + dataPoint.setYValue(demodulated); + + previous = current; + } + } +} 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/FrequencyLineChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/FrequencyLineChart.java new file mode 100644 index 00000000..b5cd0248 --- /dev/null +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/FrequencyLineChart.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 FrequencyLineChart extends LineChart implements Listener +{ + private final static Logger mLog = LoggerFactory.getLogger(FrequencyLineChart.class); + + private DoubleCircularBuffer mDoubleCircularBuffer; + private ObservableList> mPhaseValues = FXCollections.observableArrayList(); + + public FrequencyLineChart(double maxFrequency, int length) + { + super(new NumberAxis("Time", 1, length, 2), + new NumberAxis("Frequency", -maxFrequency, maxFrequency, maxFrequency / 5.0)); + + Series phaseSeries = new Series<>("PLL Frequency", 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/PhaseLineChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/PhaseLineChart.java index 3857026d..df7ff76a 100644 --- 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 @@ -34,9 +34,9 @@ public class PhaseLineChart extends LineChart implements Listener public PhaseLineChart(int length) { super(new NumberAxis("Time", 1, length, 2), - new NumberAxis("Phase", -Math.PI, Math.PI, Math.PI / 2.0)); + new NumberAxis("Phase", -0.8, 0.8, 0.1)); - Series phaseSeries = new Series<>("Phase", mPhaseValues); + Series phaseSeries = new Series<>("PLL Phase Error", mPhaseValues); ObservableList> observableList = FXCollections.observableArrayList(phaseSeries); setData(observableList); 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 index 5d67bb7d..70b9d4ce 100644 --- 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 @@ -30,8 +30,8 @@ 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 ObservableList mPreviousPoints = FXCollections.observableArrayList(); +// private ObservableList mOldestPoints = FXCollections.observableArrayList(); private int mDataPointer; private int mHistory; @@ -43,22 +43,22 @@ public class SymbolChart extends ScatterChart implements Listener */ public SymbolChart(int history) { - super(new NumberAxis("I", -1.0, 1.0, 0.2), - new NumberAxis("Q", -1.0, 1.0, 0.2)); + 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)); +// 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("Current", mCurrentPoints)); - data.add(new ScatterChart.Series("Previous", mPreviousPoints)); - data.add(new ScatterChart.Series("Oldest", mOldestPoints)); +// data.add(new ScatterChart.Series("Previous", mPreviousPoints)); +// data.add(new ScatterChart.Series("Oldest", mOldestPoints)); this.setData(data); } @@ -67,15 +67,15 @@ public class SymbolChart extends ScatterChart implements Listener public void receive(Complex complex) { //Transfer previous to oldest - XYChart.Data oldest = mOldestPoints.get(mDataPointer); - XYChart.Data previous = mPreviousPoints.get(mDataPointer); +// 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()); +// oldest.setXValue(previous.getXValue()); +// oldest.setYValue(previous.getYValue()); +// +// previous.setXValue(current.getXValue()); +// previous.setYValue(current.getYValue()); current.setXValue(complex.inphase()); current.setYValue(complex.quadrature()); 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 index 62253028..c13f0a92 100644 --- 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 @@ -15,8 +15,9 @@ ******************************************************************************/ package io.github.dsheirer.instrument.gui.viewer.decoder; -import io.github.dsheirer.instrument.gui.viewer.chart.ComplexPhaseLineChart; import io.github.dsheirer.instrument.gui.viewer.chart.ComplexSampleLineChart; +import io.github.dsheirer.instrument.gui.viewer.chart.EyeDiagramChart; +import io.github.dsheirer.instrument.gui.viewer.chart.FrequencyLineChart; import io.github.dsheirer.instrument.gui.viewer.chart.PhaseLineChart; import io.github.dsheirer.instrument.gui.viewer.chart.SymbolChart; import io.github.dsheirer.module.decode.DecoderType; @@ -32,10 +33,11 @@ public class P25Phase1Pane extends DecoderPane private HBox mSampleChartBox; private ComplexSampleLineChart mSampleLineChart; - private ComplexPhaseLineChart mPhaseLineChart; + private EyeDiagramChart mEyeDiagramChart; private HBox mDecoderChartBox; private SymbolChart mSymbolChart; private PhaseLineChart mPLLPhaseErrorLineChart; + private FrequencyLineChart mPLLFrequencyLineChart; private P25_C4FMDecoder2 mDecoder = new P25_C4FMDecoder2(null); public P25Phase1Pane() @@ -47,11 +49,12 @@ public class P25Phase1Pane extends DecoderPane private void init() { addListener(getSampleLineChart()); - addListener(getPhaseLineChart()); addListener(getDecoder()); - getDecoder().setSymbolListener(getSymbolChart()); - getDecoder().setPLLPhaseErrorListener(getPLLPhaseErrorLineChart()); + getDecoder().setDEBUGSymbolListener(getSymbolChart()); + getDecoder().setDEBUGPLLPhaseErrorListener(getPLLPhaseErrorLineChart()); + getDecoder().setDEBUGPLLFrequencyListener(getPLLFrequencyLineChart()); + getDecoder().setDEBUGSymbolDecisionDataListener(getEyeDiagramChart()); getChildren().addAll(getSampleChartBox(), getDecoderChartBox()); } @@ -73,7 +76,7 @@ public class P25Phase1Pane extends DecoderPane { if(mSymbolChart == null) { - mSymbolChart = new SymbolChart(20); + mSymbolChart = new SymbolChart(10); } return mSymbolChart; @@ -86,9 +89,11 @@ public class P25Phase1Pane extends DecoderPane mDecoderChartBox = new HBox(); 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); - mDecoderChartBox.getChildren().addAll(getSymbolChart(), getPLLPhaseErrorLineChart()); + HBox.setHgrow(getPLLFrequencyLineChart(), Priority.ALWAYS); + mDecoderChartBox.getChildren().addAll(getSymbolChart(), getPLLPhaseErrorLineChart(), getPLLFrequencyLineChart()); } return mDecoderChartBox; @@ -100,10 +105,10 @@ public class P25Phase1Pane extends DecoderPane { mSampleChartBox = new HBox(); getSampleLineChart().setMaxWidth(Double.MAX_VALUE); - getPhaseLineChart().setMaxWidth(Double.MAX_VALUE); + getEyeDiagramChart().setMaxWidth(Double.MAX_VALUE); HBox.setHgrow(getSampleLineChart(), Priority.ALWAYS); - HBox.setHgrow(getPhaseLineChart(), Priority.ALWAYS); - mSampleChartBox.getChildren().addAll(getSampleLineChart(), getPhaseLineChart()); + HBox.setHgrow(getEyeDiagramChart(), Priority.ALWAYS); + mSampleChartBox.getChildren().addAll(getSampleLineChart(), getEyeDiagramChart()); } return mSampleChartBox; @@ -119,23 +124,33 @@ public class P25Phase1Pane extends DecoderPane return mSampleLineChart; } - private ComplexPhaseLineChart getPhaseLineChart() + private EyeDiagramChart getEyeDiagramChart() { - if(mPhaseLineChart == null) + if(mEyeDiagramChart == null) { - mPhaseLineChart = new ComplexPhaseLineChart(50); + mEyeDiagramChart = new EyeDiagramChart(10); } - return mPhaseLineChart; + return mEyeDiagramChart; } private PhaseLineChart getPLLPhaseErrorLineChart() { if(mPLLPhaseErrorLineChart == null) { - mPLLPhaseErrorLineChart = new PhaseLineChart(80); + mPLLPhaseErrorLineChart = new PhaseLineChart(40); } return mPLLPhaseErrorLineChart; } + + private FrequencyLineChart getPLLFrequencyLineChart() + { + if(mPLLFrequencyLineChart == null) + { + mPLLFrequencyLineChart = new FrequencyLineChart(2400, 40); + } + + return mPLLFrequencyLineChart; + } } 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 54047262..4d38ed79 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 @@ -698,7 +698,8 @@ public class P25MessageFramer implements Listener @Override public void syncDetected() { - mPhaseLockedLoop.correctInversion(mCorrection); + mLog.warn("Phase Inversion SYNC detected - applying correction: " + mCorrection); +// mPhaseLockedLoop.correctInversion(mCorrection); /* Since we detected a sync pattern, start a message assembler */ for(P25MessageAssembler assembler : mAssemblers) diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2.java index 5a992365..f18ad3f3 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2.java @@ -26,8 +26,9 @@ import io.github.dsheirer.dsp.psk.IQPSKSymbolDecoder; import io.github.dsheirer.dsp.psk.ISymbolPhaseErrorCalculator; import io.github.dsheirer.dsp.psk.QPSKDemodulator; import io.github.dsheirer.dsp.psk.QPSKSymbolDecoder; +import io.github.dsheirer.dsp.psk.SymbolDecisionData; import io.github.dsheirer.dsp.psk.pll.CostasLoop; -import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop; +import io.github.dsheirer.dsp.psk.pll.Tracking; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.complex.Complex; import io.github.dsheirer.sample.complex.ComplexBuffer; @@ -49,16 +50,21 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen private Map mBasebandFilters = new HashMap<>(); private ComplexFIRFilter mBasebandFilter; + private Map mSymbolFilters = new HashMap<>(); + private ComplexFIRFilter mSymbolFilter; + private QPSKDemodulator mQPSKDemodulator; - private IPhaseLockedLoop mPhaseLockedLoop; + private CostasLoop mCostasLoop; private ComplexBufferToStreamConverter mStreamConverter = new ComplexBufferToStreamConverter(); private ComplexFeedForwardGainControl mAGC = new ComplexFeedForwardGainControl(32); private QPSKSymbolDecoder mQPSKSlicer = new QPSKSymbolDecoder(); private P25MessageFramer mMessageFramer; private ISymbolPhaseErrorCalculator mSymbolPhaseErrorCalculator = new DQPSKSymbolPhaseErrorCalculator(); private IQPSKSymbolDecoder mSymbolDecoder = new QPSKSymbolDecoder(); - private Listener mSymbolListener; - private Listener mPLLPhaseErrorListener; + private Listener mDEBUGSymbolListener; + private Listener mDEBUGPLLPhaseErrorListener; + private Listener mDEBUGPLLFrequencyListener; + private Listener mDEBUGSymbolDecisionDataListener; public P25_C4FMDecoder2(AliasList aliasList) { @@ -78,37 +84,57 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen mSampleRate = sampleRate; mBasebandFilter = new ComplexFIRFilter(getBasebandFilter(), 1.0f); + mSymbolFilter = new ComplexFIRFilter(getSymbolFilter(), 1.0f); - mPhaseLockedLoop = new CostasLoop(mSampleRate, SYMBOL_RATE); - mQPSKDemodulator = new QPSKDemodulator(mPhaseLockedLoop, mSymbolPhaseErrorCalculator, mSymbolDecoder, + mCostasLoop = new CostasLoop(mSampleRate, SYMBOL_RATE); + mCostasLoop.setTracking(Tracking.FINE); + mCostasLoop.setAutomaticTracking(false); + + mQPSKDemodulator = new QPSKDemodulator(mCostasLoop, mSymbolPhaseErrorCalculator, mSymbolDecoder, mSampleRate, SYMBOL_RATE); - mQPSKDemodulator.setSymbolListener(mSymbolListener); - mQPSKDemodulator.setPLLErrorListener(mPLLPhaseErrorListener); + mQPSKDemodulator.setSymbolListener(mDEBUGSymbolListener); + mQPSKDemodulator.setPLLErrorListener(mDEBUGPLLPhaseErrorListener); + mQPSKDemodulator.setPLLFrequencyListener(mDEBUGPLLFrequencyListener); + mQPSKDemodulator.setSymbolDecisionDataListener(mDEBUGSymbolDecisionDataListener); //Message framer can trigger a symbol-inversion correction to the PLL when detected - mMessageFramer = new P25MessageFramer(getAliasList(), mPhaseLockedLoop); + mMessageFramer = new P25MessageFramer(getAliasList(), mCostasLoop); mMessageFramer.setListener(getMessageProcessor()); mQPSKDemodulator.setDibitListener(mMessageFramer); } - public void setSymbolListener(Listener listener) + public void setDEBUGSymbolListener(Listener listener) { - mSymbolListener = listener; + mDEBUGSymbolListener = listener; mQPSKDemodulator.setSymbolListener(listener); } - public void setPLLPhaseErrorListener(Listener listener) + public void setDEBUGPLLPhaseErrorListener(Listener listener) { - mPLLPhaseErrorListener = listener; + mDEBUGPLLPhaseErrorListener = listener; mQPSKDemodulator.setPLLErrorListener(listener); } + public void setDEBUGPLLFrequencyListener(Listener listener) + { + mDEBUGPLLFrequencyListener = listener; + mQPSKDemodulator.setPLLFrequencyListener(listener); + } + + public void setDEBUGSymbolDecisionDataListener(Listener listener) + { + mDEBUGSymbolDecisionDataListener = listener; + mQPSKDemodulator.setSymbolDecisionDataListener(listener); + } + @Override public void receive(ComplexBuffer complexBuffer) { - ComplexBuffer filtered = mBasebandFilter.filter(complexBuffer); - ComplexBuffer amplified = mAGC.filter(filtered); - mQPSKDemodulator.receive(amplified); + ComplexBuffer basebandFiltered = mBasebandFilter.filter(complexBuffer); +// ComplexBuffer symbolFiltered = mBasebandFilter.filter(basebandFiltered); +// ComplexBuffer amplified = mAGC.filter(symbolFiltered); + mQPSKDemodulator.receive(basebandFiltered); +// mQPSKDemodulator.receive(complexBuffer); } private double getSampleRate() @@ -124,24 +150,44 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen { FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() .sampleRate((int)getSampleRate()) - .passBandCutoff(6000) + .passBandCutoff(2880) .passBandAmplitude(1.0) - .passBandRipple(0.1) + .passBandRipple(0.01) .stopBandAmplitude(0.0) - .stopBandStart(6250) + .stopBandStart(3600) .stopBandRipple(0.01) .build(); try { filter = FilterFactory.getTaps(specification); - mBasebandFilters.put(getSampleRate(), filter); } catch(FilterDesignException fde) { mLog.error("Couldn't design low pass baseband filter for sample rate: " + getSampleRate()); } + if(filter != null) + { + mBasebandFilters.put(getSampleRate(), filter); + } + else + { + throw new IllegalStateException("Couldn't design a C4FM symbol filter for sample rate: " + mSampleRate); + } + } + + return filter; + } + + private float[] getSymbolFilter() + { + float[] filter = mSymbolFilters.get(getSampleRate()); + + if(filter == null) + { + filter = FilterFactory.getRootRaisedCosine(10, 10, 0.2f); + mSymbolFilters.put(getSampleRate(), filter); } return filter; 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 d227a199..9b825533 100644 --- a/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java +++ b/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java @@ -167,13 +167,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; } /** From 3d1ac30646dc4c29c128255c67f68742f74231ba Mon Sep 17 00:00:00 2001 From: Denny Date: Tue, 20 Feb 2018 05:27:33 -0500 Subject: [PATCH 14/23] 330 WIP - changing to 45 degree rotation before demodulation. --- .../dsheirer/buffer/FloatCircularBuffer.java | 172 +++++++++--------- .../filter/interpolator/RealInterpolator.java | 124 ++++++++----- .../psk/DQPSKSymbolPhaseErrorCalculator.java | 11 +- .../dsp/psk/InterpolatingSymbolBuffer.java | 72 ++++---- .../dsheirer/dsp/psk/QPSKDemodulator.java | 25 +-- .../dsheirer/dsp/psk/SymbolDecisionData2.java | 68 +++++++ .../dsheirer/dsp/psk/pll/CostasLoop.java | 7 +- .../github/dsheirer/dsp/psk/pll/Tracking.java | 6 +- .../viewer/chart/ComplexSampleLineChart.java | 27 ++- .../gui/viewer/chart/EyeDiagramChart.java | 49 ++--- .../gui/viewer/chart/SymbolChart.java | 2 +- .../gui/viewer/decoder/P25Phase1Pane.java | 7 +- .../module/decode/p25/P25_C4FMDecoder2.java | 10 +- 13 files changed, 360 insertions(+), 220 deletions(-) create mode 100644 src/main/java/io/github/dsheirer/dsp/psk/SymbolDecisionData2.java diff --git a/src/main/java/io/github/dsheirer/buffer/FloatCircularBuffer.java b/src/main/java/io/github/dsheirer/buffer/FloatCircularBuffer.java index ce3d0bd3..640e6506 100644 --- a/src/main/java/io/github/dsheirer/buffer/FloatCircularBuffer.java +++ b/src/main/java/io/github/dsheirer/buffer/FloatCircularBuffer.java @@ -1,17 +1,17 @@ /******************************************************************************* * SDR Trunk * Copyright (C) 2014 Dennis Sheirer - * + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with this program. If not, see ******************************************************************************/ @@ -23,96 +23,98 @@ 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 - * @author denny * + * Can be used as a delay-type buffer, to delay samples by the 'size' amount + * + * @author denny */ public class FloatCircularBuffer { - float[] mBuffer; - int mBufferPointer = 0; - - public FloatCircularBuffer( int size ) - { - mBuffer = new float[ size ]; - - Arrays.fill( mBuffer, (float)0.0 ); - } - - public int getSize() - { - return mBuffer.length; - } - - public float[] getBuffer() - { - return mBuffer; - } + float[] mBuffer; + int mBufferPointer = 0; - /** - * Puts the new value into the buffer and returns the oldest buffer value - * that it replaced - * @param newValue - * @return - */ - public float get( float newValue ) - { - float oldestSample = mBuffer[ mBufferPointer ]; + /** + * 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 FloatCircularBuffer(int size, float initialFillValue) + { + mBuffer = new float[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 FloatCircularBuffer(int size) + { + this(size, 0.0f); + } - /** - * Convenience method to put a new value in the buffer, while discarding - * the old value - */ - public void put( float newValue ) - { - get( newValue ); - } + public float[] getBuffer() + { + return mBuffer; + } - /** - * Returns an array of arraySize, loaded with sampleCount of the - * oldest samples, loaded into the array starting at index 0. - * - * This is useful for fetching an array filled half way with samples to - * use in FFT processing. - * @param arraySize - size of the array (must be larger than sampleCount) - * @param sampleCount - number of the oldest samples in the buffer to load - * @return - */ - public float[] get( int arraySize, int sampleCount ) - { - float[] samples = new float[ arraySize ]; + /** + * 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 float get(float newValue) + { + float oldestSample = mBuffer[mBufferPointer]; - if( arraySize > sampleCount ) - { - int tempCounter = mBufferPointer; + mBuffer[mBufferPointer] = newValue; - for( int x = 0; x < sampleCount; x++ ) - { - samples[ x ] = mBuffer[ tempCounter ]; - - tempCounter++; - - if( tempCounter > mBuffer.length ) - { - tempCounter = 0; - } - } - } - - return samples; - } - + mBufferPointer++; + + if(mBufferPointer >= mBuffer.length) + { + mBufferPointer = 0; + } + + return oldestSample; + } + + public float[] getAll() + { + int valuePointer = 0; + float[] values = new float[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(float newValue) + { + mBuffer[mBufferPointer] = newValue; + + mBufferPointer++; + + if(mBufferPointer >= mBuffer.length) + { + mBufferPointer = 0; + } + } } 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/psk/DQPSKSymbolPhaseErrorCalculator.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolPhaseErrorCalculator.java index c299d122..cf5dd9c6 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolPhaseErrorCalculator.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolPhaseErrorCalculator.java @@ -20,11 +20,20 @@ import io.github.dsheirer.sample.complex.Complex; public class DQPSKSymbolPhaseErrorCalculator extends QPSKSymbolPhaseErrorCalculator { //Negative 45 degrees rotation to offset for differential additive 45 degrees of rotation - public static final Complex DIFFERENTIAL_OFFSET = Complex.fromAngle(Math.PI / -4.0d); + public static final Complex DIFFERENTIAL_OFFSET = Complex.fromAngle(Math.PI / 4.0d); @Override public void adjust(Complex symbol) { symbol.multiply(DIFFERENTIAL_OFFSET); } + + @Override + public float getPhaseError(Complex symbol) + { + Complex copy = symbol.copy(); + + copy.multiply(DIFFERENTIAL_OFFSET); + return super.getPhaseError(copy); + } } diff --git a/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer.java b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer.java index b949efe2..fa050e5e 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer.java @@ -25,33 +25,34 @@ public class InterpolatingSymbolBuffer private float[] mDelayLineInphase; private float[] mDelayLineQuadrature; - private float mMu; //Sample Counter - private float mGainMu = 0.05f; //Sample Counter Gain - private float mOmega; - private float mGainOmega = 0.1f * mGainMu * mGainMu; - private float mOmegaRel = 0.005f; - private float mOmegaMid; + private float mSamplingPoint; //Sample Counter + private float mSampleCounterGain = 0.05f; //Sample Counter Gain + private float mSamplesPerSymbol; + private float mDetectedSamplesPerSymbol; + private float mDetectedSamplesPerSymbolGain = 0.1f * mSampleCounterGain * mSampleCounterGain; + private float mMaxDeviation = 0.005f; private int mDelayLinePointer = 0; - private float mSamplesPerSymbol; private int mTwiceSamplesPerSymbol; private RealInterpolator mInterpolator = new RealInterpolator(1.0f); + private SymbolDecisionData2 mSymbolDecisionData2; public InterpolatingSymbolBuffer(float samplesPerSymbol) { - mSamplesPerSymbol = samplesPerSymbol; - mMu = samplesPerSymbol; + mSamplingPoint = samplesPerSymbol; mTwiceSamplesPerSymbol = (int)Math.floor(2.0 * samplesPerSymbol); mDelayLineInphase = new float[2 * mTwiceSamplesPerSymbol]; mDelayLineQuadrature = new float[2 * mTwiceSamplesPerSymbol]; - mOmega = samplesPerSymbol; - mOmegaMid = samplesPerSymbol; + mDetectedSamplesPerSymbol = samplesPerSymbol; + mSamplesPerSymbol = samplesPerSymbol; + mSymbolDecisionData2 = new SymbolDecisionData2((int)samplesPerSymbol); } public void receive(Complex currentSample) { - mMu--; + mSymbolDecisionData2.receive(currentSample); + mSamplingPoint--; //Fill up the delay line to use with the interpolator mDelayLineInphase[mDelayLinePointer] = currentSample.inphase(); @@ -62,6 +63,7 @@ public class InterpolatingSymbolBuffer //Increment pointer and keep pointer in bounds mDelayLinePointer++; mDelayLinePointer = mDelayLinePointer % mTwiceSamplesPerSymbol; + } /** @@ -70,7 +72,7 @@ public class InterpolatingSymbolBuffer */ public void increaseSampleCounter(float samplesToAdd) { - mMu += samplesToAdd; + mSamplingPoint += samplesToAdd; } /** @@ -81,13 +83,13 @@ public class InterpolatingSymbolBuffer */ public void resetAndAdjust(float symbolTimingError) { - //mOmega is samples per symbol and is constrained to floating between +/- .005 of the nominal samples per + //mDetectedSamplesPerSymbol is samples per symbol and is constrained to floating between +/- .005 of the nominal samples per //symbol - mOmega = mOmega + mGainOmega * -symbolTimingError; - mOmega = mOmegaMid + clip(mOmega - mOmegaMid, mOmegaRel); + mDetectedSamplesPerSymbol = mDetectedSamplesPerSymbol + mDetectedSamplesPerSymbolGain * symbolTimingError; + mDetectedSamplesPerSymbol = mSamplesPerSymbol + clip(mDetectedSamplesPerSymbol - mSamplesPerSymbol, mMaxDeviation); //Add another symbol's worth of samples to the counter and adjust timing based on gardner error - increaseSampleCounter((mOmega + (mGainMu * -symbolTimingError))); + increaseSampleCounter((mDetectedSamplesPerSymbol + (mSampleCounterGain * symbolTimingError))); } /** @@ -95,7 +97,7 @@ public class InterpolatingSymbolBuffer */ public boolean hasSymbol() { - return mMu < 1.0f; + return mSamplingPoint < 1.0f; } /** @@ -105,7 +107,7 @@ public class InterpolatingSymbolBuffer public Complex getMiddleSample() { /* Calculate interpolated middle sample and current sample */ - mMiddleSample.setValues(getInphase(mMu), getQuadrature(mMu)); + mMiddleSample.setValues(getInphase(mSamplingPoint), getQuadrature(mSamplingPoint)); return mMiddleSample; } @@ -116,17 +118,18 @@ public class InterpolatingSymbolBuffer */ public Complex getCurrentSample() { - float half_omega = mOmega / 2.0f; - int half_sps = (int)Math.floor(half_omega); - float half_mu = mMu + half_omega - half_sps; + float halfDetectedSamplesPerSymbol = mDetectedSamplesPerSymbol / 2.0f; + int halfSamplingPointOffset = (int)Math.floor(halfDetectedSamplesPerSymbol); + float halfSamplingPoint = mSamplingPoint + halfDetectedSamplesPerSymbol - halfSamplingPointOffset; - if(half_mu > 1.0) + if(halfSamplingPoint > 1.0) { - half_mu -= 1.0; - half_sps += 1; + halfSamplingPoint -= 1.0; + halfSamplingPointOffset += 1; } - mCurrentSample.setValues(getInphase(half_mu, half_sps), getQuadrature(half_mu, half_sps)); + mCurrentSample.setValues(getInphase(halfSamplingPointOffset, halfSamplingPoint), + getQuadrature(halfSamplingPointOffset, halfSamplingPoint)); return mCurrentSample; } @@ -136,11 +139,10 @@ public class InterpolatingSymbolBuffer * be used to support an external eye-diagram chart. * @return symbol decision data. */ - public SymbolDecisionData getSymbolDecisionData() + public SymbolDecisionData2 getSymbolDecisionData() { - //TODO: I think that delay line pointer needs to have +4 added to it since the filter is 8 taps long - return new SymbolDecisionData(mDelayLineInphase, mDelayLineQuadrature, - mSamplesPerSymbol, mDelayLinePointer, mMu); + mSymbolDecisionData2.setSamplingPoint(mSamplingPoint); + return mSymbolDecisionData2; } /** @@ -155,12 +157,13 @@ public class InterpolatingSymbolBuffer /** * Returns the interpolated inphase value for the specified offset + * @param delayLineOffset offset into the inphase delay line buffer * @param interpolation into the buffer to calculate the interpolated sample * @return inphase value of the interpolated sample */ - public float getInphase(float interpolation, int indexOffet) + public float getInphase(int delayLineOffset, float interpolation) { - return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer + indexOffet, interpolation); + return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer + delayLineOffset, interpolation); } /** @@ -175,12 +178,13 @@ public class InterpolatingSymbolBuffer /** * Returns the interpolated quadrature value for the specified offset + * @param delayLineOffset offset into the quadrature delay line buffer * @param interpolation into the buffer to calculate the interpolated sample * @return quadrature value of the interpolated sample */ - public float getQuadrature(float interpolation, int indexOffset) + public float getQuadrature(int delayLineOffset, float interpolation) { - return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer + indexOffset, interpolation); + return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer + delayLineOffset, interpolation); } /** diff --git a/src/main/java/io/github/dsheirer/dsp/psk/QPSKDemodulator.java b/src/main/java/io/github/dsheirer/dsp/psk/QPSKDemodulator.java index 38194543..bb06e223 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/QPSKDemodulator.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/QPSKDemodulator.java @@ -34,7 +34,7 @@ public class QPSKDemodulator implements ComplexSampleListener private Listener mPLLErrorListener; private Listener mPLLFrequencyListener; private Listener mDibitListener; - private Listener mSymbolDecisionDataListener; + private Listener mSymbolDecisionDataListener; private Complex mPreviousSample = new Complex(0, 0); private Complex mPreviousMiddleSample = new Complex(0, 0); @@ -47,6 +47,7 @@ public class QPSKDemodulator implements ComplexSampleListener private float mSymbolTimingError; private double mSampleRate; + /** * Decoder for Quaternary Phase Shift Keying (QPSK) and Differential QPSK (DQPSK). This decoder uses both a * phase-locked loop and a gardner symbol timing error detector to automatically align to the incoming carrier @@ -129,6 +130,16 @@ public class QPSKDemodulator implements ComplexSampleListener mMiddleSymbol.normalize(); mCurrentSymbol.normalize(); + //Send to an external constellation symbol listener when registered + if(mSymbolListener != null) + { + mSymbolListener.receive(mCurrentSymbol); + } + + //We rotate the symbol from star pattern to polar pattern to facilitate phase error calculation +// mSymbolPhaseErrorCalculator.adjust(mCurrentSymbol); +// mSymbolPhaseErrorCalculator.adjust(mMiddleSymbol); + //Symbol timing error calculations mSymbolTimingError = mGardnerDetector.getError(mPreviousSymbol, mMiddleSymbol, mCurrentSymbol); mInterpolatingSymbolBuffer.resetAndAdjust(mSymbolTimingError); @@ -138,20 +149,12 @@ public class QPSKDemodulator implements ComplexSampleListener mPreviousMiddleSample.setValues(mMiddleSample); mPreviousSymbol.setValues(mCurrentSymbol); - //Send to an external constellation symbol listener when registered - if(mSymbolListener != null) - { - mSymbolListener.receive(mCurrentSymbol); - } - - //Adjust the current symbol as necessary (e.g. for differential encoding) - mSymbolPhaseErrorCalculator.adjust(mCurrentSymbol); //Calculate the phase error of the current symbol relative to the expected constellation and provide //feedback to the PLL mPhaseError = mSymbolPhaseErrorCalculator.getPhaseError(mCurrentSymbol); - mPhaseError = GardnerDetector.clip(mPhaseError, 0.2f); + mPhaseError = GardnerDetector.clip(mPhaseError, 0.15f); mPLL.adjust(mPhaseError); if(mPLLErrorListener != null) @@ -210,7 +213,7 @@ public class QPSKDemodulator implements ComplexSampleListener mPLLFrequencyListener = listener; } - public void setSymbolDecisionDataListener(Listener listener) + public void setSymbolDecisionDataListener(Listener listener) { mSymbolDecisionDataListener = listener; } diff --git a/src/main/java/io/github/dsheirer/dsp/psk/SymbolDecisionData2.java b/src/main/java/io/github/dsheirer/dsp/psk/SymbolDecisionData2.java new file mode 100644 index 00000000..e8bde425 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/SymbolDecisionData2.java @@ -0,0 +1,68 @@ +/******************************************************************************* + * 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 SymbolDecisionData2 +{ + private ComplexCircularBuffer mBuffer; + private float mSamplingPoint; + + public SymbolDecisionData2(int samplesPerSymbol) + { + mBuffer = new ComplexCircularBuffer(2 * samplesPerSymbol); + } + + public void receive(float inphase, float quadrature) + { + mBuffer.put(new Complex(inphase, quadrature)); + } + + public void receive(Complex complex) + { + mBuffer.put(complex.copy()); + } + + public void setSamplingPoint(float samplingPoint) + { + mSamplingPoint = samplingPoint; + } + + public float getSamplingPoint() + { + return mSamplingPoint; + } + + 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/pll/CostasLoop.java b/src/main/java/io/github/dsheirer/dsp/psk/pll/CostasLoop.java index 22726f09..76aee9a7 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/pll/CostasLoop.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/pll/CostasLoop.java @@ -192,7 +192,12 @@ public class CostasLoop implements IPhaseLockedLoop /* Limit frequency to +/- maximum loop frequency */ limitFrequency(); - mErrorBuffer.put(phaseError); +// checkLoopBandwidth(); + } + + private void checkLoopBandwidth() + { + mErrorBuffer.put(mLoopFrequency); double standardDeviation = mErrorBuffer.standardDeviation(); 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 index a9f07ad8..d89133b2 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/pll/Tracking.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/pll/Tracking.java @@ -22,9 +22,9 @@ public enum Tracking // FINE(150.0, 0.0001, 0.001), // LOCKED(200.0, 0.0, 0.0001); SEARCHING(100.0, 0.4, Math.PI), - COARSE(150.0, 0.2, 0.4), - FINE(175.0, 0.1, 0.2), - LOCKED(200.0, 0.0, 0.1); + COARSE(200.0, 0.2, 0.4), + FINE(400.0, 0.1, 0.2), + LOCKED(600.0, 0.0, 0.1); private double mLoopBandwidth; private double mThresholdMinimum; 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 index 13f49d7d..e0dbae54 100644 --- 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 @@ -37,14 +37,15 @@ public class ComplexSampleLineChart extends LineChart implements Listener> mISamples = FXCollections.observableArrayList(); private ObservableList> mQSamples = FXCollections.observableArrayList(); private IntegerProperty mLengthProperty = new SimpleIntegerProperty(40); - public ComplexSampleLineChart(int length) + public ComplexSampleLineChart(int length, int samplesPerSymbol) { - super(new NumberAxis("Sample Number", 1, length, 2), + super(new NumberAxis("Demodulated Samples", 0, length, 10), new NumberAxis("Value", -1.0, 1.0, 0.25)); LineChart.Series iSampleSeries = new LineChart.Series<>("Inphase", mISamples); @@ -53,10 +54,15 @@ public class ComplexSampleLineChart extends LineChart implements Listener() { @@ -67,6 +73,7 @@ public class ComplexSampleLineChart extends LineChart implements Listener +public class EyeDiagramChart extends LineChart implements Listener { private final static Logger mLog = LoggerFactory.getLogger(EyeDiagramChart.class); @@ -38,9 +37,9 @@ public class EyeDiagramChart extends LineChart implements Listener> series = getSeries(mSeriesPointer++, length + 1); + ObservableList> iSeries = getSeries(mSeriesPointer++, length + 1); if(mSeriesPointer >= mSeriesCount) { mSeriesPointer = 0; } - int index = symbolDecisionData.getSampleIndex(); + ObservableList> qSeries = getSeries(mSeriesPointer++, length + 1); - Complex previous = new Complex(symbolDecisionData.getInphaseSamples()[index], - symbolDecisionData.getQuadratureSamples()[index]); - - for(int x = 1; x <= length; x++) + if(mSeriesPointer >= mSeriesCount) { - Complex current = new Complex(symbolDecisionData.getInphaseSamples()[index + x], - symbolDecisionData.getQuadratureSamples()[index + x]); + mSeriesPointer = 0; + } - double demodulated = FMDemodulator.demodulate(previous, current); - demodulated *= 6; + 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 dataPoint = series.get(x); - dataPoint.setXValue((double)x - symbolDecisionData.getSampleIndexOffset()); - dataPoint.setYValue(demodulated); - - previous = current; + 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/SymbolChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/SymbolChart.java index 70b9d4ce..6f99f042 100644 --- 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 @@ -56,7 +56,7 @@ public class SymbolChart extends ScatterChart implements Listener } ObservableList data = FXCollections.observableArrayList(); - data.add(new ScatterChart.Series("Current", mCurrentPoints)); + data.add(new ScatterChart.Series("Constellation", mCurrentPoints)); // data.add(new ScatterChart.Series("Previous", mPreviousPoints)); // data.add(new ScatterChart.Series("Oldest", mOldestPoints)); 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 index c13f0a92..cb9eb617 100644 --- 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 @@ -56,6 +56,8 @@ public class P25Phase1Pane extends DecoderPane getDecoder().setDEBUGPLLFrequencyListener(getPLLFrequencyLineChart()); getDecoder().setDEBUGSymbolDecisionDataListener(getEyeDiagramChart()); + HBox.setHgrow(getSampleChartBox(), Priority.ALWAYS); + HBox.setHgrow(getDecoderChartBox(), Priority.ALWAYS); getChildren().addAll(getSampleChartBox(), getDecoderChartBox()); } @@ -65,6 +67,7 @@ public class P25Phase1Pane extends DecoderPane mLog.debug("Configuring for sample rate: " + sampleRate); mDecoder.setSampleRate(sampleRate); + getSampleLineChart().setSamplesPerSymbol((int)(sampleRate / 4800)); } private P25_C4FMDecoder2 getDecoder() @@ -87,6 +90,7 @@ public class P25Phase1Pane extends DecoderPane 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); @@ -104,6 +108,7 @@ public class P25Phase1Pane extends DecoderPane if(mSampleChartBox == null) { mSampleChartBox = new HBox(); + mSampleChartBox.setMaxHeight(Double.MAX_VALUE); getSampleLineChart().setMaxWidth(Double.MAX_VALUE); getEyeDiagramChart().setMaxWidth(Double.MAX_VALUE); HBox.setHgrow(getSampleLineChart(), Priority.ALWAYS); @@ -118,7 +123,7 @@ public class P25Phase1Pane extends DecoderPane { if(mSampleLineChart == null) { - mSampleLineChart = new ComplexSampleLineChart(40); + mSampleLineChart = new ComplexSampleLineChart(100, 10); } return mSampleLineChart; diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2.java index f18ad3f3..b5ccec57 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2.java @@ -26,7 +26,7 @@ import io.github.dsheirer.dsp.psk.IQPSKSymbolDecoder; import io.github.dsheirer.dsp.psk.ISymbolPhaseErrorCalculator; import io.github.dsheirer.dsp.psk.QPSKDemodulator; import io.github.dsheirer.dsp.psk.QPSKSymbolDecoder; -import io.github.dsheirer.dsp.psk.SymbolDecisionData; +import io.github.dsheirer.dsp.psk.SymbolDecisionData2; import io.github.dsheirer.dsp.psk.pll.CostasLoop; import io.github.dsheirer.dsp.psk.pll.Tracking; import io.github.dsheirer.sample.Listener; @@ -64,7 +64,7 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen private Listener mDEBUGSymbolListener; private Listener mDEBUGPLLPhaseErrorListener; private Listener mDEBUGPLLFrequencyListener; - private Listener mDEBUGSymbolDecisionDataListener; + private Listener mDEBUGSymbolDecisionDataListener; public P25_C4FMDecoder2(AliasList aliasList) { @@ -121,7 +121,7 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen mQPSKDemodulator.setPLLFrequencyListener(listener); } - public void setDEBUGSymbolDecisionDataListener(Listener listener) + public void setDEBUGSymbolDecisionDataListener(Listener listener) { mDEBUGSymbolDecisionDataListener = listener; mQPSKDemodulator.setSymbolDecisionDataListener(listener); @@ -131,9 +131,9 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen public void receive(ComplexBuffer complexBuffer) { ComplexBuffer basebandFiltered = mBasebandFilter.filter(complexBuffer); -// ComplexBuffer symbolFiltered = mBasebandFilter.filter(basebandFiltered); + ComplexBuffer symbolFiltered = mSymbolFilter.filter(basebandFiltered); // ComplexBuffer amplified = mAGC.filter(symbolFiltered); - mQPSKDemodulator.receive(basebandFiltered); + mQPSKDemodulator.receive(symbolFiltered); // mQPSKDemodulator.receive(complexBuffer); } From a0630102e661c25be42bbfbf98c583ccfeee6675 Mon Sep 17 00:00:00 2001 From: Denny Date: Sun, 25 Feb 2018 05:04:20 -0500 Subject: [PATCH 15/23] 330 WIP - P25 C4FM decoder fully functional using DQPSK decoder and new symbol timing error detector. --- .../dsheirer/dsp/filter/FilterFactory.java | 14 +- .../dsheirer/dsp/psk/DQPSKDemodulator.java | 259 ++++++++++++++++++ .../psk/DQPSKSymbolPhaseErrorCalculator.java | 17 +- .../dsheirer/dsp/psk/EarlyLateDetector.java | 76 +++++ .../dsheirer/dsp/psk/EarlyLateDetector2.java | 40 +++ .../dsheirer/dsp/psk/GardnerDetector.java | 4 +- .../dsp/psk/InterpolatingSymbolBuffer.java | 13 + .../dsp/psk/InterpolatingSymbolBuffer2.java | 245 +++++++++++++++++ .../dsheirer/dsp/psk/QPSKDemodulator.java | 24 +- .../dsheirer/dsp/psk/QPSKStarSlicer.java | 105 ++++--- .../dsheirer/dsp/psk/SymbolDecisionData2.java | 6 + .../dsheirer/dsp/psk/pll/CostasLoop.java | 3 +- .../gui/viewer/DemodulatorViewerFX.java | 6 +- .../viewer/chart/ComplexSampleLineChart.java | 3 +- ...ncyLineChart.java => DoubleLineChart.java} | 11 +- .../gui/viewer/decoder/P25Phase1Pane.java | 40 ++- .../module/decode/p25/P25_C4FMDecoder2.java | 43 ++- 17 files changed, 811 insertions(+), 98 deletions(-) create mode 100644 src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulator.java create mode 100644 src/main/java/io/github/dsheirer/dsp/psk/EarlyLateDetector.java create mode 100644 src/main/java/io/github/dsheirer/dsp/psk/EarlyLateDetector2.java create mode 100644 src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer2.java rename src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/{FrequencyLineChart.java => DoubleLineChart.java} (82%) diff --git a/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java b/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java index 01706df0..074448f4 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java @@ -666,11 +666,11 @@ public class FilterFactory * @param alpha - roll-off factor * @return - filter coefficients */ - public static float[] getRootRaisedCosine(int samplesPerSymbol, + public static float[] getRootRaisedCosine(float samplesPerSymbol, int symbols, float alpha) { - int taps = samplesPerSymbol * symbols + 1; + int taps = (int)(samplesPerSymbol * (float)symbols + 1.0f); float scale = 0; @@ -680,8 +680,8 @@ public class FilterFactory { float index = (float) x - ((float) taps / 2.0f); - float x1 = (float) Math.PI * index / (float) samplesPerSymbol; - float x2 = 4.0f * alpha * index / (float) samplesPerSymbol; + float x1 = (float) Math.PI * index / samplesPerSymbol; + float x2 = 4.0f * alpha * index / samplesPerSymbol; float x3 = x2 * x2 - 1.0f; float numerator, denominator; @@ -692,7 +692,7 @@ public class FilterFactory { numerator = (float) Math.cos((1.0 + alpha) * x1) + (float) Math.sin((1.0f - alpha) * x1) / - (4.0f * alpha * index / (float) samplesPerSymbol); + (4.0f * alpha * index / samplesPerSymbol); } else { @@ -713,9 +713,9 @@ public class FilterFactory x3 = (1.0f - alpha) * x1; x2 = (1.0f + alpha) * x1; - numerator = (float) (Math.sin(x2) * (1.0f + alpha) * Math.PI - + numerator = (float) (Math.sin(x2) * (1.0 + alpha) * Math.PI - Math.cos(x3) * ((1.0 - alpha) * Math.PI * - (double) samplesPerSymbol) / (4 * alpha * index) + + (double) samplesPerSymbol) / (4.0 * alpha * index) + Math.sin(x3) * (double) samplesPerSymbol * (double) samplesPerSymbol / (4.0 * alpha * index * index)); 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..4d4374a6 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulator.java @@ -0,0 +1,259 @@ +/******************************************************************************* + * 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.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); + private InterpolatingSymbolBuffer2 mInterpolatingSymbolBuffer; + private IPhaseLockedLoop mPLL; + private ISymbolPhaseErrorCalculator mSymbolPhaseErrorCalculator; + private IQPSKSymbolDecoder mDQPSKSymbolDecoder; +// private GardnerDetector mGardnerDetector = new GardnerDetector(); + private EarlyLateDetector mEarlyLateDetector = new EarlyLateDetector(); + private EarlyLateDetector2 mEarlyLateDetector2 = new EarlyLateDetector2(); + private Listener mSymbolListener; + private Listener mPLLErrorListener; + private Listener mPLLFrequencyListener; + private Listener mSamplesPerSymbolListener; + private Listener mDibitListener; + private Listener mSymbolDecisionDataListener; + + private Complex mReceivedSample = new Complex(0, 0); + + private Complex mPreviousPrecedingSample = new Complex(0, 0); + private Complex mPreviousCurrentSample = new Complex(0, 0); + private Complex mPreviousFollowingSample = new Complex(0, 0); + + private Complex mCurrentPrecedingSample = new Complex(0, 0); + private Complex mCurrentCurrentSample = new Complex(0, 0); + private Complex mCurrentFollowingSample = new Complex(0, 0); + + private Complex mPrecedingSymbol = new Complex(0, 0); + private Complex mCurrentSymbol = new Complex(0, 0); + private Complex mFollowingSymbol = new Complex(0, 0); + + private float mPhaseError; + private float mSymbolTimingError; + private double mSampleRate; + + + /** + * Decoder for Quaternary Phase Shift Keying (QPSK) and Differential QPSK (DQPSK). This decoder uses both a + * phase-locked loop and a gardner symbol timing error detector to automatically align to the incoming carrier + * frequency and to adjust for any changes in symbol timing. + * + * @param phaseLockedLoop for tracking carrier frequency error + * @param symbolPhaseErrorCalculator to calculate symbol phase errors and optionally adjust for differential encoding + * @param symbolDecoder to decode demodulated symbols into dibits + * @param sampleRate of the incoming complex sample stream + * @param symbolRate of the decoded QPSK symbols. + */ + public DQPSKDemodulator(IPhaseLockedLoop phaseLockedLoop, ISymbolPhaseErrorCalculator symbolPhaseErrorCalculator, + IQPSKSymbolDecoder symbolDecoder, double sampleRate, double symbolRate) + { + if(sampleRate < (symbolRate * 2)) + { + throw new IllegalArgumentException("Sample rate [" + sampleRate + + "] must be at least 2 x symbol rate [" + symbolRate + "]"); + } + + mSampleRate = sampleRate; + mPLL = phaseLockedLoop; + mSymbolPhaseErrorCalculator = symbolPhaseErrorCalculator; + mDQPSKSymbolDecoder = symbolDecoder; + + float samplesPerSymbol = (float)(sampleRate / symbolRate); + mInterpolatingSymbolBuffer = new InterpolatingSymbolBuffer2(samplesPerSymbol); + } + + /** + * Submits a buffer containing complex samples for decoding + * @param complexBuffer with complex samples + */ + public void receive(ComplexBuffer complexBuffer) + { + float[] samples = complexBuffer.getSamples(); + + for(int x = 0; x < samples.length; x += 2) + { + receive(samples[x], samples[x + 1]); + } + } + + /** + * Submits a complex sample for decoding. + * @param inphase value for the sample + * @param quadrature value for the sample + */ + @Override + public void receive(float inphase, float quadrature) + { + //Update current sample with values + 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()); + + mInterpolatingSymbolBuffer.receive(mReceivedSample); + + //Calculate the symbol once we've stored enough samples + if(mInterpolatingSymbolBuffer.hasSymbol()) + { + //Get middle and current samples from the interpolating buffer + mCurrentPrecedingSample = mInterpolatingSymbolBuffer.getPreceedingSample(); + mCurrentCurrentSample = mInterpolatingSymbolBuffer.getCurrentSample(); + mCurrentFollowingSample = mInterpolatingSymbolBuffer.getFollowingSample(); + + //Eye diagram listener + if(mSymbolDecisionDataListener != null) + { + mSymbolDecisionDataListener.receive(mInterpolatingSymbolBuffer.getSymbolDecisionData()); + } + + //Calculate preceding, current and following symbols as the delta rotation compared to the previous samples + mPrecedingSymbol.setInphase(Complex.multiplyInphase(mCurrentPrecedingSample.inphase(), mCurrentPrecedingSample.quadrature(), + mPreviousPrecedingSample.inphase(), -mPreviousPrecedingSample.quadrature())); + mPrecedingSymbol.setQuadrature(Complex.multiplyQuadrature(mCurrentPrecedingSample.inphase(), mCurrentPrecedingSample.quadrature(), + mPreviousPrecedingSample.inphase(), -mPreviousPrecedingSample.quadrature())); + + mCurrentSymbol.setInphase(Complex.multiplyInphase(mCurrentCurrentSample.inphase(), mCurrentCurrentSample.quadrature(), + mPreviousCurrentSample.inphase(), -mPreviousCurrentSample.quadrature())); + mCurrentSymbol.setQuadrature(Complex.multiplyQuadrature(mCurrentCurrentSample.inphase(), mCurrentCurrentSample.quadrature(), + mPreviousCurrentSample.inphase(), -mPreviousCurrentSample.quadrature())); + + mFollowingSymbol.setInphase(Complex.multiplyInphase(mCurrentFollowingSample.inphase(), mCurrentFollowingSample.quadrature(), + mPreviousFollowingSample.inphase(), -mPreviousFollowingSample.quadrature())); + mFollowingSymbol.setQuadrature(Complex.multiplyQuadrature(mCurrentFollowingSample.inphase(), mCurrentFollowingSample.quadrature(), + mPreviousFollowingSample.inphase(), -mPreviousFollowingSample.quadrature())); + + //Set gain to unity before we calculate the error value + mPrecedingSymbol.normalize(); + mCurrentSymbol.normalize(); + mFollowingSymbol.normalize(); + + //Send to an external constellation symbol listener when registered + if(mSymbolListener != null) + { + mSymbolListener.receive(mCurrentSymbol); + } + + //Symbol timing error calculations + mSymbolTimingError = mEarlyLateDetector.getError(mPrecedingSymbol, mCurrentSymbol, mFollowingSymbol); + + mSymbolTimingError = GardnerDetector.clip(mSymbolTimingError, 0.5f); + mInterpolatingSymbolBuffer.resetAndAdjust(mSymbolTimingError); + + if(mSamplesPerSymbolListener != null) + { + mSamplesPerSymbolListener.receive((double)mInterpolatingSymbolBuffer.getSamplingPoint()); + } + + //Store current samples/symbols to use for the next period + mPreviousPrecedingSample.setValues(mCurrentPrecedingSample); + mPreviousCurrentSample.setValues(mCurrentCurrentSample); + mPreviousFollowingSample.setValues(mCurrentFollowingSample); + + //Calculate the phase error of the current symbol relative to the expected constellation and provide + //feedback to the PLL + mSymbolPhaseErrorCalculator.adjust(mCurrentSymbol); + mPhaseError = mSymbolPhaseErrorCalculator.getPhaseError(mCurrentSymbol); + + mPhaseError = GardnerDetector.clip(mPhaseError, 0.15f); + + mPLL.adjust(mPhaseError); +// mPLL.adjust(0.0); + + if(mPLLErrorListener != null) + { + mPLLErrorListener.receive((double)mPhaseError); + } + + if(mPLLFrequencyListener != null) + { + double loopFrequency = ((CostasLoop)mPLL).getLoopFrequency(); + + loopFrequency *= mSampleRate / (2.0 * Math.PI); + + mPLLFrequencyListener.receive(loopFrequency); + } + + //Decode the dibit from the symbol and send to the listener + if(mDibitListener != null) + { + mDibitListener.receive(mDQPSKSymbolDecoder.decode(mCurrentSymbol)); + } + + //TODO: Assemble dibits here and broadcast + } + } + + /** + * Registers the listener to receive decoded QPSK symbols + */ + public void setSymbolListener(Listener listener) + { + mSymbolListener = listener; + } + + /** + * Registers the listener to receive decoded QPSK dibits + */ + public void setDibitListener(Listener listener) + { + mDibitListener = 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; + } + + /** + * Registers the listener to receive PLL error values + */ + public void setSamplesPerSymbolListener(Listener listener) + { + mSamplesPerSymbolListener = listener; + } + + public void setSymbolDecisionDataListener(Listener listener) + { + mSymbolDecisionDataListener = listener; + } + +} diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolPhaseErrorCalculator.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolPhaseErrorCalculator.java index cf5dd9c6..fce780e2 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolPhaseErrorCalculator.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolPhaseErrorCalculator.java @@ -19,21 +19,18 @@ import io.github.dsheirer.sample.complex.Complex; public class DQPSKSymbolPhaseErrorCalculator extends QPSKSymbolPhaseErrorCalculator { - //Negative 45 degrees rotation to offset for differential additive 45 degrees of rotation + //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 boolean mToggle; + + public DQPSKSymbolPhaseErrorCalculator() + { + } + @Override public void adjust(Complex symbol) { symbol.multiply(DIFFERENTIAL_OFFSET); } - - @Override - public float getPhaseError(Complex symbol) - { - Complex copy = symbol.copy(); - - copy.multiply(DIFFERENTIAL_OFFSET); - return super.getPhaseError(copy); - } } diff --git a/src/main/java/io/github/dsheirer/dsp/psk/EarlyLateDetector.java b/src/main/java/io/github/dsheirer/dsp/psk/EarlyLateDetector.java new file mode 100644 index 00000000..d3a593b6 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/EarlyLateDetector.java @@ -0,0 +1,76 @@ +/******************************************************************************* + * 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; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class EarlyLateDetector +{ + private static final float PI_4_ROTATION = (float)(Math.PI / 4.0); + private final static Logger mLog = LoggerFactory.getLogger(EarlyLateDetector.class); + + /** + * Symbol timing error detector designed to optimize the sample timing point such that the current symbol's + * inphase and quadrature approach the optimal 45 degree (.707) sampling instant. + */ + public EarlyLateDetector() + { + } + + public float getError(Complex early, Complex current, Complex late) + { + float inphaseError = getError(early.inphase(), current.inphase(), late.inphase()); + float quadratureError = getError(early.quadrature(), current.quadrature(), late.quadrature()); + float error = inphaseError + quadratureError; + + mLog.debug("I:" + inphaseError + " Q:" + quadratureError + " E:" + error); + return error; +// return getError(early.inphase(), current.inphase(), late.inphase()) - +// getError(early.quadrature(), current.quadrature(), late.quadrature()); + } + + private float getError(float early, float current, float late) + { + if(early < current) //Ascending slope + { + if(current > 0.0) + { + //Positive hemisphere, ascending slope + return PI_4_ROTATION - current; + } + else + { + //Negative hemisphere, ascending slope + return -PI_4_ROTATION - current; + } + } + else //Descending slope + { + if(current > 0.0) + { + //Positive hemisphere, descending slope + return current - PI_4_ROTATION; + } + else + { + //Negative hemisphere, descending slope + return current + PI_4_ROTATION; + } + } + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/psk/EarlyLateDetector2.java b/src/main/java/io/github/dsheirer/dsp/psk/EarlyLateDetector2.java new file mode 100644 index 00000000..16ea9bce --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/EarlyLateDetector2.java @@ -0,0 +1,40 @@ +/******************************************************************************* + * 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 EarlyLateDetector2 +{ + //45 degrees rotation to orient the symbol to a polar axis to make error calculation easy/efficient + public static final Complex POSITIVE_OFFSET = Complex.fromAngle(Math.PI / 4.0d); + private Complex mSymbol = new Complex(0,0); + + public float getError(Complex symbol) + { + mSymbol.setValues(symbol); + mSymbol.multiply(POSITIVE_OFFSET); + + if(Math.abs(mSymbol.quadrature()) > Math.abs(mSymbol.inphase())) + { + return mSymbol.inphase(); + } + else + { + return mSymbol.quadrature(); + } + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/psk/GardnerDetector.java b/src/main/java/io/github/dsheirer/dsp/psk/GardnerDetector.java index 023b3e87..9b338e97 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/GardnerDetector.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/GardnerDetector.java @@ -41,8 +41,8 @@ public class GardnerDetector */ public float getError(Complex previous, Complex middle, Complex current) { - mErrorInphase = (previous.inphase() - current.inphase()) * middle.inphase(); - mErrorQuadrature = (previous.quadrature() - current.quadrature()) * middle.quadrature(); + 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; diff --git a/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer.java b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer.java index fa050e5e..cccba75d 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer.java @@ -17,9 +17,13 @@ package io.github.dsheirer.dsp.psk; import io.github.dsheirer.dsp.filter.interpolator.RealInterpolator; import io.github.dsheirer.sample.complex.Complex; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class InterpolatingSymbolBuffer { + private final static Logger mLog = LoggerFactory.getLogger(InterpolatingSymbolBuffer.class); + private Complex mMiddleSample = new Complex(0,0); private Complex mCurrentSample = new Complex(0,0); @@ -88,10 +92,19 @@ public class InterpolatingSymbolBuffer mDetectedSamplesPerSymbol = mDetectedSamplesPerSymbol + mDetectedSamplesPerSymbolGain * symbolTimingError; mDetectedSamplesPerSymbol = mSamplesPerSymbol + clip(mDetectedSamplesPerSymbol - mSamplesPerSymbol, mMaxDeviation); + mLog.debug("Detected Samples Per Symbol:" + mDetectedSamplesPerSymbol); //Add another symbol's worth of samples to the counter and adjust timing based on gardner error increaseSampleCounter((mDetectedSamplesPerSymbol + (mSampleCounterGain * symbolTimingError))); } + /** + * 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 */ diff --git a/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer2.java b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer2.java new file mode 100644 index 00000000..ba8d731a --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer2.java @@ -0,0 +1,245 @@ +/******************************************************************************* + * 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; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class InterpolatingSymbolBuffer2 +{ + private static final float MAXIMUM_SAMPLES_PER_SYMBOL_DEVIATION = 0.02f; //2% + private final static Logger mLog = LoggerFactory.getLogger(InterpolatingSymbolBuffer2.class); + private Complex mPreceedingSample = new Complex(0,0); + private Complex mCurrentSample = new Complex(0,0); + private Complex mFollowingSample = new Complex(0,0); + + private float[] mDelayLineInphase; + private float[] mDelayLineQuadrature; + private float mSamplingPoint; + private float mSampleCounterGain = 0.05f; + private float mDetectedSamplesPerSymbol; + private float mDetectedSamplesPerSymbolGain = 0.1f * mSampleCounterGain * mSampleCounterGain; + private float mMaximumSamplesPerSymbol;; + private float mMinimumSamplesPerSymbol;; + + private int mDelayLinePointer = 0; + private int mTwiceSamplesPerSymbol; + private RealInterpolator mInterpolator = new RealInterpolator(1.0f); + private SymbolDecisionData2 mSymbolDecisionData2; + + public InterpolatingSymbolBuffer2(float samplesPerSymbol) + { + mSamplingPoint = samplesPerSymbol; + mDetectedSamplesPerSymbol = samplesPerSymbol; + mMaximumSamplesPerSymbol = samplesPerSymbol * (1.0f + MAXIMUM_SAMPLES_PER_SYMBOL_DEVIATION); + mMinimumSamplesPerSymbol = samplesPerSymbol * (1.0f - MAXIMUM_SAMPLES_PER_SYMBOL_DEVIATION); + mTwiceSamplesPerSymbol = (int)Math.floor(2.0 * samplesPerSymbol); + mDelayLineInphase = new float[2 * mTwiceSamplesPerSymbol]; + mDelayLineQuadrature = new float[2 * mTwiceSamplesPerSymbol]; + + mSymbolDecisionData2 = new SymbolDecisionData2((int)samplesPerSymbol); + } + + public void receive(Complex currentSample) + { + mSymbolDecisionData2.receive(currentSample); + mSamplingPoint--; + + //Fill up the delay line to use with the interpolator + mDelayLineInphase[mDelayLinePointer] = currentSample.inphase(); + mDelayLineInphase[mDelayLinePointer + mTwiceSamplesPerSymbol] = currentSample.inphase(); + mDelayLineQuadrature[mDelayLinePointer] = currentSample.quadrature(); + mDelayLineQuadrature[mDelayLinePointer + mTwiceSamplesPerSymbol] = currentSample.quadrature(); + + //Increment pointer and keep pointer in bounds + mDelayLinePointer++; + mDelayLinePointer = 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) + { + //mDetectedSamplesPerSymbol is samples per symbol and is constrained to floating between +/- .005 of the nominal samples per + //symbol + mDetectedSamplesPerSymbol = mDetectedSamplesPerSymbol + (symbolTimingError * mDetectedSamplesPerSymbolGain); + mDetectedSamplesPerSymbol = constrain(mDetectedSamplesPerSymbol, mMinimumSamplesPerSymbol, mMaximumSamplesPerSymbol); + + mLog.debug("Detected Samples Per Symbol: " + mDetectedSamplesPerSymbol); + //Add another symbol's worth of samples to the counter and adjust timing based on gardner error + increaseSampleCounter((mDetectedSamplesPerSymbol + (mSampleCounterGain * symbolTimingError))); +// increaseSampleCounter(mDetectedSamplesPerSymbol); + } + + /** + * 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; + } + + /** + * Samples that precedes the current sample (uninterpolated). + */ + public Complex getPreceedingSample() + { + mPreceedingSample.setValues(mDelayLineInphase[mDelayLinePointer + 3], mDelayLineQuadrature[mDelayLinePointer + 3]); + + return mPreceedingSample; + } + + /** + * 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; + } + + /** + * Samples that follows the current sample (uninterpolated). + */ + public Complex getFollowingSample() + { + mFollowingSample.setValues(mDelayLineInphase[mDelayLinePointer + 4], mDelayLineQuadrature[mDelayLinePointer + 4]); + + return mFollowingSample; + } + + + + /** + * 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 SymbolDecisionData2 getSymbolDecisionData() + { + mSymbolDecisionData2.setSamplingPoint(mSamplingPoint); + return mSymbolDecisionData2; + } + + /** + * Returns the interpolated inphase value for the specified offset + * @param interpolation into the buffer to calculate the interpolated sample + * @return inphase value of the interpolated sample + */ + public float getInphase(float interpolation) + { + return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer, interpolation); + } + + /** + * Returns the interpolated inphase value for the specified offset + * @param delayLineOffset offset into the inphase delay line buffer + * @param interpolation into the buffer to calculate the interpolated sample + * @return inphase value of the interpolated sample + */ + public float getInphase(int delayLineOffset, float interpolation) + { + return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer + delayLineOffset, interpolation); + } + + /** + * Returns the interpolated quadrature value for the specified offset + * @param interpolation into the buffer to calculate the interpolated sample + * @return quadrature value of the interpolated sample + */ + public float getQuadrature(float interpolation) + { + return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer, interpolation); + } + + /** + * Returns the interpolated quadrature value for the specified offset + * @param delayLineOffset offset into the quadrature delay line buffer + * @param interpolation into the buffer to calculate the interpolated sample + * @return quadrature value of the interpolated sample + */ + public float getQuadrature(int delayLineOffset, float interpolation) + { + return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer + delayLineOffset, interpolation); + } + + /** + * Constrains value to the range of ( -maximum <> maximum ) + */ + public static float clip(float value, float maximum) + { + if(value > maximum) + { + return maximum; + } + else if(value < -maximum) + { + return -maximum; + } + + return value; + } + + /** + * Constrains the value within the minimum-maximum range + * + * @param value to constrain + * @param minimum value + * @param maximum value + * @return constrained value + */ + public static float constrain(float value, float minimum, float maximum) + { + if(value > maximum) + { + return maximum; + } + + if(value < minimum) + { + return minimum; + } + + return value; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/psk/QPSKDemodulator.java b/src/main/java/io/github/dsheirer/dsp/psk/QPSKDemodulator.java index bb06e223..9bcda428 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/QPSKDemodulator.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/QPSKDemodulator.java @@ -33,6 +33,7 @@ public class QPSKDemodulator implements ComplexSampleListener private Listener mSymbolListener; private Listener mPLLErrorListener; private Listener mPLLFrequencyListener; + private Listener mSamplesPerSymbolListener; private Listener mDibitListener; private Listener mSymbolDecisionDataListener; @@ -136,26 +137,29 @@ public class QPSKDemodulator implements ComplexSampleListener mSymbolListener.receive(mCurrentSymbol); } - //We rotate the symbol from star pattern to polar pattern to facilitate phase error calculation -// mSymbolPhaseErrorCalculator.adjust(mCurrentSymbol); -// mSymbolPhaseErrorCalculator.adjust(mMiddleSymbol); - //Symbol timing error calculations mSymbolTimingError = mGardnerDetector.getError(mPreviousSymbol, mMiddleSymbol, mCurrentSymbol); mInterpolatingSymbolBuffer.resetAndAdjust(mSymbolTimingError); + if(mSamplesPerSymbolListener != null) + { + mSamplesPerSymbolListener.receive((double)mInterpolatingSymbolBuffer.getSamplingPoint()); + } + //Store current samples/symbols to use for the next period mPreviousSample.setValues(mCurrentSample); mPreviousMiddleSample.setValues(mMiddleSample); mPreviousSymbol.setValues(mCurrentSymbol); - //Calculate the phase error of the current symbol relative to the expected constellation and provide //feedback to the PLL + mSymbolPhaseErrorCalculator.adjust(mCurrentSymbol); mPhaseError = mSymbolPhaseErrorCalculator.getPhaseError(mCurrentSymbol); mPhaseError = GardnerDetector.clip(mPhaseError, 0.15f); - mPLL.adjust(mPhaseError); + +// mPLL.adjust(-mPhaseError); + mPLL.adjust(0.0); if(mPLLErrorListener != null) { @@ -213,6 +217,14 @@ public class QPSKDemodulator implements ComplexSampleListener mPLLFrequencyListener = listener; } + /** + * Registers the listener to receive PLL error values + */ + public void setSamplesPerSymbolListener(Listener listener) + { + mSamplesPerSymbolListener = listener; + } + public void setSymbolDecisionDataListener(Listener listener) { mSymbolDecisionDataListener = listener; 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/SymbolDecisionData2.java b/src/main/java/io/github/dsheirer/dsp/psk/SymbolDecisionData2.java index e8bde425..64d9ebe9 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/SymbolDecisionData2.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/SymbolDecisionData2.java @@ -20,6 +20,9 @@ import io.github.dsheirer.sample.complex.Complex; public class SymbolDecisionData2 { + //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; @@ -60,6 +63,9 @@ public class SymbolDecisionData2 { Complex copy = samples[length + x].copy(); copy.multiply(samples[x].conjugate()); + + //Test - rotate the demodulated symbol by 45 degrees to see the impact on the eye diagram +// copy.multiply(DIFFERENTIAL_OFFSET); demodulated[x] = copy; } 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 index 76aee9a7..187e1c0c 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/pll/CostasLoop.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/pll/CostasLoop.java @@ -41,7 +41,8 @@ public class CostasLoop implements IPhaseLockedLoop private double mAlphaGain; private double mBetaGain; private double mLoopPhase = 0.0; - private double mLoopFrequency = 0.0; +// private double mLoopFrequency = 0.0; + private double mLoopFrequency = Math.PI * 2.0 * -10.0 / 48000.0; private double mMaximumLoopFrequency; private Complex mCurrentVector = new Complex(0, 0); 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 index 0046606f..5e9745a6 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/DemodulatorViewerFX.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/DemodulatorViewerFX.java @@ -29,6 +29,8 @@ 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; @@ -37,6 +39,8 @@ import java.io.File; */ public class DemodulatorViewerFX extends Application { + private final static Logger mLog = LoggerFactory.getLogger(DemodulatorViewerFX.class); + private Stage mStage; private MenuBar mMenuBar; private RecentFilesMenu mRecentFilesMenu; @@ -52,7 +56,7 @@ public class DemodulatorViewerFX extends Application borderPane.setTop(getMenuBar()); borderPane.setCenter(getViewerDesktop()); - Scene scene = new Scene(borderPane, 1200, 800); + Scene scene = new Scene(borderPane, 1500, 900); mStage.setScene(scene); mStage.show(); 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 index e0dbae54..2bf0fe8c 100644 --- 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 @@ -101,7 +101,8 @@ public class ComplexSampleLineChart extends LineChart implements Listener +public class DoubleLineChart extends LineChart implements Listener { - private final static Logger mLog = LoggerFactory.getLogger(FrequencyLineChart.class); + private final static Logger mLog = LoggerFactory.getLogger(DoubleLineChart.class); private DoubleCircularBuffer mDoubleCircularBuffer; private ObservableList> mPhaseValues = FXCollections.observableArrayList(); - public FrequencyLineChart(double maxFrequency, int length) + public DoubleLineChart(String seriesLabel, double min, double max, double tick, int length) { - super(new NumberAxis("Time", 1, length, 2), - new NumberAxis("Frequency", -maxFrequency, maxFrequency, maxFrequency / 5.0)); + super(new NumberAxis(1, length, 2), new NumberAxis(min, max, tick)); - Series phaseSeries = new Series<>("PLL Frequency", mPhaseValues); + Series phaseSeries = new Series<>(seriesLabel, mPhaseValues); ObservableList> observableList = FXCollections.observableArrayList(phaseSeries); setData(observableList); 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 index cb9eb617..2b2800be 100644 --- 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 @@ -16,12 +16,14 @@ 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.FrequencyLineChart; import io.github.dsheirer.instrument.gui.viewer.chart.PhaseLineChart; import io.github.dsheirer.instrument.gui.viewer.chart.SymbolChart; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.module.decode.p25.P25_C4FMDecoder2; +import io.github.dsheirer.sample.Broadcaster; +import io.github.dsheirer.sample.complex.ComplexBuffer; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import org.slf4j.Logger; @@ -37,7 +39,9 @@ public class P25Phase1Pane extends DecoderPane private HBox mDecoderChartBox; private SymbolChart mSymbolChart; private PhaseLineChart mPLLPhaseErrorLineChart; - private FrequencyLineChart mPLLFrequencyLineChart; + private DoubleLineChart mPLLFrequencyLineChart; + private DoubleLineChart mSamplesPerSymbolLineChart; + private Broadcaster mFilteredBufferBroadcaster = new Broadcaster<>(); private P25_C4FMDecoder2 mDecoder = new P25_C4FMDecoder2(null); public P25Phase1Pane() @@ -48,13 +52,16 @@ public class P25Phase1Pane extends DecoderPane private void init() { - addListener(getSampleLineChart()); +// addListener(getSampleLineChart()); addListener(getDecoder()); + getDecoder().setDEBUGFilteredBufferListener(mFilteredBufferBroadcaster); getDecoder().setDEBUGSymbolListener(getSymbolChart()); getDecoder().setDEBUGPLLPhaseErrorListener(getPLLPhaseErrorLineChart()); getDecoder().setDEBUGPLLFrequencyListener(getPLLFrequencyLineChart()); getDecoder().setDEBUGSymbolDecisionDataListener(getEyeDiagramChart()); + getDecoder().setDEBUGSamplesPerSymbolListener(getSamplesPerSymbolLineChart()); + mFilteredBufferBroadcaster.addListener(getSampleLineChart()); HBox.setHgrow(getSampleChartBox(), Priority.ALWAYS); HBox.setHgrow(getDecoderChartBox(), Priority.ALWAYS); @@ -67,7 +74,10 @@ public class P25Phase1Pane extends DecoderPane mLog.debug("Configuring for sample rate: " + sampleRate); mDecoder.setSampleRate(sampleRate); - getSampleLineChart().setSamplesPerSymbol((int)(sampleRate / 4800)); + double samplesPerSymbol = sampleRate / 4800.0; + + getSampleLineChart().setSamplesPerSymbol((int)samplesPerSymbol); +// mSamplesPerSymbolLineChart = new DoubleLineChart( "Samples/Symbol", samplesPerSymbol - 1, samplesPerSymbol + 1, 0.1, 40); } private P25_C4FMDecoder2 getDecoder() @@ -97,7 +107,8 @@ public class P25Phase1Pane extends DecoderPane HBox.setHgrow(getSymbolChart(), Priority.ALWAYS); HBox.setHgrow(getPLLPhaseErrorLineChart(), Priority.ALWAYS); HBox.setHgrow(getPLLFrequencyLineChart(), Priority.ALWAYS); - mDecoderChartBox.getChildren().addAll(getSymbolChart(), getPLLPhaseErrorLineChart(), getPLLFrequencyLineChart()); + mDecoderChartBox.getChildren().addAll(getSymbolChart(), getPLLPhaseErrorLineChart(), + getPLLFrequencyLineChart()); } return mDecoderChartBox; @@ -111,9 +122,12 @@ public class P25Phase1Pane extends DecoderPane 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); - mSampleChartBox.getChildren().addAll(getSampleLineChart(), getEyeDiagramChart()); + HBox.setHgrow(getSamplesPerSymbolLineChart(), Priority.ALWAYS); + mSampleChartBox.getChildren().addAll(getSampleLineChart(), getEyeDiagramChart(), + getSamplesPerSymbolLineChart()); } return mSampleChartBox; @@ -149,13 +163,23 @@ public class P25Phase1Pane extends DecoderPane return mPLLPhaseErrorLineChart; } - private FrequencyLineChart getPLLFrequencyLineChart() + private DoubleLineChart getPLLFrequencyLineChart() { if(mPLLFrequencyLineChart == null) { - mPLLFrequencyLineChart = new FrequencyLineChart(2400, 40); + mPLLFrequencyLineChart = new DoubleLineChart( "PLL Frequency", -200, 200, 20, 40); } return mPLLFrequencyLineChart; } + + private DoubleLineChart getSamplesPerSymbolLineChart() + { + if(mSamplesPerSymbolLineChart == null) + { + mSamplesPerSymbolLineChart = new DoubleLineChart( "Sample Point", 9.0, 11.0, 0.1, 40); + } + + return mSamplesPerSymbolLineChart; + } } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2.java index b5ccec57..706d6d9b 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2.java @@ -21,10 +21,11 @@ import io.github.dsheirer.dsp.filter.design.FilterDesignException; import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification; import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter; import io.github.dsheirer.dsp.gain.ComplexFeedForwardGainControl; +import io.github.dsheirer.dsp.psk.DQPSKDemodulator; import io.github.dsheirer.dsp.psk.DQPSKSymbolPhaseErrorCalculator; import io.github.dsheirer.dsp.psk.IQPSKSymbolDecoder; import io.github.dsheirer.dsp.psk.ISymbolPhaseErrorCalculator; -import io.github.dsheirer.dsp.psk.QPSKDemodulator; +import io.github.dsheirer.dsp.psk.QPSKStarSlicer; import io.github.dsheirer.dsp.psk.QPSKSymbolDecoder; import io.github.dsheirer.dsp.psk.SymbolDecisionData2; import io.github.dsheirer.dsp.psk.pll.CostasLoop; @@ -53,24 +54,26 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen private Map mSymbolFilters = new HashMap<>(); private ComplexFIRFilter mSymbolFilter; - private QPSKDemodulator mQPSKDemodulator; + private DQPSKDemodulator mQPSKDemodulator; private CostasLoop mCostasLoop; private ComplexBufferToStreamConverter mStreamConverter = new ComplexBufferToStreamConverter(); private ComplexFeedForwardGainControl mAGC = new ComplexFeedForwardGainControl(32); private QPSKSymbolDecoder mQPSKSlicer = new QPSKSymbolDecoder(); private P25MessageFramer mMessageFramer; private ISymbolPhaseErrorCalculator mSymbolPhaseErrorCalculator = new DQPSKSymbolPhaseErrorCalculator(); - private IQPSKSymbolDecoder mSymbolDecoder = new QPSKSymbolDecoder(); + private IQPSKSymbolDecoder mSymbolDecoder = new QPSKStarSlicer(); private Listener mDEBUGSymbolListener; private Listener mDEBUGPLLPhaseErrorListener; private Listener mDEBUGPLLFrequencyListener; private Listener mDEBUGSymbolDecisionDataListener; + private Listener mDEBUGSamplesPerSymbolListener; + private Listener mFilteredSymbolListener; public P25_C4FMDecoder2(AliasList aliasList) { super(aliasList); - setSampleRate(48000.0); + setSampleRate(48600.0); mStreamConverter.setListener(mAGC); } @@ -90,12 +93,13 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen mCostasLoop.setTracking(Tracking.FINE); mCostasLoop.setAutomaticTracking(false); - mQPSKDemodulator = new QPSKDemodulator(mCostasLoop, mSymbolPhaseErrorCalculator, mSymbolDecoder, + mQPSKDemodulator = new DQPSKDemodulator(mCostasLoop, mSymbolPhaseErrorCalculator, mSymbolDecoder, mSampleRate, SYMBOL_RATE); mQPSKDemodulator.setSymbolListener(mDEBUGSymbolListener); mQPSKDemodulator.setPLLErrorListener(mDEBUGPLLPhaseErrorListener); mQPSKDemodulator.setPLLFrequencyListener(mDEBUGPLLFrequencyListener); mQPSKDemodulator.setSymbolDecisionDataListener(mDEBUGSymbolDecisionDataListener); + mQPSKDemodulator.setSamplesPerSymbolListener(mDEBUGSamplesPerSymbolListener); //Message framer can trigger a symbol-inversion correction to the PLL when detected mMessageFramer = new P25MessageFramer(getAliasList(), mCostasLoop); @@ -121,20 +125,36 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen mQPSKDemodulator.setPLLFrequencyListener(listener); } + public void setDEBUGFilteredBufferListener(Listener listener) + { + mFilteredSymbolListener = listener; + } + public void setDEBUGSymbolDecisionDataListener(Listener listener) { mDEBUGSymbolDecisionDataListener = listener; mQPSKDemodulator.setSymbolDecisionDataListener(listener); } + public void setDEBUGSamplesPerSymbolListener(Listener listener) + { + mDEBUGSamplesPerSymbolListener = listener; + mQPSKDemodulator.setSamplesPerSymbolListener(listener); + } + @Override public void receive(ComplexBuffer complexBuffer) { ComplexBuffer basebandFiltered = mBasebandFilter.filter(complexBuffer); - ComplexBuffer symbolFiltered = mSymbolFilter.filter(basebandFiltered); -// ComplexBuffer amplified = mAGC.filter(symbolFiltered); +// ComplexBuffer symbolFiltered = mSymbolFilter.filter(complexBuffer); +// ComplexBuffer symbolFiltered = complexBuffer; + ComplexBuffer symbolFiltered = basebandFiltered; mQPSKDemodulator.receive(symbolFiltered); -// mQPSKDemodulator.receive(complexBuffer); + + if(mFilteredSymbolListener != null) + { + mFilteredSymbolListener.receive(symbolFiltered); + } } private double getSampleRate() @@ -150,11 +170,11 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen { FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() .sampleRate((int)getSampleRate()) - .passBandCutoff(2880) + .passBandCutoff(6000) .passBandAmplitude(1.0) .passBandRipple(0.01) .stopBandAmplitude(0.0) - .stopBandStart(3600) + .stopBandStart(6250) .stopBandRipple(0.01) .build(); @@ -186,7 +206,8 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen if(filter == null) { - filter = FilterFactory.getRootRaisedCosine(10, 10, 0.2f); + float samplesPerSymbol = (float)(getSampleRate() / 4800.0); + filter = FilterFactory.getRootRaisedCosine(samplesPerSymbol, 40, 0.2f); mSymbolFilters.put(getSampleRate(), filter); } From 148ddd0bd57587c7a52a09df91cc69ca8ec6569e Mon Sep 17 00:00:00 2001 From: Denny Date: Sun, 25 Feb 2018 08:56:34 -0500 Subject: [PATCH 16/23] 330 WIP - P25 C4FM decoder fully functional using DQPSK decoder and new symbol timing error detector. Each component now has a subclass that supports instrumentation for use with the DemodulatorViewerFX application. --- .../dsp/filter/design/FilterViewer.java | 6 +- .../dsheirer/dsp/psk/DQPSKDemodulator.java | 235 ++++++------------ .../dsp/psk/DQPSKDemodulatorInstrumented.java | 134 ++++++++++ .../psk/DQPSKSymbolPhaseErrorCalculator.java | 9 +- .../dsp/psk/DQPSKTimingErrorDetector.java | 89 +++++++ .../dsheirer/dsp/psk/EarlyLateDetector.java | 76 ------ .../dsp/psk/ISymbolPhaseErrorCalculator.java | 7 - ...r2.java => InterpolatingSampleBuffer.java} | 164 ++++-------- ...nterpolatingSampleBufferInstrumented.java} | 38 +-- .../dsp/psk/InterpolatingSymbolBuffer.java | 219 ---------------- .../dsheirer/dsp/psk/LSMDemodulator.java | 7 + .../dsheirer/dsp/psk/QPSKDemodulator.java | 233 ----------------- .../psk/QPSKSymbolPhaseErrorCalculator.java | 6 - .../dsheirer/dsp/psk/SymbolDecisionData.java | 81 +++--- .../dsheirer/dsp/psk/SymbolDecisionData2.java | 74 ------ .../dsheirer/dsp/psk/pll/CostasLoop.java | 215 ++-------------- .../dsp/psk/pll/IPhaseLockedLoop.java | 5 +- .../github/dsheirer/dsp/psk/pll/Tracking.java | 45 +--- .../gui/viewer/chart/EyeDiagramChart.java | 6 +- .../gui/viewer/decoder/P25Phase1Pane.java | 22 +- .../module/decode/p25/P25_C4FMDecoder2.java | 143 +++-------- .../p25/P25_C4FMDecoder2Instrumented.java | 109 ++++++++ .../tsbk/osp/control/SyncBroadcast.java | 2 +- 23 files changed, 623 insertions(+), 1302 deletions(-) create mode 100644 src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulatorInstrumented.java create mode 100644 src/main/java/io/github/dsheirer/dsp/psk/DQPSKTimingErrorDetector.java delete mode 100644 src/main/java/io/github/dsheirer/dsp/psk/EarlyLateDetector.java rename src/main/java/io/github/dsheirer/dsp/psk/{InterpolatingSymbolBuffer2.java => InterpolatingSampleBuffer.java} (52%) rename src/main/java/io/github/dsheirer/dsp/psk/{EarlyLateDetector2.java => InterpolatingSampleBufferInstrumented.java} (53%) delete mode 100644 src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer.java delete mode 100644 src/main/java/io/github/dsheirer/dsp/psk/QPSKDemodulator.java delete mode 100644 src/main/java/io/github/dsheirer/dsp/psk/SymbolDecisionData2.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2Instrumented.java diff --git a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java index ff42ad85..480463ee 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java @@ -50,12 +50,12 @@ public class FilterViewer extends Application // .stopBandRipple(0.008) // .build(); FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() - .sampleRate((int)48000.0) - .passBandCutoff(2880) + .sampleRate(48000) + .passBandCutoff(5100) .passBandAmplitude(1.0) .passBandRipple(0.01) .stopBandAmplitude(0.0) - .stopBandStart(3600) + .stopBandStart(6500) .stopBandRipple(0.01) .build(); diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulator.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulator.java index 4d4374a6..50326b13 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulator.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulator.java @@ -15,7 +15,6 @@ ******************************************************************************/ 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.dsp.symbol.Dibit; import io.github.dsheirer.sample.Listener; @@ -28,70 +27,46 @@ import org.slf4j.LoggerFactory; public class DQPSKDemodulator implements ComplexSampleListener { private final static Logger mLog = LoggerFactory.getLogger(DQPSKDemodulator.class); - private InterpolatingSymbolBuffer2 mInterpolatingSymbolBuffer; - private IPhaseLockedLoop mPLL; + protected InterpolatingSampleBuffer mInterpolatingSampleBuffer; + protected IPhaseLockedLoop mPLL; + protected IQPSKSymbolDecoder mDQPSKSymbolDecoder; private ISymbolPhaseErrorCalculator mSymbolPhaseErrorCalculator; - private IQPSKSymbolDecoder mDQPSKSymbolDecoder; -// private GardnerDetector mGardnerDetector = new GardnerDetector(); - private EarlyLateDetector mEarlyLateDetector = new EarlyLateDetector(); - private EarlyLateDetector2 mEarlyLateDetector2 = new EarlyLateDetector2(); - private Listener mSymbolListener; - private Listener mPLLErrorListener; - private Listener mPLLFrequencyListener; - private Listener mSamplesPerSymbolListener; - private Listener mDibitListener; - private Listener mSymbolDecisionDataListener; + private DQPSKTimingErrorDetector mDQPSKTimingErrorDetector = new DQPSKTimingErrorDetector(); private Complex mReceivedSample = new Complex(0, 0); - private Complex mPreviousPrecedingSample = new Complex(0, 0); private Complex mPreviousCurrentSample = new Complex(0, 0); - private Complex mPreviousFollowingSample = new Complex(0, 0); - - private Complex mCurrentPrecedingSample = new Complex(0, 0); - private Complex mCurrentCurrentSample = new Complex(0, 0); - private Complex mCurrentFollowingSample = 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); - private Complex mCurrentSymbol = new Complex(0, 0); - private Complex mFollowingSymbol = new Complex(0, 0); + protected Complex mCurrentSymbol = new Complex(0, 0); - private float mPhaseError; + protected float mPhaseError; private float mSymbolTimingError; - private double mSampleRate; - + private Listener mDibitListener; /** - * Decoder for Quaternary Phase Shift Keying (QPSK) and Differential QPSK (DQPSK). This decoder uses both a - * phase-locked loop and a gardner symbol timing error detector to automatically align to the incoming carrier - * frequency and to adjust for any changes in symbol timing. + * 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 symbolPhaseErrorCalculator to calculate symbol phase errors and optionally adjust for differential encoding + * @param symbolPhaseErrorCalculator to calculate symbol phase errors to apply against the PLL * @param symbolDecoder to decode demodulated symbols into dibits - * @param sampleRate of the incoming complex sample stream - * @param symbolRate of the decoded QPSK symbols. */ public DQPSKDemodulator(IPhaseLockedLoop phaseLockedLoop, ISymbolPhaseErrorCalculator symbolPhaseErrorCalculator, - IQPSKSymbolDecoder symbolDecoder, double sampleRate, double symbolRate) + IQPSKSymbolDecoder symbolDecoder, InterpolatingSampleBuffer interpolatingSampleBuffer) { - if(sampleRate < (symbolRate * 2)) - { - throw new IllegalArgumentException("Sample rate [" + sampleRate + - "] must be at least 2 x symbol rate [" + symbolRate + "]"); - } - - mSampleRate = sampleRate; mPLL = phaseLockedLoop; mSymbolPhaseErrorCalculator = symbolPhaseErrorCalculator; mDQPSKSymbolDecoder = symbolDecoder; - - float samplesPerSymbol = (float)(sampleRate / symbolRate); - mInterpolatingSymbolBuffer = new InterpolatingSymbolBuffer2(samplesPerSymbol); + mInterpolatingSampleBuffer = interpolatingSampleBuffer; } /** - * Submits a buffer containing complex samples for decoding + * Processes a (filtered) buffer containing complex samples for decoding * @param complexBuffer with complex samples */ public void receive(ComplexBuffer complexBuffer) @@ -105,7 +80,7 @@ public class DQPSKDemodulator implements ComplexSampleListener } /** - * Submits a complex sample for decoding. + * 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 */ @@ -118,109 +93,64 @@ public class DQPSKDemodulator implements ComplexSampleListener //Mix current sample with costas loop to remove any rotation that is present from a mis-tuned carrier frequency mReceivedSample.multiply(mPLL.incrementAndGetCurrentVector()); - mInterpolatingSymbolBuffer.receive(mReceivedSample); + //Store the sample in the interpolating buffer + mInterpolatingSampleBuffer.receive(mReceivedSample); //Calculate the symbol once we've stored enough samples - if(mInterpolatingSymbolBuffer.hasSymbol()) + if(mInterpolatingSampleBuffer.hasSymbol()) { - //Get middle and current samples from the interpolating buffer - mCurrentPrecedingSample = mInterpolatingSymbolBuffer.getPreceedingSample(); - mCurrentCurrentSample = mInterpolatingSymbolBuffer.getCurrentSample(); - mCurrentFollowingSample = mInterpolatingSymbolBuffer.getFollowingSample(); - - //Eye diagram listener - if(mSymbolDecisionDataListener != null) - { - mSymbolDecisionDataListener.receive(mInterpolatingSymbolBuffer.getSymbolDecisionData()); - } - - //Calculate preceding, current and following symbols as the delta rotation compared to the previous samples - mPrecedingSymbol.setInphase(Complex.multiplyInphase(mCurrentPrecedingSample.inphase(), mCurrentPrecedingSample.quadrature(), - mPreviousPrecedingSample.inphase(), -mPreviousPrecedingSample.quadrature())); - mPrecedingSymbol.setQuadrature(Complex.multiplyQuadrature(mCurrentPrecedingSample.inphase(), mCurrentPrecedingSample.quadrature(), - mPreviousPrecedingSample.inphase(), -mPreviousPrecedingSample.quadrature())); - - mCurrentSymbol.setInphase(Complex.multiplyInphase(mCurrentCurrentSample.inphase(), mCurrentCurrentSample.quadrature(), - mPreviousCurrentSample.inphase(), -mPreviousCurrentSample.quadrature())); - mCurrentSymbol.setQuadrature(Complex.multiplyQuadrature(mCurrentCurrentSample.inphase(), mCurrentCurrentSample.quadrature(), - mPreviousCurrentSample.inphase(), -mPreviousCurrentSample.quadrature())); - - mFollowingSymbol.setInphase(Complex.multiplyInphase(mCurrentFollowingSample.inphase(), mCurrentFollowingSample.quadrature(), - mPreviousFollowingSample.inphase(), -mPreviousFollowingSample.quadrature())); - mFollowingSymbol.setQuadrature(Complex.multiplyQuadrature(mCurrentFollowingSample.inphase(), mCurrentFollowingSample.quadrature(), - mPreviousFollowingSample.inphase(), -mPreviousFollowingSample.quadrature())); - - //Set gain to unity before we calculate the error value - mPrecedingSymbol.normalize(); - mCurrentSymbol.normalize(); - mFollowingSymbol.normalize(); - - //Send to an external constellation symbol listener when registered - if(mSymbolListener != null) - { - mSymbolListener.receive(mCurrentSymbol); - } - - //Symbol timing error calculations - mSymbolTimingError = mEarlyLateDetector.getError(mPrecedingSymbol, mCurrentSymbol, mFollowingSymbol); - - mSymbolTimingError = GardnerDetector.clip(mSymbolTimingError, 0.5f); - mInterpolatingSymbolBuffer.resetAndAdjust(mSymbolTimingError); - - if(mSamplesPerSymbolListener != null) - { - mSamplesPerSymbolListener.receive((double)mInterpolatingSymbolBuffer.getSamplingPoint()); - } - - //Store current samples/symbols to use for the next period - mPreviousPrecedingSample.setValues(mCurrentPrecedingSample); - mPreviousCurrentSample.setValues(mCurrentCurrentSample); - mPreviousFollowingSample.setValues(mCurrentFollowingSample); - - //Calculate the phase error of the current symbol relative to the expected constellation and provide - //feedback to the PLL - mSymbolPhaseErrorCalculator.adjust(mCurrentSymbol); - mPhaseError = mSymbolPhaseErrorCalculator.getPhaseError(mCurrentSymbol); - - mPhaseError = GardnerDetector.clip(mPhaseError, 0.15f); - - mPLL.adjust(mPhaseError); -// mPLL.adjust(0.0); - - if(mPLLErrorListener != null) - { - mPLLErrorListener.receive((double)mPhaseError); - } - - if(mPLLFrequencyListener != null) - { - double loopFrequency = ((CostasLoop)mPLL).getLoopFrequency(); - - loopFrequency *= mSampleRate / (2.0 * Math.PI); - - mPLLFrequencyListener.receive(loopFrequency); - } - - //Decode the dibit from the symbol and send to the listener - if(mDibitListener != null) - { - mDibitListener.receive(mDQPSKSymbolDecoder.decode(mCurrentSymbol)); - } - - //TODO: Assemble dibits here and broadcast + calculateSymbol(); } } /** - * Registers the listener to receive decoded QPSK symbols + * Calculates a symbol from the interpolating buffer */ - public void setSymbolListener(Listener listener) + protected void calculateSymbol() { - mSymbolListener = listener; + //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(); + + //Symbol timing error calculation + mSymbolTimingError = mDQPSKTimingErrorDetector.getError(mPrecedingSymbol, mCurrentSymbol); + + mSymbolTimingError = clip(mSymbolTimingError, 0.5f); + mInterpolatingSampleBuffer.resetAndAdjust(mSymbolTimingError); + + //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 + mPhaseError = mSymbolPhaseErrorCalculator.getPhaseError(mCurrentSymbol); + + mPLL.adjust(mPhaseError); + + if(mDibitListener != null) + { + mDibitListener.receive(mDQPSKSymbolDecoder.decode(mCurrentSymbol)); + } } /** - * Registers the listener to receive decoded QPSK dibits + * Registers the listener to receive dibit symbol decisions from this demodulator */ public void setDibitListener(Listener listener) { @@ -228,32 +158,19 @@ public class DQPSKDemodulator implements ComplexSampleListener } /** - * Registers the listener to receive PLL error values + * Constrains value to the range of ( -maximum <> maximum ) */ - public void setPLLErrorListener(Listener listener) + public static float clip(float value, float maximum) { - mPLLErrorListener = listener; - } + if(value > maximum) + { + return maximum; + } + else if(value < -maximum) + { + return -maximum; + } - /** - * Registers the listener to receive PLL error values - */ - public void setPLLFrequencyListener(Listener listener) - { - mPLLFrequencyListener = listener; + return value; } - - /** - * Registers the listener to receive PLL error values - */ - public void setSamplesPerSymbolListener(Listener listener) - { - mSamplesPerSymbolListener = listener; - } - - public void setSymbolDecisionDataListener(Listener listener) - { - mSymbolDecisionDataListener = listener; - } - } 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..3ca084d0 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulatorInstrumented.java @@ -0,0 +1,134 @@ +/******************************************************************************* + * 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 symbolPhaseErrorCalculator to calculate symbol phase errors to apply against the PLL + * @param symbolDecoder to decode demodulated symbols into dibits + * @param interpolatingSampleBuffer + * @param sampleRate of the incoming complex sample stream + */ + public DQPSKDemodulatorInstrumented(IPhaseLockedLoop phaseLockedLoop, ISymbolPhaseErrorCalculator + symbolPhaseErrorCalculator, IQPSKSymbolDecoder symbolDecoder, + InterpolatingSampleBufferInstrumented interpolatingSampleBuffer, + double sampleRate) + { + super(phaseLockedLoop, symbolPhaseErrorCalculator, symbolDecoder, 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)mPhaseError); + } + + 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/DQPSKSymbolPhaseErrorCalculator.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolPhaseErrorCalculator.java index fce780e2..7eafb69a 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolPhaseErrorCalculator.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolPhaseErrorCalculator.java @@ -21,16 +21,17 @@ public class DQPSKSymbolPhaseErrorCalculator extends QPSKSymbolPhaseErrorCalcula { //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 boolean mToggle; + private Complex mSymbol = new Complex(0, 0); public DQPSKSymbolPhaseErrorCalculator() { } @Override - public void adjust(Complex symbol) + public float getPhaseError(Complex symbol) { - symbol.multiply(DIFFERENTIAL_OFFSET); + mSymbol.setValues(symbol); + mSymbol.multiply(DIFFERENTIAL_OFFSET); + return super.getPhaseError(mSymbol); } } diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKTimingErrorDetector.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKTimingErrorDetector.java new file mode 100644 index 00000000..929e2b06 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKTimingErrorDetector.java @@ -0,0 +1,89 @@ +/******************************************************************************* + * 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 DQPSKTimingErrorDetector +{ + private static final float PI_4_ROTATION = (float)(Math.PI / 4.0); + + /** + * Custom early/late symbol timing error detector designed to optimize the sample timing point such that the + * current symbol's inphase and quadrature approach the optimal 45 degree (.707) sampling instant. + */ + public DQPSKTimingErrorDetector() + { + } + + /** + * Calculates a symbol sampling time error by comparing the current complex sample against a normal QPSK or + * Pi/4 rotated four symbol constellation. The error signal (advance/retard) is determined by the slope + * of each of the inphase and quadrature legs as detected by comparing an immediately preceding sample against + * the current sample and then calculating the error distance of the sampled symbol from the ideal Pi/4 sampling + * location. + * + * Note: both the preceding and the current symbols MUST be normalized (ie symbol.normalize()) to the unit circle + * prior to invoking this method. + * + * @param precedingSymbol + * @param currentSymbol + * @return timing error signal used to advance (positive) or retard (negative) the sampling point + */ + public float getError(Complex precedingSymbol, Complex currentSymbol) + { + return getError(precedingSymbol.inphase(), currentSymbol.inphase()) + + getError(precedingSymbol.quadrature(), currentSymbol.quadrature()); + } + + /** + * Calculates timing error of current value relative to an ideal 45 degree or Pi/4 radians (.707) value. + * + * @param early sample that closely precedes the current sampling instant - used to determine slope + * @param current sampling value + * @return error as the distance of the current value from an ideal value and the sign (advance/retard) + * appropriate for the detected slope + */ + private float getError(float early, float current) + { + if(early < current) //Ascending slope + { + if(current > 0.0) + { + //Positive quadrature hemisphere, ascending slope + return PI_4_ROTATION - current; + } + else + { + //Negative quadrature hemisphere, ascending slope + return -PI_4_ROTATION - current; + } + } + else //Descending slope + { + if(current > 0.0) + { + //Positive quadrature hemisphere, descending slope + return current - PI_4_ROTATION; + } + else + { + //Negative quadrature hemisphere, descending slope + return current + PI_4_ROTATION; + } + } + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/psk/EarlyLateDetector.java b/src/main/java/io/github/dsheirer/dsp/psk/EarlyLateDetector.java deleted file mode 100644 index d3a593b6..00000000 --- a/src/main/java/io/github/dsheirer/dsp/psk/EarlyLateDetector.java +++ /dev/null @@ -1,76 +0,0 @@ -/******************************************************************************* - * 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; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class EarlyLateDetector -{ - private static final float PI_4_ROTATION = (float)(Math.PI / 4.0); - private final static Logger mLog = LoggerFactory.getLogger(EarlyLateDetector.class); - - /** - * Symbol timing error detector designed to optimize the sample timing point such that the current symbol's - * inphase and quadrature approach the optimal 45 degree (.707) sampling instant. - */ - public EarlyLateDetector() - { - } - - public float getError(Complex early, Complex current, Complex late) - { - float inphaseError = getError(early.inphase(), current.inphase(), late.inphase()); - float quadratureError = getError(early.quadrature(), current.quadrature(), late.quadrature()); - float error = inphaseError + quadratureError; - - mLog.debug("I:" + inphaseError + " Q:" + quadratureError + " E:" + error); - return error; -// return getError(early.inphase(), current.inphase(), late.inphase()) - -// getError(early.quadrature(), current.quadrature(), late.quadrature()); - } - - private float getError(float early, float current, float late) - { - if(early < current) //Ascending slope - { - if(current > 0.0) - { - //Positive hemisphere, ascending slope - return PI_4_ROTATION - current; - } - else - { - //Negative hemisphere, ascending slope - return -PI_4_ROTATION - current; - } - } - else //Descending slope - { - if(current > 0.0) - { - //Positive hemisphere, descending slope - return current - PI_4_ROTATION; - } - else - { - //Negative hemisphere, descending slope - return current + PI_4_ROTATION; - } - } - } -} diff --git a/src/main/java/io/github/dsheirer/dsp/psk/ISymbolPhaseErrorCalculator.java b/src/main/java/io/github/dsheirer/dsp/psk/ISymbolPhaseErrorCalculator.java index 5b5df5a9..4e413d88 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/ISymbolPhaseErrorCalculator.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/ISymbolPhaseErrorCalculator.java @@ -19,13 +19,6 @@ import io.github.dsheirer.sample.complex.Complex; public interface ISymbolPhaseErrorCalculator { - /** - * Adjusts the calculated symbol as necessary before calculating any phase error. This might be useful for - * differential encoding to remove a 45 degree rotational spin. - * @param symbol to adjust - */ - void adjust(Complex symbol); - /** * Calculates the phase error of the symbol relative to an alignment for the constellation * @param symbol to calculate error diff --git a/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer2.java b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBuffer.java similarity index 52% rename from src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer2.java rename to src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBuffer.java index ba8d731a..bc3cf830 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer2.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBuffer.java @@ -17,59 +17,60 @@ package io.github.dsheirer.dsp.psk; import io.github.dsheirer.dsp.filter.interpolator.RealInterpolator; import io.github.dsheirer.sample.complex.Complex; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -public class InterpolatingSymbolBuffer2 +public class InterpolatingSampleBuffer { - private static final float MAXIMUM_SAMPLES_PER_SYMBOL_DEVIATION = 0.02f; //2% - private final static Logger mLog = LoggerFactory.getLogger(InterpolatingSymbolBuffer2.class); - private Complex mPreceedingSample = new Complex(0,0); + 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 Complex mFollowingSample = new Complex(0,0); private float[] mDelayLineInphase; private float[] mDelayLineQuadrature; + private int mDelayLinePointer = 0; + private int mTwiceSamplesPerSymbol; + private float mSamplingPoint; private float mSampleCounterGain = 0.05f; private float mDetectedSamplesPerSymbol; private float mDetectedSamplesPerSymbolGain = 0.1f * mSampleCounterGain * mSampleCounterGain; - private float mMaximumSamplesPerSymbol;; - private float mMinimumSamplesPerSymbol;; + private float mMaximumSamplesPerSymbol; + private float mMinimumSamplesPerSymbol; - private int mDelayLinePointer = 0; - private int mTwiceSamplesPerSymbol; private RealInterpolator mInterpolator = new RealInterpolator(1.0f); - private SymbolDecisionData2 mSymbolDecisionData2; - public InterpolatingSymbolBuffer2(float samplesPerSymbol) + /** + * 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_SAMPLES_PER_SYMBOL_DEVIATION); - mMinimumSamplesPerSymbol = samplesPerSymbol * (1.0f - MAXIMUM_SAMPLES_PER_SYMBOL_DEVIATION); + 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]; - - mSymbolDecisionData2 = new SymbolDecisionData2((int)samplesPerSymbol); } - public void receive(Complex currentSample) + /** + * Stores the sample in the buffer and updates pointers. + * @param sample + */ + public void receive(Complex sample) { - mSymbolDecisionData2.receive(currentSample); mSamplingPoint--; //Fill up the delay line to use with the interpolator - mDelayLineInphase[mDelayLinePointer] = currentSample.inphase(); - mDelayLineInphase[mDelayLinePointer + mTwiceSamplesPerSymbol] = currentSample.inphase(); - mDelayLineQuadrature[mDelayLinePointer] = currentSample.quadrature(); - mDelayLineQuadrature[mDelayLinePointer + mTwiceSamplesPerSymbol] = currentSample.quadrature(); + 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; - } /** @@ -89,15 +90,22 @@ public class InterpolatingSymbolBuffer2 */ public void resetAndAdjust(float symbolTimingError) { - //mDetectedSamplesPerSymbol is samples per symbol and is constrained to floating between +/- .005 of the nominal samples per - //symbol + //Adjust detected samples per symbol based on timing error mDetectedSamplesPerSymbol = mDetectedSamplesPerSymbol + (symbolTimingError * mDetectedSamplesPerSymbolGain); - mDetectedSamplesPerSymbol = constrain(mDetectedSamplesPerSymbol, mMinimumSamplesPerSymbol, mMaximumSamplesPerSymbol); - mLog.debug("Detected Samples Per Symbol: " + mDetectedSamplesPerSymbol); + //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 + (mSampleCounterGain * symbolTimingError))); -// increaseSampleCounter(mDetectedSamplesPerSymbol); + increaseSampleCounter((mDetectedSamplesPerSymbol + (symbolTimingError * mSampleCounterGain))); } /** @@ -117,13 +125,13 @@ public class InterpolatingSymbolBuffer2 } /** - * Samples that precedes the current sample (uninterpolated). + * 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 getPreceedingSample() + public Complex getPrecedingSample() { - mPreceedingSample.setValues(mDelayLineInphase[mDelayLinePointer + 3], mDelayLineQuadrature[mDelayLinePointer + 3]); - - return mPreceedingSample; + mPrecedingSample.setValues(mDelayLineInphase[mDelayLinePointer + 3], mDelayLineQuadrature[mDelayLinePointer + 3]); + return mPrecedingSample; } /** @@ -134,33 +142,9 @@ public class InterpolatingSymbolBuffer2 { /* Calculate interpolated current sample */ mCurrentSample.setValues(getInphase(mSamplingPoint), getQuadrature(mSamplingPoint)); - return mCurrentSample; } - /** - * Samples that follows the current sample (uninterpolated). - */ - public Complex getFollowingSample() - { - mFollowingSample.setValues(mDelayLineInphase[mDelayLinePointer + 4], mDelayLineQuadrature[mDelayLinePointer + 4]); - - return mFollowingSample; - } - - - - /** - * 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 SymbolDecisionData2 getSymbolDecisionData() - { - mSymbolDecisionData2.setSamplingPoint(mSamplingPoint); - return mSymbolDecisionData2; - } - /** * Returns the interpolated inphase value for the specified offset * @param interpolation into the buffer to calculate the interpolated sample @@ -171,17 +155,6 @@ public class InterpolatingSymbolBuffer2 return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer, interpolation); } - /** - * Returns the interpolated inphase value for the specified offset - * @param delayLineOffset offset into the inphase delay line buffer - * @param interpolation into the buffer to calculate the interpolated sample - * @return inphase value of the interpolated sample - */ - public float getInphase(int delayLineOffset, float interpolation) - { - return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer + delayLineOffset, interpolation); - } - /** * Returns the interpolated quadrature value for the specified offset * @param interpolation into the buffer to calculate the interpolated sample @@ -191,55 +164,4 @@ public class InterpolatingSymbolBuffer2 { return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer, interpolation); } - - /** - * Returns the interpolated quadrature value for the specified offset - * @param delayLineOffset offset into the quadrature delay line buffer - * @param interpolation into the buffer to calculate the interpolated sample - * @return quadrature value of the interpolated sample - */ - public float getQuadrature(int delayLineOffset, float interpolation) - { - return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer + delayLineOffset, interpolation); - } - - /** - * Constrains value to the range of ( -maximum <> maximum ) - */ - public static float clip(float value, float maximum) - { - if(value > maximum) - { - return maximum; - } - else if(value < -maximum) - { - return -maximum; - } - - return value; - } - - /** - * Constrains the value within the minimum-maximum range - * - * @param value to constrain - * @param minimum value - * @param maximum value - * @return constrained value - */ - public static float constrain(float value, float minimum, float maximum) - { - if(value > maximum) - { - return maximum; - } - - if(value < minimum) - { - return minimum; - } - - return value; - } } diff --git a/src/main/java/io/github/dsheirer/dsp/psk/EarlyLateDetector2.java b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBufferInstrumented.java similarity index 53% rename from src/main/java/io/github/dsheirer/dsp/psk/EarlyLateDetector2.java rename to src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBufferInstrumented.java index 16ea9bce..6c71fc25 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/EarlyLateDetector2.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBufferInstrumented.java @@ -17,24 +17,32 @@ package io.github.dsheirer.dsp.psk; import io.github.dsheirer.sample.complex.Complex; -public class EarlyLateDetector2 +public class InterpolatingSampleBufferInstrumented extends InterpolatingSampleBuffer { - //45 degrees rotation to orient the symbol to a polar axis to make error calculation easy/efficient - public static final Complex POSITIVE_OFFSET = Complex.fromAngle(Math.PI / 4.0d); - private Complex mSymbol = new Complex(0,0); + private SymbolDecisionData mSymbolDecisionData; - public float getError(Complex symbol) + public InterpolatingSampleBufferInstrumented(float samplesPerSymbol) { - mSymbol.setValues(symbol); - mSymbol.multiply(POSITIVE_OFFSET); + super(samplesPerSymbol); - if(Math.abs(mSymbol.quadrature()) > Math.abs(mSymbol.inphase())) - { - return mSymbol.inphase(); - } - else - { - return mSymbol.quadrature(); - } + 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/InterpolatingSymbolBuffer.java b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer.java deleted file mode 100644 index cccba75d..00000000 --- a/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer.java +++ /dev/null @@ -1,219 +0,0 @@ -/******************************************************************************* - * 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; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class InterpolatingSymbolBuffer -{ - private final static Logger mLog = LoggerFactory.getLogger(InterpolatingSymbolBuffer.class); - - private Complex mMiddleSample = new Complex(0,0); - private Complex mCurrentSample = new Complex(0,0); - - private float[] mDelayLineInphase; - private float[] mDelayLineQuadrature; - private float mSamplingPoint; //Sample Counter - private float mSampleCounterGain = 0.05f; //Sample Counter Gain - private float mSamplesPerSymbol; - private float mDetectedSamplesPerSymbol; - private float mDetectedSamplesPerSymbolGain = 0.1f * mSampleCounterGain * mSampleCounterGain; - private float mMaxDeviation = 0.005f; - - private int mDelayLinePointer = 0; - private int mTwiceSamplesPerSymbol; - private RealInterpolator mInterpolator = new RealInterpolator(1.0f); - private SymbolDecisionData2 mSymbolDecisionData2; - - public InterpolatingSymbolBuffer(float samplesPerSymbol) - { - mSamplingPoint = samplesPerSymbol; - mTwiceSamplesPerSymbol = (int)Math.floor(2.0 * samplesPerSymbol); - mDelayLineInphase = new float[2 * mTwiceSamplesPerSymbol]; - mDelayLineQuadrature = new float[2 * mTwiceSamplesPerSymbol]; - mDetectedSamplesPerSymbol = samplesPerSymbol; - mSamplesPerSymbol = samplesPerSymbol; - - mSymbolDecisionData2 = new SymbolDecisionData2((int)samplesPerSymbol); - } - - public void receive(Complex currentSample) - { - mSymbolDecisionData2.receive(currentSample); - mSamplingPoint--; - - //Fill up the delay line to use with the interpolator - mDelayLineInphase[mDelayLinePointer] = currentSample.inphase(); - mDelayLineInphase[mDelayLinePointer + mTwiceSamplesPerSymbol] = currentSample.inphase(); - mDelayLineQuadrature[mDelayLinePointer] = currentSample.quadrature(); - mDelayLineQuadrature[mDelayLinePointer + mTwiceSamplesPerSymbol] = currentSample.quadrature(); - - //Increment pointer and keep pointer in bounds - mDelayLinePointer++; - mDelayLinePointer = 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) - { - //mDetectedSamplesPerSymbol is samples per symbol and is constrained to floating between +/- .005 of the nominal samples per - //symbol - mDetectedSamplesPerSymbol = mDetectedSamplesPerSymbol + mDetectedSamplesPerSymbolGain * symbolTimingError; - mDetectedSamplesPerSymbol = mSamplesPerSymbol + clip(mDetectedSamplesPerSymbol - mSamplesPerSymbol, mMaxDeviation); - - mLog.debug("Detected Samples Per Symbol:" + mDetectedSamplesPerSymbol); - //Add another symbol's worth of samples to the counter and adjust timing based on gardner error - increaseSampleCounter((mDetectedSamplesPerSymbol + (mSampleCounterGain * symbolTimingError))); - } - - /** - * 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; - } - - /** - * Interpolated middle sample for the symbol. Note: this method should only be invoked after testing for a - * complete symbol with the hasSymbol() method. - */ - public Complex getMiddleSample() - { - /* Calculate interpolated middle sample and current sample */ - mMiddleSample.setValues(getInphase(mSamplingPoint), getQuadrature(mSamplingPoint)); - - return mMiddleSample; - } - - /** - * Interpolated current sample for the symbol. This sample represents the ideal symbol sampling point. - * Note: this method should only be invoked after testing for a complete symbol with the hasSymbol() method. - */ - public Complex getCurrentSample() - { - float halfDetectedSamplesPerSymbol = mDetectedSamplesPerSymbol / 2.0f; - int halfSamplingPointOffset = (int)Math.floor(halfDetectedSamplesPerSymbol); - float halfSamplingPoint = mSamplingPoint + halfDetectedSamplesPerSymbol - halfSamplingPointOffset; - - if(halfSamplingPoint > 1.0) - { - halfSamplingPoint -= 1.0; - halfSamplingPointOffset += 1; - } - - mCurrentSample.setValues(getInphase(halfSamplingPointOffset, halfSamplingPoint), - getQuadrature(halfSamplingPointOffset, halfSamplingPoint)); - - return mCurrentSample; - } - - /** - * 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 SymbolDecisionData2 getSymbolDecisionData() - { - mSymbolDecisionData2.setSamplingPoint(mSamplingPoint); - return mSymbolDecisionData2; - } - - /** - * Returns the interpolated inphase value for the specified offset - * @param interpolation into the buffer to calculate the interpolated sample - * @return inphase value of the interpolated sample - */ - public float getInphase(float interpolation) - { - return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer, interpolation); - } - - /** - * Returns the interpolated inphase value for the specified offset - * @param delayLineOffset offset into the inphase delay line buffer - * @param interpolation into the buffer to calculate the interpolated sample - * @return inphase value of the interpolated sample - */ - public float getInphase(int delayLineOffset, float interpolation) - { - return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer + delayLineOffset, interpolation); - } - - /** - * Returns the interpolated quadrature value for the specified offset - * @param interpolation into the buffer to calculate the interpolated sample - * @return quadrature value of the interpolated sample - */ - public float getQuadrature(float interpolation) - { - return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer, interpolation); - } - - /** - * Returns the interpolated quadrature value for the specified offset - * @param delayLineOffset offset into the quadrature delay line buffer - * @param interpolation into the buffer to calculate the interpolated sample - * @return quadrature value of the interpolated sample - */ - public float getQuadrature(int delayLineOffset, float interpolation) - { - return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer + delayLineOffset, interpolation); - } - - /** - * Constrains value to the range of ( -maximum <> maximum ) - */ - public static float clip(float value, float maximum) - { - if(value > maximum) - { - return maximum; - } - else if(value < -maximum) - { - return -maximum; - } - - return value; - } -} 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 aba3a907..1a21ae75 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/LSMDemodulator.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/LSMDemodulator.java @@ -449,6 +449,13 @@ public class LSMDemodulator implements Instrumentable, ComplexSampleListener unwrapPhase(); } + @Override + public void reset() + { + mLoopPhase = 0.0f; + mLoopFrequency = 0.0f; + } + @Override public void adjust(double error) { diff --git a/src/main/java/io/github/dsheirer/dsp/psk/QPSKDemodulator.java b/src/main/java/io/github/dsheirer/dsp/psk/QPSKDemodulator.java deleted file mode 100644 index 9bcda428..00000000 --- a/src/main/java/io/github/dsheirer/dsp/psk/QPSKDemodulator.java +++ /dev/null @@ -1,233 +0,0 @@ -/******************************************************************************* - * 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.dsp.symbol.Dibit; -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.Complex; -import io.github.dsheirer.sample.complex.ComplexBuffer; -import io.github.dsheirer.sample.complex.ComplexSampleListener; - -public class QPSKDemodulator implements ComplexSampleListener -{ - private InterpolatingSymbolBuffer mInterpolatingSymbolBuffer; - private IPhaseLockedLoop mPLL; - private ISymbolPhaseErrorCalculator mSymbolPhaseErrorCalculator; - private IQPSKSymbolDecoder mQPSKSymbolDecoder; - private GardnerDetector mGardnerDetector = new GardnerDetector(); - private Listener mSymbolListener; - private Listener mPLLErrorListener; - private Listener mPLLFrequencyListener; - private Listener mSamplesPerSymbolListener; - private Listener mDibitListener; - private Listener mSymbolDecisionDataListener; - - private Complex mPreviousSample = new Complex(0, 0); - private Complex mPreviousMiddleSample = new Complex(0, 0); - private Complex mCurrentSample = new Complex(0, 0); - private Complex mMiddleSample = new Complex(0, 0); - private Complex mPreviousSymbol = new Complex(0, 0); - private Complex mMiddleSymbol = new Complex(0, 0); - private Complex mCurrentSymbol = new Complex(0, 0); - private float mPhaseError; - private float mSymbolTimingError; - private double mSampleRate; - - - /** - * Decoder for Quaternary Phase Shift Keying (QPSK) and Differential QPSK (DQPSK). This decoder uses both a - * phase-locked loop and a gardner symbol timing error detector to automatically align to the incoming carrier - * frequency and to adjust for any changes in symbol timing. - * - * @param phaseLockedLoop for tracking carrier frequency error - * @param symbolPhaseErrorCalculator to calculate symbol phase errors and optionally adjust for differential encoding - * @param symbolDecoder to decode demodulated symbols into dibits - * @param sampleRate of the incoming complex sample stream - * @param symbolRate of the decoded QPSK symbols. - */ - public QPSKDemodulator(IPhaseLockedLoop phaseLockedLoop, ISymbolPhaseErrorCalculator symbolPhaseErrorCalculator, - IQPSKSymbolDecoder symbolDecoder, double sampleRate, double symbolRate) - { - if(sampleRate < (symbolRate * 2)) - { - throw new IllegalArgumentException("Sample rate [" + sampleRate + - "] must be at least 2 x symbol rate [" + symbolRate + "]"); - } - - mSampleRate = sampleRate; - mPLL = phaseLockedLoop; - mSymbolPhaseErrorCalculator = symbolPhaseErrorCalculator; - mQPSKSymbolDecoder = symbolDecoder; - - float samplesPerSymbol = (float)(sampleRate / symbolRate); - mInterpolatingSymbolBuffer = new InterpolatingSymbolBuffer(samplesPerSymbol); - } - - /** - * Submits a buffer containing complex samples for decoding - * @param complexBuffer with complex samples - */ - public void receive(ComplexBuffer complexBuffer) - { - float[] samples = complexBuffer.getSamples(); - - for(int x = 0; x < samples.length; x += 2) - { - receive(samples[x], samples[x + 1]); - } - } - - /** - * Submits a complex sample for decoding. - * @param inphase value for the sample - * @param quadrature value for the sample - */ - @Override - public void receive(float inphase, float quadrature) - { - //Update current sample with values - mCurrentSample.setValues(inphase, quadrature); - - //Mix current sample with costas loop to remove any rotation that is present from a mis-tuned carrier frequency - mCurrentSample.multiply(mPLL.incrementAndGetCurrentVector()); - - mInterpolatingSymbolBuffer.receive(mCurrentSample); - - //Calculate the symbol once we've stored enough samples - if(mInterpolatingSymbolBuffer.hasSymbol()) - { - //Get middle and current samples from the interpolating buffer - mMiddleSample = mInterpolatingSymbolBuffer.getMiddleSample(); - mCurrentSample = mInterpolatingSymbolBuffer.getCurrentSample(); - - //Eye diagram listener - if(mSymbolDecisionDataListener != null) - { - mSymbolDecisionDataListener.receive(mInterpolatingSymbolBuffer.getSymbolDecisionData()); - } - - //Calculate middle and current symbols as the delta between the previous and current samples - mMiddleSymbol.setInphase(Complex.multiplyInphase(mMiddleSample.inphase(), mMiddleSample.quadrature(), mPreviousMiddleSample.inphase(), -mPreviousMiddleSample.quadrature())); - mMiddleSymbol.setQuadrature(Complex.multiplyQuadrature(mMiddleSample.inphase(), mMiddleSample.quadrature(), mPreviousMiddleSample.inphase(), -mPreviousMiddleSample.quadrature())); - mCurrentSymbol.setInphase(Complex.multiplyInphase(mCurrentSample.inphase(), mCurrentSample.quadrature(), mPreviousSample.inphase(), -mPreviousSample.quadrature())); - mCurrentSymbol.setQuadrature(Complex.multiplyQuadrature(mCurrentSample.inphase(), mCurrentSample.quadrature(), mPreviousSample.inphase(), -mPreviousSample.quadrature())); - - //Set gain to unity before we calculate the error value - mMiddleSymbol.normalize(); - mCurrentSymbol.normalize(); - - //Send to an external constellation symbol listener when registered - if(mSymbolListener != null) - { - mSymbolListener.receive(mCurrentSymbol); - } - - //Symbol timing error calculations - mSymbolTimingError = mGardnerDetector.getError(mPreviousSymbol, mMiddleSymbol, mCurrentSymbol); - mInterpolatingSymbolBuffer.resetAndAdjust(mSymbolTimingError); - - if(mSamplesPerSymbolListener != null) - { - mSamplesPerSymbolListener.receive((double)mInterpolatingSymbolBuffer.getSamplingPoint()); - } - - //Store current samples/symbols to use for the next period - mPreviousSample.setValues(mCurrentSample); - mPreviousMiddleSample.setValues(mMiddleSample); - mPreviousSymbol.setValues(mCurrentSymbol); - - //Calculate the phase error of the current symbol relative to the expected constellation and provide - //feedback to the PLL - mSymbolPhaseErrorCalculator.adjust(mCurrentSymbol); - mPhaseError = mSymbolPhaseErrorCalculator.getPhaseError(mCurrentSymbol); - - mPhaseError = GardnerDetector.clip(mPhaseError, 0.15f); - -// mPLL.adjust(-mPhaseError); - mPLL.adjust(0.0); - - if(mPLLErrorListener != null) - { - mPLLErrorListener.receive((double)mPhaseError); - } - - if(mPLLFrequencyListener != null) - { - double loopFrequency = ((CostasLoop)mPLL).getLoopFrequency(); - - loopFrequency *= mSampleRate / (2.0 * Math.PI); - - mPLLFrequencyListener.receive(loopFrequency); - } - - //Decode the dibit from the symbol and send to the listener - if(mDibitListener != null) - { - mDibitListener.receive(mQPSKSymbolDecoder.decode(mCurrentSymbol)); - } - - //TODO: Assemble dibits here and broadcast - } - } - - /** - * Registers the listener to receive decoded QPSK symbols - */ - public void setSymbolListener(Listener listener) - { - mSymbolListener = listener; - } - - /** - * Registers the listener to receive decoded QPSK dibits - */ - public void setDibitListener(Listener listener) - { - mDibitListener = 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; - } - - /** - * Registers the listener to receive PLL error values - */ - public void setSamplesPerSymbolListener(Listener listener) - { - mSamplesPerSymbolListener = listener; - } - - public void setSymbolDecisionDataListener(Listener listener) - { - mSymbolDecisionDataListener = listener; - } - -} diff --git a/src/main/java/io/github/dsheirer/dsp/psk/QPSKSymbolPhaseErrorCalculator.java b/src/main/java/io/github/dsheirer/dsp/psk/QPSKSymbolPhaseErrorCalculator.java index d7c94323..fed580ce 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/QPSKSymbolPhaseErrorCalculator.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/QPSKSymbolPhaseErrorCalculator.java @@ -24,12 +24,6 @@ public class QPSKSymbolPhaseErrorCalculator implements ISymbolPhaseErrorCalculat { private float mPhaseError; - @Override - public void adjust(Complex symbol) - { - //No-op - since there is no differential, we don't have to adjust - } - @Override public float getPhaseError(Complex symbol) { diff --git a/src/main/java/io/github/dsheirer/dsp/psk/SymbolDecisionData.java b/src/main/java/io/github/dsheirer/dsp/psk/SymbolDecisionData.java index 75a70332..e06f3d22 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/SymbolDecisionData.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/SymbolDecisionData.java @@ -15,56 +15,79 @@ ******************************************************************************/ package io.github.dsheirer.dsp.psk; +import io.github.dsheirer.buffer.ComplexCircularBuffer; +import io.github.dsheirer.sample.complex.Complex; public class SymbolDecisionData { - private float[] mSamplesInphase; - private float[] mSamplesQuadrature; - private float mSamplesPerSymbol; - private int mSampleIndex; - private float mSampleIndexOffset; + //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; /** - * Symbol decision data - samples and timing decision for a single symbol decision instant. - * - * @param samplesInphase - * @param samplesQuadrature - * @param samplesPerSymbol - * @param sampleIndex - * @param sampleIndexOffset + * Circular buffer for capturing two symbols worth of sample data for use in instrumentation. + * @param samplesPerSymbol to size the buffer */ - public SymbolDecisionData(float[] samplesInphase, float[] samplesQuadrature, float samplesPerSymbol, - int sampleIndex, float sampleIndexOffset) + public SymbolDecisionData(int samplesPerSymbol) { - mSamplesInphase = samplesInphase; - mSamplesQuadrature = samplesQuadrature; - mSamplesPerSymbol = samplesPerSymbol; - mSampleIndex = sampleIndex; - mSampleIndexOffset = sampleIndexOffset; + mBuffer = new ComplexCircularBuffer(2 * samplesPerSymbol); } - public float[] getInphaseSamples() + /** + * Stores the complex sample in the buffer + */ + public void receive(float inphase, float quadrature) { - return mSamplesInphase; + mBuffer.put(new Complex(inphase, quadrature)); } - public float[] getQuadratureSamples() + /** + * Stores the complex sample in the buffer + */ + public void receive(Complex complex) { - return mSamplesQuadrature; + mBuffer.put(complex.copy()); } - public float getSamplesPerSymbol() + /** + * Sets the fractional sampling point for the current symbol to use in interpolating the current symbol. + */ + public void setSamplingPoint(float samplingPoint) { - return mSamplesPerSymbol; + mSamplingPoint = samplingPoint; } - public int getSampleIndex() + /** + * Fractional sampling point + */ + public float getSamplingPoint() { - return mSampleIndex; + return mSamplingPoint; } - public float getSampleIndexOffset() + /** + * 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() { - return mSampleIndexOffset; + 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/SymbolDecisionData2.java b/src/main/java/io/github/dsheirer/dsp/psk/SymbolDecisionData2.java deleted file mode 100644 index 64d9ebe9..00000000 --- a/src/main/java/io/github/dsheirer/dsp/psk/SymbolDecisionData2.java +++ /dev/null @@ -1,74 +0,0 @@ -/******************************************************************************* - * 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 SymbolDecisionData2 -{ - //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; - - public SymbolDecisionData2(int samplesPerSymbol) - { - mBuffer = new ComplexCircularBuffer(2 * samplesPerSymbol); - } - - public void receive(float inphase, float quadrature) - { - mBuffer.put(new Complex(inphase, quadrature)); - } - - public void receive(Complex complex) - { - mBuffer.put(complex.copy()); - } - - public void setSamplingPoint(float samplingPoint) - { - mSamplingPoint = samplingPoint; - } - - public float getSamplingPoint() - { - return mSamplingPoint; - } - - 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()); - - //Test - rotate the demodulated symbol by 45 degrees to see the impact on the eye diagram -// copy.multiply(DIFFERENTIAL_OFFSET); - demodulated[x] = copy; - } - - return demodulated; - } -} 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 index 187e1c0c..38b8d101 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/pll/CostasLoop.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/pll/CostasLoop.java @@ -15,13 +15,7 @@ ******************************************************************************/ package io.github.dsheirer.dsp.psk.pll; -import io.github.dsheirer.buffer.DoubleCircularBuffer; -import io.github.dsheirer.dsp.mixer.Oscillator; import io.github.dsheirer.sample.complex.Complex; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.text.DecimalFormat; /** * Costas Loop - phase locked loop designed to automatically synchronize to the incoming carrier frequency in order to @@ -34,21 +28,16 @@ import java.text.DecimalFormat; */ public class CostasLoop implements IPhaseLockedLoop { - private final static Logger mLog = LoggerFactory.getLogger(CostasLoop.class); public static final double TWO_PI = 2.0 * Math.PI; - private DoubleCircularBuffer mErrorBuffer = new DoubleCircularBuffer(10); + + private 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 double mLoopPhase = 0.0; -// private double mLoopFrequency = 0.0; - private double mLoopFrequency = Math.PI * 2.0 * -10.0 / 48000.0; - private double mMaximumLoopFrequency; - private Complex mCurrentVector = new Complex(0, 0); - - private Tracking mTracking = Tracking.SEARCHING; - private ITrackingStateListener mTrackingStateListener; - private boolean mAutomaticTrackingEnabled = true; + private Tracking mTracking = Tracking.NORMAL; /** * Costas Loop for tracking and correcting frequency error in a received carrier signal. @@ -75,12 +64,12 @@ public class CostasLoop implements IPhaseLockedLoop { mLoopFrequency += correction; - if(mLoopFrequency > mMaximumLoopFrequency) + while(mLoopFrequency > mMaximumLoopFrequency) { mLoopFrequency -= 2.0 * mMaximumLoopFrequency; } - if(mLoopFrequency < -mMaximumLoopFrequency) + while(mLoopFrequency < -mMaximumLoopFrequency) { mLoopFrequency += 2.0 * mMaximumLoopFrequency; } @@ -107,35 +96,6 @@ public class CostasLoop implements IPhaseLockedLoop { mTracking = tracking; updateLoopBandwidth(); - - mLog.debug("Tracking: " + tracking.name()); - - if(mTrackingStateListener != null) - { - mTrackingStateListener.trackingStateChanged(tracking); - } - } - - /** - * Sets the listener to receive notifications when the tracking state changes - */ - public void setTrackingStateListener(ITrackingStateListener listener) - { - mTrackingStateListener = listener; - } - - /** - * Enables or disables automatic tracking state updates. When enabled, the loop will automatically increase or - * decrease the loop alpha/beta gain values based on the variance of the error tracking values submitted to the - * loop control. As the variance decreases, so will the gain values, and vice-versa. Setting this to disabled - * will force the tracker to retain the gain values associated with either the default or user-specified tracking - * value. - * - * @param enabled true (default) to enable automatic gain tracking - */ - public void setAutomaticTracking(boolean enabled) - { - mAutomaticTrackingEnabled = enabled; } /** @@ -145,8 +105,16 @@ public class CostasLoop implements IPhaseLockedLoop { mLoopPhase += mLoopFrequency; - /* Keep the loop phase in bounds */ - normalizePhase(); + /* Normalize phase between +/- 2 * PI */ + if(mLoopPhase > TWO_PI) + { + mLoopPhase -= TWO_PI; + } + + if(mLoopPhase < -TWO_PI) + { + mLoopPhase += TWO_PI; + } } /** @@ -171,11 +139,6 @@ public class CostasLoop implements IPhaseLockedLoop return mLoopFrequency; } - public double getMaximumLoopFrequency() - { - return mMaximumLoopFrequency; - } - /** * Updates the costas loop frequency and phase to adjust for the phase * error value @@ -187,97 +150,18 @@ public class CostasLoop implements IPhaseLockedLoop mLoopFrequency += (mBetaGain * phaseError); mLoopPhase += mLoopFrequency + (mAlphaGain * phaseError); - /* Maintain phase between +/- 2 * PI */ - normalizePhase(); - - /* Limit frequency to +/- maximum loop frequency */ - limitFrequency(); - -// checkLoopBandwidth(); - } - - private void checkLoopBandwidth() - { - mErrorBuffer.put(mLoopFrequency); - - double standardDeviation = mErrorBuffer.standardDeviation(); - - switch(mTracking) - { - case SEARCHING: - if(Tracking.COARSE.contains(standardDeviation)) - { - setTracking(Tracking.COARSE); - } - break; - case COARSE: - if(Tracking.COARSE.contains(standardDeviation)) - { - //Promote if possible - otherwise remain - if(Tracking.FINE.contains(standardDeviation)) - { - setTracking(Tracking.FINE); - } - } - else - { - setTracking(Tracking.SEARCHING); - } - break; - case FINE: - if(Tracking.FINE.contains(standardDeviation)) - { - //Promote if possible - otherwise remain - if(Tracking.LOCKED.contains(standardDeviation)) - { - setTracking(Tracking.LOCKED); - } - } - else - { - setTracking(Tracking.COARSE); - } - break; - case LOCKED: - if(!Tracking.LOCKED.contains(standardDeviation)) - { - setTracking(Tracking.FINE); - } - break; - } - } - - public double getErrorMean() - { - return mErrorBuffer.mean(); - } - - public double getErrorVariance() - { - return mErrorBuffer.variance(); - } - - /** - * Normalizes the phase tracker to maintain within +/- 2 Pi - */ - private void normalizePhase() - { - while(mLoopPhase > TWO_PI) + /* Normalize phase between +/- 2 * PI */ + if(mLoopPhase > TWO_PI) { mLoopPhase -= TWO_PI; } - while(mLoopPhase < -TWO_PI) + if(mLoopPhase < -TWO_PI) { mLoopPhase += TWO_PI; } - } - /** - * Constrains the frequency within the bounds of +/- loop frequency - */ - private void limitFrequency() - { + /* Limit frequency to +/- maximum loop frequency */ if(mLoopFrequency > mMaximumLoopFrequency) { mLoopFrequency = mMaximumLoopFrequency; @@ -289,57 +173,10 @@ public class CostasLoop implements IPhaseLockedLoop } } - /** - * Tracking state listener interface for receiving updates when the tracking state of the costas loop changes - */ - public interface ITrackingStateListener + @Override + public void reset() { - void trackingStateChanged(Tracking tracking); + mLoopPhase = 0.0; + mLoopFrequency = 0.0; } - - public static void main(String[] arguments) - { - DecimalFormat decimalFormat = new DecimalFormat("0.000000"); - double sampleRate = 25000.0; - int errorFrequency = 411; - double loopBandwidth = 50.0; - - Oscillator oscillator = new Oscillator(errorFrequency, (int)sampleRate); - CostasLoop costasLoop = new CostasLoop(sampleRate, 4800.0); - costasLoop.setTrackingStateListener(new ITrackingStateListener() - { - @Override - public void trackingStateChanged(Tracking tracking) - { - mLog.debug("Tracking State Changed: " + tracking.name()); - } - }); - - for(int x = 0; x < 1000; x++) - { - Complex complex = oscillator.getComplex(); - - oscillator.rotate(); - costasLoop.increment(); - - if(x % 5 == 0) - { - complex.multiply(costasLoop.getCurrentVector().conjugate()); - - double phaseError = complex.angle(); - costasLoop.adjust(phaseError); - - double loopFrequency = costasLoop.getLoopFrequency() * sampleRate / TWO_PI; - double maxFrequency = costasLoop.getMaximumLoopFrequency() * sampleRate / TWO_PI; - - mLog.debug(x + " Frequency: " + errorFrequency + - " Loop:" + decimalFormat.format(loopFrequency) + - " Max:" + maxFrequency + - " Detected Error:" + decimalFormat.format(phaseError) + - " Avg:" + decimalFormat.format(costasLoop.getErrorMean()) + - " Var:" + decimalFormat.format(costasLoop.getErrorVariance())); - } - } - } - } 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 index 1f4f2a52..631a772c 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/pll/IPhaseLockedLoop.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/pll/IPhaseLockedLoop.java @@ -52,5 +52,8 @@ public interface IPhaseLockedLoop */ 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 index d89133b2..a4611d20 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/pll/Tracking.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/pll/Tracking.java @@ -15,26 +15,21 @@ ******************************************************************************/ 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 { -// SEARCHING(50.0, 0.01, Math.PI), -// COARSE(100.0, 0.001, 0.01), -// FINE(150.0, 0.0001, 0.001), -// LOCKED(200.0, 0.0, 0.0001); - SEARCHING(100.0, 0.4, Math.PI), - COARSE(200.0, 0.2, 0.4), - FINE(400.0, 0.1, 0.2), - LOCKED(600.0, 0.0, 0.1); + FASTEST(50.0), + FAST(100.0), + NORMAL(200.0), + SLOW(400.0); private double mLoopBandwidth; - private double mThresholdMinimum; - private double mThresholdMaximum; - Tracking(double loopBandwidth, double thresholdMinimum, double thresholdMaximum) + Tracking(double loopBandwidth) { mLoopBandwidth = loopBandwidth; - mThresholdMinimum = thresholdMinimum; - mThresholdMaximum = thresholdMaximum; } /** @@ -44,28 +39,4 @@ public enum Tracking { return mLoopBandwidth; } - - /** - * Minimum error value for this entry - */ - public double getThresholdMinimum() - { - return mThresholdMinimum; - } - - /** - * Maximum error value for this entry - */ - public double getThresholdMaximum() - { - return mThresholdMaximum; - } - - /** - * Indicates if the error value is within the range for this entry - */ - public boolean contains(double standardDeviation) - { - return getThresholdMinimum() <= standardDeviation && standardDeviation < getThresholdMaximum(); - } } 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 index d71c0911..8c9db35d 100644 --- 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 @@ -15,7 +15,7 @@ ******************************************************************************/ package io.github.dsheirer.instrument.gui.viewer.chart; -import io.github.dsheirer.dsp.psk.SymbolDecisionData2; +import io.github.dsheirer.dsp.psk.SymbolDecisionData; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.complex.Complex; import javafx.collections.FXCollections; @@ -25,7 +25,7 @@ import javafx.scene.chart.NumberAxis; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class EyeDiagramChart extends LineChart implements Listener +public class EyeDiagramChart extends LineChart implements Listener { private final static Logger mLog = LoggerFactory.getLogger(EyeDiagramChart.class); @@ -81,7 +81,7 @@ public class EyeDiagramChart extends LineChart implements Listener mFilteredBufferBroadcaster = new Broadcaster<>(); - private P25_C4FMDecoder2 mDecoder = new P25_C4FMDecoder2(null); + private P25_C4FMDecoder2Instrumented mDecoder = new P25_C4FMDecoder2Instrumented(null); public P25Phase1Pane() { @@ -52,15 +52,14 @@ public class P25Phase1Pane extends DecoderPane private void init() { -// addListener(getSampleLineChart()); addListener(getDecoder()); - getDecoder().setDEBUGFilteredBufferListener(mFilteredBufferBroadcaster); - getDecoder().setDEBUGSymbolListener(getSymbolChart()); - getDecoder().setDEBUGPLLPhaseErrorListener(getPLLPhaseErrorLineChart()); - getDecoder().setDEBUGPLLFrequencyListener(getPLLFrequencyLineChart()); - getDecoder().setDEBUGSymbolDecisionDataListener(getEyeDiagramChart()); - getDecoder().setDEBUGSamplesPerSymbolListener(getSamplesPerSymbolLineChart()); + 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); @@ -77,10 +76,9 @@ public class P25Phase1Pane extends DecoderPane double samplesPerSymbol = sampleRate / 4800.0; getSampleLineChart().setSamplesPerSymbol((int)samplesPerSymbol); -// mSamplesPerSymbolLineChart = new DoubleLineChart( "Samples/Symbol", samplesPerSymbol - 1, samplesPerSymbol + 1, 0.1, 40); } - private P25_C4FMDecoder2 getDecoder() + private P25_C4FMDecoder2Instrumented getDecoder() { return mDecoder; } @@ -167,7 +165,7 @@ public class P25Phase1Pane extends DecoderPane { if(mPLLFrequencyLineChart == null) { - mPLLFrequencyLineChart = new DoubleLineChart( "PLL Frequency", -200, 200, 20, 40); + mPLLFrequencyLineChart = new DoubleLineChart( "PLL Frequency", -500, 500, 50, 40); } return mPLLFrequencyLineChart; diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2.java index 706d6d9b..8d938239 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2.java @@ -20,20 +20,16 @@ import io.github.dsheirer.dsp.filter.FilterFactory; import io.github.dsheirer.dsp.filter.design.FilterDesignException; import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification; import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter; -import io.github.dsheirer.dsp.gain.ComplexFeedForwardGainControl; import io.github.dsheirer.dsp.psk.DQPSKDemodulator; import io.github.dsheirer.dsp.psk.DQPSKSymbolPhaseErrorCalculator; import io.github.dsheirer.dsp.psk.IQPSKSymbolDecoder; import io.github.dsheirer.dsp.psk.ISymbolPhaseErrorCalculator; +import io.github.dsheirer.dsp.psk.InterpolatingSampleBuffer; +import io.github.dsheirer.dsp.psk.InterpolatingSampleBufferInstrumented; import io.github.dsheirer.dsp.psk.QPSKStarSlicer; -import io.github.dsheirer.dsp.psk.QPSKSymbolDecoder; -import io.github.dsheirer.dsp.psk.SymbolDecisionData2; import io.github.dsheirer.dsp.psk.pll.CostasLoop; -import io.github.dsheirer.dsp.psk.pll.Tracking; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.Complex; import io.github.dsheirer.sample.complex.ComplexBuffer; -import io.github.dsheirer.sample.complex.ComplexBufferToStreamConverter; import io.github.dsheirer.sample.complex.IComplexBufferListener; import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent; import org.slf4j.Logger; @@ -46,36 +42,21 @@ import java.util.concurrent.ScheduledExecutorService; public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListener, Listener { private final static Logger mLog = LoggerFactory.getLogger(P25_C4FMDecoder2.class); - private static final double SYMBOL_RATE = 4800; - private double mSampleRate; - + protected static final double SYMBOL_RATE = 4800; + protected ISymbolPhaseErrorCalculator mSymbolPhaseErrorCalculator = new DQPSKSymbolPhaseErrorCalculator(); + protected IQPSKSymbolDecoder mSymbolDecoder = new QPSKStarSlicer(); + protected InterpolatingSampleBuffer mInterpolatingSampleBuffer; + protected DQPSKDemodulator mQPSKDemodulator; + protected CostasLoop mCostasLoop; + protected double mSampleRate; private Map mBasebandFilters = new HashMap<>(); private ComplexFIRFilter mBasebandFilter; - private Map mSymbolFilters = new HashMap<>(); - private ComplexFIRFilter mSymbolFilter; - - private DQPSKDemodulator mQPSKDemodulator; - private CostasLoop mCostasLoop; - private ComplexBufferToStreamConverter mStreamConverter = new ComplexBufferToStreamConverter(); - private ComplexFeedForwardGainControl mAGC = new ComplexFeedForwardGainControl(32); - private QPSKSymbolDecoder mQPSKSlicer = new QPSKSymbolDecoder(); private P25MessageFramer mMessageFramer; - private ISymbolPhaseErrorCalculator mSymbolPhaseErrorCalculator = new DQPSKSymbolPhaseErrorCalculator(); - private IQPSKSymbolDecoder mSymbolDecoder = new QPSKStarSlicer(); - private Listener mDEBUGSymbolListener; - private Listener mDEBUGPLLPhaseErrorListener; - private Listener mDEBUGPLLFrequencyListener; - private Listener mDEBUGSymbolDecisionDataListener; - private Listener mDEBUGSamplesPerSymbolListener; - private Listener mFilteredSymbolListener; public P25_C4FMDecoder2(AliasList aliasList) { super(aliasList); - - setSampleRate(48600.0); - - mStreamConverter.setListener(mAGC); + setSampleRate(48000.0); } public void setSampleRate(double sampleRate) @@ -87,19 +68,12 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen mSampleRate = sampleRate; mBasebandFilter = new ComplexFIRFilter(getBasebandFilter(), 1.0f); - mSymbolFilter = new ComplexFIRFilter(getSymbolFilter(), 1.0f); mCostasLoop = new CostasLoop(mSampleRate, SYMBOL_RATE); - mCostasLoop.setTracking(Tracking.FINE); - mCostasLoop.setAutomaticTracking(false); + mInterpolatingSampleBuffer = new InterpolatingSampleBufferInstrumented((float)(sampleRate / SYMBOL_RATE)); mQPSKDemodulator = new DQPSKDemodulator(mCostasLoop, mSymbolPhaseErrorCalculator, mSymbolDecoder, - mSampleRate, SYMBOL_RATE); - mQPSKDemodulator.setSymbolListener(mDEBUGSymbolListener); - mQPSKDemodulator.setPLLErrorListener(mDEBUGPLLPhaseErrorListener); - mQPSKDemodulator.setPLLFrequencyListener(mDEBUGPLLFrequencyListener); - mQPSKDemodulator.setSymbolDecisionDataListener(mDEBUGSymbolDecisionDataListener); - mQPSKDemodulator.setSamplesPerSymbolListener(mDEBUGSamplesPerSymbolListener); + mInterpolatingSampleBuffer); //Message framer can trigger a symbol-inversion correction to the PLL when detected mMessageFramer = new P25MessageFramer(getAliasList(), mCostasLoop); @@ -107,54 +81,16 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen mQPSKDemodulator.setDibitListener(mMessageFramer); } - public void setDEBUGSymbolListener(Listener listener) - { - mDEBUGSymbolListener = listener; - mQPSKDemodulator.setSymbolListener(listener); - } - - public void setDEBUGPLLPhaseErrorListener(Listener listener) - { - mDEBUGPLLPhaseErrorListener = listener; - mQPSKDemodulator.setPLLErrorListener(listener); - } - - public void setDEBUGPLLFrequencyListener(Listener listener) - { - mDEBUGPLLFrequencyListener = listener; - mQPSKDemodulator.setPLLFrequencyListener(listener); - } - - public void setDEBUGFilteredBufferListener(Listener listener) - { - mFilteredSymbolListener = listener; - } - - public void setDEBUGSymbolDecisionDataListener(Listener listener) - { - mDEBUGSymbolDecisionDataListener = listener; - mQPSKDemodulator.setSymbolDecisionDataListener(listener); - } - - public void setDEBUGSamplesPerSymbolListener(Listener listener) - { - mDEBUGSamplesPerSymbolListener = listener; - mQPSKDemodulator.setSamplesPerSymbolListener(listener); - } - @Override public void receive(ComplexBuffer complexBuffer) { - ComplexBuffer basebandFiltered = mBasebandFilter.filter(complexBuffer); -// ComplexBuffer symbolFiltered = mSymbolFilter.filter(complexBuffer); -// ComplexBuffer symbolFiltered = complexBuffer; - ComplexBuffer symbolFiltered = basebandFiltered; - mQPSKDemodulator.receive(symbolFiltered); + ComplexBuffer filtered = filter(complexBuffer); + mQPSKDemodulator.receive(filtered); + } - if(mFilteredSymbolListener != null) - { - mFilteredSymbolListener.receive(symbolFiltered); - } + protected ComplexBuffer filter(ComplexBuffer complexBuffer) + { + return mBasebandFilter.filter(complexBuffer); } private double getSampleRate() @@ -170,11 +106,11 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen { FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() .sampleRate((int)getSampleRate()) - .passBandCutoff(6000) + .passBandCutoff(5100) .passBandAmplitude(1.0) .passBandRipple(0.01) .stopBandAmplitude(0.0) - .stopBandStart(6250) + .stopBandStart(6500) .stopBandRipple(0.01) .build(); @@ -200,20 +136,6 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen return filter; } - private float[] getSymbolFilter() - { - float[] filter = mSymbolFilters.get(getSampleRate()); - - if(filter == null) - { - float samplesPerSymbol = (float)(getSampleRate() / 4800.0); - filter = FilterFactory.getRootRaisedCosine(samplesPerSymbol, 40, 0.2f); - mSymbolFilters.put(getSampleRate(), filter); - } - - return filter; - } - public void dispose() { super.dispose(); @@ -221,15 +143,6 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen mBasebandFilter.dispose(); mBasebandFilter = null; - mStreamConverter.dispose(); - mStreamConverter = null; - - mAGC.dispose(); - mAGC = null; - - mQPSKSlicer.dispose(); - mQPSKSlicer = null; - mMessageFramer.dispose(); mMessageFramer = null; } @@ -252,7 +165,15 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen @Override public void receive(FrequencyChangeEvent frequencyChangeEvent) { - //Ignored + 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; + } } }; } @@ -271,20 +192,16 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen @Override public void reset() { - // TODO Auto-generated method stub - + mCostasLoop.reset(); } @Override public void start(ScheduledExecutorService executor) { - // TODO Auto-generated method stub - } @Override public void stop() { - // TODO Auto-generated method stub } } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2Instrumented.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2Instrumented.java new file mode 100644 index 00000000..e5b93aa2 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2Instrumented.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_C4FMDecoder2Instrumented extends P25_C4FMDecoder2 +{ + private Listener mPLLPhaseErrorListener; + private Listener mPLLFrequencyListener; + private Listener mSamplesPerSymbolListener; + private Listener mSymbolListener; + private Listener mFilteredSymbolListener; + private Listener mSymbolDecisionDataListener; + + public P25_C4FMDecoder2Instrumented(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, + mSymbolPhaseErrorCalculator, mSymbolDecoder, instrumentedBuffer, mSampleRate); + mQPSKDemodulator = instrumented; + + instrumented.setSymbolListener(mSymbolListener); + instrumented.setPLLErrorListener(mPLLPhaseErrorListener); + instrumented.setPLLFrequencyListener(mPLLFrequencyListener); + instrumented.setSymbolDecisionDataListener(mSymbolDecisionDataListener); + instrumented.setSamplesPerSymbolListener(mSamplesPerSymbolListener); + } + + 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/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 { From b563f2d11b5e9505461a17a53e529d1041a2c16b Mon Sep 17 00:00:00 2001 From: Denny Date: Tue, 27 Feb 2018 05:57:48 -0500 Subject: [PATCH 17/23] 330 WIP - created new symbol evaluator class that combines phase and timing error calculation with symbol decision. In the process of changing over to this new class and remove the previous implementations for the 3 values. This commit includes the partial implementation so I can access from another dev machine. --- .../dsheirer/dsp/psk/DQPSKDemodulator.java | 15 ++- .../dsp/psk/DQPSKSymbolEvaluator.java | 125 ++++++++++++++++++ .../dsp/psk/IDQPSKSymbolEvaluator.java | 48 +++++++ 3 files changed, 183 insertions(+), 5 deletions(-) create mode 100644 src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolEvaluator.java create mode 100644 src/main/java/io/github/dsheirer/dsp/psk/IDQPSKSymbolEvaluator.java diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulator.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulator.java index 50326b13..2dfd2e21 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulator.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulator.java @@ -29,9 +29,10 @@ public class DQPSKDemodulator implements ComplexSampleListener private final static Logger mLog = LoggerFactory.getLogger(DQPSKDemodulator.class); protected InterpolatingSampleBuffer mInterpolatingSampleBuffer; protected IPhaseLockedLoop mPLL; - protected IQPSKSymbolDecoder mDQPSKSymbolDecoder; + protected IQPSKSymbolDecoder mSymbolDecoder; private ISymbolPhaseErrorCalculator mSymbolPhaseErrorCalculator; - private DQPSKTimingErrorDetector mDQPSKTimingErrorDetector = new DQPSKTimingErrorDetector(); + private DQPSKTimingErrorDetector mSymbolTimingErrorDetector = new DQPSKTimingErrorDetector(); + private DQPSKSymbolEvaluator mSymbolEvaluator = new DQPSKSymbolEvaluator(); private Complex mReceivedSample = new Complex(0, 0); private Complex mPreviousPrecedingSample = new Complex(0, 0); @@ -61,7 +62,7 @@ public class DQPSKDemodulator implements ComplexSampleListener { mPLL = phaseLockedLoop; mSymbolPhaseErrorCalculator = symbolPhaseErrorCalculator; - mDQPSKSymbolDecoder = symbolDecoder; + mSymbolDecoder = symbolDecoder; mInterpolatingSampleBuffer = interpolatingSampleBuffer; } @@ -127,8 +128,10 @@ public class DQPSKDemodulator implements ComplexSampleListener mPrecedingSymbol.normalize(); mCurrentSymbol.normalize(); + mSymbolEvaluator.setSymbol(mCurrentSymbol); + //Symbol timing error calculation - mSymbolTimingError = mDQPSKTimingErrorDetector.getError(mPrecedingSymbol, mCurrentSymbol); + mSymbolTimingError = mSymbolTimingErrorDetector.getError(mPrecedingSymbol, mCurrentSymbol); mSymbolTimingError = clip(mSymbolTimingError, 0.5f); mInterpolatingSampleBuffer.resetAndAdjust(mSymbolTimingError); @@ -141,11 +144,13 @@ public class DQPSKDemodulator implements ComplexSampleListener //feedback to the PLL mPhaseError = mSymbolPhaseErrorCalculator.getPhaseError(mCurrentSymbol); + mLog.debug("Phase error - ORIG:" + mPhaseError + " NEW:" + mSymbolEvaluator.getPhaseError()); + mPLL.adjust(mPhaseError); if(mDibitListener != null) { - mDibitListener.receive(mDQPSKSymbolDecoder.decode(mCurrentSymbol)); + mDibitListener.receive(mSymbolDecoder.decode(mCurrentSymbol)); } } 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..3ae2d214 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolEvaluator.java @@ -0,0 +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.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 mTimingErrorSign = 1.0f; + private Dibit mSymbolDecision = Dibit.D00_PLUS_1; + + public DQPSKSymbolEvaluator() + { + } + + /** + * Sets the symbol to be evaluated and calculates the symbol decision and phase and timing errors of the symbol + * relative to the four QPSK reference symbol locations. 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 determining the symbol's quadrant and multiplying the symbol + * by the complex conjugate of the reference symbol for that quadrant and then deriving the radian angle error + * value. Both phase and timing error use this angle error, however the timing error is corrected with the + * appropriate sign so that the error value indicates the correct error direction. + * + * @param complex symbol to be evaluated + */ + @Override + public void setSymbol(Complex complex) + { + mSymbolError.setValues(complex); + + if(mSymbolError.quadrature() > 0.0f) + { + if(mSymbolError.inphase() > 0.0f) + { + mSymbolDecision = Dibit.D00_PLUS_1; + mSymbolError.multiply(ROTATE_FROM_PLUS_45); + } + else + { + mSymbolDecision = Dibit.D01_PLUS_3; + mSymbolError.multiply(ROTATE_FROM_PLUS_135); + } + + mTimingErrorSign = 1.0f; + } + else + { + if(mSymbolError.inphase() > 0.0f) + { + mSymbolDecision = Dibit.D10_MINUS_1; + mSymbolError.multiply(ROTATE_FROM_MINUS_45); + } + else + { + mSymbolDecision = Dibit.D11_MINUS_3; + mSymbolError.multiply(ROTATE_FROM_MINUS_135); + } + + mTimingErrorSign = -1.0f; + } + + //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 symbol's hemisphere + mTimingError = mPhaseError * mTimingErrorSign; + } + + /** + * 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/IDQPSKSymbolEvaluator.java b/src/main/java/io/github/dsheirer/dsp/psk/IDQPSKSymbolEvaluator.java new file mode 100644 index 00000000..da650c53 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/IDQPSKSymbolEvaluator.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.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 complex symbol + */ + void setSymbol(Complex complex); + + /** + * 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(); +} From 45a6c5a0b61bf9da115d212bf43725046df8afca Mon Sep 17 00:00:00 2001 From: Denny Date: Sun, 4 Mar 2018 11:42:23 -0500 Subject: [PATCH 18/23] 330 WIP - implements the new data-directed symbol timing and phase error calculator. After increasing the gain for symbol timing, this new symbol evaluator class seems to track the signal very accurately despite lots of noise and ISI. --- .../dsheirer/dsp/psk/DQPSKDemodulator.java | 33 +++---------- .../dsp/psk/DQPSKDemodulatorInstrumented.java | 9 ++-- .../dsp/psk/DQPSKSymbolEvaluator.java | 46 ++++++++++++------- .../dsp/psk/IDQPSKSymbolEvaluator.java | 6 ++- .../dsp/psk/InterpolatingSampleBuffer.java | 8 ++-- .../gui/viewer/decoder/P25Phase1Pane.java | 2 +- .../module/decode/p25/P25_C4FMDecoder2.java | 10 +--- .../p25/P25_C4FMDecoder2Instrumented.java | 3 +- .../dsheirer/sample/complex/Complex.java | 17 +++---- 9 files changed, 58 insertions(+), 76 deletions(-) diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulator.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulator.java index 2dfd2e21..f8b6aabd 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulator.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulator.java @@ -29,11 +29,7 @@ public class DQPSKDemodulator implements ComplexSampleListener private final static Logger mLog = LoggerFactory.getLogger(DQPSKDemodulator.class); protected InterpolatingSampleBuffer mInterpolatingSampleBuffer; protected IPhaseLockedLoop mPLL; - protected IQPSKSymbolDecoder mSymbolDecoder; - private ISymbolPhaseErrorCalculator mSymbolPhaseErrorCalculator; - private DQPSKTimingErrorDetector mSymbolTimingErrorDetector = new DQPSKTimingErrorDetector(); - private DQPSKSymbolEvaluator mSymbolEvaluator = new DQPSKSymbolEvaluator(); - + 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); @@ -41,9 +37,6 @@ public class DQPSKDemodulator implements ComplexSampleListener private Complex mCurrentSample = new Complex(0, 0); private Complex mPrecedingSymbol = new Complex(0, 0); protected Complex mCurrentSymbol = new Complex(0, 0); - - protected float mPhaseError; - private float mSymbolTimingError; private Listener mDibitListener; /** @@ -54,15 +47,11 @@ public class DQPSKDemodulator implements ComplexSampleListener * This detector is optimized for constant amplitude DQPSK symbols like C4FM. * * @param phaseLockedLoop for tracking carrier frequency error - * @param symbolPhaseErrorCalculator to calculate symbol phase errors to apply against the PLL - * @param symbolDecoder to decode demodulated symbols into dibits + * @param interpolatingSampleBuffer to hold samples for interpolating a symbol */ - public DQPSKDemodulator(IPhaseLockedLoop phaseLockedLoop, ISymbolPhaseErrorCalculator symbolPhaseErrorCalculator, - IQPSKSymbolDecoder symbolDecoder, InterpolatingSampleBuffer interpolatingSampleBuffer) + public DQPSKDemodulator(IPhaseLockedLoop phaseLockedLoop, InterpolatingSampleBuffer interpolatingSampleBuffer) { mPLL = phaseLockedLoop; - mSymbolPhaseErrorCalculator = symbolPhaseErrorCalculator; - mSymbolDecoder = symbolDecoder; mInterpolatingSampleBuffer = interpolatingSampleBuffer; } @@ -128,13 +117,9 @@ public class DQPSKDemodulator implements ComplexSampleListener mPrecedingSymbol.normalize(); mCurrentSymbol.normalize(); - mSymbolEvaluator.setSymbol(mCurrentSymbol); + mSymbolEvaluator.setSymbol(mPrecedingSymbol, mCurrentSymbol); - //Symbol timing error calculation - mSymbolTimingError = mSymbolTimingErrorDetector.getError(mPrecedingSymbol, mCurrentSymbol); - - mSymbolTimingError = clip(mSymbolTimingError, 0.5f); - mInterpolatingSampleBuffer.resetAndAdjust(mSymbolTimingError); + mInterpolatingSampleBuffer.resetAndAdjust(mSymbolEvaluator.getTimingError()); //Store current samples/symbols to use for the next period mPreviousPrecedingSample.setValues(mPrecedingSample); @@ -142,15 +127,11 @@ public class DQPSKDemodulator implements ComplexSampleListener //Calculate the phase error of the current symbol relative to the expected constellation and provide //feedback to the PLL - mPhaseError = mSymbolPhaseErrorCalculator.getPhaseError(mCurrentSymbol); - - mLog.debug("Phase error - ORIG:" + mPhaseError + " NEW:" + mSymbolEvaluator.getPhaseError()); - - mPLL.adjust(mPhaseError); + mPLL.adjust(clip(mSymbolEvaluator.getPhaseError(), 0.5f)); if(mDibitListener != null) { - mDibitListener.receive(mSymbolDecoder.decode(mCurrentSymbol)); + mDibitListener.receive(mSymbolEvaluator.getSymbolDecision()); } } diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulatorInstrumented.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulatorInstrumented.java index 3ca084d0..cc90a742 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulatorInstrumented.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulatorInstrumented.java @@ -37,17 +37,14 @@ public class DQPSKDemodulatorInstrumented extends DQPSKDemodulator * This detector is optimized for constant amplitude DQPSK symbols like C4FM. * * @param phaseLockedLoop for tracking carrier frequency error - * @param symbolPhaseErrorCalculator to calculate symbol phase errors to apply against the PLL - * @param symbolDecoder to decode demodulated symbols into dibits * @param interpolatingSampleBuffer * @param sampleRate of the incoming complex sample stream */ - public DQPSKDemodulatorInstrumented(IPhaseLockedLoop phaseLockedLoop, ISymbolPhaseErrorCalculator - symbolPhaseErrorCalculator, IQPSKSymbolDecoder symbolDecoder, + public DQPSKDemodulatorInstrumented(IPhaseLockedLoop phaseLockedLoop, InterpolatingSampleBufferInstrumented interpolatingSampleBuffer, double sampleRate) { - super(phaseLockedLoop, symbolPhaseErrorCalculator, symbolDecoder, interpolatingSampleBuffer); + super(phaseLockedLoop, interpolatingSampleBuffer); mSampleRate = sampleRate; } @@ -78,7 +75,7 @@ public class DQPSKDemodulatorInstrumented extends DQPSKDemodulator if(mPLLErrorListener != null) { - mPLLErrorListener.receive((double)mPhaseError); + mPLLErrorListener.receive((double)mSymbolEvaluator.getPhaseError()); } if(mPLLFrequencyListener != null) diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolEvaluator.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolEvaluator.java index 3ae2d214..16b9018d 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolEvaluator.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolEvaluator.java @@ -25,74 +25,86 @@ public class DQPSKSymbolEvaluator implements IDQPSKSymbolEvaluator 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 Complex mSymbolError = new Complex(0, 0); private float mPhaseError = 0.0f; private float mTimingError = 0.0f; - private float mTimingErrorSign = 1.0f; + private float mTimingErrorPolarity = 1.0f; private Dibit mSymbolDecision = Dibit.D00_PLUS_1; + /** + * Data-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 symbol to be evaluated and calculates the symbol decision and phase and timing errors of the symbol - * relative to the four QPSK reference symbol locations. After invoking this method, you can access the phase and - * timing errors and the symbol decision via their respective accessor methods. + * 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 determining the symbol's quadrant and multiplying the symbol - * by the complex conjugate of the reference symbol for that quadrant and then deriving the radian angle error - * value. Both phase and timing error use this angle error, however the timing error is corrected with the - * appropriate sign so that the error value indicates the correct error direction. + * 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 complex symbol to be evaluated + * @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 complex) + public void setSymbol(Complex preceding, Complex current) { - mSymbolError.setValues(complex); + 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); } - mTimingErrorSign = 1.0f; } 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); } - - mTimingErrorSign = -1.0f; } //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 symbol's hemisphere - mTimingError = mPhaseError * mTimingErrorSign; + //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 diff --git a/src/main/java/io/github/dsheirer/dsp/psk/IDQPSKSymbolEvaluator.java b/src/main/java/io/github/dsheirer/dsp/psk/IDQPSKSymbolEvaluator.java index da650c53..d0213a0d 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/IDQPSKSymbolEvaluator.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/IDQPSKSymbolEvaluator.java @@ -23,9 +23,11 @@ public interface IDQPSKSymbolEvaluator { /** * Sets the sampled symbol to be evaluated. Note: the symbol's gain value MUST be normalized to the unit circle. - * @param complex symbol + * + * @param preceding symbol used to detect (counter-)clock-wise rotation + * @param symbol currently being sampled */ - void setSymbol(Complex complex); + void setSymbol(Complex preceding, Complex symbol); /** * Phase error of the symbol relative to the closest reference 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 index bc3cf830..15adbd9a 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBuffer.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBuffer.java @@ -31,7 +31,7 @@ public class InterpolatingSampleBuffer private int mTwiceSamplesPerSymbol; private float mSamplingPoint; - private float mSampleCounterGain = 0.05f; + private float mSampleCounterGain = 1.0f; private float mDetectedSamplesPerSymbol; private float mDetectedSamplesPerSymbolGain = 0.1f * mSampleCounterGain * mSampleCounterGain; private float mMaximumSamplesPerSymbol; @@ -130,7 +130,7 @@ public class InterpolatingSampleBuffer */ public Complex getPrecedingSample() { - mPrecedingSample.setValues(mDelayLineInphase[mDelayLinePointer + 3], mDelayLineQuadrature[mDelayLinePointer + 3]); + mPrecedingSample.setValues(mDelayLineInphase[mDelayLinePointer + 4], mDelayLineQuadrature[mDelayLinePointer + 3]); return mPrecedingSample; } @@ -152,7 +152,7 @@ public class InterpolatingSampleBuffer */ public float getInphase(float interpolation) { - return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer, interpolation); + return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer + 1, interpolation); } /** @@ -162,6 +162,6 @@ public class InterpolatingSampleBuffer */ public float getQuadrature(float interpolation) { - return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer, interpolation); + return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer + 1, interpolation); } } 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 index 4bcee01e..42d9e813 100644 --- 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 @@ -175,7 +175,7 @@ public class P25Phase1Pane extends DecoderPane { if(mSamplesPerSymbolLineChart == null) { - mSamplesPerSymbolLineChart = new DoubleLineChart( "Sample Point", 9.0, 11.0, 0.1, 40); + 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/p25/P25_C4FMDecoder2.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2.java index 8d938239..7ae55d67 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2.java @@ -21,12 +21,8 @@ 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.DQPSKSymbolPhaseErrorCalculator; -import io.github.dsheirer.dsp.psk.IQPSKSymbolDecoder; -import io.github.dsheirer.dsp.psk.ISymbolPhaseErrorCalculator; import io.github.dsheirer.dsp.psk.InterpolatingSampleBuffer; import io.github.dsheirer.dsp.psk.InterpolatingSampleBufferInstrumented; -import io.github.dsheirer.dsp.psk.QPSKStarSlicer; import io.github.dsheirer.dsp.psk.pll.CostasLoop; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.complex.ComplexBuffer; @@ -43,8 +39,6 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen { private final static Logger mLog = LoggerFactory.getLogger(P25_C4FMDecoder2.class); protected static final double SYMBOL_RATE = 4800; - protected ISymbolPhaseErrorCalculator mSymbolPhaseErrorCalculator = new DQPSKSymbolPhaseErrorCalculator(); - protected IQPSKSymbolDecoder mSymbolDecoder = new QPSKStarSlicer(); protected InterpolatingSampleBuffer mInterpolatingSampleBuffer; protected DQPSKDemodulator mQPSKDemodulator; protected CostasLoop mCostasLoop; @@ -72,8 +66,7 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen mCostasLoop = new CostasLoop(mSampleRate, SYMBOL_RATE); mInterpolatingSampleBuffer = new InterpolatingSampleBufferInstrumented((float)(sampleRate / SYMBOL_RATE)); - mQPSKDemodulator = new DQPSKDemodulator(mCostasLoop, mSymbolPhaseErrorCalculator, mSymbolDecoder, - mInterpolatingSampleBuffer); + mQPSKDemodulator = new DQPSKDemodulator(mCostasLoop, mInterpolatingSampleBuffer); //Message framer can trigger a symbol-inversion correction to the PLL when detected mMessageFramer = new P25MessageFramer(getAliasList(), mCostasLoop); @@ -91,6 +84,7 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen protected ComplexBuffer filter(ComplexBuffer complexBuffer) { return mBasebandFilter.filter(complexBuffer); +// return complexBuffer; } private double getSampleRate() diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2Instrumented.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2Instrumented.java index e5b93aa2..48b28e15 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2Instrumented.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2Instrumented.java @@ -61,8 +61,7 @@ public class P25_C4FMDecoder2Instrumented extends P25_C4FMDecoder2 InterpolatingSampleBufferInstrumented instrumentedBuffer = new InterpolatingSampleBufferInstrumented((float)(sampleRate / SYMBOL_RATE)); mInterpolatingSampleBuffer = instrumentedBuffer; - DQPSKDemodulatorInstrumented instrumented = new DQPSKDemodulatorInstrumented(mCostasLoop, - mSymbolPhaseErrorCalculator, mSymbolDecoder, instrumentedBuffer, mSampleRate); + DQPSKDemodulatorInstrumented instrumented = new DQPSKDemodulatorInstrumented(mCostasLoop, instrumentedBuffer, mSampleRate); mQPSKDemodulator = instrumented; instrumented.setSymbolListener(mSymbolListener); diff --git a/src/main/java/io/github/dsheirer/sample/complex/Complex.java b/src/main/java/io/github/dsheirer/sample/complex/Complex.java index 8b98e9e8..93c15627 100644 --- a/src/main/java/io/github/dsheirer/sample/complex/Complex.java +++ b/src/main/java/io/github/dsheirer/sample/complex/Complex.java @@ -437,17 +437,14 @@ public class Complex implements Serializable public static void main(String[] args) { - double angle = Math.PI; //180 degrees + 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 s1 = new Complex((float)Math.sin(angle), (float)Math.cos(angle)); - - Complex tap = new Complex(1.0f, -0.1f); - - Complex convolved = Complex.multiply(s1, tap); - - mLog.debug("s: " + s1.toString() + - " t: " + tap.toString() + - " convolved: " + convolved.toString()); + Complex symbol = new Complex(0.7665009f, -0.6422432f); + mLog.debug("SYM: " + symbol.toString()); + symbol.multiply(ROTATE_FROM_MINUS_45); + mLog.debug("DER: " + symbol.toString()); } } From 5af4cb87062bc0a607116f4c13464a88acef1526 Mon Sep 17 00:00:00 2001 From: Denny Date: Sun, 4 Mar 2018 12:07:29 -0500 Subject: [PATCH 19/23] 330-WIP - adds the message decoder to see the P25 messages being decoded as we step through the recording. --- .../instrument/gui/viewer/decoder/P25Phase1Pane.java | 12 ++++++++++++ .../dsheirer/module/decode/p25/P25_C4FMDecoder2.java | 3 +-- .../decode/p25/P25_C4FMDecoder2Instrumented.java | 1 + 3 files changed, 14 insertions(+), 2 deletions(-) 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 index 42d9e813..1b3f31d9 100644 --- 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 @@ -20,9 +20,11 @@ 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_C4FMDecoder2Instrumented; 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; @@ -65,6 +67,16 @@ public class P25Phase1Pane extends DecoderPane 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 diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2.java index 7ae55d67..dbd56609 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2.java @@ -42,10 +42,10 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen protected InterpolatingSampleBuffer mInterpolatingSampleBuffer; protected DQPSKDemodulator mQPSKDemodulator; protected CostasLoop mCostasLoop; + protected P25MessageFramer mMessageFramer; protected double mSampleRate; private Map mBasebandFilters = new HashMap<>(); private ComplexFIRFilter mBasebandFilter; - private P25MessageFramer mMessageFramer; public P25_C4FMDecoder2(AliasList aliasList) { @@ -84,7 +84,6 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen protected ComplexBuffer filter(ComplexBuffer complexBuffer) { return mBasebandFilter.filter(complexBuffer); -// return complexBuffer; } private double getSampleRate() diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2Instrumented.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2Instrumented.java index 48b28e15..4877261f 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2Instrumented.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2Instrumented.java @@ -69,6 +69,7 @@ public class P25_C4FMDecoder2Instrumented extends P25_C4FMDecoder2 instrumented.setPLLFrequencyListener(mPLLFrequencyListener); instrumented.setSymbolDecisionDataListener(mSymbolDecisionDataListener); instrumented.setSamplesPerSymbolListener(mSamplesPerSymbolListener); + instrumented.setDibitListener(mMessageFramer); } public void setSymbolListener(Listener listener) From 646bd780372e95ecdd59005584badc1f7459b101 Mon Sep 17 00:00:00 2001 From: Denny Date: Sun, 4 Mar 2018 20:41:43 -0500 Subject: [PATCH 20/23] 330-WIP - adds the message decoder to see the P25 messages being decoded as we step through the recording. --- .../instrument/gui/viewer/chart/ComplexSampleLineChart.java | 2 +- .../dsheirer/instrument/gui/viewer/chart/EyeDiagramChart.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 index 2bf0fe8c..d2b25b4b 100644 --- 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 @@ -45,7 +45,7 @@ public class ComplexSampleLineChart extends LineChart implements Listener iSampleSeries = new LineChart.Series<>("Inphase", mISamples); 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 index 8c9db35d..5764cc8e 100644 --- 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 @@ -36,7 +36,7 @@ public class EyeDiagramChart extends LineChart implements Listener Date: Sat, 17 Mar 2018 09:45:23 -0400 Subject: [PATCH 21/23] 330 - updates P25 C4FM decoder to use the new DQPSK demodulator and removes the previous FM demodulator based decoder. --- .../dsp/psk/DQPSKSymbolEvaluator.java | 2 +- .../psk/DQPSKSymbolPhaseErrorCalculator.java | 37 --- .../dsp/psk/DQPSKTimingErrorDetector.java | 89 ------ .../gui/viewer/decoder/P25Phase1Pane.java | 6 +- .../module/decode/DecoderFactory.java | 4 +- .../module/decode/p25/P25MessageFramer.java | 3 +- .../module/decode/p25/P25_C4FMDecoder.java | 297 ++++++++++-------- .../module/decode/p25/P25_C4FMDecoder2.java | 200 ------------ ....java => P25_C4FMDecoderInstrumented.java} | 4 +- .../source/tuner/TunerChannelSource.java | 1 + 10 files changed, 181 insertions(+), 462 deletions(-) delete mode 100644 src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolPhaseErrorCalculator.java delete mode 100644 src/main/java/io/github/dsheirer/dsp/psk/DQPSKTimingErrorDetector.java delete mode 100644 src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2.java rename src/main/java/io/github/dsheirer/module/decode/p25/{P25_C4FMDecoder2Instrumented.java => P25_C4FMDecoderInstrumented.java} (97%) diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolEvaluator.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolEvaluator.java index 16b9018d..cab0fd22 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolEvaluator.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolEvaluator.java @@ -32,7 +32,7 @@ public class DQPSKSymbolEvaluator implements IDQPSKSymbolEvaluator private Dibit mSymbolDecision = Dibit.D00_PLUS_1; /** - * Data-directed symbol phase and timing error detector and symbol decision slicer. + * 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. * diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolPhaseErrorCalculator.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolPhaseErrorCalculator.java deleted file mode 100644 index 7eafb69a..00000000 --- a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolPhaseErrorCalculator.java +++ /dev/null @@ -1,37 +0,0 @@ -/******************************************************************************* - * 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 DQPSKSymbolPhaseErrorCalculator extends QPSKSymbolPhaseErrorCalculator -{ - //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 Complex mSymbol = new Complex(0, 0); - - public DQPSKSymbolPhaseErrorCalculator() - { - } - - @Override - public float getPhaseError(Complex symbol) - { - mSymbol.setValues(symbol); - mSymbol.multiply(DIFFERENTIAL_OFFSET); - return super.getPhaseError(mSymbol); - } -} diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKTimingErrorDetector.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKTimingErrorDetector.java deleted file mode 100644 index 929e2b06..00000000 --- a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKTimingErrorDetector.java +++ /dev/null @@ -1,89 +0,0 @@ -/******************************************************************************* - * 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 DQPSKTimingErrorDetector -{ - private static final float PI_4_ROTATION = (float)(Math.PI / 4.0); - - /** - * Custom early/late symbol timing error detector designed to optimize the sample timing point such that the - * current symbol's inphase and quadrature approach the optimal 45 degree (.707) sampling instant. - */ - public DQPSKTimingErrorDetector() - { - } - - /** - * Calculates a symbol sampling time error by comparing the current complex sample against a normal QPSK or - * Pi/4 rotated four symbol constellation. The error signal (advance/retard) is determined by the slope - * of each of the inphase and quadrature legs as detected by comparing an immediately preceding sample against - * the current sample and then calculating the error distance of the sampled symbol from the ideal Pi/4 sampling - * location. - * - * Note: both the preceding and the current symbols MUST be normalized (ie symbol.normalize()) to the unit circle - * prior to invoking this method. - * - * @param precedingSymbol - * @param currentSymbol - * @return timing error signal used to advance (positive) or retard (negative) the sampling point - */ - public float getError(Complex precedingSymbol, Complex currentSymbol) - { - return getError(precedingSymbol.inphase(), currentSymbol.inphase()) + - getError(precedingSymbol.quadrature(), currentSymbol.quadrature()); - } - - /** - * Calculates timing error of current value relative to an ideal 45 degree or Pi/4 radians (.707) value. - * - * @param early sample that closely precedes the current sampling instant - used to determine slope - * @param current sampling value - * @return error as the distance of the current value from an ideal value and the sign (advance/retard) - * appropriate for the detected slope - */ - private float getError(float early, float current) - { - if(early < current) //Ascending slope - { - if(current > 0.0) - { - //Positive quadrature hemisphere, ascending slope - return PI_4_ROTATION - current; - } - else - { - //Negative quadrature hemisphere, ascending slope - return -PI_4_ROTATION - current; - } - } - else //Descending slope - { - if(current > 0.0) - { - //Positive quadrature hemisphere, descending slope - return current - PI_4_ROTATION; - } - else - { - //Negative quadrature hemisphere, descending slope - return current + PI_4_ROTATION; - } - } - } -} 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 index 1b3f31d9..526d3e7f 100644 --- 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 @@ -22,7 +22,7 @@ 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_C4FMDecoder2Instrumented; +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; @@ -44,7 +44,7 @@ public class P25Phase1Pane extends DecoderPane private DoubleLineChart mPLLFrequencyLineChart; private DoubleLineChart mSamplesPerSymbolLineChart; private Broadcaster mFilteredBufferBroadcaster = new Broadcaster<>(); - private P25_C4FMDecoder2Instrumented mDecoder = new P25_C4FMDecoder2Instrumented(null); + private P25_C4FMDecoderInstrumented mDecoder = new P25_C4FMDecoderInstrumented(null); public P25Phase1Pane() { @@ -90,7 +90,7 @@ public class P25Phase1Pane extends DecoderPane getSampleLineChart().setSamplesPerSymbol((int)samplesPerSymbol); } - private P25_C4FMDecoder2Instrumented getDecoder() + private P25_C4FMDecoderInstrumented getDecoder() { return mDecoder; } 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 29dbcf5b..94ec4ae7 100644 --- a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java @@ -246,9 +246,7 @@ 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 P25_C4FMDecoder(aliasList)); modules.add(new P25DecoderState(aliasList, channelType, P25Decoder.Modulation.C4FM, p25Config.getIgnoreDataCalls())); break; 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 4d38ed79..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 @@ -698,8 +698,7 @@ public class P25MessageFramer implements Listener @Override public void syncDetected() { - mLog.warn("Phase Inversion SYNC detected - applying correction: " + mCorrection); -// mPhaseLockedLoop.correctInversion(mCorrection); + mPhaseLockedLoop.correctInversion(mCorrection); /* Since we detected a sync pattern, start a message assembler */ for(P25MessageAssembler assembler : mAssemblers) 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 c51a7adf..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,153 +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.TapGroup; +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.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.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 ); + public P25_C4FMDecoder(AliasList aliasList) + { + super(aliasList); + setSampleRate(48000.0); + } - /* Convert samples to symbols */ - mC4FMSlicer = new C4FMSlicer(); - mSymbolFilter.setListener( mC4FMSlicer ); + public void setSampleRate(double sampleRate) + { + if(sampleRate <= SYMBOL_RATE * 2) + { + throw new IllegalArgumentException("Sample rate must be at least twice the symbol rate [4800]"); + } - /* Sync pattern detection and message construction */ - mMessageFramer = new P25MessageFramer( aliasList ); - mC4FMSlicer.addListener( mMessageFramer ); - - /* Process and broadcast messages */ - mMessageFramer.setListener( getMessageProcessor() ); - } + mSampleRate = sampleRate; + mBasebandFilter = new ComplexFIRFilter(getBasebandFilter(), 1.0f); - @Override - public void dispose() - { - super.dispose(); - - mSymbolFilter.dispose(); - mSymbolFilter = null; - - mMessageFramer.dispose(); - mMessageFramer = null; + mCostasLoop = new CostasLoop(mSampleRate, SYMBOL_RATE); - mC4FMSlicer.dispose(); - mC4FMSlicer = null; - } - - public Modulation getModulation() - { - return Modulation.C4FM; - } - - @Override - public Listener getFilteredRealBufferListener() - { - return mSymbolFilter; - } - - @Override - public void reset() - { - // TODO Auto-generated method stub - - } + mInterpolatingSampleBuffer = new InterpolatingSampleBufferInstrumented((float)(sampleRate / SYMBOL_RATE)); + mQPSKDemodulator = new DQPSKDemodulator(mCostasLoop, mInterpolatingSampleBuffer); - @Override - public void start( ScheduledExecutorService executor ) - { - // TODO Auto-generated method stub - - } + //Message framer can trigger a symbol-inversion correction to the PLL when detected + mMessageFramer = new P25MessageFramer(getAliasList(), mCostasLoop); + mMessageFramer.setListener(getMessageProcessor()); + mQPSKDemodulator.setDibitListener(mMessageFramer); + } - @Override - public void stop() - { - // TODO Auto-generated method stub - - } + @Override + public void receive(ComplexBuffer complexBuffer) + { + ComplexBuffer filtered = filter(complexBuffer); + mQPSKDemodulator.receive(filtered); + } - @Override - public void setFrequencyChangeListener( Listener listener ) - { - if( mSymbolFilter != null ) - { - mSymbolFilter.setFrequencyChangeListener( listener ); - } - } + protected ComplexBuffer filter(ComplexBuffer complexBuffer) + { + return mBasebandFilter.filter(complexBuffer); + } - @Override - public void removeFrequencyChangeListener() - { - if( mSymbolFilter != null ) - { - mSymbolFilter.setFrequencyChangeListener( null ); - } - } + private double getSampleRate() + { + return mSampleRate; + } - @Override - public Listener getFrequencyChangeListener() - { - if( mSymbolFilter != null ) - { - return mSymbolFilter.getFrequencyChangeListener(); - } - - 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_C4FMDecoder2.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2.java deleted file mode 100644 index dbd56609..00000000 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2.java +++ /dev/null @@ -1,200 +0,0 @@ -/******************************************************************************* - * 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.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.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.HashMap; -import java.util.Map; -import java.util.concurrent.ScheduledExecutorService; - -public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListener, Listener -{ - private final static Logger mLog = LoggerFactory.getLogger(P25_C4FMDecoder2.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; - - public P25_C4FMDecoder2(AliasList aliasList) - { - super(aliasList); - setSampleRate(48000.0); - } - - public void setSampleRate(double sampleRate) - { - if(sampleRate <= SYMBOL_RATE * 2) - { - throw new IllegalArgumentException("Sample rate must be at least twice the symbol rate [4800]"); - } - - mSampleRate = sampleRate; - mBasebandFilter = new ComplexFIRFilter(getBasebandFilter(), 1.0f); - - 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); - } - - @Override - public void receive(ComplexBuffer complexBuffer) - { - ComplexBuffer filtered = filter(complexBuffer); - mQPSKDemodulator.receive(filtered); - } - - protected ComplexBuffer filter(ComplexBuffer complexBuffer) - { - return mBasebandFilter.filter(complexBuffer); - } - - private double getSampleRate() - { - return mSampleRate; - } - - 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_C4FMDecoder2.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_C4FMDecoder2Instrumented.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoderInstrumented.java similarity index 97% rename from src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2Instrumented.java rename to src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoderInstrumented.java index 4877261f..903396a7 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2Instrumented.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoderInstrumented.java @@ -23,7 +23,7 @@ import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.complex.Complex; import io.github.dsheirer.sample.complex.ComplexBuffer; -public class P25_C4FMDecoder2Instrumented extends P25_C4FMDecoder2 +public class P25_C4FMDecoderInstrumented extends P25_C4FMDecoder { private Listener mPLLPhaseErrorListener; private Listener mPLLFrequencyListener; @@ -32,7 +32,7 @@ public class P25_C4FMDecoder2Instrumented extends P25_C4FMDecoder2 private Listener mFilteredSymbolListener; private Listener mSymbolDecisionDataListener; - public P25_C4FMDecoder2Instrumented(AliasList aliasList) + public P25_C4FMDecoderInstrumented(AliasList aliasList) { super(aliasList); } diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerChannelSource.java b/src/main/java/io/github/dsheirer/source/tuner/TunerChannelSource.java index c577789e..6804d8d7 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/TunerChannelSource.java +++ b/src/main/java/io/github/dsheirer/source/tuner/TunerChannelSource.java @@ -469,6 +469,7 @@ public class TunerChannelSource extends ComplexSource implements IFrequencyChang { //Frequency correction requests are the only change requests supported from downstream components case REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE: + mLog.debug("Got a freq corr change request: " + event.getValue().intValue()); setFrequencyCorrection(event.getValue().intValue()); break; } From 78bb5ffe43f98323023a5ff6bd09d0a36cb683e3 Mon Sep 17 00:00:00 2001 From: Denny Date: Sat, 17 Mar 2018 10:39:35 -0400 Subject: [PATCH 22/23] Resolves #336. Updates audio recorder to use a *.tmp file extension while the file is open/in-use and change the file extension to *.wav when the recording is closed. --- .../dsheirer/record/RecorderManager.java | 3 +- .../record/wave/AudioPacketWaveRecorder.java | 2 +- .../dsheirer/record/wave/WaveWriter.java | 35 +++++++++++++++++-- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/src/main/java/io/github/dsheirer/record/RecorderManager.java b/src/main/java/io/github/dsheirer/record/RecorderManager.java index c5845cdc..7f3563cf 100644 --- a/src/main/java/io/github/dsheirer/record/RecorderManager.java +++ b/src/main/java/io/github/dsheirer/record/RecorderManager.java @@ -93,6 +93,7 @@ public class RecorderManager implements Listener /** * Primary ingest point for audio packets from all decoding channels + * * @param audioPacket to process */ @Override @@ -115,7 +116,7 @@ public class RecorderManager implements Listener while(!audioPackets.isEmpty()) { - for(AudioPacket audioPacket: audioPackets) + for(AudioPacket audioPacket : audioPackets) { String identifier = audioPacket.getMetadata().getUniqueIdentifier(); 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 ab3d806a..1af76cd4 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 Date: Sat, 17 Mar 2018 10:43:24 -0400 Subject: [PATCH 23/23] Updates build to 0.3.4 beta 1. Note: build 0.3.3 was never promoted beyond beta 3, although there were no reported issues that would have prevented a 0.3.3 release. --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b89f9ef5..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 {