#1178 removes synchronization on channel processing manager's source event handler to enable more fine-grained channel start/stop event processing. This was causing the channel start and stop events to occur sequentially. Adds stream processing with coordinated heartbeat. (#1185)

Co-authored-by: Dennis Sheirer <dsheirer@github.com>
This commit is contained in:
Denny Sheirer
2022-03-21 05:47:20 -04:00
committed by GitHub
parent f9a7bbeddf
commit 1b80f5f1b3
7 changed files with 331 additions and 444 deletions

View File

@@ -1,247 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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.source.tuner.channel;
import io.github.dsheirer.buffer.INativeBuffer;
import io.github.dsheirer.buffer.NativeBufferPoisonPill;
import io.github.dsheirer.dsp.filter.cic.PrimeCicDecimationFilter;
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
import io.github.dsheirer.dsp.mixer.ComplexMixer;
import io.github.dsheirer.dsp.mixer.ComplexMixerFactory;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.sample.complex.ComplexSamplesRepackager;
import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.util.Dispatcher;
import java.util.Iterator;
import java.util.List;
/**
* Complex sample source that provides a frequency-translated and decimated sample buffer stream. Uses a
* CIC decimation filter that requires the decimation rate to be an integer multiple. Sample buffer processing
* occurs on a scheduled runnable thread.
*/
public class CICTunerChannelSource<T extends INativeBuffer> extends TunerChannelSource implements Listener<T>
{
//Maximum number of filled buffers for the blocking queue
private static final int BUFFER_MAX_CAPACITY = 300;
//Threshold for resetting buffer overflow condition
private static final int BUFFER_OVERFLOW_RESET_THRESHOLD = 100;
//Samples repackager is set to 2048 (a power of 2) to align with channel processing chain requirements.
private final ComplexSamplesRepackager mSamplesRepackager = new ComplexSamplesRepackager(1024);
private Dispatcher<T> mBufferDispatcher;
private ComplexMixer mFrequencyCorrectionMixer;
private PrimeCicDecimationFilter mIDecimationFilter;
private PrimeCicDecimationFilter mQDecimationFilter;
private double mChannelSampleRate;
private long mChannelFrequencyCorrection = 0;
private long mTunerFrequency;
private Listener<ComplexSamples> mListener;
/**
* Constructs a frequency translating and CIC decimating channel source.
*
* @param producerSourceEventListener to receive sample stream start/stop requests
* @param tunerChannel that details the desired channel frequency and bandwidth
* @param sampleRate of the incoming sample stream
* @param channelSpecification for the requested channel.
* @throws FilterDesignException if a final cleanup filter cannot be designed using the remez filter
* designer and the filter parameters.
*/
public CICTunerChannelSource(Listener<SourceEvent> producerSourceEventListener, TunerChannel tunerChannel,
double sampleRate, ChannelSpecification channelSpecification) throws FilterDesignException
{
super(producerSourceEventListener, tunerChannel);
int decimation = (int)(sampleRate / channelSpecification.getMinimumSampleRate());
mIDecimationFilter = new PrimeCicDecimationFilter(sampleRate, decimation, channelSpecification.getPassFrequency(),
channelSpecification.getStopFrequency());
mQDecimationFilter = new PrimeCicDecimationFilter(sampleRate, decimation, channelSpecification.getPassFrequency(),
channelSpecification.getStopFrequency());
mBufferDispatcher = new Dispatcher(BUFFER_MAX_CAPACITY, "sdrtrunk cic channel " +
tunerChannel.getFrequency(), new NativeBufferPoisonPill());
mBufferDispatcher.setListener(new NativeBufferProcessor());
//Setup the frequency mixer to the current source frequency
mChannelSampleRate = sampleRate / (double)decimation;
mTunerFrequency = tunerChannel.getFrequency();
long frequencyOffset = mTunerFrequency - getTunerChannel().getFrequency();
mFrequencyCorrectionMixer = ComplexMixerFactory.getMixer(frequencyOffset, sampleRate);
}
@Override
public void start()
{
super.start();
mBufferDispatcher.start();
}
@Override
public void stop()
{
super.stop();
mBufferDispatcher.stop();
}
@Override
public void dispose()
{
//
}
/**
* Primary interface for receiving incoming complex sample buffers to be frequency translated and decimated.
*/
@Override
public void receive(T complexSamples)
{
mBufferDispatcher.receive(complexSamples);
}
/**
* Sets/updates the center frequency for the sample streaming being sent from the producer.
*
* @param frequency in hertz
*/
@Override
public void setFrequency(long frequency)
{
mTunerFrequency = frequency;
updateMixerFrequencyOffset();
}
/**
* Current frequency correction being applied to the channel sample stream
*
* @return correction in hertz
*/
public long getFrequencyCorrection()
{
return mChannelFrequencyCorrection;
}
/**
* Changes the frequency correction value and broadcasts the change to the registered downstream listener.
*
* @param correction current frequency correction value.
*/
public void setFrequencyCorrection(long correction)
{
mChannelFrequencyCorrection = correction;
updateMixerFrequencyOffset();
broadcastConsumerSourceEvent(SourceEvent.frequencyCorrectionChange(mChannelFrequencyCorrection));
}
/**
* Calculates the local mixer frequency offset from the tuned frequency,
* channel's requested frequency, and channel frequency correction.
*/
private void updateMixerFrequencyOffset()
{
long offset = mTunerFrequency - getTunerChannel().getFrequency() - mChannelFrequencyCorrection;
mFrequencyCorrectionMixer.setFrequency(offset);
}
/**
* Sets the sample rate of the incoming sample stream from the producer
*
* @param sampleRate in hertz
*/
@Override
protected void setSampleRate(double sampleRate)
{
//Not implemented. Sample rate changes are not permitted once sample stream starts
}
/**
* Sets the frequency correction for the outbound sample stream to the consumer
*
* @param correction in hertz
*/
@Override
protected void setChannelFrequencyCorrection(long correction)
{
mChannelFrequencyCorrection = correction;
broadcastConsumerSourceEvent(SourceEvent.channelFrequencyCorrectionChange(correction));
updateMixerFrequencyOffset();
}
/**
* Frequency correction value currently being applied to the outbound sample stream
*/
@Override
public long getChannelFrequencyCorrection()
{
return mChannelFrequencyCorrection;
}
/**
* Sets the listener to receive the complex buffer sample output from this channel
*
* @param listener to receive complex buffers
*/
@Override
public void setListener(Listener<ComplexSamples> listener)
{
mListener = listener;
}
@Override
public double getSampleRate()
{
return mChannelSampleRate;
}
/**
* Processes native buffers received from the dispatcher and sends to the registered listener
*/
public class NativeBufferProcessor implements Listener<T>
{
@Override
public void receive(T nativeBuffer)
{
getHeartbeatManager().broadcast();
if(mListener != null)
{
Iterator<ComplexSamples> iterator = nativeBuffer.iterator();
while(iterator.hasNext())
{
ComplexSamples basebanded = mFrequencyCorrectionMixer.mix(iterator.next());
float[] i = mIDecimationFilter.decimate(basebanded.i());
float[] q = mQDecimationFilter.decimate(basebanded.q());
List<ComplexSamples> repackaged = mSamplesRepackager.process(i, q);
for(ComplexSamples samples: repackaged)
{
mListener.receive(samples);
}
}
}
}
}
}

View File

@@ -54,6 +54,7 @@ public class HalfBandTunerChannelSource<T extends INativeBuffer> extends TunerCh
private long mChannelFrequencyCorrection = 0;
private long mTunerFrequency;
private Listener<ComplexSamples> mListener;
private StreamProcessorWithHeartbeat<ComplexSamples> mStreamHeartbeatProcessor;
/**
* Constructs a frequency translating and CIC decimating channel source.
@@ -70,6 +71,8 @@ public class HalfBandTunerChannelSource<T extends INativeBuffer> extends TunerCh
{
super(producerSourceEventListener, tunerChannel);
mStreamHeartbeatProcessor = new StreamProcessorWithHeartbeat<>(getHeartbeatManager(), HEARTBEAT_INTERVAL_MS);
int desiredDecimation = (int)(sampleRate / channelSpecification.getMinimumSampleRate());
int decimation = DecimationFilterFactory.getDecimationRate(desiredDecimation);
@@ -91,6 +94,7 @@ public class HalfBandTunerChannelSource<T extends INativeBuffer> extends TunerCh
public void start()
{
super.start();
mStreamHeartbeatProcessor.start();
mBufferDispatcher.start();
}
@@ -99,6 +103,7 @@ public class HalfBandTunerChannelSource<T extends INativeBuffer> extends TunerCh
{
super.stop();
mBufferDispatcher.stop();
mStreamHeartbeatProcessor.stop();
}
@Override
@@ -200,13 +205,7 @@ public class HalfBandTunerChannelSource<T extends INativeBuffer> extends TunerCh
@Override
public void setListener(Listener<ComplexSamples> listener)
{
if(listener == null)
{
//Wait for the native buffer processor thread to die before we nullify the listener
stop();
}
mListener = listener;
mStreamHeartbeatProcessor.setListener(listener);
}
@Override
@@ -223,21 +222,14 @@ public class HalfBandTunerChannelSource<T extends INativeBuffer> extends TunerCh
@Override
public void receive(T nativeBuffer)
{
getHeartbeatManager().broadcast();
Iterator<ComplexSamples> iterator = nativeBuffer.iterator();
Listener<ComplexSamples> listener = mListener;
if(listener != null)
while(iterator.hasNext())
{
Iterator<ComplexSamples> iterator = nativeBuffer.iterator();
while(iterator.hasNext())
{
ComplexSamples basebanded = mFrequencyCorrectionMixer.mix(iterator.next());
float[] i = mIDecimationFilter.decimateReal(basebanded.i());
float[] q = mQDecimationFilter.decimateReal(basebanded.q());
listener.receive(new ComplexSamples(i, q));
}
ComplexSamples basebanded = mFrequencyCorrectionMixer.mix(iterator.next());
float[] i = mIDecimationFilter.decimateReal(basebanded.i());
float[] q = mQDecimationFilter.decimateReal(basebanded.q());
mStreamHeartbeatProcessor.receive(new ComplexSamples(i, q));
}
}
}

View File

@@ -42,7 +42,7 @@ public class PassThroughChannelSource extends TunerChannelSource implements ISou
private final static Logger mLog = LoggerFactory.getLogger(PassThroughChannelSource.class);
private TunerController mTunerController;
private Dispatcher<INativeBuffer> mBufferDispatcher;
private Listener<ComplexSamples> mComplexSamplesListener;
private StreamProcessorWithHeartbeat<ComplexSamples> mStreamHeartbeatProcessor;
/**
* Constructs an instance
@@ -59,12 +59,14 @@ public class PassThroughChannelSource extends TunerChannelSource implements ISou
mBufferDispatcher = new Dispatcher<>(500, "sdrtrunk pass-through channel " +
tunerChannel.getFrequency(), new NativeBufferPoisonPill());
mBufferDispatcher.setListener(new BufferProcessor());
mStreamHeartbeatProcessor = new StreamProcessorWithHeartbeat<>(getHeartbeatManager(), HEARTBEAT_INTERVAL_MS);
}
@Override
public void start()
{
super.start();
mStreamHeartbeatProcessor.stop();
mBufferDispatcher.start();
}
@@ -73,6 +75,7 @@ public class PassThroughChannelSource extends TunerChannelSource implements ISou
{
super.stop();
mBufferDispatcher.stop();
mStreamHeartbeatProcessor.stop();
}
@Override
@@ -100,9 +103,9 @@ public class PassThroughChannelSource extends TunerChannelSource implements ISou
}
@Override
public void setListener(Listener<ComplexSamples> complexSamplesListener)
public void setListener(Listener<ComplexSamples> listener)
{
mComplexSamplesListener = complexSamplesListener;
mStreamHeartbeatProcessor.setListener(listener);
}
@Override
@@ -122,16 +125,11 @@ public class PassThroughChannelSource extends TunerChannelSource implements ISou
@Override
public void receive(INativeBuffer nativeBuffer)
{
getHeartbeatManager().broadcast();
Iterator<ComplexSamples> iterator = nativeBuffer.iterator();
if(mComplexSamplesListener != null)
while(iterator.hasNext())
{
Iterator<ComplexSamples> iterator = nativeBuffer.iterator();
while(iterator.hasNext())
{
mComplexSamplesListener.receive(iterator.next());
}
mStreamHeartbeatProcessor.receive(iterator.next());
}
}
}

View File

@@ -0,0 +1,135 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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.source.tuner.channel;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.heartbeat.HeartbeatManager;
import io.github.dsheirer.util.ThreadPool;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
* Thread-safe implementation that coordinates the flow of data/buffer stream processing with a parallel, periodic
* heartbeat using an internal locking mechanism to coordinate the two activities. This class is intended to be used
* by TunerChannelSource implementations to coordinate the flow of sample buffers produced on a dispatch thread with a
* scheduled timer driven heartbeat mechanism, to ensure the downstream listener receives either samples or a
* heartbeat, but not both at the same time.
*
* Samples are processed as they arrive and are immediately followed by a heartbeat. A separate scheduled timer runs
* on a periodic schedule to optionally fire a heartbeat, independent of the sample stream rate. The scheduled
* heartbeat is optional and will skip a heartbeat interval if this processor is currently dispatching a sample buffer.
* Sample stream processing will block until a currently running scheduled heartbeat timer event completes, when these
* two events occur at the same time.
*/
public class StreamProcessorWithHeartbeat<T> implements Listener<T>
{
private ReentrantLock mLock = new ReentrantLock();
private HeartbeatManager mHeartbeatManager;
private long mHeartbeatInterval;
private Listener<T> mListener;
private ScheduledFuture<?> mHeartbeatTimerFuture;
/**
* Constructs an instance
* @param heartbeatManager that will be invoked by an internal scheduled timer
* @param heartbeatInterval in milliseconds
*/
public StreamProcessorWithHeartbeat(HeartbeatManager heartbeatManager, long heartbeatInterval)
{
mHeartbeatManager = heartbeatManager;
mHeartbeatInterval = heartbeatInterval;
}
/**
* Starts the heartbeat timer
*/
public void start()
{
if(mHeartbeatTimerFuture == null)
{
mHeartbeatTimerFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(() -> fireOptionalHeartbeat(),
mHeartbeatInterval, mHeartbeatInterval, TimeUnit.MILLISECONDS);
}
}
/**
* Stops the heartbeat timer
*/
public void stop()
{
if(mHeartbeatTimerFuture != null)
{
mHeartbeatTimerFuture.cancel(true);
mHeartbeatTimerFuture = null;
}
}
/**
* Optionally fires a heartbeat if we're not currently processing a native buffer
*/
private void fireOptionalHeartbeat()
{
//Acquire lock only if it's not already locked, otherwise ignore heartbeat for this timer interval.
if(mLock.tryLock())
{
try
{
mHeartbeatManager.broadcast();
}
finally
{
mLock.unlock();
}
}
}
@Override
public void receive(T data)
{
mLock.lock();
try
{
mHeartbeatManager.broadcast();
Listener<T> listener = mListener;
if(listener != null)
{
listener.receive(data);
}
}
finally
{
mLock.unlock();
}
}
/**
* Registers the listener to receive data/buffers for the defined generic type that are distributed to this processor.
* @param listener to register or null to deregister an existing listener
*/
public void setListener(Listener<T> listener)
{
mListener = listener;
}
}

View File

@@ -31,12 +31,11 @@ import org.slf4j.LoggerFactory;
public abstract class TunerChannelSource extends ComplexSource implements ISourceEventProcessor
{
private final static Logger mLog = LoggerFactory.getLogger(TunerChannelSource.class);
private static final long BUFFER_PROCESSOR_RUN_INTERVAL_MILLISECONDS = 50;
protected static final long HEARTBEAT_INTERVAL_MS = 100;
private SourceEventListenerToProcessorAdapter mConsumerSourceEventListenerAdapter;
protected TunerChannel mTunerChannel;
private Listener<SourceEvent> mProducerSourceEventListener;
private Listener<SourceEvent> mConsumerSourceEventListener;
// private ScheduledIntervalProcessor mScheduledIntervalProcessor = new ScheduledIntervalProcessor();
/**
* Tuner Channel Source is a Digital Drop Channel (DDC) abstract class that defines the minimum functionality
@@ -245,95 +244,4 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc
{
mConsumerSourceEventListener = null;
}
// /**
// * Processor to periodically invoke buffer sample processing on an interval timer. At each interval this processor
// * sends a heartbeat to the registered consumer and then commands the sub-class implementation to process any
// * queued buffers and distribute complex buffer sample(s) to the registered consumer.
// */
// public class ScheduledIntervalProcessor implements Runnable
// {
// private ScheduledFuture<?> mScheduledFuture;
// private boolean mStopped = false;
//
// /**
// * Commands this processor to do a shutdown at the end of this or the next iteration. Once successfully
// * shutdown, it will invoke the performDisposal() method to cleanup this instance.
// */
// public void stop()
// {
// mStopped = true;
// }
//
// /**
// * Starts buffer processing on a fixed interval using a scheduled thread pool
// */
// public void start()
// {
// if(mScheduledFuture == null)
// {
// mScheduledFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(this, 0,
// BUFFER_PROCESSOR_RUN_INTERVAL_MILLISECONDS, TimeUnit.MILLISECONDS);
// }
// }
//
// /**
// * Implementation of the Runnable interface to periodically send a heartbeat and then process buffer samples.
// */
// @Override
// public void run()
// {
// try
// {
// if(!mStopped)
// {
// try
// {
// getHeartbeatManager().broadcast();
// }
// catch(Throwable t)
// {
// mLog.error("Error while sending heartbeat", t);
// }
// }
//
// if(!mStopped)
// {
// try
// {
// processSamples();
// }
// catch(Throwable t)
// {
// mLog.error("Error while processing samples", t);
// }
// }
//
// if(mStopped)
// {
// if(mScheduledFuture != null)
// {
// //Set may-interrupt to false so that we can complete this iteration
// mScheduledFuture.cancel(false);
// }
//
// mScheduledFuture = null;
//
// try
// {
// getHeartbeatManager().broadcast();
// performDisposal();
// }
// catch(Throwable t)
// {
// mLog.error("Error during final shutdown processing of samples", t);
// }
// }
// }
// catch(Throwable t)
// {
// mLog.error("Error during heartbeat processing", t);
// }
// }
// }
}