diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java index 85b2e832..25d0de82 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/AbstractComplexPolyphaseChannelizer.java @@ -90,13 +90,13 @@ public abstract class AbstractComplexPolyphaseChannelizer implements Listener polyphaseIndexes = mChannelCalculator.getChannelIndexes(tunerChannel); + mLog.debug("Getting polyphase channel for: " + tunerChannel + " indexes:" + polyphaseIndexes); IPolyphaseChannelOutputProcessor outputProcessor = getOutputProcessor(polyphaseIndexes); @@ -180,7 +183,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor synchronized(mBufferProcessor) { mPolyphaseChannelizer.addChannel(channelSource); - mSourceEventBroadcaster.broadcast(SourceEvent.channelCountChange(mChannelSources.size())); + mSourceEventBroadcaster.broadcast(SourceEvent.channelCountChange(getTunerChannelCount())); if(mPolyphaseChannelizer.getRegisteredChannelCount() == 1) { @@ -201,7 +204,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor synchronized(mBufferProcessor) { mChannelSources.remove(channelSource); - mSourceEventBroadcaster.broadcast(SourceEvent.channelCountChange(mChannelSources.size())); + mSourceEventBroadcaster.broadcast(SourceEvent.channelCountChange(getTunerChannelCount())); if(mPolyphaseChannelizer.getRegisteredChannelCount() == 0) { @@ -209,52 +212,33 @@ public class PolyphaseChannelManager implements ISourceEventProcessor mBufferProcessor.stop(); } } - } - /** - * Implements the ISourceEventProcessor interface for receiving notifications of frequency and sample rate - * changes from the tuner. - */ - @Override - public void process(SourceEvent sourceEvent) - { - switch(sourceEvent.getEvent()) + try { - 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; - default: - mLog.info("Received an unrecognized source event: " + sourceEvent.getEvent()); - break; + //Broadcast a stop sample stream notification in case this was a forced-stop so consumers are aware + channelSource.process(SourceEvent.stopSampleStreamNotification(channelSource)); + } + catch(SourceException se) + { + //Do nothing } } /** * Creates or updates the channelizer to process the incoming sample rate and updates any channel processors. * + * Note: this method should only be invoked on the mBufferProcessor thread or prior to starting the mBufferProcessor. + * Sample rate source events will normally arrive via the incoming complex buffer stream from the mBufferProcessor + * and will be handled as they arrive. + * * @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) + private void updateChannelizerSampleRate(double sampleRate) { - mChannelCalculator.setSampleRate(sampleRate); - - //Lock on the buffer processor to block any channel create/start/stop requests while we configure - synchronized(mBufferProcessor) + if(mPolyphaseChannelizer == null || Math.abs(mPolyphaseChannelizer.getSampleRate() - sampleRate) > 1.0) { - //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(); + mLog.debug("Creating or Updating Channelizer for sample rate " + sampleRate); + mChannelCalculator.setSampleRate(sampleRate); try { @@ -262,8 +246,6 @@ public class PolyphaseChannelManager implements ISourceEventProcessor { mPolyphaseChannelizer = new ComplexPolyphaseChannelizerM2(sampleRate, POLYPHASE_FILTER_TAPS_PER_CHANNEL); - - mBufferProcessor.setListener(mPolyphaseChannelizer); } else { @@ -279,16 +261,23 @@ public class PolyphaseChannelManager implements ISourceEventProcessor 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(); - } + //Broadcast the new channel sample rate + double updatedChannelSampleRate = mChannelCalculator.getChannelSampleRate(); + updateOutputProcessors(SourceEvent.sampleRateChange(updatedChannelSampleRate)); } + else + { + mLog.debug("Request to update current sample rate ignored: " + sampleRate); + } + } + + private void updateChannelizerCenterFrequency(long frequency) + { + mChannelCalculator.setCenterFrequency(frequency); + + mLog.debug("Updating output processors - center frequency is now: " + frequency); + + updateOutputProcessors(null); } /** @@ -310,6 +299,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor { try { +//TODO: this notification event should go to the channelizer to be embedded in the channel buffer stream channelSource.process(sourceEvent); } catch(SourceException se) @@ -331,6 +321,8 @@ public class PolyphaseChannelManager implements ISourceEventProcessor { try { + //If a change in sample rate or center frequency makes this channel no longer viable, then the channel + //calculator will throw an IllegalArgException ... handled below List indexes = mChannelCalculator.getChannelIndexes(channelSource.getTunerChannel()); long centerFrequency = mChannelCalculator.getCenterFrequencyForIndexes(indexes); @@ -338,11 +330,13 @@ public class PolyphaseChannelManager implements ISourceEventProcessor //If the indexes size is the same then update the current processor, otherwise create a new one if(channelSource.getPolyphaseChannelOutputProcessor().getInputChannelCount() == indexes.size()) { + mLog.debug("Updating indexes to " + indexes + " for: " + channelSource.getTunerChannel()); channelSource.getPolyphaseChannelOutputProcessor().setPolyphaseChannelIndices(indexes); channelSource.setFrequency(centerFrequency); } else { + mLog.debug("Creating new output processor for indexes " + indexes); channelSource.setPolyphaseChannelOutputProcessor(getOutputProcessor(indexes), centerFrequency); } } @@ -433,4 +427,49 @@ public class PolyphaseChannelManager implements ISourceEventProcessor } } } + + /** + * Processes the incoming buffer stream from the provider to detect frequency and sample rate change source events + * and update the channelizer configuration as necessary. + */ + public class BufferSourceEventMonitor implements Listener + { + @Override + public void receive(ComplexBuffer complexBuffer) + { + if(complexBuffer.hasSourceEvent()) + { + process(complexBuffer.getSourceEvent()); + } + + if(mPolyphaseChannelizer != null) + { + 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/dsp/filter/channelizer/PolyphaseChannelResultsBuffer.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelResultsBuffer.java new file mode 100644 index 00000000..f0ccc96c --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelResultsBuffer.java @@ -0,0 +1,166 @@ +/******************************************************************************* + * sdr-trunk + * Copyright (C) 2014-2018 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 java.util.Arrays; + +public class PolyphaseChannelResultsBuffer +{ + private long mTimestamp; + private int mPointer = 0; + private int mMaxSize; + private float[][] mChannelResults; + + /** + * Buffer containing an array of polyphase channelizer complex channel results. + * @param timestamp reference relative to the first channel results. + * @param maxSize of channel results to store in this buffer + */ + public PolyphaseChannelResultsBuffer(long timestamp, int maxSize) + { + mTimestamp = timestamp; + mMaxSize = maxSize; + } + + /** + * Reference timestamp relative to the first channel results contained in this buffer. + * @return reference timestamp in milliseconds since epoch + */ + public long getTimestamp() + { + return mTimestamp; + } + + /** + * Indicates if this buffer is full + */ + public boolean isFull() + { + return mPointer >= mMaxSize; + } + + /** + * Indicates if this buffer is empty + */ + public boolean isEmpty() + { + return mPointer == 0; + } + + /** + * Adds the channel results to this buffer + * @param channelResults to add + * @throws IllegalArgumentException if the buffer is full or if the size of the channel results array is a different + * length than any of the preceeding channel results that were added to this buffer + */ + public void add(float[] channelResults) throws IllegalArgumentException + { + if(mPointer >= mMaxSize) + { + throw new IllegalArgumentException("Buffer is full - cannot add new channel results"); + } + if(mPointer == 0) + { + mChannelResults = new float[mMaxSize][channelResults.length]; + mChannelResults[mPointer++] = channelResults; + } + else + { + int currentLength = mChannelResults[0].length; + + if(channelResults.length != currentLength) + { + throw new IllegalArgumentException("Channel results length [" + channelResults.length + "] cannot be " + + "a different length than the channel results that are already contained in this buffer [" + + currentLength + "]"); + } + + mChannelResults[mPointer++] = channelResults; + } + } + + /** + * Channel results array contained in this buffer + */ + public float[][] getChannelResults() + { + if(mPointer == mMaxSize) + { + return mChannelResults; + } + else + { + return Arrays.copyOf(mChannelResults, mPointer); + } + } + + /** + * Extracts a single I/Q interleaved sample buffer from the channel results + * @param iChannelIndex to extract for the inphase sample. The q index is assumed to be one greater than i. + * @return float[] containing interleaved I and Q samples + * @throws IllegalArgumentException if this buffer doesn't contain any samples or if the requested channel is + * not within the channel range of the contained channel results. + */ + public float[] getChannel(int iChannelIndex) + { + if(isEmpty()) + { + throw new IllegalArgumentException("Buffer is empty - cannot extract channel [" + iChannelIndex + "]"); + } + if(!isValidChannelIndex(iChannelIndex)) + { + throw new IllegalArgumentException("Channel [" + iChannelIndex + "] is not valid for the contained channel " + + "results -- max channel is " + getMaxChannelIndex()); + } + + float[] samples = new float[mChannelResults.length * 2]; + int pointer = 0; + + int qChannelIndex = iChannelIndex + 1; + + for(float[] channelResults: mChannelResults) + { + samples[pointer++] = channelResults[iChannelIndex]; + samples[pointer++] = channelResults[qChannelIndex]; + } + + return samples; + } + + /** + * Indicates if the channel argument is valid for the contained channel results. + * @param channelIndex number (0 to N) to check + * @return false if the buffer is empty or if the channel is greater than the contained channel results + */ + private boolean isValidChannelIndex(int channelIndex) + { + return channelIndex <= getMaxChannelIndex(); + } + + /** + * Maximum channel index represented in the channel results contained in this buffer. + * @return max channel number, or 0 if the buffer is empty + */ + private int getMaxChannelIndex() + { + if(isEmpty()) + { + return 0; + } + + return mChannelResults[0].length; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java index e2985c2a..2b5145ef 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java @@ -1,7 +1,6 @@ /******************************************************************************* - * ********************************************************************************************************************* * sdr-trunk - * Copyright (C) 2014-2017 Dennis Sheirer + * Copyright (C) 2014-2018 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 @@ -12,14 +11,14 @@ * * 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.channelizer.output.IPolyphaseChannelOutputProcessor; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.complex.ComplexBuffer; -import io.github.dsheirer.sample.complex.ComplexToComplexBufferAssembler; +import io.github.dsheirer.sample.complex.TimestampedBufferAssembler; import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.source.tuner.channel.TunerChannel; import io.github.dsheirer.source.tuner.channel.TunerChannelSource; @@ -30,7 +29,7 @@ public class PolyphaseChannelSource extends TunerChannelSource { private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelSource.class); - private ComplexToComplexBufferAssembler mComplexBufferAssembler = new ComplexToComplexBufferAssembler(2500); + private TimestampedBufferAssembler mTimestampedBufferAssembler; private IPolyphaseChannelOutputProcessor mPolyphaseChannelOutputProcessor; private double mChannelSampleRate; private long mCenterFrequency; @@ -52,6 +51,7 @@ public class PolyphaseChannelSource extends TunerChannelSource super(producerSourceEventListener, tunerChannel); mPolyphaseChannelOutputProcessor = outputProcessor; mChannelSampleRate = channelSampleRate; + mTimestampedBufferAssembler = new TimestampedBufferAssembler(2500, mChannelSampleRate); } /** @@ -60,7 +60,7 @@ public class PolyphaseChannelSource extends TunerChannelSource @Override public void setListener(final Listener listener) { - mComplexBufferAssembler.setListener(listener); + mTimestampedBufferAssembler.setListener(listener); } /** @@ -69,7 +69,7 @@ public class PolyphaseChannelSource extends TunerChannelSource @Override public void removeListener(Listener listener) { - mComplexBufferAssembler.setListener(null); + mTimestampedBufferAssembler.setListener(null); } /** @@ -108,7 +108,7 @@ public class PolyphaseChannelSource extends TunerChannelSource //Fully process the residual channel results buffer of the previous channel output processor if(existingProcessor != null) { - existingProcessor.processChannelResults(mComplexBufferAssembler); + existingProcessor.processChannelResults(mTimestampedBufferAssembler); } //Finally, setup the frequency offset for the output processor and reset the frequency correction value @@ -121,14 +121,15 @@ public class PolyphaseChannelSource extends TunerChannelSource } /** - * Primary method for receiving channel results output from a polyphase channelizer. The results array will be + * Primary method for receiving channel results output from a polyphase channelizer. The results buffer 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. + * @param channelResultsBuffer containing an array of polyphase channelizer outputs. */ - public void receiveChannelResults(float[] channelResultsBuffer) + public void receiveChannelResults(PolyphaseChannelResultsBuffer channelResultsBuffer) { + mTimestampedBufferAssembler.updateTimestamp(channelResultsBuffer.getTimestamp()); mPolyphaseChannelOutputProcessor.receiveChannelResults(channelResultsBuffer); } @@ -186,16 +187,19 @@ public class PolyphaseChannelSource extends TunerChannelSource protected void setFrequency(long frequency) { mCenterFrequency = frequency; + + //Set frequency correction to zero to trigger an update to the mixer and allow downstream monitors to + //recalculate the frequency error correction again setFrequencyCorrection(0); } @Override protected void processSamples() { - //Lock on the output processor so that it can't be changed out in the middle of processing + //Lock on the output processor so that it can't be changed in the middle of processing synchronized(mPolyphaseChannelOutputProcessor) { - mPolyphaseChannelOutputProcessor.processChannelResults(mComplexBufferAssembler); + mPolyphaseChannelOutputProcessor.processChannelResults(mTimestampedBufferAssembler); } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/SampleTimestampManager.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/SampleTimestampManager.java new file mode 100644 index 00000000..38c8a943 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/SampleTimestampManager.java @@ -0,0 +1,83 @@ +/******************************************************************************* + * sdr-trunk + * Copyright (C) 2014-2018 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; + +public class SampleTimestampManager +{ + private long mReferenceTimestamp = System.currentTimeMillis(); + private double mSamplesPerMilliSecond; + private long mSampleCount; + + /** + * Tracks the number of samples produced relative to a reference timestamp to derive a current reference timestamp + * for use in generating sample buffers. + * + * @param sampleRate per second + */ + public SampleTimestampManager(double sampleRate) + { + } + + /** + * Updates the internal reference timestamp and resets the sample counter to zero. + * + * @param referenceTimestamp in milli-seconds since epoch + */ + public void setReferenceTimestamp(long referenceTimestamp) + { + mReferenceTimestamp = referenceTimestamp; + mSampleCount = 0; + } + + /** + * Updates the sample rate that is used in calculating the reference timestamp + * + * @param sampleRate + */ + public void setSampleRate(double sampleRate) + { + //Convert to samples per milli-second for compatibility with reference timestamp + mSamplesPerMilliSecond = sampleRate / 1000.0d; + } + + /** + * Increments the internal count of dispatched samples to use in calculating an accurate reference timestamp. + */ + public void increment() + { + mSampleCount++; + } + + /** + * Increments the internal count of dispatched samples to use in calculating an accurate reference timestamp. + * + * @param value to add to the sample count + */ + public void increment(int value) + { + mSampleCount += value; + } + + public long getCurrentTimestamp() + { + if(mSampleCount > 0) + { + return mReferenceTimestamp + (long)((double)mSampleCount / mSamplesPerMilliSecond); + } + + return mReferenceTimestamp; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java index 272f69d0..b699edae 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java @@ -18,10 +18,10 @@ ******************************************************************************/ package io.github.dsheirer.dsp.filter.channelizer.output; +import io.github.dsheirer.dsp.filter.channelizer.PolyphaseChannelResultsBuffer; import io.github.dsheirer.dsp.mixer.Oscillator; import io.github.dsheirer.sample.OverflowableTransferQueue; -import io.github.dsheirer.sample.complex.Complex; -import io.github.dsheirer.sample.complex.ComplexSampleListener; +import io.github.dsheirer.sample.complex.TimestampedBufferAssembler; import io.github.dsheirer.sample.real.IOverflowListener; import io.github.dsheirer.source.Source; import org.slf4j.Logger; @@ -34,8 +34,8 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP { private final static Logger mLog = LoggerFactory.getLogger(ChannelOutputProcessor.class); - private OverflowableTransferQueue mChannelResultsQueue; - private List mChannelResultsToProcess = new ArrayList<>(); + private OverflowableTransferQueue mChannelResultsQueue; + private List mChannelResultsToProcess = new ArrayList<>(); private int mMaxResultsToProcess; private int mInputChannelCount; @@ -71,26 +71,34 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP mFrequencyCorrectionEnabled = (frequencyOffset != 0); } + /** + * Oscillator/mixer to use for frequency correction against incominb sample stream + */ + protected Oscillator getFrequencyCorrectionMixer() + { + return mFrequencyCorrectionMixer; + } + @Override - public void receiveChannelResults(float[] channelResults) + public void receiveChannelResults(PolyphaseChannelResultsBuffer channelResults) { mChannelResultsQueue.offer(channelResults); } /** * Processes all enqueued polyphase channelizer results until the internal queue is empty - * @param listener to receive the processed channel results + * @param timestampedBufferAssembler to receive the processed channel results */ @Override - public void processChannelResults(ComplexSampleListener listener) + public void processChannelResults(TimestampedBufferAssembler timestampedBufferAssembler) { try { - int count = mChannelResultsQueue.drainTo(mChannelResultsToProcess, mMaxResultsToProcess); + int toProcess = mChannelResultsQueue.drainTo(mChannelResultsToProcess, mMaxResultsToProcess); - if(count > 0) + if(toProcess > 0) { - process(mChannelResultsToProcess, listener); + process(mChannelResultsToProcess, timestampedBufferAssembler); } } catch(Throwable throwable) @@ -104,9 +112,10 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP /** * Sub-class implementation to process one polyphase channelizer result array. * @param channelResults to process - * @param listener to receive the channel complex sample + * @param timestampedBufferAssembler to receive the processed channelizer results */ - public abstract void process(List channelResults, ComplexSampleListener listener); + public abstract void process(List channelResults, + TimestampedBufferAssembler timestampedBufferAssembler); /** @@ -131,34 +140,6 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP 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 */ @@ -166,5 +147,4 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP { return mFrequencyCorrectionEnabled; } - } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java index ce56168a..fe71eba8 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java @@ -18,7 +18,8 @@ ******************************************************************************/ package io.github.dsheirer.dsp.filter.channelizer.output; -import io.github.dsheirer.sample.complex.ComplexSampleListener; +import io.github.dsheirer.dsp.filter.channelizer.PolyphaseChannelResultsBuffer; +import io.github.dsheirer.sample.complex.TimestampedBufferAssembler; import io.github.dsheirer.source.Source; import java.util.List; @@ -27,16 +28,16 @@ public interface IPolyphaseChannelOutputProcessor { /** * Receive and enqueue output results from the polyphase analysis channelizer - * @param channelResults to enqueue + * @param channelResultsBuffer to enqueue */ - void receiveChannelResults(float[] channelResults); + void receiveChannelResults(PolyphaseChannelResultsBuffer channelResultsBuffer); /** * Process the channel output channel results queue and deliver the output to the listener * - * @param listener to receive the processed channel results + * @param timestampedBufferAssembler to receive the processed channel results */ - void processChannelResults(ComplexSampleListener listener); + void processChannelResults(TimestampedBufferAssembler timestampedBufferAssembler); /** * Sets the desired frequency offset from center. The samples will be mixed with an oscillator set to this offset diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/OneChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/OneChannelOutputProcessor.java index e8adb854..d1c1f687 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/OneChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/OneChannelOutputProcessor.java @@ -18,7 +18,8 @@ ******************************************************************************/ package io.github.dsheirer.dsp.filter.channelizer.output; -import io.github.dsheirer.sample.complex.ComplexSampleListener; +import io.github.dsheirer.dsp.filter.channelizer.PolyphaseChannelResultsBuffer; +import io.github.dsheirer.sample.complex.TimestampedBufferAssembler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -67,29 +68,28 @@ public class OneChannelOutputProcessor extends ChannelOutputProcessor * 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 + * @param timestampedBufferAssembler to receive the extracted, frequency-translated channel results */ @Override - public void process(List channelResults, ComplexSampleListener listener) + public void process(List channelResults, + TimestampedBufferAssembler timestampedBufferAssembler) { - for(float[] channels: channelResults) + for(PolyphaseChannelResultsBuffer channelResultsBuffer: channelResults) { - if(channels.length < mChannelOffset + 1) + try { - throw new IllegalArgumentException("Polyphase channelizer output channels array is not large enough to " + - "cover this single channel output processor with channel offset [" + mChannelOffset + "]"); - } + float[] samples = channelResultsBuffer.getChannel(mChannelOffset); - float i = channels[mChannelOffset]; - float q = channels[mChannelOffset + 1]; + if(hasFrequencyCorrection()) + { + getFrequencyCorrectionMixer().mixComplex(samples); + } - if(hasFrequencyCorrection()) - { - listener.receive(getFrequencyCorrectedInphase(i, q), getFrequencyCorrectedQuadrature(i, q)); + timestampedBufferAssembler.receive(samples); } - else + catch(IllegalArgumentException iae) { - listener.receive(i, q); + mLog.error("Error extracting channel samples from polyphase channel results buffer"); } } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java index 23c3e120..3e904d4d 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java @@ -18,7 +18,8 @@ ******************************************************************************/ package io.github.dsheirer.dsp.filter.channelizer.output; -import io.github.dsheirer.sample.complex.ComplexSampleListener; +import io.github.dsheirer.dsp.filter.channelizer.PolyphaseChannelResultsBuffer; +import io.github.dsheirer.sample.complex.TimestampedBufferAssembler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -70,22 +71,13 @@ public class TwoChannelOutputProcessor extends ChannelOutputProcessor * 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 + * @param channelResultsBuffers to process containing an array of channel I/Q sample pairs (I0,Q0,I1,Q1...In,Qn) + * @param timestampedBufferAssembler to receive the extracted, frequency-translated channel results */ @Override - public void process(List channelResults, ComplexSampleListener listener) + public void process(List channelResultsBuffers, + TimestampedBufferAssembler timestampedBufferAssembler) { 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)); } } diff --git a/src/main/java/io/github/dsheirer/dsp/mixer/Oscillator.java b/src/main/java/io/github/dsheirer/dsp/mixer/Oscillator.java index 3099a5bf..a5354ded 100644 --- a/src/main/java/io/github/dsheirer/dsp/mixer/Oscillator.java +++ b/src/main/java/io/github/dsheirer/dsp/mixer/Oscillator.java @@ -149,4 +149,22 @@ public class Oscillator return new ComplexBuffer(buffer); } + + /** + * Performs complex heterodyne against the samples using this oscillator + * @param samples to mix with this oscillator + */ + public void mixComplex(float[] samples) + { + for(int x = 0; x < samples.length; x += 2) + { + float i = Complex.multiplyInphase(samples[x], samples[x + 1], inphase(), quadrature()); + float q = Complex.multiplyQuadrature(samples[x], samples[x + 1], inphase(), quadrature()); + + samples[x] = i; + samples[x + 1] = q; + + rotate(); + } + } } diff --git a/src/main/java/io/github/dsheirer/sample/Buffer.java b/src/main/java/io/github/dsheirer/sample/Buffer.java index 62197e04..61d9fe8e 100644 --- a/src/main/java/io/github/dsheirer/sample/Buffer.java +++ b/src/main/java/io/github/dsheirer/sample/Buffer.java @@ -1,24 +1,83 @@ +/******************************************************************************* + * sdr-trunk + * Copyright (C) 2014-2018 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.sample; +import io.github.dsheirer.source.SourceEvent; + public class Buffer { - protected float[] mSamples; + private float[] mSamples; + private SourceEvent mSourceEvent; - public Buffer( float[] samples ) - { - mSamples = samples; - } + /** + * 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; + } - public float[] getSamples() - { - return mSamples; - } + /** + * Buffer containing float sample data + * @param samples + */ + public Buffer(float[] samples) + { + mSamples = samples; + } - /** - * Cleanup method to nullify all data and references - */ - public void dispose() - { - mSamples = null; - } + /** + * Float sample array + */ + public float[] getSamples() + { + 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 336b61e8..bfd8971e 100644 --- a/src/main/java/io/github/dsheirer/sample/complex/ComplexBuffer.java +++ b/src/main/java/io/github/dsheirer/sample/complex/ComplexBuffer.java @@ -1,26 +1,61 @@ +/******************************************************************************* + * sdr-trunk + * Copyright (C) 2014-2018 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.sample.complex; import io.github.dsheirer.sample.Buffer; - -import java.util.Arrays; +import io.github.dsheirer.source.SourceEvent; public class ComplexBuffer extends Buffer { - /** - * Wrapper around float array containing interleaved I/Q samples - */ - public ComplexBuffer( float[] samples ) - { - super( samples ); - } + //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; - /** - * Creates a deep copy of the buffer - */ - public ComplexBuffer copyOf() - { - float[] copy = Arrays.copyOf( mSamples, mSamples.length ); - - return new ComplexBuffer( copy ); - } + /** + * 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 + */ + public ComplexBuffer(float[] samples) + { + super(samples); + } + + + /** + * Reference timestamp for the first sample in this buffer. This functionality is intended to be implemented by a + * subclass implementation. The default value is 0. + * + * @return reference timestamp or a value of 0 if hasTimestamp() indicates false. + */ + public long getTimestamp() + { + return DEFAULT_TIMESTAMP; + } + + /** + * Indicates if this buffer has a timestamp that references the first sample in the buffer. + */ + public boolean hasTimestamp() + { + return false; + } } diff --git a/src/main/java/io/github/dsheirer/sample/complex/TimestampedBufferAssembler.java b/src/main/java/io/github/dsheirer/sample/complex/TimestampedBufferAssembler.java new file mode 100644 index 00000000..fbc7cfed --- /dev/null +++ b/src/main/java/io/github/dsheirer/sample/complex/TimestampedBufferAssembler.java @@ -0,0 +1,132 @@ +/******************************************************************************* + * sdr-trunk + * Copyright (C) 2014-2018 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.sample.complex; + +import io.github.dsheirer.dsp.filter.channelizer.SampleTimestampManager; +import io.github.dsheirer.sample.Listener; +import org.apache.commons.lang3.Validate; + +import java.nio.FloatBuffer; + +/** + * Assembles complex float samples into a ComplexBuffer containing an array of floats. Monitors incoming sample count + * to accurately assign a relative timestamp to each assembled buffer. + */ +public class TimestampedBufferAssembler +{ + private SampleTimestampManager mTimestampManager; + private FloatBuffer mBuffer; + private long mCurrentBufferTimestamp; + private Listener mListener; + + public TimestampedBufferAssembler(int bufferSize, double sampleRate) + { + Validate.isTrue(bufferSize % 2 == 0); + mBuffer = FloatBuffer.allocate(bufferSize); + mTimestampManager = new SampleTimestampManager(sampleRate); + } + + /** + * Updates the reference timestamp to use for all subsequent samples that are assembled. This timestamp should + * identify the relative timestamp of the first/next sample that is received by this assembler. + * @param timestamp to use relative to the incoming sample arrays. + */ + public void updateTimestamp(long timestamp) + { + mTimestampManager.setReferenceTimestamp(timestamp); + + if(mBuffer.position() == 0) + { + mCurrentBufferTimestamp = timestamp; + } + } + + public void dispose() + { + mListener = null; + mBuffer = null; + } + + /** + * Adds the samples to this assembler. As each buffer is assembled, it will be dispatched to the registered + * listener. + * + * @param samples to assemble + */ + public void receive(float[] samples) + { + if(mBuffer.remaining() >= samples.length) + { + mBuffer.put(samples); + mTimestampManager.increment(samples.length / 2); + + if(!mBuffer.hasRemaining()) + { + flush(); + } + } + else + { + int offset = 0; + + while(offset < samples.length) + { + int toCopy = mBuffer.remaining(); + + if((samples.length - offset) < toCopy) + { + toCopy = samples.length - offset; + } + + mBuffer.put(samples, offset, toCopy); + + offset += toCopy; + mTimestampManager.increment(toCopy / 2); + + if(!mBuffer.hasRemaining()) + { + flush(); + } + } + } + } + + /** + * Flushes the current buffer contents to the registered listener + */ + public void flush() + { + if(mListener != null && mBuffer.position() > 0) + { + float[] samples = new float[mBuffer.position()]; + mBuffer.rewind(); + mBuffer.get(samples); + + mListener.receive(new TimestampedComplexBuffer(samples, mCurrentBufferTimestamp)); + } + + mBuffer.rewind(); + mCurrentBufferTimestamp = mTimestampManager.getCurrentTimestamp(); + } + + /** + * Sets the listener to receive the output from this buffer assembler. + */ + public void setListener(Listener listener) + { + mListener = listener; + } +} diff --git a/src/main/java/io/github/dsheirer/sample/complex/TimestampedComplexBuffer.java b/src/main/java/io/github/dsheirer/sample/complex/TimestampedComplexBuffer.java new file mode 100644 index 00000000..ac40103d --- /dev/null +++ b/src/main/java/io/github/dsheirer/sample/complex/TimestampedComplexBuffer.java @@ -0,0 +1,88 @@ +/******************************************************************************* + * sdr-trunk + * Copyright (C) 2014-2018 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.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. + * + * @param samples of data + */ + public TimestampedComplexBuffer(float[] samples, long timestamp) + { + this(samples, null, timestamp); + } + + /** + * 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()); + } + + /** + * Constructs a timestamped complex buffer using the current system time in milliseconds. + * + * @param samples of data + */ + public TimestampedComplexBuffer(float[] samples) + { + this(samples, null, System.currentTimeMillis()); + } + + /** + * Reference timestamp for this complex buffer. Timestamp is relative to the first sample in the buffer. + * @return timestamp as milliseconds since epoch + */ + @Override + public long getTimestamp() + { + return mTimestamp; + } + + /** + * Indicates that this complex buffer has a reference timestamp + * @return true + */ + @Override + public boolean hasTimestamp() + { + return true; + } +} 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 5631a9bd..3628165e 100644 --- a/src/main/java/io/github/dsheirer/sample/real/RealBuffer.java +++ b/src/main/java/io/github/dsheirer/sample/real/RealBuffer.java @@ -1,26 +1,51 @@ +/******************************************************************************* + * sdr-trunk + * Copyright (C) 2014-2018 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.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 - */ - public RealBuffer( float[] samples ) - { - super( samples ); - } + /** + * 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); + } - /** - * Creates a deep copy of the buffer - */ - public RealBuffer copyOf() - { - float[] copy = Arrays.copyOf( mSamples, mSamples.length ); - - return new RealBuffer( copy ); - } + /** + * Wrapper around float array containing real float samples + */ + public RealBuffer(float[] samples) + { + super(samples); + } + + /** + * Creates a deep copy of the buffer and a shallow (reference) copy of the source event + */ + public RealBuffer copyOf() + { + float[] copy = Arrays.copyOf(getSamples(), getSamples().length); + + return new RealBuffer(copy, getSourceEvent()); + } } diff --git a/src/main/java/io/github/dsheirer/source/SourceEvent.java b/src/main/java/io/github/dsheirer/source/SourceEvent.java index 53ef609f..a5c23087 100644 --- a/src/main/java/io/github/dsheirer/source/SourceEvent.java +++ b/src/main/java/io/github/dsheirer/source/SourceEvent.java @@ -30,6 +30,7 @@ public class SourceEvent NOTIFICATION_FREQUENCY_CHANGE, NOTIFICATION_FREQUENCY_CORRECTION_CHANGE, NOTIFICATION_SAMPLE_RATE_CHANGE, + NOTIFICATION_STOP_SAMPLE_STREAM, REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE, REQUEST_FREQUENCY_CHANGE, @@ -38,7 +39,7 @@ public class SourceEvent REQUEST_STOP_SAMPLE_STREAM; public static EnumSet NOTIFICATION_EVENTS = - EnumSet.range(NOTIFICATION_CHANNEL_COUNT_CHANGE, NOTIFICATION_SAMPLE_RATE_CHANGE); + EnumSet.range(NOTIFICATION_CHANNEL_COUNT_CHANGE, NOTIFICATION_STOP_SAMPLE_STREAM); public static EnumSet REQUEST_EVENTS = EnumSet.range(NOTIFICATION_CHANNEL_COUNT_CHANGE, NOTIFICATION_SAMPLE_RATE_CHANGE); } @@ -201,7 +202,7 @@ public class SourceEvent /** * Creates a new start sample stream request event */ - public static SourceEvent startSampleStream(Source source) + public static SourceEvent startSampleStreamRequest(Source source) { return new SourceEvent(Event.REQUEST_START_SAMPLE_STREAM, source); } @@ -209,11 +210,19 @@ public class SourceEvent /** * Creates a new stop sample stream request event */ - public static SourceEvent stopSampleStream(Source source) + public static SourceEvent stopSampleStreamRequest(Source source) { return new SourceEvent(Event.REQUEST_STOP_SAMPLE_STREAM, source); } + /** + * Creates a new stop sample stream request event + */ + public static SourceEvent stopSampleStreamNotification(Source source) + { + return new SourceEvent(Event.NOTIFICATION_STOP_SAMPLE_STREAM, source); + } + /** * Creates a request to dispose of the source. This is normally used for TunerChannelSource to notify the * parent source manager that the channel is no longer needed diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerController.java b/src/main/java/io/github/dsheirer/source/tuner/TunerController.java index 688ee442..36b177e9 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/TunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/TunerController.java @@ -197,12 +197,13 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor, } /** - * Usable half bandwidth - total bandwidth minus unusable space at either end of the spectrum, minus the blocked - * center bandwidth. + * Usable half bandwidth - total bandwidth minus unusable space at either end of the spectrum. + * + * Note: this does not account for any DC spike protected frequency region at the center of the tuner */ public int getUsableHalfBandwidth() { - return (int)(getUsableBandwidth() / 2) - mMiddleUnusableHalfBandwidth; + return (int)(getUsableBandwidth() / 2); } /** diff --git a/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannel.java b/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannel.java index 1c2e5424..0fb0bd1c 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannel.java +++ b/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannel.java @@ -77,10 +77,8 @@ public class TunerChannel implements Comparable { StringBuilder sb = new StringBuilder(); - sb.append("Channel: "); - sb.append(getMinFrequency()); - sb.append("-"); - sb.append(getMaxFrequency()); + sb.append("Channel:").append(getFrequency()).append(" (").append(getMinFrequency()); + sb.append("-").append(getMaxFrequency()).append(")"); return sb.toString(); } diff --git a/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java b/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java index 96386b45..217f7409 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java +++ b/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java @@ -122,7 +122,7 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc if(mProducerSourceEventListener != null) { - mProducerSourceEventListener.receive(SourceEvent.startSampleStream(this)); + mProducerSourceEventListener.receive(SourceEvent.startSampleStreamRequest(this)); } mBufferProcessor.start(); @@ -136,7 +136,7 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc { if(mProducerSourceEventListener != null) { - mProducerSourceEventListener.receive(SourceEvent.stopSampleStream(this)); + mProducerSourceEventListener.receive(SourceEvent.stopSampleStreamRequest(this)); mProducerSourceEventListener.receive(SourceEvent.sourceDisposeRequest(this)); } 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 9276f649..2a5be07e 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 @@ -42,8 +42,6 @@ public class TunerSourceManager extends AbstractSourceManager public TunerSourceManager(TunerController tunerController) { mTunerController = tunerController; - //Register to receive frequency and sample rate notifications - mTunerController.addListener(this); mPolyphaseChannelManager = new PolyphaseChannelManager(tunerController); //Register to receive channel count change notifications for rebroadcasting @@ -166,6 +164,13 @@ public class TunerSourceManager extends AbstractSourceManager return currentCenterFrequency; } + long channelSetBandwidth = getChannelSetBandwidth(channels); + + if(channelSetBandwidth > mTunerController.getUsableBandwidth()) + { + throw new IllegalArgumentException("Channel set bandwidth is greater than tuner's available bandwidth"); + } + long bestIntegralFrequency = getIntegralFrequency(channels); //See if we can reuse the current frequency @@ -175,32 +180,42 @@ public class TunerSourceManager extends AbstractSourceManager return currentCenterFrequency; } - int integralHalfChannelCount = (int)(mTunerController.getUsableHalfBandwidth() / - mPolyphaseChannelManager.getChannelBandwidth()) + 1; + //Start by placing the first channel at the minimum location within the tuner bandwidth + double usableHalfBandwidth = mTunerController.getUsableHalfBandwidth(); + double startFrequency = channels.first().getMinFrequency() + usableHalfBandwidth; - int integralHalfBandwidth = (int)(integralHalfChannelCount * mPolyphaseChannelManager.getChannelBandwidth()); + //Align the test frequency to the next greater integral frequency placing to place the first channel at the + // minimum extreme of the available bandwidth as a starting location + double delta = Math.abs(startFrequency - bestIntegralFrequency); + double adjustment = delta % mPolyphaseChannelManager.getChannelBandwidth(); - long minimumFrequency = channels.last().getFrequency() - integralHalfBandwidth; - long testFrequency = channels.first().getFrequency() + integralHalfBandwidth; + startFrequency += adjustment; - if(isValidCenterFrequency(channels,testFrequency)) + long availableTestBandwidth = mTunerController.getUsableBandwidth() - channelSetBandwidth; + int availableTestChannels = (int)(availableTestBandwidth / mPolyphaseChannelManager.getChannelBandwidth()) + 1; + + if(isValidCenterFrequency(channels,(long)startFrequency)) { - return testFrequency; + return (long)startFrequency; } - while(testFrequency >= minimumFrequency) + int currentChannel = 1; + + while(currentChannel <= availableTestChannels) { - testFrequency -= (long)mPolyphaseChannelManager.getChannelBandwidth(); + long testFrequency = (long)(startFrequency - (currentChannel * mPolyphaseChannelManager.getChannelBandwidth())); if(isValidCenterFrequency(channels, testFrequency)) { return testFrequency; } + + currentChannel++; } //If we can't find an integral center frequency, choose a frequency that will fit the channels - minimumFrequency = channels.last().getFrequency() - mTunerController.getUsableHalfBandwidth(); - testFrequency = channels.first().getMinFrequency() + mTunerController.getUsableHalfBandwidth(); + long testFrequency = channels.first().getMinFrequency() + mTunerController.getUsableHalfBandwidth(); + long minimumFrequency = testFrequency - availableTestBandwidth; if(isValidCenterFrequency(channels,testFrequency)) { @@ -220,6 +235,11 @@ public class TunerSourceManager extends AbstractSourceManager throw new IllegalArgumentException("Can't calculate valid center frequency for the channel set"); } + private long getChannelSetBandwidth(SortedSet channels) + { + return channels.last().getMaxFrequency() - channels.first().getMinFrequency(); + } + /** * Indicates if the candidate center frequency is valid to ensure that all of the channels fit within the tuner's * bandwidth and avoid any center DC spike blocked frequency region @@ -406,12 +426,6 @@ public class TunerSourceManager extends AbstractSourceManager //Rebroadcast this event to any registered listeners (ie tuner and tuner controller) broadcast(sourceEvent); break; - case NOTIFICATION_FREQUENCY_CHANGE: - mPolyphaseChannelManager.process(sourceEvent); - break; - case NOTIFICATION_SAMPLE_RATE_CHANGE: - mPolyphaseChannelManager.process(sourceEvent); - break; default: mLog.info("Received an 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 b08403eb..51d157b5 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 @@ -21,10 +21,14 @@ package io.github.dsheirer.source.tuner.test; 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; @@ -38,6 +42,7 @@ 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 @@ -79,6 +84,16 @@ 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 */ @@ -170,7 +185,7 @@ public class SampleGenerator if(mListener != null) { float[] samples = mOscillator.generate(mSamplesPerInterval); - mListener.receive(new ComplexBuffer(samples)); + mListener.receive(new TimestampedComplexBuffer(samples, mSourceEventQueue.poll())); if(mSweepUpdateInterval != 0) { diff --git a/src/main/java/io/github/dsheirer/source/tuner/test/TestTuner.java b/src/main/java/io/github/dsheirer/source/tuner/test/TestTuner.java index fa918b56..4a81a60b 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/test/TestTuner.java +++ b/src/main/java/io/github/dsheirer/source/tuner/test/TestTuner.java @@ -47,7 +47,7 @@ public class TestTuner extends Tuner */ public TestTunerController getTunerController() { - return (TestTunerController)getTunerController(); + return (TestTunerController)super.getTunerController(); } @Override 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 0c94dd5d..bac4b008 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,6 +20,7 @@ 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; @@ -33,9 +34,9 @@ public class TestTunerController extends TunerController public static final long MINIMUM_FREQUENCY = 1000000l; //1 MHz public static final long MAXIMUM_FREQUENCY = 10000000000l; //10 GHz public static final long DEFAULT_CENTER_FREQUENCY = 100000000; //100 MHz - public static final int DEFAULT_SAMPLE_RATE = 50000; //25 kHz - 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 + public static final int DEFAULT_SAMPLE_RATE = 100000; //25 kHz + public static final int DC_NOISE_BANDWIDTH = 0; // +/-5 kHz + public static final double USABLE_BANDWIDTH = 1.00; //100% usable bandwidth public static final int SPECTRAL_FRAME_RATE = 20; public static final long SAMPLE_GENERATION_INTERVAL = 1000 / SPECTRAL_FRAME_RATE; @@ -129,6 +130,7 @@ public class TestTunerController extends TunerController { long frequency = toneFrequency - mReferenceFrequency; mSampleGenerator.setFrequency(frequency); + mSampleGenerator.queue(SourceEvent.frequencyChange(frequency)); } /** @@ -155,5 +157,6 @@ public class TestTunerController extends TunerController public void setSampleRate(int sampleRate) throws SourceException { mFrequencyController.setSampleRate(sampleRate); + mSampleGenerator.queue(SourceEvent.sampleRateChange(sampleRate)); } }