mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 08:37:25 -05:00
#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:
@@ -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.
|
||||
|
||||
@@ -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 + "]");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -109,7 +109,6 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc
|
||||
public void stop()
|
||||
{
|
||||
broadcastProducerSourceEvent(SourceEvent.stopSampleStreamRequest(this));
|
||||
broadcastProducerSourceEvent(SourceEvent.sourceDisposeRequest(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user