diff --git a/src/source/tuner/channel/polyphase/ChannelCalculator.java b/src/source/tuner/channel/polyphase/ChannelCalculator.java
new file mode 100644
index 00000000..a1b0940c
--- /dev/null
+++ b/src/source/tuner/channel/polyphase/ChannelCalculator.java
@@ -0,0 +1,588 @@
+/*******************************************************************************
+ * sdrtrunk
+ * Copyright (C) 2014-2017 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 source.tuner.channel.polyphase;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import source.tuner.channel.TunerChannel;
+
+import java.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.List;
+
+public class ChannelCalculator
+{
+ private final static Logger mLog = LoggerFactory.getLogger(ChannelCalculator.class);
+
+ /**
+ * Policy to determine desired corrective action for methods that calculate an index for a frequency or a frequency
+ * from an index where the frequency may exist either at the overlapping boundary of two indexes, or in the middle
+ * of the wrap-around (N/2) index.
+ */
+ private enum IndexBoundaryPolicy
+ {
+ ADJUST_POSITIVE,
+ ADJUST_NEGATIVE
+ }
+
+ private double mSampleRate;
+ private int mChannelCount;
+ private double mCenterFrequency;
+ private double mChannelBandwidth;
+ private double mOversampling = 1.0;
+
+ /**
+ * Calculates channel index(es) from a polyphase channelizer output to source a DDC polyphase channel source
+ * with a specified channel center frequency and bandwidth.
+ *
+ * Polyphase channel indexes are not linear/consecutive owing to the use of the FFT to rotate channels to their
+ * center frequencies following filtering in the polyphase filter bank. As such, this calculator provides methods
+ * to determine the min/center/max frequency of each channel that is adjusted for the current center frequency
+ * value and to determine the channel index(es) that need to be targeted to produce a requested output channel
+ * with a specific frequency and bandwdith.
+ *
+ * @param sampleRate in hertz for the input sample rate from the sample source (ie tuner)
+ * @param channelCount number of channels in the polyphase filter bank
+ * @param centerFrequency center frequency for the input sample stream
+ * @param oversampling set to 1.0 for no oversample, or greater than 1.0 for oversampling
+ */
+ public ChannelCalculator(double sampleRate, int channelCount, double centerFrequency, double oversampling)
+ {
+ mSampleRate = sampleRate;
+ mChannelCount = channelCount;
+ mCenterFrequency = centerFrequency;
+ mOversampling = oversampling;
+
+ updateChannelBandwidth();;
+ }
+
+ /**
+ * Calculates the channel bandwidth from the sample rate and channel count.
+ */
+ private void updateChannelBandwidth()
+ {
+ mChannelBandwidth = (double)mSampleRate / (double)mChannelCount;
+ }
+
+ /**
+ * Center frequency for the channelizer
+ */
+ public double getCenterFrequency()
+ {
+ return mCenterFrequency;
+ }
+
+ /**
+ * Updates the center frequency for the channelizer
+ */
+ public void setCenterFrequency(double frequency)
+ {
+ mCenterFrequency = frequency;
+ }
+
+ /**
+ * Number of channels being channelized by the polyphase channelizer
+ */
+ public int getChannelCount()
+ {
+ return mChannelCount;
+ }
+
+ /**
+ * Updates the number of channels being channelized by the polyphase channelizer
+ */
+ public void setChannelCount(int channelCount)
+ {
+ mChannelCount = channelCount;
+ updateChannelBandwidth();
+ }
+
+ /**
+ * Identifies the index of the wrap-around channel meaning that the lower half of the channel exists at the upper
+ * end of the spectrum and the upper half of the channel exists at the lower end of the spectrum
+ *
+ * @return wrap-around channel index
+ */
+ public int getWrapAroundIndex()
+ {
+ return mChannelCount / 2;
+ }
+
+ /**
+ * PolyphaseChannelManager input sample rate
+ * @return input sample rate in hertz
+ */
+ public double getSampleRate()
+ {
+ return mSampleRate;
+ }
+
+ /**
+ * Updates the channel count using the sample rate argument to calculate the number of channels from the
+ * already established channel bandwidth.
+ *
+ * @param sampleRate to use for channel count
+ */
+ public void setSampleRate(double sampleRate)
+ {
+ setChannelCount((int)(sampleRate / getChannelBandwidth()));
+ }
+
+ /**
+ * Channel bandwidth. Note: this value may be smaller than the channel sample rate when oversampling
+ * is being used.
+ */
+ public double getChannelBandwidth()
+ {
+ return mChannelBandwidth;
+ }
+
+ /**
+ * Half of the channel bandwidth.
+ */
+ public double getHalfChannelBandwidth()
+ {
+ return mChannelBandwidth / 2.0;
+ }
+
+ /**
+ * Oversampling rate produced by the channelizer.
+ *
+ * @return 1.0 for no oversampling, or a value greater than 1.0 when oversampling is applied.
+ */
+ public double getOversampling()
+ {
+ return mOversampling;
+ }
+
+ /**
+ * Sets the oversampling rate being applied by the polyphase channelizer
+ * @param oversampling rate where 1.0 indicates no oversampling, and a value greater than 1.0 indicates
+ * that the polyphase channelizer is oversampling each channel.
+ */
+ public void setOversampling(double oversampling)
+ {
+ mOversampling = oversampling;
+ }
+
+ /**
+ * Channel sample rate. This value is the product of the filtered channel bandwidth times the
+ * oversampling rate.
+ */
+ public double getChannelSampleRate()
+ {
+ return (double)getChannelBandwidth() * getOversampling();
+ }
+
+ /**
+ * PolyphaseChannelManager input minimum frequency
+ * @return minimum frequency in hertz
+ */
+ public long getMinimumFrequency()
+ {
+ return (long)(getCenterFrequency() - (getSampleRate() / 2.0));
+ }
+
+ /**
+ * PolyphaseChannelManager input maximum frequency
+ * @return maximum frequency in hertz
+ */
+ public long getMaximumFrequency()
+ {
+ return (long)(getCenterFrequency() + (getSampleRate() / 2.0));
+ }
+
+ /**
+ * Identifies the channelizer channel index(es) required to produce a tuner channel with the specified center
+ * frequency and channel bandwidth.
+ *
+ * @param tunerChannel specifying the desired channel frequency and bandwidth
+ *
+ * @return an array of one or more polyphase channelizer index(es) to target in order to produce an output channel
+ * for the desired frequency and bandwidth.
+ *
+ * @throws IllegalArgumentException if the tuner channel's minimum or maximum frequency is not contained
+ * within the channelizer's minimum or maximum frequency range
+ */
+ public List getChannelIndexes(TunerChannel tunerChannel) throws IllegalArgumentException
+ {
+ if(tunerChannel.getMinFrequency() < getMinimumFrequency() ||
+ tunerChannel.getMaxFrequency() > getMaximumFrequency())
+ {
+ throw new IllegalArgumentException("Requested channel cannot be provided by this " +
+ "channelizer. Requested channel [" + tunerChannel.getMinFrequency() + " - " +
+ tunerChannel.getMaxFrequency() + "] exceeds current channelizer frequency range [" +
+ getMinimumFrequency() + " - " + getMaximumFrequency() + "]");
+ }
+
+ int minIndex = getIndexForFrequency(tunerChannel.getMinFrequency(), IndexBoundaryPolicy.ADJUST_POSITIVE);
+ int maxIndex = getIndexForFrequency(tunerChannel.getMaxFrequency(), IndexBoundaryPolicy.ADJUST_NEGATIVE);
+
+ //Detect the unsourceable situation where the wrap around index would be included in both the minimum index
+ //and the maximum index, meaning that the tuner channel bandwidth is nearly the same as the input sample rate.
+ if(minIndex == getWrapAroundIndex() && maxIndex == getWrapAroundIndex())
+ {
+ throw new IllegalArgumentException("Requested tuner channel cannot be provided. Requested bandwidth is " +
+ "within two channel bandwidths of the sample rate.");
+ }
+
+ List indexes = new ArrayList<>();
+
+ indexes.add(minIndex);
+
+ if(minIndex != maxIndex)
+ {
+ //Ensure our min/max indexes are within the correct bounds 0 <> N-1 to safeguard the while loop below
+ if(minIndex < 0 || minIndex >= getChannelCount() || maxIndex < 0 || maxIndex >= getChannelCount())
+ {
+ throw new IllegalArgumentException("Something went wrong while calculating the min and max polyphase " +
+ "channel indexes for tuner channel: " + tunerChannel);
+ }
+
+ int pointer = minIndex + 1;
+
+ if(pointer >= getChannelCount())
+ {
+ pointer -= getChannelCount();
+ }
+
+ while(pointer != maxIndex)
+ {
+ indexes.add(pointer);
+
+ pointer++;
+
+ if(pointer >= getChannelCount())
+ {
+ pointer -= getChannelCount();
+ }
+ }
+
+ indexes.add(maxIndex);
+ }
+
+ return indexes;
+ }
+
+ /**
+ * Calculates the index for the specified frequency. Certain frequencies can exist as both the maximum frequency
+ * of one channel and the minimum frequency of an adjacent channel. The index boundary policy identifies your
+ * choice to select the higher frequency channel index (ADJUST_POSITIVE) or the lower frequency channel
+ * index (ADJUST_NEGATIVE).
+ *
+ * @param frequency to determine channel index for
+ * @param indexBoundaryPolicy to determine the behavior when the frequency overlaps two channel boundaries
+ * @return index for the frequency
+ */
+ public int getIndexForFrequency(long frequency, IndexBoundaryPolicy indexBoundaryPolicy)
+ {
+ double offset = frequency - getCenterFrequency();
+
+ if(Math.abs(offset) < getHalfChannelBandwidth())
+ {
+ return 0; //Center index
+ }
+
+ if(offset > 0)
+ {
+ offset += getHalfChannelBandwidth();
+ }
+ else
+ {
+ offset -= getHalfChannelBandwidth();
+ }
+
+ int indexOffset = (int)(offset / getChannelBandwidth());
+
+ if(indexOffset < 0)
+ {
+ indexOffset += getChannelCount();
+ }
+
+ if(indexBoundaryPolicy == IndexBoundaryPolicy.ADJUST_POSITIVE &&
+ isOverlapFrequency(frequency, indexOffset, indexOffset + 1))
+ {
+ indexOffset = normalize(indexOffset + 1);
+ }
+ else if(indexBoundaryPolicy == IndexBoundaryPolicy.ADJUST_NEGATIVE &&
+ isOverlapFrequency(frequency, indexOffset - 1, indexOffset))
+ {
+ indexOffset = normalize(indexOffset - 1);
+ }
+
+ return indexOffset;
+ }
+
+ /**
+ * Indicates if the frequency is an overlapping frequency between channel index 1 and channel index 2 where the
+ * maximum frequency of index 1 is the same as the minimum frequency of index 2 and both of these frequencies are
+ * the same as the frequency argument.
+ *
+ * Note: both index 1 and index 2 will be normalized to the range of 0 <> N-1 prior to determining overlap
+ *
+ * @param frequency to test for overlap between index 1 and 2
+ * @param index1 which is the lower channel index located next to index 2.
+ * @param index2 which is the higher channel index located next to index 1.
+ * @return true if frequency = index1 maximum frequency = index2 minimum frequency
+ */
+ public boolean isOverlapFrequency(long frequency, int index1, int index2)
+ {
+ index1 = normalize(index1);
+ index2 = normalize(index2);
+
+ //Check that these two indexes are side by side
+ int delta = normalize(index2 - index1);
+
+ if(delta != 1)
+ {
+ return false;
+ }
+
+ long index1MaximumFrequency = (long)getIndexMaximumFrequency(index1, IndexBoundaryPolicy.ADJUST_POSITIVE);
+ long index2MinimumFrequency = (long)getIndexMinimumFrequency(index2, IndexBoundaryPolicy.ADJUST_NEGATIVE);
+
+ if(index1 == getWrapAroundIndex())
+ {
+ index1MaximumFrequency = (long)getIndexMaximumFrequency(index1, IndexBoundaryPolicy.ADJUST_NEGATIVE);
+ }
+
+ if(index2 == getWrapAroundIndex())
+ {
+ index2MinimumFrequency = (long)getIndexMinimumFrequency(index2, IndexBoundaryPolicy.ADJUST_POSITIVE);
+ }
+
+ return frequency == index1MaximumFrequency && frequency == index2MinimumFrequency;
+ }
+
+ /**
+ * Normalizes the index to the range of 0 <> N-1 channel indexes>
+ */
+ private int normalize(int index)
+ {
+ while(index < 0)
+ {
+ index += getChannelCount();
+ }
+
+ while(index >= getChannelCount())
+ {
+ index -= getChannelCount();
+ }
+
+ return index;
+ }
+
+ /**
+ * Calculates the center frequency for the channel identified by the index in the channel results array.
+ *
+ * @param index to determine the channel center frequency
+ * @return center frequency for the indexed channel
+ */
+ public double getIndexCenterFrequency(int index, IndexBoundaryPolicy indexBoundaryPolicy)
+ {
+ if(index < 0 || index >= getChannelCount())
+ {
+ throw new IllegalArgumentException("Illegal channel index");
+ }
+
+ int wrapAroundIndex = getWrapAroundIndex();
+
+ if(index == wrapAroundIndex)
+ {
+ if(indexBoundaryPolicy == IndexBoundaryPolicy.ADJUST_POSITIVE)
+ {
+ return getCenterFrequency() + (index * getChannelBandwidth());
+ }
+ else
+ {
+ return getCenterFrequency() - (index * getChannelBandwidth());
+ }
+ }
+ else if(index < wrapAroundIndex)
+ {
+ return getCenterFrequency() + (index * getChannelBandwidth());
+ }
+ else
+ {
+ return getCenterFrequency() - ((getChannelCount() - index) * getChannelBandwidth());
+ }
+ }
+
+ /**
+ * Calculates the minimum frequency band edge for the channel identified by the index in the channel results
+ * array.
+ * @param index to calculate the minimum band edge frequency for the channel.
+ * @param indexBoundaryPolicy for the wrap around index to indicate if this method should return the minimum
+ * frequency of the upper spectrum half of the wrap around channel (ADJUST_POSITIVE) or the minimum frequency of the
+ * lower spectrum half of the wrap around channel (ADJUST_NEGATIVE)
+ * @return frequency of the minimum band edge
+ */
+ public double getIndexMinimumFrequency(int index, IndexBoundaryPolicy indexBoundaryPolicy)
+ {
+ if(index < 0 || index >= getChannelCount())
+ {
+ throw new IllegalArgumentException("Can't determine minimum frequency - illegal channel index [" +
+ index + "]");
+ }
+
+ int wrapAroundIndex = getWrapAroundIndex();
+
+ if(index == wrapAroundIndex)
+ {
+ if(indexBoundaryPolicy == IndexBoundaryPolicy.ADJUST_POSITIVE)
+ {
+ return getCenterFrequency() + ((double)index * getChannelBandwidth()) - getHalfChannelBandwidth();
+ }
+ else
+ {
+ return getIndexCenterFrequency(index, indexBoundaryPolicy);
+ }
+ }
+ else if(index <= wrapAroundIndex)
+ {
+ return getCenterFrequency() + ((double)index * getChannelBandwidth()) - getHalfChannelBandwidth();
+ }
+ else
+ {
+ return getCenterFrequency() - ((double)(getChannelCount() - index) * getChannelBandwidth()) -
+ getHalfChannelBandwidth();
+ }
+ }
+
+ /**
+ * Calculates the maximum frequency band edge for the channel identified by the index in the channel results
+ * array.
+ * @param index to calculate the maximum band edge frequency for the channel.
+ * @param indexBoundaryPolicy for the wrap around index to indicate if this method should return the maximum
+ * frequency of the upper spectrum half of the wrap around channel (ADJUST_POSITIVE) or the maximum frequency of the
+ * lower spectrum half of the wrap around channel (ADJUST_NEGATIVE)
+ * @return frequency of the maximum band edge
+ */
+ public double getIndexMaximumFrequency(int index, IndexBoundaryPolicy indexBoundaryPolicy)
+ {
+ if(index < 0 || index >= getChannelCount())
+ {
+ throw new IllegalArgumentException("Can't determine maximum frequency - illegal channel index [" +
+ index + "]");
+ }
+
+ int wrapAroundIndex = getWrapAroundIndex();
+
+ if(index == wrapAroundIndex)
+ {
+ if(indexBoundaryPolicy == IndexBoundaryPolicy.ADJUST_POSITIVE)
+ {
+ return getIndexCenterFrequency(index, indexBoundaryPolicy);
+ }
+ else
+ {
+ return getCenterFrequency() - ((double)index * getChannelBandwidth() - getHalfChannelBandwidth());
+ }
+ }
+ else if(index <= wrapAroundIndex)
+ {
+ return getCenterFrequency() + ((double)index * getChannelBandwidth()) + getHalfChannelBandwidth();
+ }
+ else
+ {
+ return getCenterFrequency() - ((double)(getChannelCount() - index) * getChannelBandwidth()) +
+ getHalfChannelBandwidth();
+ }
+ }
+
+ /**
+ * Calculates the center frequency for a channel that will be synthesized from the channel indexes.
+ * @param indexes that are contiguous in the spectrum
+ * @return center frequency in hertz
+ */
+ public long getCenterFrequencyForIndexes(List indexes)
+ {
+ if(indexes.isEmpty())
+ {
+ throw new IllegalArgumentException("Indexes cannot be empty");
+ }
+
+ int middleIndex = indexes.get(indexes.size() / 2);
+
+ if(indexes.size() % 2 == 0)
+ {
+ //Even number of indexes
+ return (long)getIndexMinimumFrequency(middleIndex, IndexBoundaryPolicy.ADJUST_POSITIVE);
+ }
+ else
+ {
+ //Odd number of indexes
+ return (long)getIndexCenterFrequency(middleIndex, IndexBoundaryPolicy.ADJUST_POSITIVE);
+ }
+ }
+
+ /**
+ * String representation of the channel calculator settings
+ */
+ public String toString()
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append("Channels:").append(getChannelCount());
+ sb.append(" Sample Rate:").append(getSampleRate());
+ sb.append(" Channel Bandwidth:").append(getChannelBandwidth());
+ sb.append(" Channel Rate:").append(getChannelSampleRate());
+ sb.append(" Min:").append(getMinimumFrequency());
+ sb.append(" Max:").append(getMaximumFrequency());
+ return sb.toString();
+ }
+
+
+ public static void main(String[] args)
+ {
+ mLog.debug("Starting ....");
+
+ int channelBandwidth = 12500;
+ int sampleRate = 100000; //Airspy
+ int channelCount = sampleRate / channelBandwidth;
+ double centerFrequency = 455000000;
+
+ ChannelCalculator calculator = new ChannelCalculator(sampleRate, channelCount, centerFrequency, 2.0);
+
+ mLog.debug(calculator.toString());
+
+ DecimalFormat decimalFormat = new DecimalFormat("000,000,000.0");
+
+ int wrapAroundIndex = calculator.getWrapAroundIndex();
+
+ IndexBoundaryPolicy indexBoundaryPolicy = IndexBoundaryPolicy.ADJUST_NEGATIVE;
+
+ for(int index = wrapAroundIndex; index < calculator.getChannelCount(); index++)
+ {
+ mLog.debug("Index:" + index + " Min:" + decimalFormat.format(calculator.getIndexMinimumFrequency(index, indexBoundaryPolicy)) +
+ " Center:" + decimalFormat.format(calculator.getIndexCenterFrequency(index, indexBoundaryPolicy)) +
+ " Max:" + decimalFormat.format(calculator.getIndexMaximumFrequency(index, indexBoundaryPolicy)));
+ }
+
+ indexBoundaryPolicy = IndexBoundaryPolicy.ADJUST_POSITIVE;
+
+ for(int index = 0; index <= wrapAroundIndex; index++)
+ {
+ mLog.debug("Index:" + index + " Min:" + decimalFormat.format(calculator.getIndexMinimumFrequency(index, indexBoundaryPolicy)) +
+ " Center:" + decimalFormat.format(calculator.getIndexCenterFrequency(index, indexBoundaryPolicy)) +
+ " Max:" + decimalFormat.format(calculator.getIndexMaximumFrequency(index, indexBoundaryPolicy)));
+ }
+
+ mLog.debug("Finished");
+ }
+}
diff --git a/src/source/tuner/channel/polyphase/ChannelizerViewer.java b/src/source/tuner/channel/polyphase/ChannelizerViewer.java
new file mode 100644
index 00000000..b4dbcd30
--- /dev/null
+++ b/src/source/tuner/channel/polyphase/ChannelizerViewer.java
@@ -0,0 +1,343 @@
+/*******************************************************************************
+ * sdrtrunk
+ * Copyright (C) 2014-2017 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 source.tuner.channel.polyphase;
+
+import net.miginfocom.swing.MigLayout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import sample.Listener;
+import sample.SampleType;
+import sample.complex.ComplexBuffer;
+import settings.SettingsManager;
+import source.ISourceEventProcessor;
+import source.SourceEvent;
+import source.SourceException;
+import source.tuner.channel.TunerChannel;
+import source.tuner.channel.TunerChannelSource;
+import source.tuner.configuration.TunerConfigurationModel;
+import source.tuner.test.TestTuner;
+import spectrum.DFTProcessor;
+import spectrum.DFTSize;
+import spectrum.SpectrumPanel;
+import spectrum.converter.ComplexDecibelConverter;
+
+import javax.swing.*;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import java.awt.*;
+
+public class ChannelizerViewer extends JFrame
+{
+ private final static Logger mLog = LoggerFactory.getLogger(ChannelizerViewer.class);
+
+ private static final int CHANNEL_BANDWIDTH = 12500;
+ private static final int CHANNEL_FFT_FRAME_RATE = 20;
+
+ private SettingsManager mSettingsManager = new SettingsManager(new TunerConfigurationModel());
+ private JPanel mPrimaryPanel;
+ private JPanel mControlPanel;
+ private PrimarySpectrumPanel mPrimarySpectrumPanel;
+ private ChannelArrayPanel mChannelPanel;
+ private int mChannelCount;
+ private int mChannelsPerRow;
+ private long mToneFrequency;
+ private DFTSize mMainPanelDFTSize = DFTSize.FFT32768;
+ private DFTSize mChannelPanelDFTSize = DFTSize.FFT01024;
+ private TestTuner mTestTuner;
+
+ /**
+ * GUI Test utility for researching polyphase channelizers.
+ *
+ * @param channelsPerRow
+ */
+ public ChannelizerViewer(int channelsPerRow)
+ {
+ mTestTuner = new TestTuner();
+
+ mChannelCount = (int)(mTestTuner.getTunerController().getFrequencyController().getSampleRate() / CHANNEL_BANDWIDTH);
+ mChannelsPerRow = channelsPerRow;
+
+ mToneFrequency = mTestTuner.getTunerController().getFrequencyController().getFrequency() + 19200;
+
+ mTestTuner.getTunerController().setToneFrequency(mToneFrequency);
+
+ init();
+ }
+
+ private void init()
+ {
+ setTitle("TunerChannelizer Viewer");
+ setSize(1000, 800);
+ setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+ setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill]"));
+ setLocationRelativeTo(null);
+ add(getPrimaryPanel());
+ }
+
+ private JPanel getPrimaryPanel()
+ {
+ if(mPrimaryPanel == null)
+ {
+ mPrimaryPanel = new JPanel();
+ mPrimaryPanel.setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[][][grow,fill]"));
+ mPrimaryPanel.add(getSpectrumPanel(), "wrap");
+ mPrimaryPanel.add(getControlPanel(), "wrap");
+ mPrimaryPanel.add(getChannelArrayPanel());
+ }
+
+ return mPrimaryPanel;
+ }
+
+ private PrimarySpectrumPanel getSpectrumPanel()
+ {
+ if(mPrimarySpectrumPanel == null)
+ {
+ mPrimarySpectrumPanel = new PrimarySpectrumPanel(mSettingsManager,
+ mTestTuner.getTunerController().getFrequencyController().getSampleRate());
+ mPrimarySpectrumPanel.setPreferredSize(new Dimension(1000, 200));
+ mPrimarySpectrumPanel.setDFTSize(mMainPanelDFTSize);
+ mTestTuner.getSourceManager().addComplexBufferListener(mPrimarySpectrumPanel);
+ }
+
+ return mPrimarySpectrumPanel;
+ }
+
+ private JPanel getControlPanel()
+ {
+ if(mControlPanel == null)
+ {
+ mControlPanel = new JPanel();
+ mControlPanel.setLayout(new MigLayout("insets 0 0 0 0", "", ""));
+
+ mControlPanel.add(new JLabel("Tone:"), "align left");
+ int maximumFrequencyOffset = (int)(mTestTuner.getTunerController().getFrequencyController().getSampleRate() / 2);
+ long minimumFrequency = mTestTuner.getTunerController().getFrequencyController().getFrequency() - maximumFrequencyOffset;
+ long maximumFrequency = mTestTuner.getTunerController().getFrequencyController().getFrequency() + maximumFrequencyOffset;
+
+ SpinnerNumberModel model = new SpinnerNumberModel(mToneFrequency, minimumFrequency, maximumFrequency,
+ 100 );
+
+ model.addChangeListener(new ChangeListener()
+ {
+ @Override
+ public void stateChanged(ChangeEvent e)
+ {
+ int toneFrequency = model.getNumber().intValue();
+ mTestTuner.getTunerController().setToneFrequency(toneFrequency);
+ }
+ });
+
+ JSpinner spinner = new JSpinner(model);
+
+ mControlPanel.add(spinner);
+ mControlPanel.add(new JLabel("Hz"));
+
+ mControlPanel.add(new JLabel("Sample Rate: " +
+ mTestTuner.getTunerController().getFrequencyController().getSampleRate() + " Hz"), "push,align right");
+
+ mControlPanel.add(new JLabel("Max: " +
+ (mTestTuner.getTunerController().getFrequencyController().getSampleRate() / 2) + " Hz"), "push,align right");
+ mControlPanel.add(new JLabel("Channels: " + mChannelCount), "push,align right");
+ }
+
+ return mControlPanel;
+ }
+
+ private ChannelArrayPanel getChannelArrayPanel()
+ {
+ if(mChannelPanel == null)
+ {
+ mChannelPanel = new ChannelArrayPanel();
+ }
+
+ return mChannelPanel;
+ }
+
+ public class ChannelArrayPanel extends JPanel
+ {
+ public ChannelArrayPanel()
+ {
+ int bufferSize = CHANNEL_BANDWIDTH / CHANNEL_FFT_FRAME_RATE;
+ if(bufferSize % 2 == 1)
+ {
+ bufferSize++;
+ }
+
+ init();
+ }
+
+ private void init()
+ {
+ setLayout(new MigLayout("insets 0 0 0 0", "fill", "fill"));
+
+ double spectralBandwidth = mTestTuner.getTunerController().getFrequencyController().getSampleRate();
+ double halfSpectralBandwidth = spectralBandwidth / 2.0;
+
+ int channelToLog = -1;
+
+ for(int x = 0; x < mChannelCount; x++)
+ {
+ //place the channels left and right of 10.0 MHz
+ long frequency = (long)(mTestTuner.getTunerController().getFrequencyController().getFrequency() +
+ ((x * CHANNEL_BANDWIDTH) - halfSpectralBandwidth) + (CHANNEL_BANDWIDTH / 2));
+
+ mLog.debug("Channel " + x + " Frequency: " + frequency);
+
+ ChannelPanel channelPanel = new ChannelPanel(mSettingsManager, CHANNEL_BANDWIDTH * 2, frequency, CHANNEL_BANDWIDTH, (x == channelToLog));
+ channelPanel.setDFTSize(mChannelPanelDFTSize);
+
+ if(x % mChannelsPerRow == mChannelsPerRow - 1)
+ {
+ add(channelPanel, "grow,push,wrap 2px");
+ }
+ else
+ {
+ add(channelPanel, "grow,push");
+ }
+ }
+ }
+ }
+
+ public class PrimarySpectrumPanel extends JPanel implements Listener, ISourceEventProcessor
+ {
+ private DFTProcessor mDFTProcessor = new DFTProcessor(SampleType.COMPLEX);
+ private ComplexDecibelConverter mComplexDecibelConverter = new ComplexDecibelConverter();
+ private SpectrumPanel mSpectrumPanel;
+
+ public PrimarySpectrumPanel(SettingsManager settingsManager, double sampleRate)
+ {
+ setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill]"));
+ mSpectrumPanel = new SpectrumPanel(settingsManager);
+ add(mSpectrumPanel);
+
+ mDFTProcessor.addConverter(mComplexDecibelConverter);
+ mDFTProcessor.process(SourceEvent.sampleRateChange(sampleRate));
+ mComplexDecibelConverter.addListener(mSpectrumPanel);
+ }
+
+ public void setDFTSize(DFTSize dftSize)
+ {
+ mDFTProcessor.setDFTSize(dftSize);
+ }
+
+ @Override
+ public void receive(ComplexBuffer complexBuffer)
+ {
+ mDFTProcessor.receive(complexBuffer);
+ }
+
+ @Override
+ public void process(SourceEvent event) throws SourceException
+ {
+ mLog.debug("Source Event! Add handler support for this to channelizer viewer");
+ }
+ }
+
+ public class ChannelPanel extends JPanel implements Listener, ISourceEventProcessor
+ {
+ private TunerChannelSource mSource;
+ private DFTProcessor mDFTProcessor = new DFTProcessor(SampleType.COMPLEX);
+ private ComplexDecibelConverter mComplexDecibelConverter = new ComplexDecibelConverter();
+ private SpectrumPanel mSpectrumPanel;
+ private long mFrequency;
+
+ public ChannelPanel(SettingsManager settingsManager, double sampleRate, long frequency, int bandwidth, boolean enableLogging)
+ {
+ setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill]"));
+ mSpectrumPanel = new SpectrumPanel(settingsManager);
+ add(mSpectrumPanel);
+
+ mDFTProcessor.addConverter(mComplexDecibelConverter);
+ mDFTProcessor.process(SourceEvent.sampleRateChange(sampleRate));
+
+ mComplexDecibelConverter.addListener(mSpectrumPanel);
+
+ mFrequency = frequency;
+
+ TunerChannel tunerChannel = new TunerChannel(frequency, bandwidth);
+ mSource = mTestTuner.getSourceManager().getSource(tunerChannel);
+
+ if(mSource != null)
+ {
+ if(enableLogging)
+ {
+ mSource.setListener(new Listener()
+ {
+ @Override
+ public void receive(ComplexBuffer complexBuffer)
+ {
+ mLog.debug("Sending samples to DFT processor: " + complexBuffer.getSamples().length);
+ mDFTProcessor.receive(complexBuffer);
+ }
+ });
+ }
+ else
+ {
+ mSource.setListener(mDFTProcessor);
+ }
+
+ mSource.start();
+ }
+ else
+ {
+ mLog.error("Couldn't get a source from the tuner for frequency: " + frequency);
+ }
+ }
+
+ public TunerChannelSource getSource()
+ {
+ return mSource;
+ }
+
+ public void setDFTSize(DFTSize dftSize)
+ {
+ mDFTProcessor.setDFTSize(dftSize);
+ }
+
+ @Override
+ public void receive(ComplexBuffer complexBuffer)
+ {
+ mLog.debug("Got a buffer for the channel dft processor");
+ mDFTProcessor.receive(complexBuffer);
+ }
+
+ @Override
+ public void process(SourceEvent event) throws SourceException
+ {
+ mLog.debug("Source Event! Add handler support for this to channelizer viewer");
+ }
+ }
+
+ public static void main(String[] args)
+ {
+ int channelsPerRow = 4;
+
+ final ChannelizerViewer frame = new ChannelizerViewer(channelsPerRow);
+
+ EventQueue.invokeLater(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ frame.setVisible(true);
+ }
+ });
+ }
+
+}
diff --git a/src/source/tuner/channel/polyphase/PolyphaseChannelManager.java b/src/source/tuner/channel/polyphase/PolyphaseChannelManager.java
new file mode 100644
index 00000000..c15b64a2
--- /dev/null
+++ b/src/source/tuner/channel/polyphase/PolyphaseChannelManager.java
@@ -0,0 +1,384 @@
+/*******************************************************************************
+ * sdrtrunk
+ * Copyright (C) 2014-2017 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 source.tuner.channel.polyphase;
+
+import dsp.filter.channelizer.ComplexPolyphaseChannelizerM2;
+import dsp.filter.design.FilterDesignException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import sample.Listener;
+import sample.complex.IComplexBufferProvider;
+import source.SourceEvent;
+import source.SourceException;
+import source.tuner.channel.ChannelManager;
+import source.tuner.channel.TunerChannel;
+import source.tuner.channel.TunerChannelSource;
+import source.tuner.channel.polyphase.output.IPolyphaseChannelOutputProcessor;
+import source.tuner.channel.polyphase.output.OneChannelOutputProcessor;
+import source.tuner.channel.polyphase.output.TwoChannelOutputProcessor;
+import source.tuner.channel.FrequencyController;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+public class PolyphaseChannelManager extends ChannelManager
+{
+ private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelManager.class);
+ private static final int CHANNEL_BANDWIDTH = 12500;
+ private static final double CHANNEL_OVERSAMPLING = 2.0;
+ private static final int CHANNEL_SAMPLE_RATE = (int)(CHANNEL_BANDWIDTH * CHANNEL_OVERSAMPLING);
+ private static final int POLYPHASE_FILTER_TAPS_PER_CHANNEL = 17;
+
+ private IComplexBufferProvider mComplexBufferProvider;
+ private List mChannelSources = new CopyOnWriteArrayList<>();
+ private ChannelCalculator mChannelCalculator;
+ private ComplexPolyphaseChannelizerM2 mPolyphaseChannelizer;
+ private ChannelSourceEventListener mChannelSourceEventListener = new ChannelSourceEventListener();
+ private ScheduledBufferProcessor mBufferProcessor;
+
+ /**
+ * Polyphase Channel Manager is a DDC channel manager and complex buffer queue/processor for a tuner. This class
+ * provides DDC polyphase channel sources and wraps a polyphase channelizer processing sample buffers produced by
+ * the tuner and distributing channelized sample buffers to each allocated DDC polyphase channel source. This
+ * class is responsible for monitoring the tuner for changes in center frequency and/or sample rate and updating
+ * active DDC polyphase channel sources accordingly. This class also monitors source event requests and
+ * notifications received from active DDC polyphase channel sources to adjust sample streams as required.
+ *
+ * Note: add this channel manager as a source event listener to the complex buffer provider to ensure this manager
+ * adapts to changes in source frequency and sample rate.
+ *
+ * @param frequencyController that can be controlled by this channel manager
+ * @param complexBufferProvider that supports register/deregister of this channel manager
+ */
+ public PolyphaseChannelManager(FrequencyController frequencyController, IComplexBufferProvider complexBufferProvider)
+ {
+ super(frequencyController);
+
+ if(complexBufferProvider == null)
+ {
+ throw new IllegalArgumentException("Complex buffer provider argument cannot be null");
+ }
+
+ mComplexBufferProvider = complexBufferProvider;
+
+ //Ensure that sample rate is a multiple of channel sample rate since polyphase channelizer is M2
+ if(getFrequencyController().getSampleRate() % (CHANNEL_SAMPLE_RATE) != 0)
+ {
+ throw new IllegalArgumentException("Tuner sample rate [" + getFrequencyController().getSampleRate() +
+ "] must be an even multiple of " + CHANNEL_SAMPLE_RATE + " Hz");
+ }
+
+ int channelCount = (int)(getFrequencyController().getSampleRate() / CHANNEL_BANDWIDTH);
+
+ mChannelCalculator = new ChannelCalculator(getFrequencyController().getSampleRate(), channelCount,
+ getFrequencyController().getFrequency(), CHANNEL_OVERSAMPLING);
+
+ mBufferProcessor = new ScheduledBufferProcessor(500, 100, 50, 50);
+
+ updateChannelizer(getFrequencyController().getSampleRate(), null);
+ }
+
+ @Override
+ protected long calculateCenterFrequency()
+ {
+ return 0;
+ }
+
+ /**
+ * Provides a Digital Drop Channel (DDC) for the specified tuner channel or returns null if the channel can't be
+ * sourced due to the current center frequency and/or sample rate.
+ *
+ * @param tunerChannel specifying center frequency and bandwidth.
+ * @return source or null.
+ */
+ @Override
+ protected TunerChannelSource getTunerChannelSource(TunerChannel tunerChannel)
+ {
+ PolyphaseChannelSource channelSource = null;
+
+ try
+ {
+ List polyphaseIndexes = mChannelCalculator.getChannelIndexes(tunerChannel);
+
+ IPolyphaseChannelOutputProcessor outputProcessor = getOutputProcessor(polyphaseIndexes);
+
+ if(outputProcessor != null)
+ {
+ channelSource = new PolyphaseChannelSource(tunerChannel, outputProcessor, mChannelSourceEventListener,
+ CHANNEL_SAMPLE_RATE);
+
+ channelSource.setFrequency(mChannelCalculator.getCenterFrequencyForIndexes(polyphaseIndexes));
+
+ mChannelSources.add(channelSource);
+ }
+ }
+ catch(IllegalArgumentException iae)
+ {
+ mLog.info("Can't provide DDC for " + tunerChannel.toString() + " due to channelizer frequency [" +
+ mChannelCalculator.getCenterFrequency() + "] and sample rate [" +
+ (mChannelCalculator.getChannelCount() * mChannelCalculator.getChannelBandwidth()) + "]");
+ }
+
+ return channelSource;
+ }
+
+ /**
+ * Creates a processor to process the channelizer channel indexes into a composite output stream providing
+ * channelized complex sample buffers to a registered source listener.
+ *
+ * @param indexes to target by the output processor
+ * @return output processor compatible with the number of indexes to monitor
+ */
+ private IPolyphaseChannelOutputProcessor getOutputProcessor(List indexes)
+ {
+ switch(indexes.size())
+ {
+ case 1:
+ return new OneChannelOutputProcessor(CHANNEL_SAMPLE_RATE, indexes);
+ case 2:
+ return new TwoChannelOutputProcessor(CHANNEL_SAMPLE_RATE, indexes);
+ default:
+ //TODO: create output processor for greater than 2 input channels
+ mLog.error("Request to create an output processor for unexpected channel index size:" + indexes.size());
+ return null;
+ }
+ }
+
+ /**
+ * Starts/adds the channel source to receive channelized sample buffers, registering with the tuner to receive
+ * sample buffers when this is the first channel.
+ *
+ * @param channelSource to start
+ */
+ private void startChannelSource(PolyphaseChannelSource channelSource)
+ {
+ synchronized(mBufferProcessor)
+ {
+ mPolyphaseChannelizer.addChannel(channelSource);
+
+ if(mPolyphaseChannelizer.getRegisteredChannelCount() == 1)
+ {
+ mComplexBufferProvider.addComplexBufferListener(mBufferProcessor);
+ mBufferProcessor.start();
+ }
+ }
+ }
+
+ /**
+ * Stops/removes the channel source from receiving channelized sample buffers and deregisters from the tuner
+ * when this is the last channel being sourced.
+ *
+ * @param channelSource to stop
+ */
+ private void stopChannelSource(PolyphaseChannelSource channelSource)
+ {
+ synchronized(mBufferProcessor)
+ {
+ super.releaseSource(channelSource);
+
+ mChannelSources.remove(channelSource);
+
+ if(mPolyphaseChannelizer.getRegisteredChannelCount() == 0)
+ {
+ mComplexBufferProvider.removeComplexBufferListener(mBufferProcessor);
+ mBufferProcessor.stop();
+ }
+ }
+ }
+
+ /**
+ * Implements the Listener interface for receiving notifications of frequency and sample rate
+ * changes from the tuner.
+ */
+ @Override
+ public void receive(SourceEvent sourceEvent)
+ {
+ switch(sourceEvent.getEvent())
+ {
+ case NOTIFICATION_FREQUENCY_CHANGE:
+ //Update the index calculator and each of the output processors to update each channel frequency
+ mChannelCalculator.setCenterFrequency(sourceEvent.getValue().longValue());
+ updateOutputProcessors(sourceEvent);
+ break;
+ case NOTIFICATION_SAMPLE_RATE_CHANGE:
+ //Update the channelizer (and the output processors) for the new sample rate
+ updateChannelizer(sourceEvent.getValue().intValue(), sourceEvent);
+ break;
+ }
+ }
+
+ /**
+ * Creates or updates the channelizer to process the incoming sample rate and updates any channel processors.
+ *
+ * @param sampleRate to use for the channelizer
+ * @param sourceEvent containing a notification of sample rate change (optional - can be null). This source
+ * event will be passed to all output processors to ensure the change is fully promulgated before restarting the
+ * buffer processor if it was already running.
+ */
+ private void updateChannelizer(double sampleRate, SourceEvent sourceEvent)
+ {
+ mChannelCalculator.setSampleRate(sampleRate);
+
+ //Lock on the buffer processor to block any channel create/start/stop requests while we configure
+ synchronized(mBufferProcessor)
+ {
+ //Get current processor state so we can restart it after we update the channelizer
+ boolean bufferProcessorWasRunning = mBufferProcessor.isRunning();
+
+ //Stop will be ignored if it is not currently running
+ mBufferProcessor.stop();
+
+ try
+ {
+ if(mPolyphaseChannelizer == null)
+ {
+ mPolyphaseChannelizer = new ComplexPolyphaseChannelizerM2(sampleRate,
+ POLYPHASE_FILTER_TAPS_PER_CHANNEL);
+
+ mBufferProcessor.setListener(mPolyphaseChannelizer);
+ }
+ else
+ {
+ mPolyphaseChannelizer.setSampleRate(sampleRate);
+ }
+ }
+ catch(IllegalArgumentException iae)
+ {
+ mLog.error("Could not create polyphase channelizer for sample rate [" + sampleRate + "]", iae);
+ }
+ catch(FilterDesignException fde)
+ {
+ mLog.error("Could not create filter for polyphase channelizer for sample rate [" + sampleRate + "]", fde);
+ }
+
+ if(sourceEvent != null && sourceEvent.getEvent() == SourceEvent.Event.NOTIFICATION_SAMPLE_RATE_CHANGE)
+ {
+ updateOutputProcessors(sourceEvent);
+ }
+
+ if(bufferProcessorWasRunning)
+ {
+ mBufferProcessor.start();
+ }
+ }
+ }
+
+ /**
+ * Updates each of the output processors for any changes in the tuner's center frequency or sample rate, which
+ * would cause the output processors to change the polyphase channelizer results channel(s) that the processor is
+ * consuming
+ *
+ * @param sourceEvent that requires an update to the output processors
+ */
+ private void updateOutputProcessors(SourceEvent sourceEvent)
+ {
+ for(PolyphaseChannelSource channelSource : mChannelSources)
+ {
+ updateOutputProcessor(channelSource);
+
+ //Notify the channel source that the frequency or sample rate has changed so that it can reset
+ //any frequency correction value.
+ if(sourceEvent != null)
+ {
+ try
+ {
+ channelSource.process(sourceEvent);
+ }
+ catch(SourceException se)
+ {
+ mLog.error("Error while notifying polyphase channel source of a source event", se);
+ }
+ }
+ }
+ }
+
+ /**
+ * Updates the polyphase channel source's output processor due to a change in the center frequency or sample
+ * rate for the source providing sample buffers to the polyphase channelizer, or whenever the DDC channel's
+ * center tuned frequency changes.
+ *
+ * @param channelSource that requires an update to its output processor
+ */
+ private void updateOutputProcessor(PolyphaseChannelSource channelSource)
+ {
+ try
+ {
+ List indexes = mChannelCalculator.getChannelIndexes(channelSource.getTunerChannel());
+
+ long centerFrequency = mChannelCalculator.getCenterFrequencyForIndexes(indexes);
+
+ //If the indexes size is the same then update the current processor, otherwise create a new one
+ if(channelSource.getPolyphaseChannelOutputProcessor().getInputChannelCount() == indexes.size())
+ {
+ channelSource.getPolyphaseChannelOutputProcessor().setPolyphaseChannelIndices(indexes);
+ channelSource.setFrequency(centerFrequency);
+ }
+ else
+ {
+ channelSource.setPolyphaseChannelOutputProcessor(getOutputProcessor(indexes), centerFrequency);
+ }
+ }
+ catch(IllegalArgumentException iae)
+ {
+ mLog.error("Error updating polyphase channel source - can't determine output channel indexes for " +
+ "updated tuner center frequency and sample rate. Stopping channel source", iae);
+
+ stopChannelSource(channelSource);
+ }
+ }
+
+ /**
+ * Internal class for handling requests for start/stop sample stream from polyphase channel sources
+ */
+ private class ChannelSourceEventListener implements Listener
+ {
+ @Override
+ public void receive(SourceEvent sourceEvent)
+ {
+ switch(sourceEvent.getEvent())
+ {
+ case REQUEST_START_SAMPLE_STREAM:
+ if(sourceEvent.hasSource() && sourceEvent.getSource() instanceof PolyphaseChannelSource)
+ {
+ startChannelSource((PolyphaseChannelSource)sourceEvent.getSource());
+ }
+ else
+ {
+ mLog.error("Request to start sample stream for unrecognized source: " +
+ (sourceEvent.hasSource() ? sourceEvent.getSource().getClass() : "null source"));
+ }
+ break;
+ case REQUEST_STOP_SAMPLE_STREAM:
+ if(sourceEvent.hasSource() && sourceEvent.getSource() instanceof PolyphaseChannelSource)
+ {
+ stopChannelSource((PolyphaseChannelSource)sourceEvent.getSource());
+ }
+ else
+ {
+ mLog.error("Request to stop sample stream for unrecognized source: " +
+ (sourceEvent.hasSource() ? sourceEvent.getSource().getClass() : "null source"));
+ }
+ break;
+ default:
+ mLog.error("Received unrecognized source event from polyphase channel source [" +
+ sourceEvent.getEvent() + "]");
+ }
+ }
+ }
+}
diff --git a/src/source/tuner/channel/polyphase/PolyphaseChannelSource.java b/src/source/tuner/channel/polyphase/PolyphaseChannelSource.java
new file mode 100644
index 00000000..8b7babaa
--- /dev/null
+++ b/src/source/tuner/channel/polyphase/PolyphaseChannelSource.java
@@ -0,0 +1,224 @@
+/*******************************************************************************
+ * sdrtrunk
+ * Copyright (C) 2014-2017 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 source.tuner.channel.polyphase;
+
+import source.tuner.channel.polyphase.output.IPolyphaseChannelOutputProcessor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import sample.Listener;
+import sample.complex.ComplexBuffer;
+import sample.complex.ComplexToComplexBufferAssembler;
+import source.SourceEvent;
+import source.tuner.channel.TunerChannel;
+import source.tuner.channel.TunerChannelSource;
+
+public class PolyphaseChannelSource extends TunerChannelSource
+{
+ private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelSource.class);
+
+ private ComplexToComplexBufferAssembler mComplexBufferAssembler = new ComplexToComplexBufferAssembler(2500);
+ private IPolyphaseChannelOutputProcessor mPolyphaseChannelOutputProcessor;
+ private double mChannelSampleRate;
+ private long mCenterFrequency;
+ private long mFrequencyCorrection;
+
+ /**
+ * Polyphase channelizer tuner channel source implementation. Adapts the channel array output samples from the
+ * polyphase channelizer into a single channel, or a channel synthesized from two adjacent channels that is
+ * frequency translated and decimated to a single channel.
+ *
+ * @param tunerChannel describing the desired channel frequency and bandwidth/minimum sample rate
+ * @param outputProcessor - to process polyphase channelizer channel results into a channel stream
+ * @param producerSourceEventListener to receive source event requests (e.g. start/stop sample stream)
+ * @param channelSampleRate for the downstream sample output
+ */
+ public PolyphaseChannelSource(TunerChannel tunerChannel, IPolyphaseChannelOutputProcessor outputProcessor,
+ Listener producerSourceEventListener, double channelSampleRate)
+ {
+ super(producerSourceEventListener, tunerChannel);
+ mPolyphaseChannelOutputProcessor = outputProcessor;
+ mChannelSampleRate = channelSampleRate;
+ }
+
+ /**
+ * Registers the listener to receive complex sample buffers from this channel source
+ */
+ @Override
+ public void setListener(final Listener listener)
+ {
+ mComplexBufferAssembler.setListener(listener);
+ }
+
+ /**
+ * Removes the listener from receiving complex sample buffers from this channel source
+ */
+ @Override
+ public void removeListener(Listener listener)
+ {
+ mComplexBufferAssembler.setListener(null);
+ }
+
+ /**
+ * Channel output processor used by this channel source to convert polyphase channel results into a specific
+ * channel complex buffer output stream.
+ */
+ public IPolyphaseChannelOutputProcessor getPolyphaseChannelOutputProcessor()
+ {
+ return mPolyphaseChannelOutputProcessor;
+ }
+
+ /**
+ * Sets/updates the output processor for this channel source, replacing the existing output processor.
+ *
+ * @param outputProcessor to use
+ * @param frequency center for the channels processed by the output processor
+ */
+ public void setPolyphaseChannelOutputProcessor(IPolyphaseChannelOutputProcessor outputProcessor, long frequency)
+ {
+ //Lock on the channel output processor to block the channel results processor thread from servicing the
+ //channel output processor's buffer while we change out the processors.
+ synchronized(mPolyphaseChannelOutputProcessor)
+ {
+ IPolyphaseChannelOutputProcessor existingProcessor = mPolyphaseChannelOutputProcessor;
+
+ //Swap out the processor so that incoming samples can accumulate in the new channel output processor
+ mPolyphaseChannelOutputProcessor = outputProcessor;
+
+ //Remove the buffer overflow listener and clear any previous buffer overflow state
+ existingProcessor.setSourceOverflowListener(null);
+ broadcastOverflowState(false);
+
+ //Register to receive buffer overflow notifications so we can push them up through the source chain
+ mPolyphaseChannelOutputProcessor.setSourceOverflowListener(this);
+
+ //Fully process the residual channel results buffer of the previous channel output processor
+ if(existingProcessor != null)
+ {
+ existingProcessor.processChannelResults(mComplexBufferAssembler);
+ }
+
+ //Finally, setup the frequency offset for the output processor and reset the frequency correction value
+ //to allow consumers to adjust and calculate a new frequency correction value, if needed.
+ mCenterFrequency = frequency;
+ mFrequencyCorrection = 0;
+ mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
+ broadcastConsumerSourceEvent(SourceEvent.frequencyCorrectionChange(mFrequencyCorrection));
+ }
+ }
+
+ /**
+ * Primary method for receiving channel results output from a polyphase channelizer. The results array will be
+ * queued for processing to extract the target channel samples, process them for frequency correction and/or
+ * channel aggregation, and dispatch the results to the downstream sample listener/consumer.
+ *
+ * @param channelResultsBuffer containing the polyphase channelizer output for a single complex sample period.
+ */
+ public void receiveChannelResults(float[] channelResultsBuffer)
+ {
+ mPolyphaseChannelOutputProcessor.receiveChannelResults(channelResultsBuffer);
+ }
+
+ /**
+ * Downstream channel sample rate
+ *
+ * @return sample rate in Hertz
+ */
+ @Override
+ public double getSampleRate()
+ {
+ return mChannelSampleRate;
+ }
+
+ /**
+ * Sets the downstream channel sample rate
+ * @param sampleRate in hertz
+ */
+ @Override
+ protected void setSampleRate(double sampleRate)
+ {
+ mChannelSampleRate = sampleRate;
+ }
+
+ @Override
+ public void dispose()
+ {
+ mLog.info("Polyphase channel source received request to dispose");
+ }
+
+ @Override
+ public long getFrequencyCorrection()
+ {
+ return mFrequencyCorrection;
+ }
+
+ /**
+ * Adjusts the frequency correction value that is being applied to the channelized output stream by the
+ * polyphase channel output processor.
+ *
+ * @param value to apply for frequency correction in hertz
+ */
+ protected void setFrequencyCorrection(long value)
+ {
+ mFrequencyCorrection = value;
+ updateFrequencyOffset();
+ broadcastConsumerSourceEvent(SourceEvent.frequencyCorrectionChange(mFrequencyCorrection));
+ }
+
+ /**
+ * Sets the center frequency for the incoming sample stream channel results and resets frequency correction to zero.
+ * @param frequency in hertz
+ */
+ @Override
+ protected void setFrequency(long frequency)
+ {
+ mCenterFrequency = frequency;
+ setFrequencyCorrection(0);
+ }
+
+ @Override
+ protected void processSamples()
+ {
+ //Lock on the output processor so that it can't be changed out in the middle of processing
+ synchronized(mPolyphaseChannelOutputProcessor)
+ {
+ mPolyphaseChannelOutputProcessor.processChannelResults(mComplexBufferAssembler);
+ }
+ }
+
+ /**
+ * Calculates the frequency offset required to mix the incoming signal to the desired output baseband
+ */
+ private long getFrequencyOffset()
+ {
+ return getTunerChannel().getFrequency() - mCenterFrequency + mFrequencyCorrection;
+ }
+
+ /**
+ * Updates the frequency offset being applied by the output processor. Calculates the offset using the center
+ * frequency of the incoming polyphase channel results and the desired output channel frequency adjusted by any
+ * requested frequency correction value.
+ */
+ private void updateFrequencyOffset()
+ {
+ synchronized(mPolyphaseChannelOutputProcessor)
+ {
+ mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
+ }
+ }
+}
diff --git a/src/source/tuner/channel/polyphase/ScheduledBufferProcessor.java b/src/source/tuner/channel/polyphase/ScheduledBufferProcessor.java
new file mode 100644
index 00000000..810cb838
--- /dev/null
+++ b/src/source/tuner/channel/polyphase/ScheduledBufferProcessor.java
@@ -0,0 +1,171 @@
+/*******************************************************************************
+ * sdrtrunk
+ * Copyright (C) 2014-2017 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 source.tuner.channel.polyphase;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import sample.Listener;
+import sample.OverflowableTransferQueue;
+import sample.real.IOverflowListener;
+import util.ThreadPool;
+
+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 ScheduledBufferProcessor implements Listener
+{
+ private final static Logger mLog = LoggerFactory.getLogger(ScheduledBufferProcessor.class);
+
+ private OverflowableTransferQueue mQueue;
+ private Listener mListener;
+ private ScheduledFuture> mScheduledFuture;
+ private long mDistributionInterval;
+ private int mMaxBuffersPerInterval;
+ private AtomicBoolean mRunning = new AtomicBoolean();
+
+ /**
+ * Scheduled Buffer Processor combines an internal overflowable buffer with a scheduled runnable processing task
+ * for periodically distributing internally queued elements to the registered listener. This processor provides
+ * a convenient way to create a thread-safe buffer for receiving elements from one thread/runnable and then
+ * distributing those elements to a registered listener where distribution occurs on a separate scheduled thread
+ * pool runnable thread. This allows the calling input thread to quickly return without incurring any subsequent
+ * processing workload.
+ *
+ * The internal queue is an overflowable queue implementation that allows a listener to be registered to receive
+ * notifications of overflow and reset state. Queue sizing parameters are specified in the constructor.
+ *
+ * @param maximumSize of the internal queue (overflow happens when this is exceeded)
+ * @param resetThreshold of the internal queue (overflow reset happens once queue size falls below this threshold
+ * @param distributionInterval in milliseconds specifying how often the internal scheduled thread pool buffer
+ * distributor should run
+ * @param maxBuffersPerInterval maximum number of buffers to pull from the internal queue during each distribution
+ * interval
+ */
+ public ScheduledBufferProcessor(int maximumSize, int resetThreshold, long distributionInterval, int maxBuffersPerInterval)
+ {
+ mQueue = new OverflowableTransferQueue<>(maximumSize, resetThreshold);
+
+ mQueue.setOverflowListener(new IOverflowListener()
+ {
+ @Override
+ public void sourceOverflow(boolean overflow)
+ {
+ mLog.error("Overflow state changed - overflow:" + overflow);
+ }
+ });
+ mDistributionInterval = distributionInterval;
+ mMaxBuffersPerInterval = maxBuffersPerInterval;
+ }
+
+ /**
+ * Sets the listener to receive notifications of buffer overflow and/or reset. Note: this method can also be
+ * invoked with a null argument to clear the previously registered overflow listener.
+ *
+ * @param listener to receive overflow/reset notifications.
+ */
+ public void setOverflowListener(IOverflowListener listener)
+ {
+ mQueue.setOverflowListener(listener);
+ }
+
+ /**
+ * Sets or changes the listener to receive buffers from this processor.
+ * @param listener to receive buffers
+ */
+ public void setListener(Listener listener)
+ {
+ mListener = listener;
+ }
+
+ /**
+ * Primary input method for adding buffers to this processor. Note: incoming buffers will be ignored if this
+ * processor is in a stopped state. You must invoke start() to allow incoming buffers and initiate buffer
+ * processing.
+ *
+ * @param e to enqueue for distribution to a registered listener
+ */
+ @Override
+ public void receive(E e)
+ {
+ mQueue.offer(e);
+ }
+
+ /**
+ * Starts this buffer processor and allows queuing of incoming buffers.
+ */
+ public void start()
+ {
+ if(mRunning.compareAndSet(false, true))
+ {
+ mScheduledFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new Processor(), 0, mDistributionInterval, TimeUnit.MILLISECONDS);
+ }
+ }
+
+ /**
+ * Stops this buffer processor, stops queuing of incoming buffers and clears the internal buffer queue.
+ */
+ public void stop()
+ {
+ if(mRunning.compareAndSet(true, false))
+ {
+ if(mScheduledFuture != null)
+ {
+ mScheduledFuture.cancel(true);
+ mScheduledFuture = null;
+ }
+
+ mQueue.clear();
+ }
+ }
+
+ /**
+ * Indicates if this processor is currently running
+ */
+ public boolean isRunning()
+ {
+ return mRunning.get();
+ }
+
+ /**
+ * Processor to service the buffer queue and distribute the buffers to the registered listener
+ */
+ class Processor implements Runnable
+ {
+ private List mBuffers = new ArrayList<>();
+
+ @Override
+ public void run()
+ {
+ mQueue.drainTo(mBuffers, mMaxBuffersPerInterval);
+
+ if(mListener != null)
+ {
+ for(E buffer: mBuffers)
+ {
+ mListener.receive(buffer);
+ }
+ }
+
+ mBuffers.clear();
+ }
+ }
+}
diff --git a/src/source/tuner/channel/polyphase/output/ChannelOutputProcessor.java b/src/source/tuner/channel/polyphase/output/ChannelOutputProcessor.java
new file mode 100644
index 00000000..2a32baa4
--- /dev/null
+++ b/src/source/tuner/channel/polyphase/output/ChannelOutputProcessor.java
@@ -0,0 +1,170 @@
+/*******************************************************************************
+ * sdrtrunk
+ * Copyright (C) 2014-2017 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 source.tuner.channel.polyphase.output;
+
+import dsp.mixer.Oscillator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import sample.OverflowableTransferQueue;
+import sample.complex.Complex;
+import sample.complex.ComplexSampleListener;
+import sample.real.IOverflowListener;
+import source.Source;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputProcessor
+{
+ private final static Logger mLog = LoggerFactory.getLogger(ChannelOutputProcessor.class);
+
+ private OverflowableTransferQueue mChannelResultsQueue;
+ private List mChannelResultsToProcess = new ArrayList<>();
+ private int mMaxResultsToProcess;
+
+ private int mInputChannelCount;
+//TODO: swap this out and use the LowPhaseNoiseOscillator
+ private Oscillator mFrequencyCorrectionMixer;
+ private boolean mFrequencyCorrectionEnabled;
+
+ /**
+ * Base class for polyphase channelizer output channel processing. Provides built-in frequency translation
+ * oscillator support to apply frequency correction to the channel sample stream as requested by sample consumer.
+ *
+ * @param inputChannelCount is the number of input channels for this output processor
+ * @param sampleRate of the output channel. This is used to match the oscillator's sample rate to the output
+ * channel sample rate for frequency translation/correction.
+ */
+ public ChannelOutputProcessor(int inputChannelCount, double sampleRate)
+ {
+ mInputChannelCount = inputChannelCount;
+ mFrequencyCorrectionMixer = new Oscillator(0, sampleRate);
+ mMaxResultsToProcess = (int)(sampleRate / 10) * 2; //process at 100 millis interval, twice the expected inflow rate
+
+ mChannelResultsQueue = new OverflowableTransferQueue<>((int)(sampleRate * 3), (int)(sampleRate * 0.5));
+ }
+
+ /**
+ * Sets the frequency offset to apply to the incoming samples to mix the desired signal to baseband.
+ * @param frequencyOffset in hertz
+ */
+ @Override
+ public void setFrequencyOffset(long frequencyOffset)
+ {
+ mFrequencyCorrectionMixer.setFrequency(frequencyOffset);
+ mFrequencyCorrectionEnabled = (frequencyOffset != 0);
+ }
+
+ @Override
+ public void receiveChannelResults(float[] channelResults)
+ {
+ mChannelResultsQueue.offer(channelResults);
+ }
+
+ /**
+ * Processes all enqueued polyphase channelizer results until the internal queue is empty
+ * @param listener to receive the processed channel results
+ */
+ @Override
+ public void processChannelResults(ComplexSampleListener listener)
+ {
+ try
+ {
+ int count = mChannelResultsQueue.drainTo(mChannelResultsToProcess, mMaxResultsToProcess);
+
+ if(count > 0)
+ {
+ process(mChannelResultsToProcess, listener);
+ }
+ }
+ catch(Throwable throwable)
+ {
+ mLog.error("Error while processing polyphase channel samples", throwable);
+ }
+
+ mChannelResultsToProcess.clear();
+ }
+
+ /**
+ * Sub-class implementation to process one polyphase channelizer result array.
+ * @param channelResults to process
+ * @param listener to receive the channel complex sample
+ */
+ public abstract void process(List channelResults, ComplexSampleListener listener);
+
+
+ /**
+ * Sets the overflow listener to monitor the internal channelizer channel results queue overflow state
+ */
+ public void setSourceOverflowListener(Source source)
+ {
+ mChannelResultsQueue.setSourceOverflowListener(source);
+ }
+
+ /**
+ * Removes the overflow listener from monitoring the internal channelizer channel results queue overflow state
+ */
+ public void removeOverflowListener(IOverflowListener listener)
+ {
+ mChannelResultsQueue.setOverflowListener(null);
+ }
+
+ @Override
+ public int getInputChannelCount()
+ {
+ return mInputChannelCount;
+ }
+
+ /**
+ * Provides frequency correction of the inphase component or simply returns the inphase argument value when no
+ * frequency correction value has been specified.
+ *
+ * @param inphase value of the sample to translate/correct
+ * @param quadrature value of the sample to translate/correct
+ * @return corrected inphase value
+ */
+ protected float getFrequencyCorrectedInphase(float inphase, float quadrature)
+ {
+ return Complex.multiplyInphase(inphase, quadrature, mFrequencyCorrectionMixer.inphase(),
+ mFrequencyCorrectionMixer.quadrature());
+ }
+
+ /**
+ * Provides frequency correction of the quadrature component or simply returns the quadrature argument value when no
+ * frequency correction value has been applied,
+ *
+ * @param inphase value of the sample to translate/correct
+ * @param quadrature value of the sample to translate/correct
+ * @return translated inphase value
+ */
+ protected float getFrequencyCorrectedQuadrature(float inphase, float quadrature)
+ {
+ return Complex.multiplyQuadrature(inphase, quadrature, mFrequencyCorrectionMixer.inphase(),
+ mFrequencyCorrectionMixer.quadrature());
+ }
+
+ /**
+ * Indicates if this channel has a frequency correction offset
+ */
+ public boolean hasFrequencyCorrection()
+ {
+ return mFrequencyCorrectionEnabled;
+ }
+
+}
diff --git a/src/source/tuner/channel/polyphase/output/IPolyphaseChannelOutputProcessor.java b/src/source/tuner/channel/polyphase/output/IPolyphaseChannelOutputProcessor.java
new file mode 100644
index 00000000..f8348990
--- /dev/null
+++ b/src/source/tuner/channel/polyphase/output/IPolyphaseChannelOutputProcessor.java
@@ -0,0 +1,66 @@
+/*******************************************************************************
+ * sdrtrunk
+ * Copyright (C) 2014-2017 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 source.tuner.channel.polyphase.output;
+
+import sample.complex.ComplexSampleListener;
+import source.Source;
+
+import java.util.List;
+
+public interface IPolyphaseChannelOutputProcessor
+{
+ /**
+ * Receive and enqueue output results from the polyphase analysis channelizer
+ * @param channelResults to enqueue
+ */
+ void receiveChannelResults(float[] channelResults);
+
+ /**
+ * Process the channel output channel results queue and deliver the output to the listener
+ *
+ * @param listener to receive the processed channel results
+ */
+ void processChannelResults(ComplexSampleListener listener);
+
+ /**
+ * Sets the desired frequency offset from center. The samples will be mixed with an oscillator set to this offset
+ * frequency to produce an output where the desired signal is centered in the passband.
+ *
+ * @param frequency in hertz
+ */
+ void setFrequencyOffset(long frequency);
+
+ /**
+ * Indicates the number of input channels processed by this output processor
+ * @return
+ */
+ int getInputChannelCount();
+
+ /**
+ * Updates the input polyphase channel index(es) used by this output processor
+ * @param indexes
+ */
+ void setPolyphaseChannelIndices(List indexes);
+
+ /**
+ * Sets the listener to receive buffer overflow notifications
+ * @param source to receive overflow notifications
+ */
+ void setSourceOverflowListener(Source source);
+}
diff --git a/src/source/tuner/channel/polyphase/output/OneChannelOutputProcessor.java b/src/source/tuner/channel/polyphase/output/OneChannelOutputProcessor.java
new file mode 100644
index 00000000..44fd12f9
--- /dev/null
+++ b/src/source/tuner/channel/polyphase/output/OneChannelOutputProcessor.java
@@ -0,0 +1,96 @@
+/*******************************************************************************
+ * sdrtrunk
+ * Copyright (C) 2014-2017 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 source.tuner.channel.polyphase.output;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import sample.complex.ComplexSampleListener;
+
+import java.util.List;
+
+public class OneChannelOutputProcessor extends ChannelOutputProcessor
+{
+ private final static Logger mLog = LoggerFactory.getLogger(OneChannelOutputProcessor.class);
+
+ private int mChannelOffset;
+
+ /**
+ * Processor to extract a single channel from a polyphase channelizer and produce an output I/Q complex sample
+ * from each polyphase channelizer output results array.
+ *
+ * @param sampleRate of the output sample stream.
+ * @param channelIndexes containing a single channel index.
+ */
+ public OneChannelOutputProcessor(double sampleRate, List channelIndexes)
+ {
+ super(1, sampleRate);
+ setPolyphaseChannelIndices(channelIndexes);
+ mLog.debug("Sample Rate:" + sampleRate + " PolyPhase Channel Indexes:" + channelIndexes);
+ }
+
+ /**
+ * Updates this processor to extract a single, specified channel index.
+ * @param indexes containing a single channel index value.
+ * @throws IllegalArgumentException if the list of indexes does not contain a single channel index.
+ */
+ public void setPolyphaseChannelIndices(List indexes)
+ {
+ if(indexes.size() != 1)
+ {
+ throw new IllegalArgumentException("Single channel output processor requires a single index to " +
+ "process - provided indexes " + indexes.toString());
+ }
+
+ //Set the channelized output results offset to twice the channel index to account for each channel having
+ //an I/Q pair
+ mChannelOffset = indexes.get(0) * 2;
+ }
+
+ /**
+ * Extract the channel from the channel results array, apply frequency translation, and deliver the
+ * extracted frequency-corrected channel I/Q sample set to the complex sample listener.
+ *
+ * @param channelResults to process containing a list of channel array of I/Q sample pairs (I0,Q0,I1,Q1...In,Qn)
+ * @param listener to receive the extracted, frequency-translated channel results
+ */
+ @Override
+ public void process(List channelResults, ComplexSampleListener listener)
+ {
+ for(float[] channels: channelResults)
+ {
+ if(channels.length < mChannelOffset + 1)
+ {
+ throw new IllegalArgumentException("Polyphase channelizer output channels array is not large enough to " +
+ "cover this single channel output processor with channel offset [" + mChannelOffset + "]");
+ }
+
+ float i = channels[mChannelOffset];
+ float q = channels[mChannelOffset + 1];
+
+ if(hasFrequencyCorrection())
+ {
+ listener.receive(getFrequencyCorrectedInphase(i, q), getFrequencyCorrectedQuadrature(i, q));
+ }
+ else
+ {
+ listener.receive(i, q);
+ }
+ }
+ }
+}
diff --git a/src/source/tuner/channel/polyphase/output/TwoChannelOutputProcessor.java b/src/source/tuner/channel/polyphase/output/TwoChannelOutputProcessor.java
new file mode 100644
index 00000000..e96818e5
--- /dev/null
+++ b/src/source/tuner/channel/polyphase/output/TwoChannelOutputProcessor.java
@@ -0,0 +1,91 @@
+/*******************************************************************************
+ * sdrtrunk
+ * Copyright (C) 2014-2017 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 source.tuner.channel.polyphase.output;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import sample.complex.ComplexSampleListener;
+
+import java.util.List;
+
+public class TwoChannelOutputProcessor extends ChannelOutputProcessor
+{
+ private final static Logger mLog = LoggerFactory.getLogger(TwoChannelOutputProcessor.class);
+
+ private int mChannelOffset1;
+ private int mChannelOffset2;
+
+ /**
+ * Processor to extract two channels from a polyphase channelizer, synthesize/recombine the channels, apply
+ * frequency translation and frequency correction, down-sample by a factor of two and output an I/Q complex sample.
+ *
+ * @param sampleRate of the output sample stream.
+ * @param channelIndexes containing two channel indices.
+ */
+ public TwoChannelOutputProcessor(int sampleRate, List channelIndexes)
+ {
+ //Set the frequency correction oscillator to 2 x output sample rate since we'll be correcting the frequency
+ //after synthesizing both input channels
+ super(2, sampleRate * 2);
+ setPolyphaseChannelIndices(channelIndexes);
+ }
+
+ /**
+ * Updates this processor to extract the two specified channel indexes.
+ *
+ * @param indexes containing a single channel index value.
+ * @throws IllegalArgumentException if the list of indexes does not contain a single channel index.
+ */
+ public void setPolyphaseChannelIndices(List indexes)
+ {
+ if(indexes.size() != 2)
+ {
+ throw new IllegalArgumentException("Double channel output processor requires two indexes to " +
+ "process - provided indexes " + indexes.toString());
+ }
+
+ //Set the channelized output results offsets to twice the channel index to account for each channel having
+ //an I/Q pair
+ mChannelOffset1 = indexes.get(0) * 2;
+ mChannelOffset2 = indexes.get(1) * 2;
+ }
+
+ /**
+ * Extract the channel from the channel results array, apply frequency translation, and deliver the
+ * extracted frequency-corrected channel I/Q sample set to the complex sample listener.
+ *
+ * @param channelResults to process containing an array of channel I/Q sample pairs (I0,Q0,I1,Q1...In,Qn)
+ * @param listener to receive the extracted, frequency-translated channel results
+ */
+ @Override
+ public void process(List channelResults, ComplexSampleListener listener)
+ {
+ mLog.debug("I got channel results - doing nothing !!");
+// if(channels.length < mChannelOffset + 1)
+// {
+// throw new IllegalArgumentException("Polyphase channelizer output channels array is not large enough to " +
+// "cover this single channel output processor with channel offset [" + mChannelOffset + "]");
+// }
+//
+// float i = channels[mChannelOffset];
+// float q = channels[mChannelOffset + 1];
+
+// listener.receive(getFrequencyCorrectedInphase(i, q), getFrequencyCorrectedQuadrature(i, q));
+ }
+}