mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 08:37:25 -05:00
#39 WIP -
This commit is contained in:
588
src/source/tuner/channel/polyphase/ChannelCalculator.java
Normal file
588
src/source/tuner/channel/polyphase/ChannelCalculator.java
Normal file
@@ -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 <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
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<Integer> 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<Integer> 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<Integer> 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");
|
||||
}
|
||||
}
|
||||
343
src/source/tuner/channel/polyphase/ChannelizerViewer.java
Normal file
343
src/source/tuner/channel/polyphase/ChannelizerViewer.java
Normal file
@@ -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 <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
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<ComplexBuffer>, 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<ComplexBuffer>, 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<ComplexBuffer>()
|
||||
{
|
||||
@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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
384
src/source/tuner/channel/polyphase/PolyphaseChannelManager.java
Normal file
384
src/source/tuner/channel/polyphase/PolyphaseChannelManager.java
Normal file
@@ -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 <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
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<PolyphaseChannelSource> 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<Integer> 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<Integer> 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<SourceEvent> 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<Integer> 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<SourceEvent>
|
||||
{
|
||||
@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() + "]");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
224
src/source/tuner/channel/polyphase/PolyphaseChannelSource.java
Normal file
224
src/source/tuner/channel/polyphase/PolyphaseChannelSource.java
Normal file
@@ -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 <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
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<SourceEvent> 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<ComplexBuffer> listener)
|
||||
{
|
||||
mComplexBufferAssembler.setListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the listener from receiving complex sample buffers from this channel source
|
||||
*/
|
||||
@Override
|
||||
public void removeListener(Listener<ComplexBuffer> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
171
src/source/tuner/channel/polyphase/ScheduledBufferProcessor.java
Normal file
171
src/source/tuner/channel/polyphase/ScheduledBufferProcessor.java
Normal file
@@ -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 <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
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<E> implements Listener<E>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ScheduledBufferProcessor.class);
|
||||
|
||||
private OverflowableTransferQueue<E> mQueue;
|
||||
private Listener<E> 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<E> 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<E> mBuffers = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
mQueue.drainTo(mBuffers, mMaxBuffersPerInterval);
|
||||
|
||||
if(mListener != null)
|
||||
{
|
||||
for(E buffer: mBuffers)
|
||||
{
|
||||
mListener.receive(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
mBuffers.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
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<float[]> mChannelResultsQueue;
|
||||
private List<float[]> 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<float[]> 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
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<Integer> indexes);
|
||||
|
||||
/**
|
||||
* Sets the listener to receive buffer overflow notifications
|
||||
* @param source to receive overflow notifications
|
||||
*/
|
||||
void setSourceOverflowListener(Source source);
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
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<Integer> 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<Integer> 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<float[]> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
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<Integer> 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<Integer> 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<float[]> 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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user