diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizer.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizer.java deleted file mode 100644 index 9ab818b9..00000000 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizer.java +++ /dev/null @@ -1,171 +0,0 @@ -/******************************************************************************* - * 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 io.github.dsheirer.dsp.filter.channelizer; - -import io.github.dsheirer.dsp.filter.FilterFactory; -import io.github.dsheirer.dsp.filter.Window; -import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter; -import io.github.dsheirer.dsp.mixer.Oscillator; -import io.github.dsheirer.sample.complex.ComplexBuffer; -import org.jtransforms.fft.FloatFFT_1D; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class ComplexPolyphaseChannelizer extends AbstractComplexPolyphaseChannelizer -{ - private final static Logger mLog = LoggerFactory.getLogger(ComplexPolyphaseChannelizer.class); - - private RealFIRFilter[] mInphaseFilters; - private RealFIRFilter[] mQuadratureFilters; - private int mFilterPointer = 0; - private float[] mFilteredSamples; - private FloatFFT_1D mFFT; - - /** - * Maximally Decimated Polyphase Filter Bank (MDPFB) channelizer that divides the input frequency band into - * equal bandwidth channels. - * - * @param coefficients of a prototype low-pass filter designed for the inbound sample rate with a cutoff frequency - * 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 channelCount - number of channels to output. - * @param channelSampleRate - channel sample rate (after decimation) - */ - public ComplexPolyphaseChannelizer(float[] coefficients, int channelCount, int channelSampleRate) - { - super(channelCount, channelSampleRate); - - initFilters(coefficients); - } - - @Override - public void receive(ComplexBuffer complexBuffer) - { - float[] samples = complexBuffer.getSamples(); - - for(int x = 0; x < samples.length; x += 2) - { - filter(samples[x], samples[x + 1]); - } - } - - protected void filter(float inphase, float quadrature) - { - int index = (getChannelCount() - mFilterPointer - 1) * 2; - - mFilteredSamples[index] = mInphaseFilters[mFilterPointer].filter(inphase); - mFilteredSamples[index + 1] = mQuadratureFilters[mFilterPointer].filter(quadrature); - - if(mFilterPointer == 0) - { - calculate(); - mFilterPointer = getChannelCount() - 1; - } - else - { - mFilterPointer--; - } - } - - private void calculate() - { - float[] samples = new float[getChannelCount() * 2]; - System.arraycopy(mFilteredSamples, 0, samples, 0, mFilteredSamples.length); - - //FFT is executed in-place where the output overwrites the input - mFFT.complexForward(samples); - - dispatch(samples); - } - - /** - * Distributes the filter taps to each of the polyphase filters - * - * @param taps - */ - private void initFilters(float[] taps) - { - int tapCount = (int)Math.ceil((double)taps.length / (double)getChannelCount()); - - float[][] coefficients = new float[getChannelCount()][tapCount]; - - for(int tap = 0; tap < tapCount; tap++) - { - for(int filter = 0; filter < getChannelCount(); filter++) - { - int index = tap * getChannelCount() + filter; - - if(index < taps.length) - { - coefficients[filter][tap] = taps[index]; - } - } - } - - mInphaseFilters = new RealFIRFilter[getChannelCount()]; - mQuadratureFilters = new RealFIRFilter[getChannelCount()]; - - for(int filter = 0; filter < getChannelCount(); filter++) - { - mInphaseFilters[filter] = new RealFIRFilter(coefficients[filter], 1.0f); - mQuadratureFilters[filter] = new RealFIRFilter(coefficients[filter], 1.0f); - } - - mFilteredSamples = new float[getChannelCount() * 2]; - mFFT = new FloatFFT_1D(getChannelCount()); - mFilterPointer = getChannelCount() -1; - } - - public static void main(String[] args) - { - mLog.debug("Starting ..."); - - int symbolRate = 4800; - int samplesPerSymbol = 2; - int channels = 800; - int channelBandwidth = 12500; - int sampleRate = channels * channelBandwidth; - - float[] taps = FilterFactory.getLowPass( 10000000, 12500, 5600, Window.WindowType.BLACKMAN_HARRIS_7 ); - - int channelCenter = (int)(12500 * 1.5); - - StringBuilder sb = new StringBuilder(); - sb.append("\nPolyphase TunerChannelizer\n"); - sb.append("Sample Rate:" + sampleRate + " Channels:" + channels + " Channel Rate:" + channelBandwidth + "\n"); - sb.append("Tap Count:" + taps.length + "\n"); - sb.append("Channel:" + channelCenter); - - mLog.debug(sb.toString()); - - ComplexPolyphaseChannelizer channelizer = new ComplexPolyphaseChannelizer(taps, 8, channelBandwidth); - - Oscillator oscillator = new Oscillator(channelCenter, 2 * sampleRate); - - for(int x = 0; x < 2000; x++) - { - channelizer.filter(oscillator.getComplex().inphase(), oscillator.getComplex().quadrature()); - oscillator.rotate(); - } - - mLog.debug("Finished!"); - } -} diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java index 7774050a..81ed8b03 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java @@ -23,6 +23,7 @@ import io.github.dsheirer.sample.Broadcaster; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.complex.ComplexBuffer; import io.github.dsheirer.sample.complex.IComplexBufferProvider; +import io.github.dsheirer.source.ISourceEventProcessor; import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.source.SourceException; import io.github.dsheirer.source.tuner.TunerController; @@ -32,11 +33,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.List; +import java.util.Queue; import java.util.SortedSet; import java.util.TreeSet; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CopyOnWriteArrayList; -public class PolyphaseChannelManager +public class PolyphaseChannelManager implements ISourceEventProcessor { private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelManager.class); private static final double MINIMUM_CHANNEL_BANDWIDTH = 12500.0; @@ -224,6 +227,40 @@ public class PolyphaseChannelManager } } + /** + * Process source events received from the source (ie tuner controller) for frequency and sample rate change + * notifications. + * @param sourceEvent to process + * @throws SourceException + */ + @Override + public void process(SourceEvent sourceEvent) throws SourceException + { + switch(sourceEvent.getEvent()) + { + case NOTIFICATION_FREQUENCY_CHANGE: + //Update channel calculator immediately so that channels can be allocated + mLog.debug("Updating channel calculator to frequency: " + sourceEvent.getValue()); + mChannelCalculator.setCenterFrequency(sourceEvent.getValue().longValue()); + + //Defer channelizer configuration changes to be handled on the buffer processor thread + mBufferSourceEventMonitor.receive(sourceEvent); + break; + case NOTIFICATION_SAMPLE_RATE_CHANGE: + //Update channel calculator immediately so that channels can be allocated + mLog.debug("Updating channel calculator to sample rate: " + sourceEvent.getValue()); + mChannelCalculator.setSampleRate(sourceEvent.getValue().doubleValue()); + + //Defer channelizer configuration changes to be handled on the buffer processor thread + mBufferSourceEventMonitor.receive(sourceEvent); + break; + default: + mLog.info("Unrecognized source event: " + sourceEvent.toString()); + break; + + } + } + /** * Creates or updates the channelizer to process the incoming sample rate and updates any channel processors. * @@ -235,10 +272,9 @@ public class PolyphaseChannelManager */ private void updateChannelizerSampleRate(double sampleRate) { - if(mPolyphaseChannelizer == null || Math.abs(mPolyphaseChannelizer.getSampleRate() - sampleRate) > 1.0) + if(mPolyphaseChannelizer == null || Math.abs(mPolyphaseChannelizer.getSampleRate() - sampleRate) > 0.5) { mLog.debug("Creating or Updating Channelizer for sample rate " + sampleRate); - mChannelCalculator.setSampleRate(sampleRate); try { @@ -271,21 +307,12 @@ public class PolyphaseChannelManager } } - private void updateChannelizerCenterFrequency(long frequency) - { - mChannelCalculator.setCenterFrequency(frequency); - - mLog.debug("Updating output processors - center frequency is now: " + frequency); - - updateOutputProcessors(null); - } - /** * 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 + * @param sourceEvent (optional-can be null) to broadcast to each output processor following the update */ private void updateOutputProcessors(SourceEvent sourceEvent) { @@ -293,13 +320,11 @@ public class PolyphaseChannelManager { updateOutputProcessor(channelSource); - //Notify the channel source that the frequency or sample rate has changed so that it can reset - //any frequency correction value. + //Send the non-null source event to each channel source if(sourceEvent != null) { try { -//TODO: this notification event should go to the channelizer to be embedded in the channel buffer stream channelSource.process(sourceEvent); } catch(SourceException se) @@ -429,17 +454,44 @@ public class PolyphaseChannelManager } /** - * Processes the incoming buffer stream from the provider to detect frequency and sample rate change source events - * and update the channelizer configuration as necessary. + * Processes the incoming buffer stream from the provider and transfers the buffers to the polyphase channelizer. + * + * This monitor incorporates a queue for receiving source events that affect configuration of the channelizer so + * that they can be processed on the buffer processor calling thread, avoiding unnecessary locks on the channelizer + * and/or the channel sources and output processors. */ public class BufferSourceEventMonitor implements Listener { + private Queue mQueuedSourceEvents = new ConcurrentLinkedQueue<>(); + + /** + * Queues the source event for deferred execution on the buffer processing thread. + * @param event that affects configuration of the channelizer (frequency or sample rate change events) + */ + public void receive(SourceEvent event) + { + mQueuedSourceEvents.offer(event); + } + @Override public void receive(ComplexBuffer complexBuffer) { - if(complexBuffer.hasSourceEvent()) + //Process any queued source events before processing the buffer + SourceEvent queuedSourceEvent = mQueuedSourceEvents.poll(); + + while(queuedSourceEvent != null) { - process(complexBuffer.getSourceEvent()); + switch(queuedSourceEvent.getEvent()) + { + case NOTIFICATION_FREQUENCY_CHANGE: + updateOutputProcessors(queuedSourceEvent); + break; + case NOTIFICATION_SAMPLE_RATE_CHANGE: + updateChannelizerSampleRate(queuedSourceEvent.getValue().intValue()); + break; + } + + queuedSourceEvent = mQueuedSourceEvents.poll(); } if(mPolyphaseChannelizer != null) @@ -447,29 +499,5 @@ public class PolyphaseChannelManager mPolyphaseChannelizer.receive(complexBuffer); } } - - /** - * Processes frequency and sample rate change events that are embedded in the complex buffer stream from the - * tuner or sample source - */ - public void process(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()); - mLog.debug("Updating output processors - center frequency is now: " + sourceEvent.getValue().longValue()); - updateOutputProcessors(sourceEvent); - break; - case NOTIFICATION_SAMPLE_RATE_CHANGE: - //Update the channelizer (and the output processors) for the new sample rate - updateChannelizerSampleRate(sourceEvent.getValue().intValue()); - break; - default: - mLog.info("Received an unrecognized source event: " + sourceEvent.getEvent()); - break; - } - } } } \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/sample/Buffer.java b/src/main/java/io/github/dsheirer/sample/Buffer.java index 61d9fe8e..bb1f6aa4 100644 --- a/src/main/java/io/github/dsheirer/sample/Buffer.java +++ b/src/main/java/io/github/dsheirer/sample/Buffer.java @@ -15,25 +15,9 @@ ******************************************************************************/ package io.github.dsheirer.sample; -import io.github.dsheirer.source.SourceEvent; - public class Buffer { private float[] mSamples; - private SourceEvent mSourceEvent; - - /** - * Buffer containing float sample data and a source event that should be processed prior to processing the - * sample data. - * - * @param samples data - * @param sourceEvent indicating a change to the sample data that should be processed first - */ - public Buffer(float[] samples, SourceEvent sourceEvent) - { - mSamples = samples; - mSourceEvent = sourceEvent; - } /** * Buffer containing float sample data @@ -52,32 +36,11 @@ public class Buffer return mSamples; } - /** - * Optional source event that should be processed before processing the included samples. Use the hasSourceEvent() - * method to check for the existence of an event before accessing. - * - * @return optional source event or null - */ - public SourceEvent getSourceEvent() - { - return mSourceEvent; - } - - /** - * Indicates if this buffer contains an optional source event that should be processed prior to processing the - * included float samples array. - */ - public boolean hasSourceEvent() - { - return mSourceEvent != null; - } - /** * Cleanup method to nullify all data and references */ public void dispose() { mSamples = null; - mSourceEvent = null; } } \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/sample/complex/ComplexBuffer.java b/src/main/java/io/github/dsheirer/sample/complex/ComplexBuffer.java index bfd8971e..884a6f20 100644 --- a/src/main/java/io/github/dsheirer/sample/complex/ComplexBuffer.java +++ b/src/main/java/io/github/dsheirer/sample/complex/ComplexBuffer.java @@ -16,21 +16,12 @@ package io.github.dsheirer.sample.complex; import io.github.dsheirer.sample.Buffer; -import io.github.dsheirer.source.SourceEvent; public class ComplexBuffer extends Buffer { //Default timestamp - we use a static variable here so that there is only ever 1 instance in memory private static final long DEFAULT_TIMESTAMP = 0; - /** - * Wrapper around float array containing interleaved I/Q samples and an optional source event that should be - * processed prior to processing the sample data. - */ - public ComplexBuffer(float[] samples, SourceEvent sourceEvent) - { - super(samples, sourceEvent); - } /** * Wrapper around float array containing interleaved I/Q samples */ diff --git a/src/main/java/io/github/dsheirer/sample/complex/TimestampedComplexBuffer.java b/src/main/java/io/github/dsheirer/sample/complex/TimestampedComplexBuffer.java index ac40103d..9e453ec7 100644 --- a/src/main/java/io/github/dsheirer/sample/complex/TimestampedComplexBuffer.java +++ b/src/main/java/io/github/dsheirer/sample/complex/TimestampedComplexBuffer.java @@ -15,26 +15,10 @@ ******************************************************************************/ package io.github.dsheirer.sample.complex; -import io.github.dsheirer.source.SourceEvent; - public class TimestampedComplexBuffer extends ComplexBuffer { private long mTimestamp; - /** - * Constructs a timestamped complex buffer using the specified time in milliseconds. Also includes a source event - * that should be processed prior to processing the sample data. - * - * @param samples of data - * @param sourceEvent - optional source event to embed with this buffer - * @param timestamp relative to the first sample in the sample array - */ - public TimestampedComplexBuffer(float[] samples, SourceEvent sourceEvent, long timestamp) - { - super(samples, sourceEvent); - mTimestamp = timestamp; - } - /** * Constructs a timestamped complex buffer using the specified time in milliseconds. * @@ -42,18 +26,9 @@ public class TimestampedComplexBuffer extends ComplexBuffer */ public TimestampedComplexBuffer(float[] samples, long timestamp) { - this(samples, null, timestamp); - } + super(samples); - /** - * Constructs a timestamped complex buffer using the current system time in milliseconds. Also includes a source - * event that should be processed prior to processing the sample data. - * - * @param samples of data - */ - public TimestampedComplexBuffer(float[] samples, SourceEvent sourceEvent) - { - this(samples, sourceEvent, System.currentTimeMillis()); + mTimestamp = timestamp; } /** @@ -63,7 +38,7 @@ public class TimestampedComplexBuffer extends ComplexBuffer */ public TimestampedComplexBuffer(float[] samples) { - this(samples, null, System.currentTimeMillis()); + this(samples, System.currentTimeMillis()); } /** diff --git a/src/main/java/io/github/dsheirer/sample/real/RealBuffer.java b/src/main/java/io/github/dsheirer/sample/real/RealBuffer.java index 3628165e..7050e1bf 100644 --- a/src/main/java/io/github/dsheirer/sample/real/RealBuffer.java +++ b/src/main/java/io/github/dsheirer/sample/real/RealBuffer.java @@ -16,21 +16,11 @@ package io.github.dsheirer.sample.real; import io.github.dsheirer.sample.Buffer; -import io.github.dsheirer.source.SourceEvent; import java.util.Arrays; public class RealBuffer extends Buffer { - /** - * Wrapper around float array containing real float samples and an optional source event that should be processed - * prior to processing the sample array data. - */ - public RealBuffer(float[] samples, SourceEvent sourceEvent) - { - super(samples, sourceEvent); - } - /** * Wrapper around float array containing real float samples */ @@ -46,6 +36,6 @@ public class RealBuffer extends Buffer { float[] copy = Arrays.copyOf(getSamples(), getSamples().length); - return new RealBuffer(copy, getSourceEvent()); + return new RealBuffer(copy); } } diff --git a/src/main/java/io/github/dsheirer/source/tuner/manager/TunerSourceManager.java b/src/main/java/io/github/dsheirer/source/tuner/manager/TunerSourceManager.java index 2a5be07e..f3c659f6 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/manager/TunerSourceManager.java +++ b/src/main/java/io/github/dsheirer/source/tuner/manager/TunerSourceManager.java @@ -46,6 +46,7 @@ public class TunerSourceManager extends AbstractSourceManager mPolyphaseChannelManager = new PolyphaseChannelManager(tunerController); //Register to receive channel count change notifications for rebroadcasting mPolyphaseChannelManager.addSourceEventListener(this::process); + mTunerController.addListener(mPolyphaseChannelManager); } /** @@ -427,7 +428,7 @@ public class TunerSourceManager extends AbstractSourceManager broadcast(sourceEvent); break; default: - mLog.info("Received an unrecognized source event: " + sourceEvent); + mLog.info("Unrecognized source event: " + sourceEvent); break; } } diff --git a/src/main/java/io/github/dsheirer/source/tuner/test/SampleGenerator.java b/src/main/java/io/github/dsheirer/source/tuner/test/SampleGenerator.java index 51d157b5..11cf4f14 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/test/SampleGenerator.java +++ b/src/main/java/io/github/dsheirer/source/tuner/test/SampleGenerator.java @@ -22,13 +22,10 @@ import io.github.dsheirer.dsp.mixer.LowPhaseNoiseOscillator; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.complex.ComplexBuffer; import io.github.dsheirer.sample.complex.TimestampedComplexBuffer; -import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.util.ThreadPool; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -42,7 +39,6 @@ public class SampleGenerator private long mInterval; private int mSamplesPerInterval; private ScheduledFuture mScheduledFuture; - private Queue mSourceEventQueue = new ConcurrentLinkedQueue(); /** * Generates complex sample buffers at the specified sample rate with a unity gain tone at the specified @@ -84,16 +80,6 @@ public class SampleGenerator this(sampleRate, frequency, interval, 0); } - /** - * Queues the source event for inclusion in the complex buffers generated by this sample generator. Source events - * are queued FIFO and embedded one at a time in the subsequent buffers produced by this generator. - * @param sourceEvent to queue - */ - public void queue(SourceEvent sourceEvent) - { - mSourceEventQueue.offer(sourceEvent); - } - /** * Updates the number of complex samples generated per interval */ @@ -185,7 +171,7 @@ public class SampleGenerator if(mListener != null) { float[] samples = mOscillator.generate(mSamplesPerInterval); - mListener.receive(new TimestampedComplexBuffer(samples, mSourceEventQueue.poll())); + mListener.receive(new TimestampedComplexBuffer(samples)); if(mSweepUpdateInterval != 0) { diff --git a/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java index bac4b008..4561d0c4 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java @@ -20,7 +20,6 @@ package io.github.dsheirer.source.tuner.test; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.complex.ComplexBuffer; -import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.source.SourceException; import io.github.dsheirer.source.tuner.TunerController; import io.github.dsheirer.source.tuner.configuration.TunerConfiguration; @@ -56,7 +55,7 @@ public class TestTunerController extends TunerController try { - mLog.debug("Setting frequency ..."); + mLog.debug("Setting frequency to:" + DEFAULT_CENTER_FREQUENCY); mFrequencyController.setFrequency(DEFAULT_CENTER_FREQUENCY); mLog.debug("Setting sample rate of freq controller to: " + DEFAULT_SAMPLE_RATE); mFrequencyController.setSampleRate(DEFAULT_SAMPLE_RATE); @@ -130,7 +129,6 @@ public class TestTunerController extends TunerController { long frequency = toneFrequency - mReferenceFrequency; mSampleGenerator.setFrequency(frequency); - mSampleGenerator.queue(SourceEvent.frequencyChange(frequency)); } /** @@ -157,6 +155,5 @@ public class TestTunerController extends TunerController public void setSampleRate(int sampleRate) throws SourceException { mFrequencyController.setSampleRate(sampleRate); - mSampleGenerator.queue(SourceEvent.sampleRateChange(sampleRate)); } }