#1506 Resolves multiple performance issues: tuner freezing after a period of run time, buffer overflows, degraded audio playback. (#1513)

-Reduces message and event histories per channel from 500 to 200.
-Resolves issue of tuner freezing due to cancelled USB transfer buffers not being resubmitted.
-Reworks the audio playback system to remove in-progress call preemption and remove thread locks which can cause glitchy behavior during audio playback.
-Updates audio outputs to also use single thread pool for processing audio segments.
-Updates MBECallSequenceConverter to batch convert MBE to WAV audio.
-Unifies recording filename prefixes across baseband, mbe and binary recordings.
-Resolves issue with registering heartbeat manager to dispatcher.
-Uses single-thread thread pools for Dispatcher to support batch & process to overcome context switching consistency issue in Windows.

Co-authored-by: Dennis Sheirer <dsheirer@github.com>
This commit is contained in:
Denny Sheirer
2023-04-11 04:07:41 -04:00
committed by GitHub
parent 32a8363b45
commit 372d2ff870
34 changed files with 802 additions and 1112 deletions

View File

@@ -52,7 +52,6 @@ public class SourceEvent
REQUEST_FREQUENCY_CHANGE,
REQUEST_FREQUENCY_ROTATION,
REQUEST_FREQUENCY_SELECTION,
REQUEST_SOURCE_DISPOSE,
REQUEST_CHANGE_SQUELCH_THRESHOLD,
REQUEST_CURRENT_SQUELCH_THRESHOLD,
REQUEST_START_SAMPLE_STREAM,
@@ -368,15 +367,6 @@ public class SourceEvent
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
*/
public static SourceEvent sourceDisposeRequest(Source source)
{
return new SourceEvent(Event.REQUEST_SOURCE_DISPOSE, source);
}
/**
* Creates a request to cycle to the next source frequency. This is normally used for decoders to request the
* next frequency in a list when a multiple-frequency source configuration is defined.

View File

@@ -19,7 +19,6 @@
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.decimate.DecimationFilterFactory;
import io.github.dsheirer.dsp.filter.decimate.IRealDecimationFilter;
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
@@ -49,9 +48,9 @@ public class HalfBandTunerChannelSource<T extends INativeBuffer> extends TunerCh
private ComplexMixer mFrequencyCorrectionMixer;
private IRealDecimationFilter mIDecimationFilter;
private IRealDecimationFilter mQDecimationFilter;
private Listener<ComplexSamples> mSamplesListener;
private double mChannelSampleRate;
private long mTunerFrequency;
private StreamProcessorWithHeartbeat<ComplexSamples> mStreamHeartbeatProcessor;
/**
* Constructs a frequency translating and CIC decimating channel source.
@@ -68,16 +67,15 @@ 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);
mIDecimationFilter = DecimationFilterFactory.getRealDecimationFilter(decimation);
mQDecimationFilter = DecimationFilterFactory.getRealDecimationFilter(decimation);
mBufferDispatcher = new Dispatcher(BUFFER_MAX_CAPACITY, "sdrtrunk heterodyne channel " +
tunerChannel.getFrequency(), new NativeBufferPoisonPill());
//Set dispatcher to process 1/10 of estimated sample arrival rate, 20 times per second (up to 200% per interval)
mBufferDispatcher = new Dispatcher("sdrtrunk heterodyne channel " + tunerChannel.getFrequency(),
(int)(sampleRate / 10), 50, getHeartbeatManager());
mBufferDispatcher.setListener(new NativeBufferProcessor());
//Setup the frequency mixer to the current source frequency
@@ -91,7 +89,6 @@ public class HalfBandTunerChannelSource<T extends INativeBuffer> extends TunerCh
public void start()
{
super.start();
mStreamHeartbeatProcessor.start();
mBufferDispatcher.start();
}
@@ -100,7 +97,6 @@ public class HalfBandTunerChannelSource<T extends INativeBuffer> extends TunerCh
{
super.stop();
mBufferDispatcher.stop();
mStreamHeartbeatProcessor.stop();
}
@Override
@@ -167,7 +163,7 @@ public class HalfBandTunerChannelSource<T extends INativeBuffer> extends TunerCh
@Override
public void setListener(Listener<ComplexSamples> listener)
{
mStreamHeartbeatProcessor.setListener(listener);
mSamplesListener = listener;
}
@Override
@@ -184,14 +180,29 @@ public class HalfBandTunerChannelSource<T extends INativeBuffer> extends TunerCh
@Override
public void receive(T nativeBuffer)
{
Iterator<ComplexSamples> iterator = nativeBuffer.iterator();
while(iterator.hasNext())
if(mSamplesListener != null)
{
ComplexSamples basebanded = mFrequencyCorrectionMixer.mix(iterator.next());
float[] i = mIDecimationFilter.decimateReal(basebanded.i());
float[] q = mQDecimationFilter.decimateReal(basebanded.q());
mStreamHeartbeatProcessor.receive(new ComplexSamples(i, q, basebanded.timestamp()));
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());
try
{
mSamplesListener.receive(new ComplexSamples(i, q, basebanded.timestamp()));
}
catch(Throwable t)
{
//The listener can be made null and cause the error - only log if we have a non-null listener
if(mSamplesListener != null)
{
mLog.error("Error dispatching complex samples to listener [" + mSamplesListener + "]");
}
}
}
}
}
}

View File

@@ -20,7 +20,6 @@
package io.github.dsheirer.source.tuner.channel;
import io.github.dsheirer.buffer.INativeBuffer;
import io.github.dsheirer.buffer.NativeBufferPoisonPill;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.source.ISourceEventListener;
@@ -41,7 +40,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 StreamProcessorWithHeartbeat<ComplexSamples> mStreamHeartbeatProcessor;
private Listener<ComplexSamples> mBufferListener;
/**
* Constructs an instance
@@ -55,17 +54,15 @@ public class PassThroughChannelSource extends TunerChannelSource implements ISou
{
super(listener, tunerChannel);
mTunerController = tunerController;
mBufferDispatcher = new Dispatcher<>(500, "sdrtrunk pass-through channel " +
tunerChannel.getFrequency(), new NativeBufferPoisonPill());
mBufferDispatcher = new Dispatcher<>("sdrtrunk pass-through channel " + tunerChannel.getFrequency(),
250, 50, getHeartbeatManager());
mBufferDispatcher.setListener(new BufferProcessor());
mStreamHeartbeatProcessor = new StreamProcessorWithHeartbeat<>(getHeartbeatManager(), HEARTBEAT_INTERVAL_MS);
}
@Override
public void start()
{
super.start();
mStreamHeartbeatProcessor.stop();
mBufferDispatcher.start();
}
@@ -74,7 +71,6 @@ public class PassThroughChannelSource extends TunerChannelSource implements ISou
{
super.stop();
mBufferDispatcher.stop();
mStreamHeartbeatProcessor.stop();
}
@Override
@@ -92,7 +88,7 @@ public class PassThroughChannelSource extends TunerChannelSource implements ISou
@Override
public void setListener(Listener<ComplexSamples> listener)
{
mStreamHeartbeatProcessor.setListener(listener);
mBufferListener = listener;
}
@Override
@@ -112,11 +108,21 @@ public class PassThroughChannelSource extends TunerChannelSource implements ISou
@Override
public void receive(INativeBuffer nativeBuffer)
{
Iterator<ComplexSamples> iterator = nativeBuffer.iterator();
while(iterator.hasNext())
if(mBufferListener != null)
{
mStreamHeartbeatProcessor.receive(iterator.next());
Iterator<ComplexSamples> iterator = nativeBuffer.iterator();
while(iterator.hasNext())
{
try
{
mBufferListener.receive(iterator.next());
}
catch(Throwable t)
{
mLog.error("Error dispatching complex sample buffers to listener [" + mBufferListener + "]", t);
}
}
}
}
}

View File

@@ -1,129 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 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 static final Logger mLog = LoggerFactory.getLogger(StreamProcessorWithHeartbeat.class);
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()
{
try
{
mHeartbeatManager.broadcast();
}
catch(Exception e)
{
mLog.error("Error firing optional heartbeat", e);
}
}
@Override
public void receive(T data)
{
try
{
mHeartbeatManager.broadcast();
Listener<T> listener = mListener;
if(listener != null)
{
listener.receive(data);
}
}
catch(Exception e)
{
mLog.error("Error distributing data to listeners following heartbeat", e);
}
}
/**
* 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

@@ -109,7 +109,6 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc
public void stop()
{
broadcastProducerSourceEvent(SourceEvent.stopSampleStreamRequest(this));
broadcastProducerSourceEvent(SourceEvent.sourceDisposeRequest(this));
}
@Override

View File

@@ -291,6 +291,7 @@ public class HeterodyneChannelSourceManager extends ChannelSourceManager
{
switch(sourceEvent.getEvent())
{
//TODO: protect start/stop processing with a reentrant lock
case REQUEST_START_SAMPLE_STREAM:
if(sourceEvent.getSource() instanceof HalfBandTunerChannelSource)
{
@@ -304,19 +305,13 @@ public class HeterodyneChannelSourceManager extends ChannelSourceManager
}
break;
case REQUEST_STOP_SAMPLE_STREAM:
if(sourceEvent.getSource() instanceof HalfBandTunerChannelSource)
if(sourceEvent.getSource() instanceof HalfBandTunerChannelSource halfBandSource)
{
mSampleDelayBuffer.removeListener((HalfBandTunerChannelSource)sourceEvent.getSource());
mSampleDelayBuffer.removeListener(halfBandSource);
stopDelayBuffer();
}
break;
case REQUEST_SOURCE_DISPOSE:
if(sourceEvent.getSource() instanceof HalfBandTunerChannelSource)
{
HalfBandTunerChannelSource channelSource = (HalfBandTunerChannelSource) sourceEvent.getSource();
mChannelSources.remove(channelSource);
mTunerChannels.remove(channelSource.getTunerChannel());
channelSource.dispose();
mChannelSources.remove(halfBandSource);
mTunerChannels.remove(halfBandSource.getTunerChannel());
halfBandSource.dispose();
//Unlock the tuner controller if there are no more channels
if(getTunerChannelCount() == 0)

View File

@@ -123,7 +123,6 @@ public class R820TTunerEditor extends TunerEditor<RTL2832Tuner,R820TTunerConfigu
@Override
protected void tunerStatusUpdated()
{
mLog.info("Tuner status was updated.");
setLoading(true);
if(hasTuner())

View File

@@ -699,23 +699,23 @@ public abstract class USBTunerController extends TunerController
case LibUsb.TRANSFER_STALL:
case LibUsb.TRANSFER_TIMED_OUT:
case LibUsb.TRANSFER_ERROR:
//Note: cancel flag can be set by libusb, independent of commanded cancel of transfers - we simply
//resubmit the transfer for continued use.
case LibUsb.TRANSFER_CANCELLED:
int transferLength = transfer.actualLength();
if(transferLength > 0)
{
dispatchTransfer(transfer);
transfer.buffer().rewind();
}
transfer.buffer().rewind();
if(mAutoResubmitTransfers)
{
submitTransfer(transfer);
}
break;
case LibUsb.TRANSFER_CANCELLED:
//Reset the transfer but don't do anything else since we're shutting down
transfer.buffer().rewind();
break;
default:
//Unexpected transfer error - shutdown the tuner
transfer.buffer().rewind();