diff --git a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java index 242d0c5e..7a3aea62 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java @@ -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; diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDecisionDirectedSymbolEvaluator.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDecisionDirectedSymbolEvaluator.java index 61c82abc..ead3c1ed 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDecisionDirectedSymbolEvaluator.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDecisionDirectedSymbolEvaluator.java @@ -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); + } } /** diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKGardnerSymbolEvaluator.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKGardnerSymbolEvaluator.java index e2e1b544..1ccfbf25 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKGardnerSymbolEvaluator.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKGardnerSymbolEvaluator.java @@ -100,7 +100,7 @@ public class DQPSKGardnerSymbolEvaluator 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(); + mPhaseError = normalize(-mEvaluationSymbol.quadrature(), 0.3f); } /** diff --git a/src/main/java/io/github/dsheirer/gui/instrument/DemodulatorViewerFX.java b/src/main/java/io/github/dsheirer/gui/instrument/DemodulatorViewerFX.java index 6006e65f..06da896d 100644 --- a/src/main/java/io/github/dsheirer/gui/instrument/DemodulatorViewerFX.java +++ b/src/main/java/io/github/dsheirer/gui/instrument/DemodulatorViewerFX.java @@ -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()); + } } }); diff --git a/src/main/java/io/github/dsheirer/gui/instrument/RecentFilesMenu.java b/src/main/java/io/github/dsheirer/gui/instrument/RecentFilesMenu.java index 27b7ece3..5b8f1d20 100644 --- a/src/main/java/io/github/dsheirer/gui/instrument/RecentFilesMenu.java +++ b/src/main/java/io/github/dsheirer/gui/instrument/RecentFilesMenu.java @@ -26,6 +26,7 @@ import java.util.prefs.Preferences; public class RecentFilesMenu extends Menu implements EventHandler { + 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 */ private String getKey(int index) { - return mIdentifier + ".recent.file." + index; + return mIdentifier + PREFERENCE_RECENT_FILE + index; } /** diff --git a/src/main/java/io/github/dsheirer/gui/instrument/chart/DoubleLineChart.java b/src/main/java/io/github/dsheirer/gui/instrument/chart/DoubleLineChart.java index cc278d8e..ab661f59 100644 --- a/src/main/java/io/github/dsheirer/gui/instrument/chart/DoubleLineChart.java +++ b/src/main/java/io/github/dsheirer/gui/instrument/chart/DoubleLineChart.java @@ -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 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); diff --git a/src/main/java/io/github/dsheirer/gui/instrument/chart/SamplesPerSymbolChart.java b/src/main/java/io/github/dsheirer/gui/instrument/chart/SamplesPerSymbolChart.java new file mode 100644 index 00000000..f9b95dd4 --- /dev/null +++ b/src/main/java/io/github/dsheirer/gui/instrument/chart/SamplesPerSymbolChart.java @@ -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 + * + ******************************************************************************/ +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 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; + } +} diff --git a/src/main/java/io/github/dsheirer/gui/instrument/decoder/P25Phase1LSMPane.java b/src/main/java/io/github/dsheirer/gui/instrument/decoder/P25Phase1LSMPane.java index f136836b..d80bf4e7 100644 --- a/src/main/java/io/github/dsheirer/gui/instrument/decoder/P25Phase1LSMPane.java +++ b/src/main/java/io/github/dsheirer/gui/instrument/decoder/P25Phase1LSMPane.java @@ -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() { @@ -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; diff --git a/src/main/java/io/github/dsheirer/sample/buffer/ReusableComplexDelayBuffer.java b/src/main/java/io/github/dsheirer/sample/buffer/ReusableComplexDelayBuffer.java new file mode 100644 index 00000000..f196dcc6 --- /dev/null +++ b/src/main/java/io/github/dsheirer/sample/buffer/ReusableComplexDelayBuffer.java @@ -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 + * + ******************************************************************************/ +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 +{ + private final static Logger mLog = LoggerFactory.getLogger(ReusableComplexDelayBuffer.class); + + private ReusableBufferBroadcaster mBroadcaster = new ReusableBufferBroadcaster<>(); + private LinkedTransferQueue 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 listener, long timestamp) + { + mActionRequestQueue.add(new ActionRequest(listener, timestamp)); + } + + /** + * Removes the listener from receiving sample buffers + */ + public void removeListener(Listener 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 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 listener, long timestamp) + { + mAction = Action.ADD_USER; + mListener = listener; + mTimestamp = timestamp; + } + + /** + * Creates a remove listener request + * @param listener + */ + public ActionRequest(Listener 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 getListener() + { + return mListener; + } + + /** + * Requested timestamp for buffers to be preloaded. + */ + public long getTimestamp() + { + return mTimestamp; + } + } +} diff --git a/src/main/java/io/github/dsheirer/source/SourceEvent.java b/src/main/java/io/github/dsheirer/source/SourceEvent.java index 9c1179d1..d3cc2890 100644 --- a/src/main/java/io/github/dsheirer/source/SourceEvent.java +++ b/src/main/java/io/github/dsheirer/source/SourceEvent.java @@ -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) { diff --git a/src/main/java/io/github/dsheirer/source/mixer/ComplexMixer.java b/src/main/java/io/github/dsheirer/source/mixer/ComplexMixer.java index 4e58e197..6ceab8ad 100644 --- a/src/main/java/io/github/dsheirer/source/mixer/ComplexMixer.java +++ b/src/main/java/io/github/dsheirer/source/mixer/ComplexMixer.java @@ -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 diff --git a/src/main/java/io/github/dsheirer/source/mixer/MixerReader.java b/src/main/java/io/github/dsheirer/source/mixer/MixerReader.java index c36af769..1cfaa45e 100644 --- a/src/main/java/io/github/dsheirer/source/mixer/MixerReader.java +++ b/src/main/java/io/github/dsheirer/source/mixer/MixerReader.java @@ -72,6 +72,14 @@ public class MixerReader 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. diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerController.java b/src/main/java/io/github/dsheirer/source/tuner/TunerController.java index 4ccfac71..4b9a102a 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/TunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/TunerController.java @@ -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 */ diff --git a/src/main/java/io/github/dsheirer/source/tuner/airspy/AirspyTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/airspy/AirspyTunerController.java index 20bc3060..37f599c6 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/airspy/AirspyTunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/airspy/AirspyTunerController.java @@ -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(); diff --git a/src/main/java/io/github/dsheirer/source/tuner/channel/CICTunerChannelSource.java b/src/main/java/io/github/dsheirer/source/tuner/channel/CICTunerChannelSource.java index c84df3fb..c27b57bf 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/channel/CICTunerChannelSource.java +++ b/src/main/java/io/github/dsheirer/source/tuner/channel/CICTunerChannelSource.java @@ -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 mListener; private List mSampleBuffers = new ArrayList<>(); private double mChannelSampleRate; private long mChannelFrequencyCorrection = 0; diff --git a/src/main/java/io/github/dsheirer/source/tuner/fcd/FCDTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/fcd/FCDTunerController.java index 7693e5af..e66cbaa4 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/fcd/FCDTunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/fcd/FCDTunerController.java @@ -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 */ diff --git a/src/main/java/io/github/dsheirer/source/tuner/hackrf/HackRFTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/hackrf/HackRFTunerController.java index 71e5c653..e635b874 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/hackrf/HackRFTunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/hackrf/HackRFTunerController.java @@ -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(); diff --git a/src/main/java/io/github/dsheirer/source/tuner/manager/HeterodyneChannelSourceManager.java b/src/main/java/io/github/dsheirer/source/tuner/manager/HeterodyneChannelSourceManager.java index 1a1ed9e6..58ee13a8 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/manager/HeterodyneChannelSourceManager.java +++ b/src/main/java/io/github/dsheirer/source/tuner/manager/HeterodyneChannelSourceManager.java @@ -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 mChannelSources = new CopyOnWriteArrayList<>(); private SortedSet 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: diff --git a/src/main/java/io/github/dsheirer/source/tuner/rtl/RTL2832TunerController.java b/src/main/java/io/github/dsheirer/source/tuner/rtl/RTL2832TunerController.java index 9ba5b8d2..8c062a1c 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/rtl/RTL2832TunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/rtl/RTL2832TunerController.java @@ -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; diff --git a/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java index 51e00065..27a7e08f 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/test/TestTunerController.java @@ -65,6 +65,12 @@ public class TestTunerController extends TunerController } } + @Override + public int getBufferSampleCount() + { + return SAMPLE_RATE / SPECTRAL_FRAME_RATE; + } + @Override public void dispose() { diff --git a/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java b/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java index 064c6f57..72abaa84 100644 --- a/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java +++ b/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java @@ -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);