mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-11 08:07:24 -05:00
#39 WIP - adds support for timestamped complex buffers so that decoded
messages and events can be accurately timestamped, and more importantly, allocated traffic channels can request to be pre-loaded with buffered samples to ensure that traffic channels don't miss any samples that are currently missed due processing latency inherent in current buffer sizing. This should reduce the clipping of starting messages or voice on the traffic channels. Restructures processing path for tuner frequency and sample rate change events. The tuner now embeds these events in the sequence of buffers that are generated immediately after the event occurs since those are the buffers that will be impacted by the change. Previously, the change event would be broadcast throughout the system and would impact any buffered/cached buffers. This also reduces the number of source event change listeners that have to be managed. Note: this change is not yet implemented in all tuners ... only in the TestTuner implementation. Updates the polyphase channelizer to now buffer channel results and then dispatch the buffer to the channel output processors to improve efficiency of using float arrays versus single value complex float processing.
This commit is contained in:
@@ -90,13 +90,13 @@ public abstract class AbstractComplexPolyphaseChannelizer implements Listener<Co
|
||||
/**
|
||||
* Dispatches the processed channel samples to any registered polyphase channel outputs.
|
||||
*
|
||||
* @param channels containing an array of I/Q samples per channel
|
||||
* @param channelResultsBuffer containing an array of an array of I/Q samples per channel
|
||||
*/
|
||||
protected void dispatch(float[] channels)
|
||||
protected void dispatch(PolyphaseChannelResultsBuffer channelResultsBuffer)
|
||||
{
|
||||
for(PolyphaseChannelSource channel : mChannels)
|
||||
{
|
||||
channel.receiveChannelResults(channels);
|
||||
channel.receiveChannelResults(channelResultsBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ public class ChannelizerViewer extends JFrame
|
||||
{
|
||||
mTestTuner = new TestTuner();
|
||||
|
||||
mChannelCount = (int)(mTestTuner.getTunerController().getSampleRate() / CHANNEL_BANDWIDTH);
|
||||
mChannelCount = (int)(mTestTuner.getTunerController().getUsableBandwidth() / CHANNEL_BANDWIDTH);
|
||||
mChannelsPerRow = channelsPerRow;
|
||||
|
||||
mToneFrequency = mTestTuner.getTunerController().getFrequency() + 19200;
|
||||
@@ -192,11 +192,11 @@ public class ChannelizerViewer extends JFrame
|
||||
|
||||
int channelToLog = -1;
|
||||
|
||||
long baseFrequency = 100006250; //100.006250 MHz
|
||||
|
||||
for(int x = 0; x < mChannelCount; x++)
|
||||
{
|
||||
//place the channels left and right of 10.0 MHz
|
||||
long frequency = (long)(mTestTuner.getTunerController().getFrequency() +
|
||||
((x * CHANNEL_BANDWIDTH) - halfSpectralBandwidth) + (CHANNEL_BANDWIDTH / 2));
|
||||
long frequency = baseFrequency + (x * CHANNEL_BANDWIDTH);
|
||||
|
||||
mLog.debug("Channel " + x + " Frequency: " + frequency);
|
||||
|
||||
@@ -283,7 +283,6 @@ public class ChannelizerViewer extends JFrame
|
||||
@Override
|
||||
public void receive(ComplexBuffer complexBuffer)
|
||||
{
|
||||
mLog.debug("Sending samples to DFT processor: " + complexBuffer.getSamples().length);
|
||||
mDFTProcessor.receive(complexBuffer);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -39,6 +39,9 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann
|
||||
private int mRowPointer;
|
||||
private int mBlockSize;
|
||||
private FloatFFT_1D mFFT;
|
||||
private PolyphaseChannelResultsBuffer mChannelResultsBuffer;
|
||||
private int mChannelResultsBufferSize;
|
||||
private SampleTimestampManager mTimestampManager;
|
||||
|
||||
/**
|
||||
* Non-Maximally Decimated Polyphase Filter Bank (NMDPFB) channelizer that divides the input frequency band into
|
||||
@@ -52,16 +55,22 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann
|
||||
* @param sampleRate of the incoming sample stream
|
||||
* @param channelCount - number of filters/channels to output. Since this filter bank oversamples each filter
|
||||
* output, this number must be even (divisible by the 2x oversample rate).
|
||||
* @param bufferSize for the channel results buffers produced by this channelizer
|
||||
*/
|
||||
public ComplexPolyphaseChannelizerM2(float[] taps, int sampleRate, int channelCount)
|
||||
public ComplexPolyphaseChannelizerM2(float[] taps, int sampleRate, int channelCount, int bufferSize)
|
||||
{
|
||||
super(sampleRate, channelCount);
|
||||
|
||||
mChannelResultsBufferSize = bufferSize;
|
||||
|
||||
if(channelCount % 2 != 0)
|
||||
{
|
||||
throw new IllegalArgumentException("Channel count must be an even multiple of the oversample rate (2x)");
|
||||
throw new IllegalArgumentException("Channel count must be an even multiple of the over-sample rate (2x)");
|
||||
}
|
||||
|
||||
double oversampledChannelSampleRate = getChannelSampleRate() * 2.0;
|
||||
mTimestampManager = new SampleTimestampManager(oversampledChannelSampleRate);
|
||||
|
||||
init(taps);
|
||||
}
|
||||
|
||||
@@ -116,6 +125,12 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann
|
||||
@Override
|
||||
public void receive(ComplexBuffer complexBuffer)
|
||||
{
|
||||
//Use the buffer's reference timestamp to update our timestamp manager (for timestamping output buffers)
|
||||
if(complexBuffer.hasTimestamp())
|
||||
{
|
||||
mTimestampManager.setReferenceTimestamp(complexBuffer.getTimestamp());
|
||||
}
|
||||
|
||||
float[] samples = complexBuffer.getSamples();
|
||||
|
||||
int samplesPointer = 0;
|
||||
@@ -209,7 +224,58 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann
|
||||
//Rotate each of the channels to the correct phase using the IFFT
|
||||
mFFT.complexInverse(processed, true);
|
||||
|
||||
dispatch(processed);
|
||||
processChannelResults(processed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Buffers the channel results and dispatches the channel results buffer to the channel output processors when full.
|
||||
* @param channelResults
|
||||
*/
|
||||
private void processChannelResults(float[] channelResults)
|
||||
{
|
||||
if(mChannelResultsBuffer == null)
|
||||
{
|
||||
mChannelResultsBuffer = new PolyphaseChannelResultsBuffer(mTimestampManager.getCurrentTimestamp(),
|
||||
mChannelResultsBufferSize);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
mChannelResultsBuffer.add(channelResults);
|
||||
}
|
||||
catch(IllegalArgumentException iae)
|
||||
{
|
||||
//If the buffer is full (unlikely) or the channel results array length has changed (possible), flush the
|
||||
//current buffer, create a new one, and store the current results
|
||||
flushChannelResultsBuffer();
|
||||
|
||||
mChannelResultsBuffer = new PolyphaseChannelResultsBuffer(mTimestampManager.getCurrentTimestamp(),
|
||||
mChannelResultsBufferSize);
|
||||
|
||||
mChannelResultsBuffer.add(channelResults);
|
||||
}
|
||||
|
||||
if(mChannelResultsBuffer.isFull())
|
||||
{
|
||||
flushChannelResultsBuffer();
|
||||
}
|
||||
|
||||
//Each channel results array is equivalent to one sample from our timestamp manager's perspective
|
||||
mTimestampManager.increment();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches the non-empty channel results buffer to the channel output processors and nullifies the reference to
|
||||
* the buffer so that a new buffer can be created upon receiving the next channel results.
|
||||
*/
|
||||
private void flushChannelResultsBuffer()
|
||||
{
|
||||
if(mChannelResultsBuffer != null && !mChannelResultsBuffer.isEmpty())
|
||||
{
|
||||
dispatch(mChannelResultsBuffer);
|
||||
}
|
||||
|
||||
mChannelResultsBuffer = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -304,46 +370,4 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
mLog.debug("Starting ...");
|
||||
|
||||
int channelCount = 4;
|
||||
int channelBandwidth = 12500;
|
||||
int sampleRate = channelCount * channelBandwidth;
|
||||
int tapsPerFilter = 14;
|
||||
|
||||
int channelCenter = (int)(12500 * 1.5);
|
||||
|
||||
float[] taps = new float[16];
|
||||
for(int x = 0; x < taps.length; x++)
|
||||
{
|
||||
taps[x] = x;
|
||||
}
|
||||
float[] samples = new float[32];
|
||||
for(int x = 0; x < samples.length; x++)
|
||||
{
|
||||
samples[x] = x;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// ComplexPolyphaseChannelizerM2 channelizer = new ComplexPolyphaseChannelizerM2(sampleRate, tapsPerFilter);
|
||||
ComplexPolyphaseChannelizerM2 channelizer = new ComplexPolyphaseChannelizerM2(taps,sampleRate, channelCount);
|
||||
|
||||
// Oscillator oscillator = new Oscillator(channelCenter, 2 * sampleRate);
|
||||
//
|
||||
// ComplexBuffer complexBuffer = oscillator.generateComplexBuffer(1000);
|
||||
ComplexBuffer complexBuffer = new ComplexBuffer(samples);
|
||||
|
||||
channelizer.receive(complexBuffer);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Error", e);
|
||||
}
|
||||
|
||||
mLog.debug("Finished!");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ import io.github.dsheirer.dsp.filter.channelizer.output.TwoChannelOutputProcesso
|
||||
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
|
||||
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;
|
||||
@@ -36,7 +36,7 @@ import java.util.SortedSet;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
public class PolyphaseChannelManager
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelManager.class);
|
||||
private static final double MINIMUM_CHANNEL_BANDWIDTH = 12500.0;
|
||||
@@ -49,6 +49,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
private ChannelCalculator mChannelCalculator;
|
||||
private ComplexPolyphaseChannelizerM2 mPolyphaseChannelizer;
|
||||
private ChannelSourceEventListener mChannelSourceEventListener = new ChannelSourceEventListener();
|
||||
private BufferSourceEventMonitor mBufferSourceEventMonitor = new BufferSourceEventMonitor();
|
||||
private ScheduledBufferProcessor mBufferProcessor;
|
||||
|
||||
/**
|
||||
@@ -86,8 +87,9 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
mChannelCalculator = new ChannelCalculator(sampleRate, channelCount, frequency, CHANNEL_OVERSAMPLING);
|
||||
|
||||
mBufferProcessor = new ScheduledBufferProcessor(500, 100, 50, 50);
|
||||
mBufferProcessor.setListener(mBufferSourceEventMonitor);
|
||||
|
||||
updateChannelizer(sampleRate, null);
|
||||
updateChannelizerSampleRate(sampleRate);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -127,6 +129,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
try
|
||||
{
|
||||
List<Integer> 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<Integer> 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<ComplexBuffer>
|
||||
{
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||
* *********************************************************************************************************************
|
||||
*
|
||||
******************************************************************************/
|
||||
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<ComplexBuffer> listener)
|
||||
{
|
||||
mComplexBufferAssembler.setListener(listener);
|
||||
mTimestampedBufferAssembler.setListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,7 +69,7 @@ public class PolyphaseChannelSource extends TunerChannelSource
|
||||
@Override
|
||||
public void removeListener(Listener<ComplexBuffer> 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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<float[]> mChannelResultsQueue;
|
||||
private List<float[]> mChannelResultsToProcess = new ArrayList<>();
|
||||
private OverflowableTransferQueue<PolyphaseChannelResultsBuffer> mChannelResultsQueue;
|
||||
private List<PolyphaseChannelResultsBuffer> 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<float[]> channelResults, ComplexSampleListener listener);
|
||||
public abstract void process(List<PolyphaseChannelResultsBuffer> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<float[]> channelResults, ComplexSampleListener listener)
|
||||
public void process(List<PolyphaseChannelResultsBuffer> 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<float[]> channelResults, ComplexSampleListener listener)
|
||||
public void process(List<PolyphaseChannelResultsBuffer> 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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
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<ComplexBuffer> 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<ComplexBuffer> listener)
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Event> 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<Event> 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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -77,10 +77,8 @@ public class TunerChannel implements Comparable<TunerChannel>
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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<TunerChannel> 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;
|
||||
|
||||
@@ -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<SourceEvent> 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)
|
||||
{
|
||||
|
||||
@@ -47,7 +47,7 @@ public class TestTuner extends Tuner
|
||||
*/
|
||||
public TestTunerController getTunerController()
|
||||
{
|
||||
return (TestTunerController)getTunerController();
|
||||
return (TestTunerController)super.getTunerController();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user