diff --git a/src/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java b/src/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java index 3f33436d..814a1b92 100644 --- a/src/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java +++ b/src/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java @@ -105,8 +105,11 @@ public abstract class AbstractComplexPolyphaseChannelizer implements Listener + public class ChannelArrayPanel extends JPanel { - private ComplexPolyphaseChannelizer mPolyphaseChannelizer; - private ChannelDistributor mChannelDistributor; - - public ChannelArrayPanel(float[] taps) + public ChannelArrayPanel() { int bufferSize = CHANNEL_BANDWIDTH / CHANNEL_FFT_FRAME_RATE; if(bufferSize % 2 == 1) @@ -226,10 +189,6 @@ public class ChannelizerViewer extends JFrame bufferSize++; } - mPolyphaseChannelizer = new ComplexPolyphaseChannelizer(taps, mChannelCount, CHANNEL_BANDWIDTH); - mChannelDistributor = new ChannelDistributor(bufferSize, mChannelCount); -// mPolyphaseChannelizer.setChannelDistributor(mChannelDistributor); - init(); } @@ -243,7 +202,8 @@ public class ChannelizerViewer extends JFrame for(int x = 0; x < mChannelCount; x++) { //place the channels left and right of 10.0 MHz - long frequency = 10000000l + ((x * CHANNEL_BANDWIDTH) - halfSpectralBandwidth); + long frequency = mTestTuner.getTunerController().getFrequency() + + ((x * CHANNEL_BANDWIDTH) - halfSpectralBandwidth); ChannelPanel channelPanel = new ChannelPanel(mSettingsManager, CHANNEL_BANDWIDTH * 2, frequency, CHANNEL_BANDWIDTH); channelPanel.setDFTSize(mChannelPanelDFTSize); @@ -255,28 +215,17 @@ public class ChannelizerViewer extends JFrame { add(channelPanel, "grow,push"); } - -// channelPanel.getPolyphaseChannelSource() - - mChannelDistributor.addListener(x, channelPanel); } } - - @Override - public void receive(ComplexBuffer complexBuffer) - { - mPolyphaseChannelizer.receive(complexBuffer); - } } - public class ChannelPanel extends JPanel implements Listener, ISourceEventProcessor + public class PrimarySpectrumPanel extends JPanel implements Listener, ISourceEventProcessor { - private PolyphaseChannelSource mPolyphaseChannelSource; private DFTProcessor mDFTProcessor = new DFTProcessor(SampleType.COMPLEX); private ComplexDecibelConverter mComplexDecibelConverter = new ComplexDecibelConverter(); private SpectrumPanel mSpectrumPanel; - public ChannelPanel(SettingsManager settingsManager, int sampleRate, long frequency, int bandwidth) + public PrimarySpectrumPanel(SettingsManager settingsManager, int sampleRate) { setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill]")); mSpectrumPanel = new SpectrumPanel(settingsManager); @@ -285,16 +234,6 @@ public class ChannelizerViewer extends JFrame mDFTProcessor.addConverter(mComplexDecibelConverter); mDFTProcessor.process(SourceEvent.sampleRateChange(sampleRate)); mComplexDecibelConverter.addListener(mSpectrumPanel); - - TunerChannel tunerChannel = new TunerChannel(frequency, bandwidth); - mPolyphaseChannelSource = new PolyphaseChannelSource(this, tunerChannel); - mPolyphaseChannelSource.setListener(mDFTProcessor); - mPolyphaseChannelSource.start(ThreadPool.SCHEDULED); - } - - public PolyphaseChannelSource getPolyphaseChannelSource() - { - return mPolyphaseChannelSource; } public void setDFTSize(DFTSize dftSize) @@ -315,12 +254,57 @@ public class ChannelizerViewer extends JFrame } } - public class SampleGenerationTask implements Runnable + public class ChannelPanel extends JPanel implements Listener, ISourceEventProcessor { - @Override - public void run() + private PolyphaseChannelSource mSource; + private DFTProcessor mDFTProcessor = new DFTProcessor(SampleType.COMPLEX); + private ComplexDecibelConverter mComplexDecibelConverter = new ComplexDecibelConverter(); + private SpectrumPanel mSpectrumPanel; + + public ChannelPanel(SettingsManager settingsManager, int sampleRate, long frequency, int bandwidth) { - generateSamples(); + 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); + + TunerChannel tunerChannel = new TunerChannel(frequency, bandwidth); + mSource = (PolyphaseChannelSource)mTestTuner.getChannelSource(tunerChannel); + + if(mSource != null) + { + mSource.setListener(mDFTProcessor); + mSource.start(ThreadPool.SCHEDULED); + } + else + { + mLog.error("Couldn't get a source from the tuner for frequency: " + frequency); + } + } + + public PolyphaseChannelSource getSource() + { + return mSource; + } + + 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"); } } @@ -338,7 +322,6 @@ public class ChannelizerViewer extends JFrame public void run() { frame.setVisible(true); - frame.start(); } }); } diff --git a/src/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java b/src/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java index 2eaa64b6..dd9f32a1 100644 --- a/src/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java +++ b/src/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java @@ -21,11 +21,11 @@ package dsp.filter.channelizer; import buffer.ReverseFloatCircularBuffer; import dsp.filter.FilterFactory; import dsp.filter.Window; +import dsp.filter.design.FilterDesignException; import dsp.mixer.Oscillator; import org.jtransforms.fft.FloatFFT_1D; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sample.complex.ComplexBuffer; import java.text.DecimalFormat; @@ -33,6 +33,8 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann { private final static Logger mLog = LoggerFactory.getLogger(ComplexPolyphaseChannelizerM2.class); + private static final int DEFAULT_CHANNEL_BANDWIDTH = 12500; + private ReverseFloatCircularBuffer[] mDataBuffers; private float[][] mFilterCoefficients; private int mFilterPointer = 0; @@ -48,8 +50,10 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann * equal to the channel bandwidth (sample rate / filters). If you need to synthesize (combine two or more * filter outputs) a new bandwidth signal from the outputs of this filter, then the filter should be designed * as a nyquist filter with -6 dB attenuation at the channel bandwidth cutoff frequency + * * @param channels - number of filters/channels to output. Since this filter bank oversamples each filter * output, this number must be even (divisible by the oversample rate). + * * @param channelSampleRate for each decimated, oversampled channel */ public ComplexPolyphaseChannelizerM2(float[] taps, int channels, int channelSampleRate) @@ -64,6 +68,72 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann initFilters(taps); } + /** + * Non-Maximally Decimated Polyphase Filter Bank (NMDPFB) channelizer that divides the input frequency band into + * equal bandwidth channels that are each oversampled by 2x for output. + * + * Note: this constructor uses a default channel bandwidth of 12.5 kHz and produces channels with an oversampled + * channel sample rate of 25.0 kHz. + * + * @param sampleRate to be channelized. + */ + private ComplexPolyphaseChannelizerM2(int sampleRate) throws FilterDesignException + { + super(sampleRate / DEFAULT_CHANNEL_BANDWIDTH, DEFAULT_CHANNEL_BANDWIDTH); + + mLog.debug("Sample Rate: " + sampleRate); + + float[] filterTaps = FilterFactory.getSincChannelizer(getChannelSampleRate(), getChannelCount(), + 15, Window.WindowType.BLACKMAN_HARRIS_7, true); + + initFilters(filterTaps); + } + + /** + * Non-Maximally Decimated Polyphase Filter Bank (NMDPFB) channelizer with a default channel bandwidth of 12.5 kHz + * and a channel output sample rate of 25.0 kHz. + * + * @param sampleRate that is an even multiple of 25.0 kHz + * @return channelizer + * @throws IllegalArgumentException if the sample rate argument is not an even multiple of 25.0 kHz + */ + public static ComplexPolyphaseChannelizerM2 create(int sampleRate) throws IllegalArgumentException, + FilterDesignException + { + if(sampleRate % (2 * DEFAULT_CHANNEL_BANDWIDTH) != 0) + { + throw new IllegalArgumentException("Sample rate must be an even multiple of 25 kHz"); + } + + return new ComplexPolyphaseChannelizerM2(sampleRate); + } + + /** + * Updates this channelizer to use the new sample rate. This method creates a new filter suitable for the + * sample rate and reinitializes all internal data structures to prepare for processing the new sample rate. + * @param sampleRate that is a multiple of 25.0 kHz + * @throws IllegalArgumentException if the sample rate is not a multiple of 25.0 kHz + * @throws FilterDesignException if a filter cannot be designed for the target sample rate + */ + public void updateSampleRate(int sampleRate) throws IllegalArgumentException, FilterDesignException + { + if(sampleRate % (2 * DEFAULT_CHANNEL_BANDWIDTH) != 0) + { + throw new IllegalArgumentException("Sample rate must be an even multiple of 25 kHz"); + } + + float[] filterTaps = FilterFactory.getSincChannelizer(getChannelSampleRate(), getChannelCount(), + 15, Window.WindowType.BLACKMAN_HARRIS_7, true); + + initFilters(filterTaps); + } + + /** + * Filters the incoming complex sample pair + * + * @param inphase sample + * @param quadrature sample + */ protected void filter(float inphase, float quadrature) { try diff --git a/src/dsp/filter/channelizer/PolyphaseChannelManager.java b/src/dsp/filter/channelizer/PolyphaseChannelManager.java index 8751cec4..b640ddec 100644 --- a/src/dsp/filter/channelizer/PolyphaseChannelManager.java +++ b/src/dsp/filter/channelizer/PolyphaseChannelManager.java @@ -21,6 +21,7 @@ package dsp.filter.channelizer; import dsp.filter.channelizer.output.IPolyphaseChannelOutputProcessor; import dsp.filter.channelizer.output.OneChannelOutputProcessor; import dsp.filter.channelizer.output.TwoChannelOutputProcessor; +import dsp.filter.design.FilterDesignException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sample.Listener; @@ -34,28 +35,29 @@ import source.tuner.TunerChannel; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; -public class PolyphaseChannelManager implements ISourceEventProcessor, Listener +public class PolyphaseChannelManager implements Listener { private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelManager.class); private static final int CHANNEL_BANDWIDTH = 12500; - private static final int CHANNEL_SAMPLE_RATE = CHANNEL_BANDWIDTH * 2; private static final double CHANNEL_OVERSAMPLING = 2.0; + private static final int CHANNEL_SAMPLE_RATE = (int)(CHANNEL_BANDWIDTH * CHANNEL_OVERSAMPLING); private Tuner mTuner; private List mChannelSources = new CopyOnWriteArrayList<>(); private ChannelIndexCalculator mChannelIndexCalculator; private ComplexPolyphaseChannelizerM2 mPolyphaseChannelizer; private ChannelSourceEventListener mChannelSourceEventListener = new ChannelSourceEventListener(); + private ScheduledBufferProcessor mBufferProcessor; /** - * Polyphase Channel Manager is a DDC channel manager 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. + * 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. * - * @param tuner providing broadband sample buffers + * @param tuner providing complex sample buffers */ public PolyphaseChannelManager(Tuner tuner) { @@ -64,25 +66,29 @@ public class PolyphaseChannelManager implements ISourceEventProcessor, Listener< throw new IllegalArgumentException("Tuner argument cannot be null"); } + mTuner = tuner; + int sampleRate = mTuner.getTunerController().getSampleRate(); //Ensure that tuner sample rate is a multiple of channel sample rate since polyphase channelizer is M2 if(sampleRate % (CHANNEL_SAMPLE_RATE) != 0) { - throw new IllegalArgumentException("Tuner sample rate [" + sampleRate + "] must be a multiple of " + + throw new IllegalArgumentException("Tuner sample rate [" + sampleRate + "] must be an even multiple of " + CHANNEL_SAMPLE_RATE + " Hz"); } - mTuner = tuner; - - int channelCount = sampleRate / CHANNEL_BANDWIDTH; - long frequency = mTuner.getTunerController().getFrequency(); + int channelCount = sampleRate / CHANNEL_BANDWIDTH; mChannelIndexCalculator = new ChannelIndexCalculator(frequency, channelCount, CHANNEL_BANDWIDTH, CHANNEL_OVERSAMPLING); + mBufferProcessor = new ScheduledBufferProcessor(500, 100, 100, 50); + + updateChannelizer(sampleRate, null); + mTuner.addSourceEventListener(this); + } /** @@ -103,7 +109,9 @@ public class PolyphaseChannelManager implements ISourceEventProcessor, Listener< if(outputProcessor != null) { - channelSource = new PolyphaseChannelSource(tunerChannel, outputProcessor, this, CHANNEL_SAMPLE_RATE); + channelSource = new PolyphaseChannelSource(tunerChannel, outputProcessor, mChannelSourceEventListener, + CHANNEL_SAMPLE_RATE); + mChannelSources.add(channelSource); } } @@ -117,7 +125,13 @@ public class PolyphaseChannelManager implements ISourceEventProcessor, Listener< return channelSource; } - private IPolyphaseChannelOutputProcessor getOutputProcessor(List indexes) throws IllegalArgumentException + /** + * 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()) { @@ -127,6 +141,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor, Listener< 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; } } @@ -139,11 +154,15 @@ public class PolyphaseChannelManager implements ISourceEventProcessor, Listener< */ private void startChannelSource(PolyphaseChannelSource channelSource) { - mPolyphaseChannelizer.addChannel(channelSource); - - if(mPolyphaseChannelizer.getRegisteredChannelCount() == 1) + synchronized(mBufferProcessor) { - mTuner.addListener(mPolyphaseChannelizer); + mPolyphaseChannelizer.addChannel(channelSource); + + if(mPolyphaseChannelizer.getRegisteredChannelCount() == 1) + { + mTuner.addListener(mBufferProcessor); + mBufferProcessor.start(); + } } } @@ -155,43 +174,91 @@ public class PolyphaseChannelManager implements ISourceEventProcessor, Listener< */ private void stopChannelSource(PolyphaseChannelSource channelSource) { - mChannelSources.remove(channelSource); - - if(mPolyphaseChannelizer.getRegisteredChannelCount() == 0) + synchronized(mBufferProcessor) { - mTuner.removeListener(mPolyphaseChannelizer); + mChannelSources.remove(channelSource); + + if(mPolyphaseChannelizer.getRegisteredChannelCount() == 0) + { + mTuner.removeListener(mBufferProcessor); + mBufferProcessor.stop(); + } } } /** - * Processes source event notifications from the tuner or source event requests from any of the child polyphase - * channel sources. - */ - @Override - public void process(SourceEvent sourceEvent) - { - switch(sourceEvent.getEvent()) - { - case NOTIFICATION_FREQUENCY_CHANGE: - mChannelIndexCalculator.setCenterFrequency(sourceEvent.getValue().longValue()); - updateOutputProcessors(sourceEvent); - break; - case NOTIFICATION_SAMPLE_RATE_CHANGE: - int sampleRate = sourceEvent.getValue().intValue(); - int channelCount = sampleRate / mChannelIndexCalculator.getChannelBandwidth(); - mChannelIndexCalculator.setChannelCount(channelCount); - updateOutputProcessors(sourceEvent); - break; - } - } - - /** - * Implements the Listener interface by delegating to the process(sourceEvent) method. + * Implements the Listener interface for receiving notifications of frequency and sample rate + * changes from the tuner. */ @Override public void receive(SourceEvent sourceEvent) { - process(sourceEvent); + switch(sourceEvent.getEvent()) + { + case NOTIFICATION_FREQUENCY_CHANGE: + //Update the index calculator and each of the output processors to update each channel frequency + mChannelIndexCalculator.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(int sampleRate, SourceEvent sourceEvent) + { + mChannelIndexCalculator.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 = ComplexPolyphaseChannelizerM2.create(sampleRate); + mBufferProcessor.setListener(mPolyphaseChannelizer); + } + else + { + mPolyphaseChannelizer.updateSampleRate(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(); + } + } } /** diff --git a/src/dsp/filter/channelizer/ScheduledBufferProcessor.java b/src/dsp/filter/channelizer/ScheduledBufferProcessor.java new file mode 100644 index 00000000..d1513219 --- /dev/null +++ b/src/dsp/filter/channelizer/ScheduledBufferProcessor.java @@ -0,0 +1,161 @@ +/******************************************************************************* + * 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 dsp.filter.channelizer; + +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 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); + 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) + { + if(mRunning.get()) + { + 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/dsp/mixer/LowPhaseNoiseOscillator.java b/src/dsp/mixer/LowPhaseNoiseOscillator.java index ad190e2d..160d0e1e 100644 --- a/src/dsp/mixer/LowPhaseNoiseOscillator.java +++ b/src/dsp/mixer/LowPhaseNoiseOscillator.java @@ -31,7 +31,7 @@ public class LowPhaseNoiseOscillator private static final double TWO_PI = 2.0 * Math.PI; private static final float THREE_HALVES = 3.0f / 2.0f; private int mSampleRate; - private int mFrequency; + private long mFrequency; private float mInphase = 1.0f; private float mQuadrature = 0.0f; private float mPreviousInphase = 1.0f; @@ -46,7 +46,7 @@ public class LowPhaseNoiseOscillator * @param sampleRate for the oscillator * @param frequency to generate */ - public LowPhaseNoiseOscillator(int sampleRate, int frequency) + public LowPhaseNoiseOscillator(int sampleRate, long frequency) { mSampleRate = sampleRate; mFrequency = frequency; @@ -54,18 +54,40 @@ public class LowPhaseNoiseOscillator update(); } + /** + * Sets the sample rate for this oscillator + */ public void setSampleRate(int sampleRate) { mSampleRate = sampleRate; update(); } - public void setFrequency(int frequency) + /** + * Sample rate for this oscillator + */ + public int getSampleRate() + { + return mSampleRate; + } + + /** + * Sets the tone frequency generated by this oscillator + */ + public void setFrequency(long frequency) { mFrequency = frequency; update(); } + /** + * Current tone frequency for this oscillator + */ + public long getFrequency() + { + return mFrequency; + } + /** * Updates the internal values after a frequency or sample rate change */ diff --git a/src/sample/Broadcaster.java b/src/sample/Broadcaster.java index d4f9cce1..b3bf951a 100644 --- a/src/sample/Broadcaster.java +++ b/src/sample/Broadcaster.java @@ -1,17 +1,17 @@ /******************************************************************************* * SDR Trunk * Copyright (C) 2014 Dennis Sheirer - * + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with this program. If not, see ******************************************************************************/ @@ -25,62 +25,100 @@ import java.util.concurrent.CopyOnWriteArrayList; */ public class Broadcaster implements Listener { - private List> mListeners = new CopyOnWriteArrayList<>(); + private List> mListeners = new CopyOnWriteArrayList<>(); - @Override - public void receive( T t ) + /** + * Implements the Listener interface to receive an element and broadcast that element to all registered + * listeners. + * + * @param t element to broadcast + */ + @Override + public void receive(T t) { - broadcast( t ); + broadcast(t); } - - /** - * Clear listeners to prepare for garbage collection - */ - public void dispose() - { - mListeners.clear(); - } - - public boolean hasListeners() - { - return !mListeners.isEmpty(); - } - - public int getListenerCount() - { - return mListeners.size(); - } - - public List> getListeners() - { - return mListeners; - } - - public void addListener( Listener listener ) - { - if(listener == null) - { - throw new IllegalArgumentException("Listener cannot be null"); - } - mListeners.add( listener ); - } - - public void removeListener( Listener listener ) - { - mListeners.remove( listener ); - } - - public void clear() - { - mListeners.clear(); - } - - public void broadcast( T t ) + /** + * Clear listeners to prepare for garbage collection + */ + public void dispose() { - for( Listener listener: mListeners ) - { - listener.receive( t ); - } + mListeners.clear(); + } + + /** + * Indicates if this broadcaster has any listeners registered + */ + public boolean hasListeners() + { + return !mListeners.isEmpty(); + } + + /** + * Indicates if the listener is currently registered with this broadcaster + */ + public boolean hasListener(Listener listener) + { + return listener != null && mListeners.contains(listener); + } + + /** + * The count of listeners currently registered with this broadcaster + */ + public int getListenerCount() + { + return mListeners.size(); + } + + /** + * The list of listeners currently registered with this broadcaster + */ + public List> getListeners() + { + return mListeners; + } + + /** + * Registers the listener to receive elements from this broadcaster + * + * @param listener + */ + public void addListener(Listener listener) + { + if(listener != null && !mListeners.contains(listener)) + { + mListeners.add(listener); + } + } + + /** + * Deregisters the listener from receiving elements from this broadcaster + */ + public void removeListener(Listener listener) + { + if(listener != null && mListeners.contains(listener)) + { + mListeners.remove(listener); + } + } + + /** + * Deregisters all listeners from this broadcaster + */ + public void clear() + { + mListeners.clear(); + } + + /** + * Broadcasts the element to all registered listeners + */ + public void broadcast(T t) + { + for(Listener listener : mListeners) + { + listener.receive(t); + } } } diff --git a/src/source/InvalidFrequencyException.java b/src/source/InvalidFrequencyException.java index 77e1bee7..e60a8843 100644 --- a/src/source/InvalidFrequencyException.java +++ b/src/source/InvalidFrequencyException.java @@ -54,4 +54,11 @@ public class InvalidFrequencyException extends SourceException { return mValidFrequency; } + + @Override + public String getMessage() + { + return super.getMessage() + " Invalid Frequency: " + getInvalidFrequency() + + " - current valid frequency:" + getValidFrequency(); + } } diff --git a/src/source/tuner/MixerTuner.java b/src/source/tuner/MixerTuner.java index 37f79bed..c873aa8c 100644 --- a/src/source/tuner/MixerTuner.java +++ b/src/source/tuner/MixerTuner.java @@ -123,7 +123,7 @@ public abstract class MixerTuner extends Tuner implements Listener> mSampleListeners = new CopyOnWriteArrayList<>(); + private Broadcaster mSampleBroadcaster = new Broadcaster<>(); protected List> mTunerChangeListeners = new ArrayList<>(); protected Broadcaster mSourceEventBroadcaster = new Broadcaster<>(); @@ -89,7 +85,7 @@ public abstract class Tuner implements ITunerChannelProvider public void dispose() { - mSampleListeners.clear(); + mSampleBroadcaster.clear(); } public void setName(String name) @@ -161,7 +157,7 @@ public abstract class Tuner implements ITunerChannelProvider */ public void addListener(Listener listener) { - mSampleListeners.add(listener); + mSampleBroadcaster.addListener(listener); } /** @@ -169,7 +165,15 @@ public abstract class Tuner implements ITunerChannelProvider */ public void removeListener(Listener listener) { - mSampleListeners.remove(listener); + mSampleBroadcaster.removeListener(listener); + } + + /** + * Provides access to the internal sample broadcaster for sub-class use (ie protected). + */ + protected Broadcaster getSampleBroadcaster() + { + return mSampleBroadcaster; } /** @@ -179,7 +183,15 @@ public abstract class Tuner implements ITunerChannelProvider */ public boolean hasListener(Listener listener) { - return mSampleListeners.contains(listener); + return mSampleBroadcaster.hasListener(listener); + } + + /** + * Indicates if this tuner has any complex buffer listeners registered + */ + public boolean hasListeners() + { + return mSampleBroadcaster.hasListeners(); } /** @@ -187,10 +199,7 @@ public abstract class Tuner implements ITunerChannelProvider */ public void broadcast(ComplexBuffer sampleBuffer) { - for(Listener listener : mSampleListeners) - { - listener.receive(sampleBuffer); - } + mSampleBroadcaster.broadcast(sampleBuffer); } /** diff --git a/src/source/tuner/TunerClass.java b/src/source/tuner/TunerClass.java index 124aefe5..9d898cda 100644 --- a/src/source/tuner/TunerClass.java +++ b/src/source/tuner/TunerClass.java @@ -51,7 +51,8 @@ public enum TunerClass TERRATEC_NOXON_REV2( TunerType.ELONICS_E4000, "0CCD", "00E0", "Terratec", "NOXON R2" ), TERRATEC_T_STICK_PLUS( TunerType.ELONICS_E4000, "0CCD", "00D7", "Terratec", "T Stick Plus" ), TWINTECH_UT40( TunerType.FITIPOWER_FC0013, "1B80", "D3A4", "Twintech", "UT-40" ), - ZAAPA_ZTMINDVBZP( TunerType.FITIPOWER_FC0012, "1B80", "D398", "Zaapa", "ZT-MINDVBZP" ), + ZAAPA_ZTMINDVBZP( TunerType.FITIPOWER_FC0012, "1B80", "D398", "Zaapa", "ZT-MINDVBZP" ), + TEST_TUNER(TunerType.TEST, "0", "0", "ABC Tuners Inc.", "Model XYZ"), UNKNOWN( TunerType.UNKNOWN, "0", "0", "Unknown Manufacturer", "Unknown Device" ); private TunerType mTunerType; diff --git a/src/source/tuner/TunerController.java b/src/source/tuner/TunerController.java index be32e591..8fd2cc54 100644 --- a/src/source/tuner/TunerController.java +++ b/src/source/tuner/TunerController.java @@ -104,7 +104,7 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor public int getSampleRate() { - return mFrequencyController.getBandwidth(); + return mFrequencyController.getSampleRate(); } public double getFrequencyCorrection() diff --git a/src/source/tuner/TunerType.java b/src/source/tuner/TunerType.java index 65b293f3..156c6dd6 100644 --- a/src/source/tuner/TunerType.java +++ b/src/source/tuner/TunerType.java @@ -32,6 +32,7 @@ public enum TunerType RAFAELMICRO_R820T( "R820T" ), RAFAELMICRO_R828D( "R828D" ), RTL2832_VARIOUS( "Generic" ), + TEST("Test"), UNKNOWN( "Unknown" ); private String mLabel; diff --git a/src/source/tuner/frequency/FrequencyController.java b/src/source/tuner/frequency/FrequencyController.java index 7a4d83de..7403ad64 100644 --- a/src/source/tuner/frequency/FrequencyController.java +++ b/src/source/tuner/frequency/FrequencyController.java @@ -50,7 +50,7 @@ public class FrequencyController } /** - * Get bandwidth/sample rate in hertz + * Get bandwidth in hertz */ public int getBandwidth() { @@ -67,6 +67,14 @@ public class FrequencyController broadcastSampleRateChange(); } + /** + * Get sample rate in hertz + */ + public int getSampleRate() + { + return mSampleRate; + } + /** * Get frequency in hertz */ diff --git a/src/source/tuner/test/SampleGenerator.java b/src/source/tuner/test/SampleGenerator.java new file mode 100644 index 00000000..723482c3 --- /dev/null +++ b/src/source/tuner/test/SampleGenerator.java @@ -0,0 +1,147 @@ +/******************************************************************************* + * 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.test; + +import dsp.mixer.LowPhaseNoiseOscillator; +import sample.Listener; +import sample.complex.ComplexBuffer; +import util.ThreadPool; + +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +public class SampleGenerator +{ + private LowPhaseNoiseOscillator mOscillator; + private Listener mListener; + private long mInterval; + private int mSamplesPerInterval; + private ScheduledFuture mScheduledFuture; + + /** + * Generates complex sample buffers at the specified sample rate with a unity gain tone at the specified + * frequency. This generator runs via a scheduled thread pool and generates samples at the specified time + * interval. + * + * @param sampleRate of the complex samples + * @param frequency of the tone produced by the generator + * @param interval in milliseconds for generating samples + */ + public SampleGenerator(int sampleRate, long frequency, long interval) + { + mOscillator = new LowPhaseNoiseOscillator(sampleRate, frequency); + mInterval = interval; + + updateSamplesPerInterval(); + } + + /** + * Updates the number of complex samples generated per interval + */ + private void updateSamplesPerInterval() + { + mSamplesPerInterval = (int)((double)mOscillator.getSampleRate() * ((double)mInterval / 1000.0) * 2.0); + } + + /** + * Starts the generator producing samples + */ + public void start() + { + if(mScheduledFuture == null) + { + mScheduledFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new Generator(), 0, mInterval, + TimeUnit.MILLISECONDS); + } + else + { + throw new IllegalStateException("Sample generator is already started"); + } + } + + /** + * Stops the generator from producing samples + */ + public void stop() + { + if(mScheduledFuture != null) + { + mScheduledFuture.cancel(true); + mScheduledFuture = null; + } + else + { + throw new IllegalStateException("Sample generator is already stopped"); + } + } + + /** + * Registers the listener to receive samples once the generator starts. Note: invoke this method with null + * argument to de-register the listener. + * + * @param listener to receive complex sample buffers + */ + public void setListener(Listener listener) + { + mListener = listener; + } + + /** + * Updates the tone frequency for this generator + * @param frequency to generate + */ + public void setFrequency(long frequency) + { + mOscillator.setFrequency(frequency); + } + + /** + * Current tone frequency for this generator + */ + public long getFrequency() + { + return mOscillator.getFrequency(); + } + + /** + * Updates the sample rate for this generator + * @param sampleRate in hertz for complex samples + */ + public void setSampleRate(int sampleRate) + { + mOscillator.setSampleRate(sampleRate); + updateSamplesPerInterval(); + } + + /** + * Generates a complex sample buffer and distributes the buffer to a registered listener + */ + public class Generator implements Runnable + { + @Override + public void run() + { + if(mListener != null) + { + float[] samples = mOscillator.generate(mSamplesPerInterval); + mListener.receive(new ComplexBuffer(samples)); + } + } + } +} diff --git a/src/source/tuner/test/TestTuner.java b/src/source/tuner/test/TestTuner.java new file mode 100644 index 00000000..e80657f2 --- /dev/null +++ b/src/source/tuner/test/TestTuner.java @@ -0,0 +1,180 @@ +/******************************************************************************* + * 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.test; + +import dsp.filter.channelizer.PolyphaseChannelManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sample.Listener; +import sample.complex.ComplexBuffer; +import source.Source; +import source.SourceException; +import source.tuner.Tuner; +import source.tuner.TunerChannel; +import source.tuner.TunerChannelSource; +import source.tuner.TunerClass; +import source.tuner.TunerType; + +import java.util.concurrent.RejectedExecutionException; + +/** + * Testing tuner that implements an internal oscillator to output a unity gain tone at a specified frequency offset + * from a configurable center tune frequency and sample rate + */ +public class TestTuner extends Tuner +{ + private final static Logger mLog = LoggerFactory.getLogger(TestTuner.class); + private static int mInstanceCounter = 1; + private final int mInstanceID = mInstanceCounter++; + + private PolyphaseChannelManager mPolyphaseChannelManager; + + public TestTuner() + { + super("Test Tuner", new TestTunerController()); + + mLog.debug("Initializing test tuner. Tuner Controller's sample rate is:" + getTunerController().getSampleRate()); + mLog.debug("Initializing test tuner. Test Tuner Controller's sample rate is:" + getTestTunerController().getSampleRate()); + + mPolyphaseChannelManager = new PolyphaseChannelManager(this); + } + + /** + * Returns the tuner controller cast as a test tuner controller. + * + * Note: this is a temporary method until the tuner controller interface can be updated with the start(listener) + * and stop() methods. + */ + private TestTunerController getTestTunerController() + { + return (TestTunerController)getTunerController(); + } + + /** + * Sets the center tuned frequency for this tuner + * @param frequency + * @throws SourceException + */ + public void setFrequency(long frequency) throws SourceException + { + getTunerController().setFrequency(frequency); + } + + public void setToneFrequency(long frequency) + { + getTestTunerController().setToneFrequency(frequency); + } + + public long getToneFrequency() + { + return getTestTunerController().getToneFrequency(); + } + + public void setSampleRate(int sampleRate) throws SourceException + { + getTestTunerController().setSampleRate(sampleRate); + } + + public int getSampleRate() + { + return getTestTunerController().getSampleRate(); + } + + @Override + public String getUniqueID() + { + return getName() + "-" + mInstanceID; + } + + @Override + public TunerClass getTunerClass() + { + return TunerClass.TEST_TUNER; + } + + @Override + public TunerType getTunerType() + { + return TunerClass.TEST_TUNER.getTunerType(); + } + + @Override + public double getSampleSize() + { + return 16.0; + } + + /** + * Creates a polyphase channel source. The provided channel is a one-use channel that can be commanded to start + * and stop processing via the source event interface. Once stopped, the channel is deregistered from this tuner + * and is no longer usable -- a new channel source must be requested from the tuner. + * + * @param tunerChannel identifying the channel center frequency and bandwidth + * @return channel source or null if the channel source cannot be provided by this tuner + */ + public Source getChannelSource(TunerChannel tunerChannel) + { + return mPolyphaseChannelManager.getChannel(tunerChannel); + } + + @Override + public TunerChannelSource getChannel(TunerChannel channel) throws RejectedExecutionException, SourceException + { + mLog.error("This method is deprecated in this test tuner implementation"); + throw new IllegalStateException("Method deprecated - this test tuner cannot provide a TunerChannelSource"); + } + + @Override + public void releaseChannel(TunerChannelSource source) + { + mLog.error("This method is deprecated in this test tuner implementation"); + } + + /** + * Adds the listener to receive complex sample buffers and auto-starts the sample stream if this is the first + * registered listener. + */ + @Override + public void addListener(Listener listener) + { + boolean isRunning = hasListeners(); + + super.addListener(listener); + + if(!isRunning) + { + getTestTunerController().start(getSampleBroadcaster()); + } + } + + /** + * Removes the listener from receiving complex sample buffers and auto-stops the sample stream when there are + * no more listeners. + */ + @Override + public void removeListener(Listener listener) + { + super.removeListener(listener); + + if(!hasListeners()) + { + getTestTunerController().stop(); + } + } +} diff --git a/src/source/tuner/test/TestTunerController.java b/src/source/tuner/test/TestTunerController.java new file mode 100644 index 00000000..9878202e --- /dev/null +++ b/src/source/tuner/test/TestTunerController.java @@ -0,0 +1,143 @@ +/******************************************************************************* + * 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.test; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sample.Listener; +import sample.complex.ComplexBuffer; +import source.SourceException; +import source.tuner.TunerController; +import source.tuner.configuration.TunerConfiguration; + +public class TestTunerController extends TunerController +{ + private final static Logger mLog = LoggerFactory.getLogger(TestTunerController.class); + + public static final long MINIMUM_FREQUENCY = 1000000l; //1 MHz + public static final long MAXIMUM_FREQUENCY = 10000000000l; //10 GHz + public static final long DEFAULT_FREQUENCY = 100000000; //100 MHz + public static final int DEFAULT_SAMPLE_RATE = 1000000; //1 MHz + public static final int DC_NOISE_BANDWIDTH = 5000; // +/-5 kHz + public static final double USABLE_BANDWIDTH = 0.90; //90% usable bandwidth - 5% unusable on either end + + private SampleGenerator mSampleGenerator; + + /** + * Tuner controller testing implementation. + */ + public TestTunerController() + { + super(MINIMUM_FREQUENCY, MAXIMUM_FREQUENCY, DC_NOISE_BANDWIDTH, USABLE_BANDWIDTH); + + mSampleGenerator = new SampleGenerator(DEFAULT_SAMPLE_RATE, DEFAULT_FREQUENCY, 100); + + try + { + mLog.debug("Setting frequency ..."); + mFrequencyController.setFrequency(DEFAULT_FREQUENCY); + mLog.debug("Setting sample rate of freq controller to: " + DEFAULT_SAMPLE_RATE); + mFrequencyController.setSampleRate(DEFAULT_SAMPLE_RATE); + } + catch(Exception e) + { + mLog.error("Error!", e); + } + } + + /** + * Starts the tuner controller producing samples that will be delivered to the specified listener + */ + public void start(Listener listener) + { + mSampleGenerator.setListener(listener); + mSampleGenerator.start(); + } + + /** + * Stops the tuner controller sample generator and deregisters the listener + */ + public void stop() + { + mSampleGenerator.stop(); + mSampleGenerator.setListener(null); + } + + @Override + public void apply(TunerConfiguration config) throws SourceException + { + mLog.error("Request to apply tuner configuration was ignored"); + } + + /** + * Current center frequency for this tuner + * @throws SourceException + */ + @Override + public long getTunedFrequency() throws SourceException + { + return mSampleGenerator.getFrequency(); + } + + /** + * Sets the center frequency for this tuner + * @param frequency in hertz + * @throws SourceException + */ + @Override + public void setTunedFrequency(long frequency) throws SourceException + { + mSampleGenerator.setFrequency(frequency); + } + + /** + * Sets the tone frequency for the sample generator. The tone frequency is offset from the center tuned frequency. + * @param toneFrequency + */ + public void setToneFrequency(long toneFrequency) + { + mSampleGenerator.setFrequency(toneFrequency - getFrequency()); + } + + /** + * Current tone frequency for this test controller. + * @return + */ + public long getToneFrequency() + { + return getFrequency() + mSampleGenerator.getFrequency(); + } + + /** + * Current sample rate for this tuner controller + */ + @Override + public int getCurrentSampleRate() throws SourceException + { + return mFrequencyController.getSampleRate(); + } + + /** + * Sets the sample rate for this tuner controller + */ + public void setSampleRate(int sampleRate) throws SourceException + { + mFrequencyController.setSampleRate(sampleRate); + } +}