mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 16:47:31 -05:00
Implements a complex sample delay buffer integrated into the tuner
channel manager so that channel source requests can have a start timestamp specified and the channel can be preloaded with samples from the delay buffer. If no timestamp is specified, current time is assumed. This feature requires corresponding updates to any decoder that wishes to implement the timestamped sample preloading functionality.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
package io.github.dsheirer.dsp.filter.design;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
|
||||
import io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesigner;
|
||||
import javafx.application.Application;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
@@ -61,14 +61,15 @@ public class FilterViewer extends Application
|
||||
// .passBandRipple(0.01)
|
||||
// .build();
|
||||
|
||||
FIRFilterSpecification specification = FIRFilterSpecification.bandPassBuilder()
|
||||
.sampleRate(8000)
|
||||
.stopFrequency1(1000)
|
||||
.passFrequencyBegin(1100)
|
||||
.passFrequencyEnd(1900)
|
||||
.stopFrequency2(2000)
|
||||
.stopRipple(0.000001)
|
||||
.passRipple(0.00001)
|
||||
FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
|
||||
.sampleRate(50000.0)
|
||||
.gridDensity(16)
|
||||
.passBandCutoff(6000)
|
||||
.passBandAmplitude(1.0)
|
||||
.passBandRipple(0.01)
|
||||
.stopBandStart(6700)
|
||||
.stopBandAmplitude(0.0)
|
||||
.stopBandRipple(0.01)
|
||||
.build();
|
||||
|
||||
float[] taps = null;
|
||||
@@ -81,37 +82,25 @@ public class FilterViewer extends Application
|
||||
// catch(FilterDesignException fde)
|
||||
// {
|
||||
// mLog.error("Error");
|
||||
// }
|
||||
|
||||
// try
|
||||
// {
|
||||
// RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification);
|
||||
//
|
||||
// if(designer.isValid())
|
||||
// {
|
||||
// taps = designer.getImpulseResponse();
|
||||
// }
|
||||
// }
|
||||
// catch(FilterDesignException fde)
|
||||
// {
|
||||
// mLog.error("Filter design error", fde);
|
||||
// }
|
||||
//
|
||||
// if(taps == null)
|
||||
// {
|
||||
// throw new IllegalStateException("Couldn't design filter");
|
||||
// }
|
||||
|
||||
try
|
||||
{
|
||||
// taps = FilterFactory.getSincM2Synthesizer(0.25, 2, 9);
|
||||
// taps = FilterFactory.getSincM2Channelizer(12500.0, 2, 9, true);
|
||||
taps = FilterFactory.getSincM2Synthesizer( 25000.0, 12500.0, 2, 9);
|
||||
RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification);
|
||||
|
||||
if(designer.isValid())
|
||||
{
|
||||
taps = designer.getImpulseResponse();
|
||||
}
|
||||
}
|
||||
catch(FilterDesignException e)
|
||||
catch(FilterDesignException fde)
|
||||
{
|
||||
e.printStackTrace();
|
||||
mLog.error("Filter design error", fde);
|
||||
}
|
||||
|
||||
if(taps == null)
|
||||
{
|
||||
throw new IllegalStateException("Couldn't design filter");
|
||||
}
|
||||
|
||||
return taps;
|
||||
|
||||
@@ -95,10 +95,45 @@ public class DQPSKDecisionDirectedSymbolEvaluator implements IPSKSymbolEvaluator
|
||||
|
||||
//Since we've rotated the error symbol back to 0 radians, the quadrature value closely approximates the
|
||||
//arctan of the error angle relative to 0 radians and this provides our error value
|
||||
mPhaseError = -mEvaluationSymbol.quadrature();
|
||||
float errorNormalized = normalize(mEvaluationSymbol.quadrature(), 0.3f);
|
||||
|
||||
mPhaseError = -errorNormalized;
|
||||
|
||||
//Timing error is the same as phase error with the sign corrected according to the vector's angular rotation
|
||||
mTimingError = mEvaluationSymbol.quadrature() * mTimingErrorPolarity;
|
||||
mTimingError = errorNormalized * mTimingErrorPolarity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constrains value to the range of ( -maximum <> maximum )
|
||||
*/
|
||||
private static float clip(float value, float maximum)
|
||||
{
|
||||
if(value > maximum)
|
||||
{
|
||||
return maximum;
|
||||
}
|
||||
else if(value < -maximum)
|
||||
{
|
||||
return -maximum;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constrains timing error to +/- the maximum value and corrects any
|
||||
* floating point invalid numbers
|
||||
*/
|
||||
private float normalize(float error, float maximum)
|
||||
{
|
||||
if(Float.isNaN(error))
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
return clip(error, maximum);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -100,7 +100,7 @@ public class DQPSKGardnerSymbolEvaluator implements IPSKSymbolEvaluator<Dibit>
|
||||
|
||||
//Since we've rotated the error symbol back to 0 radians, the quadrature value closely approximates the
|
||||
//arctan of the error angle relative to 0 radians and this provides our error value
|
||||
mPhaseError = -mEvaluationSymbol.quadrature();
|
||||
mPhaseError = normalize(-mEvaluationSymbol.quadrature(), 0.3f);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -34,6 +34,7 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.prefs.Preferences;
|
||||
|
||||
/**
|
||||
* Application for viewing and working with demodulators and recording files.
|
||||
@@ -42,6 +43,9 @@ public class DemodulatorViewerFX extends Application
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(DemodulatorViewerFX.class);
|
||||
|
||||
private static final String PREFERENCE_FILE_OPEN_DEFAULT_FOLDER = "default.folder";
|
||||
|
||||
private Preferences mPreferences = Preferences.userNodeForPackage(DemodulatorViewerFX.class);
|
||||
private Stage mStage;
|
||||
private MenuBar mMenuBar;
|
||||
private RecentFilesMenu mRecentFilesMenu;
|
||||
@@ -117,9 +121,22 @@ public class DemodulatorViewerFX extends Application
|
||||
{
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Open an I/Q recording file");
|
||||
|
||||
String directory = mPreferences.get(PREFERENCE_FILE_OPEN_DEFAULT_FOLDER, null);
|
||||
|
||||
if(directory != null)
|
||||
{
|
||||
fileChooser.setInitialDirectory(new File(directory));
|
||||
}
|
||||
|
||||
File file = fileChooser.showOpenDialog(mStage);
|
||||
getViewerDesktop().load(file);
|
||||
getRecentFilesMenu().add(file);
|
||||
|
||||
if(file != null)
|
||||
{
|
||||
getViewerDesktop().load(file);
|
||||
getRecentFilesMenu().add(file);
|
||||
mPreferences.put(PREFERENCE_FILE_OPEN_DEFAULT_FOLDER, file.getParent());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import java.util.prefs.Preferences;
|
||||
|
||||
public class RecentFilesMenu extends Menu implements EventHandler<ActionEvent>
|
||||
{
|
||||
private static final String PREFERENCE_RECENT_FILE = ".recent.file.";
|
||||
private Preferences mPreferences = Preferences.userNodeForPackage(RecentFilesMenu.class);
|
||||
private IFileSelectionListener mFileSelectionListener;
|
||||
private String mIdentifier;
|
||||
@@ -111,7 +112,7 @@ public class RecentFilesMenu extends Menu implements EventHandler<ActionEvent>
|
||||
*/
|
||||
private String getKey(int index)
|
||||
{
|
||||
return mIdentifier + ".recent.file." + index;
|
||||
return mIdentifier + PREFERENCE_RECENT_FILE + index;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,6 +21,7 @@ import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.chart.LineChart;
|
||||
import javafx.scene.chart.NumberAxis;
|
||||
import javafx.scene.chart.ValueAxis;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -42,6 +43,24 @@ public class DoubleLineChart extends LineChart implements Listener<Double>
|
||||
init(length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the minimum displayable value for the y axis
|
||||
* @param min
|
||||
*/
|
||||
public void setMin(double min)
|
||||
{
|
||||
((ValueAxis)getYAxis()).setLowerBound(min);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum displayable value for the y axis
|
||||
* @param max
|
||||
*/
|
||||
public void setMax(double max)
|
||||
{
|
||||
((ValueAxis)getYAxis()).setUpperBound(max);
|
||||
}
|
||||
|
||||
private void init(int length)
|
||||
{
|
||||
mDoubleCircularBuffer = new DoubleCircularBuffer(length);
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*******************************************************************************
|
||||
* 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.gui.instrument.chart;
|
||||
|
||||
import javafx.collections.ObservableList;
|
||||
|
||||
public class SamplesPerSymbolChart extends DoubleLineChart
|
||||
{
|
||||
private double mFractionalSamplesPerSymbol;
|
||||
private double mPreviousSamplesPerSymbol;
|
||||
|
||||
public SamplesPerSymbolChart(int length, double samplesPerSymbol)
|
||||
{
|
||||
super("Samples Per Symbol", -1.0, 1.0, 0.1, length);
|
||||
setSamplesPerSymbol(samplesPerSymbol);
|
||||
}
|
||||
|
||||
public void setSamplesPerSymbol(double samplesPerSymbol)
|
||||
{
|
||||
mFractionalSamplesPerSymbol = samplesPerSymbol - ((int)samplesPerSymbol);
|
||||
mPreviousSamplesPerSymbol = samplesPerSymbol;
|
||||
|
||||
ObservableList<Series> seriesList = getData();
|
||||
seriesList.get(0).setName("Samples Per Symbol:" + samplesPerSymbol);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(Double samplesPerSymbol)
|
||||
{
|
||||
double delta = samplesPerSymbol - mPreviousSamplesPerSymbol - mFractionalSamplesPerSymbol;
|
||||
|
||||
while(delta < (-1.0 + mFractionalSamplesPerSymbol))
|
||||
{
|
||||
delta++;
|
||||
}
|
||||
|
||||
while(delta > 1.0)
|
||||
{
|
||||
delta--;
|
||||
}
|
||||
|
||||
super.receive(delta);
|
||||
|
||||
mPreviousSamplesPerSymbol = samplesPerSymbol;
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import io.github.dsheirer.gui.instrument.chart.ComplexSampleLineChart;
|
||||
import io.github.dsheirer.gui.instrument.chart.DoubleLineChart;
|
||||
import io.github.dsheirer.gui.instrument.chart.EyeDiagramChart;
|
||||
import io.github.dsheirer.gui.instrument.chart.PhaseLineChart;
|
||||
import io.github.dsheirer.gui.instrument.chart.SamplesPerSymbolChart;
|
||||
import io.github.dsheirer.gui.instrument.chart.SymbolChart;
|
||||
import io.github.dsheirer.message.Message;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
@@ -34,14 +35,14 @@ public class P25Phase1LSMPane extends ComplexDecoderPane
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(P25Phase1LSMPane.class);
|
||||
|
||||
private HBox mSampleChartBox;
|
||||
private ComplexSampleLineChart mSampleLineChart;
|
||||
private HBox mTopChartBox;
|
||||
private ComplexSampleLineChart mDifferentialDemodulatedSamplesChartBox;
|
||||
private EyeDiagramChart mEyeDiagramChart;
|
||||
private HBox mDecoderChartBox;
|
||||
private SymbolChart mSymbolChart;
|
||||
private HBox mBottomChartBox;
|
||||
private SymbolChart mSymbolConstellationChart;
|
||||
private PhaseLineChart mPLLPhaseErrorLineChart;
|
||||
private DoubleLineChart mPLLFrequencyLineChart;
|
||||
private DoubleLineChart mSamplesPerSymbolLineChart;
|
||||
private SamplesPerSymbolChart mSamplesPerSymbolLineChart;
|
||||
private ReusableBufferBroadcaster mFilteredBufferBroadcaster = new ReusableBufferBroadcaster();
|
||||
private P25DecoderLSMInstrumented mDecoder = new P25DecoderLSMInstrumented(null);
|
||||
|
||||
@@ -56,16 +57,16 @@ public class P25Phase1LSMPane extends ComplexDecoderPane
|
||||
addListener(getDecoder());
|
||||
|
||||
getDecoder().setFilteredBufferListener(mFilteredBufferBroadcaster);
|
||||
getDecoder().setComplexSymbolListener(getSymbolChart());
|
||||
getDecoder().setComplexSymbolListener(getSymbolConstellationChart());
|
||||
getDecoder().setPLLPhaseErrorListener(getPLLPhaseErrorLineChart());
|
||||
getDecoder().setPLLFrequencyListener(getPLLFrequencyLineChart());
|
||||
getDecoder().setSymbolDecisionDataListener(getEyeDiagramChart());
|
||||
getDecoder().setSamplesPerSymbolListener(getSamplesPerSymbolLineChart());
|
||||
mFilteredBufferBroadcaster.addListener(getSampleLineChart());
|
||||
mFilteredBufferBroadcaster.addListener(getDifferentialDemodulatedSamplesChartBox());
|
||||
|
||||
HBox.setHgrow(getSampleChartBox(), Priority.ALWAYS);
|
||||
HBox.setHgrow(getDecoderChartBox(), Priority.ALWAYS);
|
||||
getChildren().addAll(getSampleChartBox(), getDecoderChartBox());
|
||||
HBox.setHgrow(getTopChartBox(), Priority.ALWAYS);
|
||||
HBox.setHgrow(getBottomChartBox(), Priority.ALWAYS);
|
||||
getChildren().addAll(getTopChartBox(), getBottomChartBox());
|
||||
|
||||
mDecoder.setMessageListener(new Listener<Message>()
|
||||
{
|
||||
@@ -86,7 +87,8 @@ public class P25Phase1LSMPane extends ComplexDecoderPane
|
||||
mDecoder.setSampleRate(sampleRate);
|
||||
double samplesPerSymbol = sampleRate / 4800.0;
|
||||
|
||||
getSampleLineChart().setSamplesPerSymbol((int)samplesPerSymbol);
|
||||
getDifferentialDemodulatedSamplesChartBox().setSamplesPerSymbol((int)samplesPerSymbol);
|
||||
getSamplesPerSymbolLineChart().setSamplesPerSymbol(samplesPerSymbol);
|
||||
}
|
||||
|
||||
private P25DecoderLSMInstrumented getDecoder()
|
||||
@@ -94,69 +96,69 @@ public class P25Phase1LSMPane extends ComplexDecoderPane
|
||||
return mDecoder;
|
||||
}
|
||||
|
||||
private SymbolChart getSymbolChart()
|
||||
private SymbolChart getSymbolConstellationChart()
|
||||
{
|
||||
if(mSymbolChart == null)
|
||||
if(mSymbolConstellationChart == null)
|
||||
{
|
||||
mSymbolChart = new SymbolChart(10);
|
||||
mSymbolConstellationChart = new SymbolChart(10);
|
||||
}
|
||||
|
||||
return mSymbolChart;
|
||||
return mSymbolConstellationChart;
|
||||
}
|
||||
|
||||
private HBox getDecoderChartBox()
|
||||
private HBox getBottomChartBox()
|
||||
{
|
||||
if(mDecoderChartBox == null)
|
||||
if(mBottomChartBox == null)
|
||||
{
|
||||
mDecoderChartBox = new HBox();
|
||||
mDecoderChartBox.setMaxHeight(Double.MAX_VALUE);
|
||||
getSymbolChart().setMaxWidth(Double.MAX_VALUE);
|
||||
mBottomChartBox = new HBox();
|
||||
mBottomChartBox.setMaxHeight(Double.MAX_VALUE);
|
||||
getSymbolConstellationChart().setMaxWidth(Double.MAX_VALUE);
|
||||
getPLLPhaseErrorLineChart().setMaxWidth(Double.MAX_VALUE);
|
||||
getPLLFrequencyLineChart().setMaxWidth(Double.MAX_VALUE);
|
||||
HBox.setHgrow(getSymbolChart(), Priority.ALWAYS);
|
||||
HBox.setHgrow(getSymbolConstellationChart(), Priority.ALWAYS);
|
||||
HBox.setHgrow(getPLLPhaseErrorLineChart(), Priority.ALWAYS);
|
||||
HBox.setHgrow(getPLLFrequencyLineChart(), Priority.ALWAYS);
|
||||
mDecoderChartBox.getChildren().addAll(getSymbolChart(), getPLLPhaseErrorLineChart(),
|
||||
mBottomChartBox.getChildren().addAll(getSymbolConstellationChart(), getPLLPhaseErrorLineChart(),
|
||||
getPLLFrequencyLineChart());
|
||||
}
|
||||
|
||||
return mDecoderChartBox;
|
||||
return mBottomChartBox;
|
||||
}
|
||||
|
||||
private HBox getSampleChartBox()
|
||||
private HBox getTopChartBox()
|
||||
{
|
||||
if(mSampleChartBox == null)
|
||||
if(mTopChartBox == null)
|
||||
{
|
||||
mSampleChartBox = new HBox();
|
||||
mSampleChartBox.setMaxHeight(Double.MAX_VALUE);
|
||||
getSampleLineChart().setMaxWidth(Double.MAX_VALUE);
|
||||
mTopChartBox = new HBox();
|
||||
mTopChartBox.setMaxHeight(Double.MAX_VALUE);
|
||||
getDifferentialDemodulatedSamplesChartBox().setMaxWidth(Double.MAX_VALUE);
|
||||
getEyeDiagramChart().setMaxWidth(Double.MAX_VALUE);
|
||||
getSamplesPerSymbolLineChart().setMaxWidth(Double.MAX_VALUE);
|
||||
HBox.setHgrow(getSampleLineChart(), Priority.ALWAYS);
|
||||
HBox.setHgrow(getDifferentialDemodulatedSamplesChartBox(), Priority.ALWAYS);
|
||||
HBox.setHgrow(getEyeDiagramChart(), Priority.ALWAYS);
|
||||
HBox.setHgrow(getSamplesPerSymbolLineChart(), Priority.ALWAYS);
|
||||
mSampleChartBox.getChildren().addAll(getSampleLineChart(), getEyeDiagramChart(),
|
||||
mTopChartBox.getChildren().addAll(getDifferentialDemodulatedSamplesChartBox(), getEyeDiagramChart(),
|
||||
getSamplesPerSymbolLineChart());
|
||||
}
|
||||
|
||||
return mSampleChartBox;
|
||||
return mTopChartBox;
|
||||
}
|
||||
|
||||
private ComplexSampleLineChart getSampleLineChart()
|
||||
private ComplexSampleLineChart getDifferentialDemodulatedSamplesChartBox()
|
||||
{
|
||||
if(mSampleLineChart == null)
|
||||
if(mDifferentialDemodulatedSamplesChartBox == null)
|
||||
{
|
||||
mSampleLineChart = new ComplexSampleLineChart(100, 10);
|
||||
mDifferentialDemodulatedSamplesChartBox = new ComplexSampleLineChart(100, 10);
|
||||
}
|
||||
|
||||
return mSampleLineChart;
|
||||
return mDifferentialDemodulatedSamplesChartBox;
|
||||
}
|
||||
|
||||
private EyeDiagramChart getEyeDiagramChart()
|
||||
{
|
||||
if(mEyeDiagramChart == null)
|
||||
{
|
||||
mEyeDiagramChart = new EyeDiagramChart(10, "Middle: 3-4 / Symbol: 8-9");
|
||||
mEyeDiagramChart = new EyeDiagramChart(10, "Symbol/Eye Diagram");
|
||||
}
|
||||
|
||||
return mEyeDiagramChart;
|
||||
@@ -182,11 +184,11 @@ public class P25Phase1LSMPane extends ComplexDecoderPane
|
||||
return mPLLFrequencyLineChart;
|
||||
}
|
||||
|
||||
private DoubleLineChart getSamplesPerSymbolLineChart()
|
||||
private SamplesPerSymbolChart getSamplesPerSymbolLineChart()
|
||||
{
|
||||
if(mSamplesPerSymbolLineChart == null)
|
||||
{
|
||||
mSamplesPerSymbolLineChart = new DoubleLineChart( "Sample Point", 4.5, 7.0, 0.1, 40);
|
||||
mSamplesPerSymbolLineChart = new SamplesPerSymbolChart(40, 6.25);
|
||||
}
|
||||
|
||||
return mSamplesPerSymbolLineChart;
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
/*******************************************************************************
|
||||
* 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.buffer;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
|
||||
/**
|
||||
* Delay buffer/queue for reusable complex buffers with support for pre-loading new buffer listeners (ie channels)
|
||||
* with delayed sample buffers and broadcasting of incoming sample buffers to a list of listeners.
|
||||
*
|
||||
* This class is designed to compensate/account for any delays in processing of a control channel and subsequent
|
||||
* channel grants so that when a new channel is allocated, that channel can be pre-loaded with time delayed sample
|
||||
* buffers that start as close as possible to the channel grant timestamp, in order to avoid cutting off the
|
||||
* beginning of a channel grant transmission.
|
||||
*
|
||||
* New channel listeners are enqueued with a request timestamp and as new buffers arrive, the new channel listeners
|
||||
* are preloaded with delayed buffers from the queue and then added to an internal broadcaster so that they
|
||||
* receive all subsequent buffers as normal.
|
||||
*
|
||||
* This class is designed to be controlled by the sample producer thread for adding new listeners and pre-loading
|
||||
* delayed buffers. Any listeners that are added to this class are expected to implement a non-blocking receive method
|
||||
* so as not to delay the stream of sample buffers. Channel listeners are expected to implement buffer queue processing
|
||||
* on another thread.
|
||||
*/
|
||||
public class ReusableComplexDelayBuffer implements Listener<ReusableComplexBuffer>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ReusableComplexDelayBuffer.class);
|
||||
|
||||
private ReusableBufferBroadcaster<ReusableComplexBuffer> mBroadcaster = new ReusableBufferBroadcaster<>();
|
||||
private LinkedTransferQueue<ActionRequest> mActionRequestQueue = new LinkedTransferQueue<>();
|
||||
private ActionRequest mActionRequest;
|
||||
|
||||
private ReusableComplexBuffer[] mDelayBuffer;
|
||||
private int mDelayBufferPointer = 0;
|
||||
private long mBufferDuration;
|
||||
|
||||
/**
|
||||
* Creates a new delay buffer with the specified delay length and the buffer sample rate.
|
||||
*
|
||||
* @param size of the delay queue
|
||||
* @param bufferDuration in milliseconds for each complex buffer processed by this instance
|
||||
*/
|
||||
public ReusableComplexDelayBuffer(int size, long bufferDuration)
|
||||
{
|
||||
mDelayBuffer = new ReusableComplexBuffer[size];
|
||||
mBufferDuration = bufferDuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares this instance for disposal by releasing all stored sample buffers.
|
||||
*/
|
||||
public void dispose()
|
||||
{
|
||||
clearBuffer();
|
||||
}
|
||||
|
||||
public void clear()
|
||||
{
|
||||
//Submit a clear buffer request to be processed upon the next buffer that arrives
|
||||
mActionRequestQueue.offer(new ActionRequest());
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears any delayed/enqueued sample buffers.
|
||||
*/
|
||||
private void clearBuffer()
|
||||
{
|
||||
for(int x = 0; x < mDelayBuffer.length; x++)
|
||||
{
|
||||
if(mDelayBuffer[x] != null)
|
||||
{
|
||||
mDelayBuffer[x].decrementUserCount();
|
||||
mDelayBuffer[x] = null;
|
||||
}
|
||||
}
|
||||
|
||||
mDelayBufferPointer = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void receive(ReusableComplexBuffer reusableComplexBuffer)
|
||||
{
|
||||
mActionRequest = mActionRequestQueue.poll();
|
||||
|
||||
while(mActionRequest != null)
|
||||
{
|
||||
switch(mActionRequest.getAction())
|
||||
{
|
||||
case ADD_USER:
|
||||
processNewListener(mActionRequest);
|
||||
break;
|
||||
case REMOVE_USER:
|
||||
mBroadcaster.removeListener(mActionRequest.getListener());
|
||||
break;
|
||||
case CLEAR_BUFFER:
|
||||
clearBuffer();
|
||||
break;
|
||||
}
|
||||
|
||||
mActionRequest = mActionRequestQueue.poll();
|
||||
}
|
||||
|
||||
//Increment user count before we hand it to the broadcaster so that we can keep it as a copy for the
|
||||
//delay buffer
|
||||
reusableComplexBuffer.incrementUserCount();
|
||||
|
||||
mBroadcaster.receive(reusableComplexBuffer);
|
||||
|
||||
//Decrement the user count on the oldest buffer before we replace it in the delay queue
|
||||
if(mDelayBuffer[mDelayBufferPointer] != null)
|
||||
{
|
||||
mDelayBuffer[mDelayBufferPointer].decrementUserCount();
|
||||
}
|
||||
|
||||
//Store the new buffer in the delay queue and increment the pointer
|
||||
mDelayBuffer[mDelayBufferPointer++] = reusableComplexBuffer;
|
||||
|
||||
//Wrap the delay buffer pointer as needed
|
||||
if(mDelayBufferPointer >= mDelayBuffer.length)
|
||||
{
|
||||
mDelayBufferPointer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if any listeners are registered with this delay buffer
|
||||
*/
|
||||
public boolean hasListeners()
|
||||
{
|
||||
return mBroadcaster.hasListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes any newly added listeners by checking all buffers in the delay queue and pre-loading the
|
||||
* listeners with any buffers that occur on or after the listener's requested start timestamp.
|
||||
*/
|
||||
private void processNewListener(ActionRequest listenerToAdd)
|
||||
{
|
||||
ReusableComplexBuffer bufferToEvaluate;
|
||||
|
||||
int pointer = mDelayBufferPointer;
|
||||
|
||||
int preloadedBufferCount = 0;
|
||||
|
||||
for(int x = 0; x < mDelayBuffer.length; x++)
|
||||
{
|
||||
bufferToEvaluate = mDelayBuffer[pointer];
|
||||
|
||||
if(bufferToEvaluate != null &&
|
||||
(bufferToEvaluate.getTimestamp() >= listenerToAdd.getTimestamp() ||
|
||||
bufferToEvaluate.getTimestamp() + mBufferDuration >= listenerToAdd.getTimestamp()))
|
||||
{
|
||||
bufferToEvaluate.incrementUserCount();
|
||||
listenerToAdd.getListener().receive(bufferToEvaluate);
|
||||
preloadedBufferCount++;
|
||||
}
|
||||
|
||||
pointer++;
|
||||
|
||||
if(pointer >= mDelayBuffer.length)
|
||||
{
|
||||
pointer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
mBroadcaster.addListener(listenerToAdd.getListener());
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the listener to receive a copy of each buffer. The listener will be preloaded with delayed buffers
|
||||
* that are at or after the specified timestamp.
|
||||
*
|
||||
* @param listener to add
|
||||
* @param timestamp of the oldest sample buffers to preload to the listener
|
||||
*/
|
||||
public void addListener(Listener<ReusableComplexBuffer> listener, long timestamp)
|
||||
{
|
||||
mActionRequestQueue.add(new ActionRequest(listener, timestamp));
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the listener from receiving sample buffers
|
||||
*/
|
||||
public void removeListener(Listener<ReusableComplexBuffer> listener)
|
||||
{
|
||||
mActionRequestQueue.add(new ActionRequest(listener));
|
||||
}
|
||||
|
||||
private enum Action{ADD_USER, REMOVE_USER, CLEAR_BUFFER};
|
||||
|
||||
/**
|
||||
* Actions that must be completed on the incoming sample stream thread.
|
||||
*/
|
||||
public class ActionRequest
|
||||
{
|
||||
private Action mAction;
|
||||
private Listener<ReusableComplexBuffer> mListener;
|
||||
private long mTimestamp;
|
||||
|
||||
/**
|
||||
* Creates an add listener request
|
||||
* @param listener to add
|
||||
* @param timestamp for the preloading buffers to the listener from the delay queue
|
||||
*/
|
||||
public ActionRequest(Listener<ReusableComplexBuffer> listener, long timestamp)
|
||||
{
|
||||
mAction = Action.ADD_USER;
|
||||
mListener = listener;
|
||||
mTimestamp = timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a remove listener request
|
||||
* @param listener
|
||||
*/
|
||||
public ActionRequest(Listener<ReusableComplexBuffer> listener)
|
||||
{
|
||||
mAction = Action.REMOVE_USER;
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a clearBuffer buffer request
|
||||
*/
|
||||
public ActionRequest()
|
||||
{
|
||||
mAction = Action.CLEAR_BUFFER;
|
||||
}
|
||||
|
||||
public Action getAction()
|
||||
{
|
||||
return mAction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener to be added/removed
|
||||
*/
|
||||
public Listener<ReusableComplexBuffer> getListener()
|
||||
{
|
||||
return mListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Requested timestamp for buffers to be preloaded.
|
||||
*/
|
||||
public long getTimestamp()
|
||||
{
|
||||
return mTimestamp;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,20 +56,28 @@ public class SourceEvent
|
||||
/**
|
||||
* Private constructor. Use the static constructor methods to create an event.
|
||||
*/
|
||||
private SourceEvent(Event event, Number value, String eventDescription)
|
||||
private SourceEvent(Event event, Source source, Number value, String eventDescription)
|
||||
{
|
||||
mEvent = event;
|
||||
mSource = source;
|
||||
mValue = value;
|
||||
mEventDescription = eventDescription;
|
||||
}
|
||||
|
||||
/**
|
||||
* Private constructor. Use the static constructor methods to create an event.
|
||||
*/
|
||||
private SourceEvent(Event event, Number value, String eventDescription)
|
||||
{
|
||||
this(event, null, value, eventDescription);
|
||||
}
|
||||
|
||||
/**
|
||||
* Private constructor. Use the static constructor methods to create an event.
|
||||
*/
|
||||
private SourceEvent(Event event, Number value)
|
||||
{
|
||||
mEvent = event;
|
||||
mValue = value;
|
||||
this(event, value, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,8 +85,12 @@ public class SourceEvent
|
||||
*/
|
||||
private SourceEvent(Event event, Source source)
|
||||
{
|
||||
mEvent = event;
|
||||
mSource = source;
|
||||
this(event, source, 0);
|
||||
}
|
||||
|
||||
private SourceEvent(Event event, Source source, Number value)
|
||||
{
|
||||
this(event, source, value, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -283,11 +295,22 @@ public class SourceEvent
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new start sample stream request event
|
||||
* Creates a new start sample stream request event. This method uses the current system time in
|
||||
* milliseconds as the requested sample start time.
|
||||
*/
|
||||
public static SourceEvent startSampleStreamRequest(Source source)
|
||||
{
|
||||
return new SourceEvent(Event.REQUEST_START_SAMPLE_STREAM, source);
|
||||
return startSampleStreamRequest(source, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new start sample stream request event and requests that samples (when available) for the
|
||||
* specified timestamp be pre-loaded into the source. The timestamp only applies for sources that incorporate
|
||||
* a sample delay buffer.
|
||||
*/
|
||||
public static SourceEvent startSampleStreamRequest(Source source, long timestamp)
|
||||
{
|
||||
return new SourceEvent(Event.REQUEST_START_SAMPLE_STREAM, source, timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -299,7 +322,7 @@ public class SourceEvent
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new stop sample stream request event
|
||||
* Creates a new stop sample stream notification event
|
||||
*/
|
||||
public static SourceEvent stopSampleStreamNotification(Source source)
|
||||
{
|
||||
|
||||
@@ -47,6 +47,14 @@ public class ComplexMixer
|
||||
mName = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Audio format used by the underlying mixer reader
|
||||
*/
|
||||
public AudioFormat getAudioFormat()
|
||||
{
|
||||
return mMixerReader.getAudioFormat();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the buffer size in bytes per buffer for each read interval.
|
||||
* @param bufferSize in bytes
|
||||
|
||||
@@ -72,6 +72,14 @@ public class MixerReader<T extends ReusableFloatBuffer> implements Runnable
|
||||
this(audioFormat, targetDataLine, abstractSampleAdapter, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Audio format used by this reader
|
||||
*/
|
||||
public AudioFormat getAudioFormat()
|
||||
{
|
||||
return mAudioFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the size of buffers to use. This reader will read from target data line 20 times per second.
|
||||
* @param bytesPerBuffer to size for each buffer read.
|
||||
|
||||
@@ -67,6 +67,21 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor,
|
||||
*/
|
||||
public abstract void dispose();
|
||||
|
||||
/**
|
||||
* Number of samples contained in each complex buffer provided by this tuner.
|
||||
*
|
||||
* @return number of complex sample in each buffer.
|
||||
*/
|
||||
public abstract int getBufferSampleCount();
|
||||
|
||||
/**
|
||||
* Duration in milliseconds for each sample buffer provided by this tuner
|
||||
*/
|
||||
public long getBufferDuration()
|
||||
{
|
||||
return (long)(1000.0 / (getSampleRate() / (double)getBufferSampleCount()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the ISourceEventListener interface to receive requests from sample consumers
|
||||
*/
|
||||
|
||||
@@ -107,6 +107,13 @@ public class AirspyTunerController extends USBTunerController
|
||||
mDevice = device;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBufferSampleCount()
|
||||
{
|
||||
//Each airspy complex sample is 4 bytes, so sample count is buffer size / 4
|
||||
return USB_TRANSFER_BUFFER_SIZE / 4;
|
||||
}
|
||||
|
||||
public void init() throws SourceException
|
||||
{
|
||||
mDeviceHandle = new DeviceHandle();
|
||||
|
||||
@@ -54,7 +54,6 @@ public class CICTunerChannelSource extends TunerChannelSource implements Listene
|
||||
private ReusableComplexBufferQueue mReusableComplexBufferQueue = new ReusableComplexBufferQueue("CICTunerChannelSource");
|
||||
private IOscillator mFrequencyCorrectionMixer;
|
||||
private ComplexPrimeCICDecimate mDecimationFilter;
|
||||
private Listener<ReusableComplexBuffer> mListener;
|
||||
private List<ReusableComplexBuffer> mSampleBuffers = new ArrayList<>();
|
||||
private double mChannelSampleRate;
|
||||
private long mChannelFrequencyCorrection = 0;
|
||||
|
||||
@@ -90,11 +90,17 @@ public abstract class FCDTunerController extends TunerController
|
||||
mComplexMixer = new ComplexMixer( mixerTDL.getTargetDataLine(), audioFormat, tunerName,
|
||||
new ComplexShortAdapter(mixerTDL.getMixerTunerType().getDisplayString()));
|
||||
|
||||
mComplexMixer.setBufferSize(audioFormat.getFrameSize() * (int)(audioFormat.getSampleRate() * 0.1));
|
||||
mComplexMixer.setBufferSize(audioFormat.getFrameSize() * getBufferSampleCount());
|
||||
|
||||
mComplexMixer.setBufferListener(mReusableBufferBroadcaster);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBufferSampleCount()
|
||||
{
|
||||
return (int)(mComplexMixer.getAudioFormat().getSampleRate() * 0.1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the super class functionality to auto-start the complex mixer and provide samples to listeners
|
||||
*/
|
||||
|
||||
@@ -109,6 +109,12 @@ public class HackRFTunerController extends USBTunerController
|
||||
mDeviceDescriptor = descriptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBufferSampleCount()
|
||||
{
|
||||
return USB_TRANSFER_BUFFER_SIZE / 2;
|
||||
}
|
||||
|
||||
public void init() throws SourceException
|
||||
{
|
||||
mDeviceHandle = new DeviceHandle();
|
||||
|
||||
@@ -17,6 +17,7 @@ package io.github.dsheirer.source.tuner.manager;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.buffer.ReusableComplexDelayBuffer;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import io.github.dsheirer.source.SourceException;
|
||||
import io.github.dsheirer.source.tuner.TunerController;
|
||||
@@ -38,12 +39,14 @@ public class HeterodyneChannelSourceManager extends ChannelSourceManager
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(HeterodyneChannelSourceManager.class);
|
||||
|
||||
private final static int OBJECTIVE_CHANNEL_SAMPLE_RATE = 30000;
|
||||
private final static int OBJECTIVE_CHANNEL_SAMPLE_RATE = 25000;
|
||||
private final static int DELAY_BUFFER_DURATION_MILLISECONDS = 2000;
|
||||
|
||||
private List<CICTunerChannelSource> mChannelSources = new CopyOnWriteArrayList<>();
|
||||
private SortedSet<TunerChannel> mTunerChannels = new TreeSet<>();
|
||||
private TunerController mTunerController;
|
||||
private ChannelSourceEventProcessor mChannelSourceEventProcessor = new ChannelSourceEventProcessor();
|
||||
private ReusableComplexDelayBuffer mSampleDelayBuffer;
|
||||
|
||||
public HeterodyneChannelSourceManager(TunerController tunerController)
|
||||
{
|
||||
@@ -136,6 +139,12 @@ public class HeterodyneChannelSourceManager extends ChannelSourceManager
|
||||
case NOTIFICATION_FREQUENCY_CHANGE:
|
||||
//Tuner center frequency has changed - update channels
|
||||
updateTunerFrequency(tunerSourceEvent.getValue().longValue());
|
||||
|
||||
//Clear the delay buffer since any delayed samples will be centered on the previous frequency
|
||||
if(mSampleDelayBuffer != null)
|
||||
{
|
||||
mSampleDelayBuffer.clear();
|
||||
}
|
||||
break;
|
||||
case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
|
||||
//The tuner is self-correcting for PPM error - relay to channels
|
||||
@@ -182,6 +191,39 @@ public class HeterodyneChannelSourceManager extends ChannelSourceManager
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a complex sample delay buffer and registers it with the tuner controller to start the flow
|
||||
* of complex sample buffers from the tuner.
|
||||
*/
|
||||
private void startDelayBuffer()
|
||||
{
|
||||
if(mSampleDelayBuffer == null)
|
||||
{
|
||||
int delayBufferSize = (int)(DELAY_BUFFER_DURATION_MILLISECONDS / mTunerController.getBufferDuration());
|
||||
mSampleDelayBuffer = new ReusableComplexDelayBuffer(delayBufferSize, mTunerController.getBufferDuration());
|
||||
|
||||
mLog.debug("Created/registered complex sample delay buffer of size [" + delayBufferSize +
|
||||
"] buffers and delay duration [" + DELAY_BUFFER_DURATION_MILLISECONDS +
|
||||
"ms] for tuner controller [" + mTunerController.getClass().getName() +
|
||||
"] with buffer duration [" + mTunerController.getBufferDuration() + "]");
|
||||
|
||||
mTunerController.addBufferListener(mSampleDelayBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deregisters the complex sample delay buffer and disposes of any queued reusable buffers.
|
||||
*/
|
||||
private void stopDelayBuffer()
|
||||
{
|
||||
if(mSampleDelayBuffer != null && !mSampleDelayBuffer.hasListeners())
|
||||
{
|
||||
mTunerController.removeBufferListener(mSampleDelayBuffer);
|
||||
mSampleDelayBuffer.dispose();
|
||||
mSampleDelayBuffer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes channel source events
|
||||
*/
|
||||
@@ -195,13 +237,20 @@ public class HeterodyneChannelSourceManager extends ChannelSourceManager
|
||||
case REQUEST_START_SAMPLE_STREAM:
|
||||
if(sourceEvent.getSource() instanceof CICTunerChannelSource)
|
||||
{
|
||||
mTunerController.addBufferListener((CICTunerChannelSource)sourceEvent.getSource());
|
||||
startDelayBuffer();
|
||||
|
||||
//The start sample stream request contains a start timestamp and the delay buffer
|
||||
//will preload the channel with delayed sample buffers that either contain the
|
||||
//timestamp or occur later/newer than the timestamp.
|
||||
mSampleDelayBuffer.addListener((CICTunerChannelSource)sourceEvent.getSource(),
|
||||
sourceEvent.getValue().longValue());
|
||||
}
|
||||
break;
|
||||
case REQUEST_STOP_SAMPLE_STREAM:
|
||||
if(sourceEvent.getSource() instanceof CICTunerChannelSource)
|
||||
{
|
||||
mTunerController.removeBufferListener((CICTunerChannelSource)sourceEvent.getSource());
|
||||
mSampleDelayBuffer.removeListener((CICTunerChannelSource)sourceEvent.getSource());
|
||||
stopDelayBuffer();
|
||||
}
|
||||
break;
|
||||
case REQUEST_SOURCE_DISPOSE:
|
||||
|
||||
@@ -95,6 +95,12 @@ public abstract class RTL2832TunerController extends USBTunerController
|
||||
mDeviceDescriptor = deviceDescriptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBufferSampleCount()
|
||||
{
|
||||
return getUSBTransferBufferSize(getSampleRate()) / 2; //2 bytes per complex sample
|
||||
}
|
||||
|
||||
|
||||
public void init() throws SourceException
|
||||
{
|
||||
@@ -987,17 +993,23 @@ public abstract class RTL2832TunerController extends USBTunerController
|
||||
|
||||
if(mUSBTransferProcessor != null)
|
||||
{
|
||||
if(sampleRate.getRate() > 300000)
|
||||
{
|
||||
mUSBTransferProcessor.setBufferSize(USB_TRANSFER_BUFFER_SIZE_HIGH_SAMPLE_RATE);
|
||||
}
|
||||
else
|
||||
{
|
||||
mUSBTransferProcessor.setBufferSize(USB_TRANSFER_BUFFER_SIZE_LOW_SAMPLE_RATE);
|
||||
}
|
||||
mUSBTransferProcessor.setBufferSize(getUSBTransferBufferSize(sampleRate.getRate()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the size of USB transfer buffers according to the sample rate
|
||||
*/
|
||||
private int getUSBTransferBufferSize(double sampleRate)
|
||||
{
|
||||
if(sampleRate > 300000)
|
||||
{
|
||||
return USB_TRANSFER_BUFFER_SIZE_HIGH_SAMPLE_RATE;
|
||||
}
|
||||
|
||||
return USB_TRANSFER_BUFFER_SIZE_LOW_SAMPLE_RATE;
|
||||
}
|
||||
|
||||
public void setSampleRateFrequencyCorrection(int ppm) throws SourceException
|
||||
{
|
||||
int offset = -ppm * TWO_TO_22_POWER / 1000000;
|
||||
|
||||
@@ -65,6 +65,12 @@ public class TestTunerController extends TunerController
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBufferSampleCount()
|
||||
{
|
||||
return SAMPLE_RATE / SPECTRAL_FRAME_RATE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
|
||||
@@ -209,6 +209,11 @@ public class ComplexWaveSource extends ComplexSource implements IControllableFil
|
||||
|
||||
if(broadcast && mListener != null)
|
||||
{
|
||||
if(samplesRead < 0)
|
||||
{
|
||||
throw new IOException("End of file reached");
|
||||
}
|
||||
|
||||
if(samplesRead < buffer.length)
|
||||
{
|
||||
buffer = Arrays.copyOf(buffer, samplesRead);
|
||||
|
||||
Reference in New Issue
Block a user