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