diff --git a/src/main/java/io/github/dsheirer/buffer/FloatAveragingBuffer.java b/src/main/java/io/github/dsheirer/buffer/FloatAveragingBuffer.java index fb34b488..bef25cbd 100644 --- a/src/main/java/io/github/dsheirer/buffer/FloatAveragingBuffer.java +++ b/src/main/java/io/github/dsheirer/buffer/FloatAveragingBuffer.java @@ -1,57 +1,153 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 - ******************************************************************************/ +/* + * ***************************************************************************** + * Copyright (C) 2014-2025 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.buffer; +import java.util.Arrays; + +/** + * Averaging buffer backed by a ring buffer implemented with a float array. + */ public class FloatAveragingBuffer { - private float[] mBuffer; - private float mAverage = 0.0f; - private int mBufferSize; - private int mBufferPointer; - - public FloatAveragingBuffer( int size ) - { - mBufferSize = size; - mBuffer = new float[ size ]; - } - - public float get( float newValue ) - { - float oldValue = mBuffer[ mBufferPointer ]; + private final float[] mBuffer; + private final int mBufferSize; + private float mAverage = 0.0f; + private int mBufferPointer; + private boolean mRapidFill = false; + private int mRapidFillCounter = 0; + private int mRapidFillIncrement; - if( Float.isInfinite( newValue ) || Float.isNaN( newValue ) ) - { - mAverage = mAverage - ( oldValue / mBufferSize ); + /** + * Constructs an instance that uses a rapid fill increment value where the initial values get counted multiple times + * to more quickly get to the average value instead of walking up to the average from the default average value of 0. + * @param size of buffer + * @param rapidFillIncrement indicating how many value additions per individual add to execute until the averaging + * buffer is filled. After the initial fill state is achieved, reverts to using a single value addition. + */ + public FloatAveragingBuffer(int size, int rapidFillIncrement) + { + this(size); + mRapidFillIncrement = rapidFillIncrement; + mRapidFill = true; + } - mBuffer[ mBufferPointer++ ] = 0.0f; - } - else - { - mAverage = mAverage - ( oldValue / mBufferSize ) + ( newValue / mBufferSize ); + /** + * Constructs an instance that uses a ring buffer of the specified size. + * @param size of the averaging buffer. + */ + public FloatAveragingBuffer(int size) + { + mBufferSize = size; + mBuffer = new float[size]; + } - mBuffer[ mBufferPointer++ ] = newValue; - } + /** + * Resets this buffer to an empty state + */ + public void reset() + { + Arrays.fill(mBuffer, 0); + mBufferPointer = 0; + mAverage = 0.0f; - if( mBufferPointer >= mBufferSize ) - { - mBufferPointer = 0; - } + if(mRapidFillIncrement > 0) + { + mRapidFill = true; + mRapidFillCounter = 0; + } + } - return mAverage; - } + /** + * Current average value. + * @return average + */ + public float getAverage() + { + return mAverage; + } + + /** + * Adds the value to the buffer and updates the average. + * @param value to add + */ + public void add(float value) + { + if(mRapidFill) + { + for(int x = 0; x < mRapidFillIncrement; x++) + { + float oldValue = mBuffer[mBufferPointer]; + + if(Float.isInfinite(value) || Float.isNaN(value)) + { + mAverage = mAverage - (oldValue / mBufferSize); + mBuffer[mBufferPointer++] = 0.0f; + } + else + { + mAverage = mAverage - (oldValue / mBufferSize) + (value / mBufferSize); + mBuffer[mBufferPointer++] = value; + } + + if(mBufferPointer >= mBufferSize) + { + mBufferPointer = 0; + } + } + + mRapidFillCounter += mRapidFillIncrement; + + if(mRapidFillCounter > mBufferSize) + { + mRapidFill = false; + } + } + else + { + float oldValue = mBuffer[mBufferPointer]; + + if(Float.isInfinite(value) || Float.isNaN(value)) + { + mAverage = mAverage - (oldValue / mBufferSize); + mBuffer[mBufferPointer++] = 0.0f; + } + else + { + mAverage = mAverage - (oldValue / mBufferSize) + (value / mBufferSize); + mBuffer[mBufferPointer++] = value; + } + + if(mBufferPointer >= mBufferSize) + { + mBufferPointer = 0; + } + } + } + + /** + * Adds the value to the buffer and calculates a new average. + * @param value to add + * @return updated average + */ + public float get(float value) + { + add(value); + return mAverage; + } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java index 751422a2..00ba4aee 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * Copyright (C) 2014-2025 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 @@ -302,10 +302,17 @@ public class PolyphaseChannelManager implements ISourceEventProcessor break; case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_LOCKED: case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_UNLOCKED: - case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE: case NOTIFICATION_RECORDING_FILE_LOADED: //no-op break; + case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE: + //Re-broadcast this event to each channel so that the decoders can reset error tracking function(s). + List channels = new ArrayList<>(mChannelSources); + for(TunerChannelSource channel: channels) + { + channel.broadcastConsumerSourceEvent(sourceEvent); + } + break; default: mLog.info("Unrecognized source event: " + sourceEvent); break; diff --git a/src/main/java/io/github/dsheirer/dsp/psk/pll/FrequencyCorrectionSyncMonitor.java b/src/main/java/io/github/dsheirer/dsp/psk/pll/FrequencyCorrectionSyncMonitor.java index 6dc2a727..2cd2358e 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/pll/FrequencyCorrectionSyncMonitor.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/pll/FrequencyCorrectionSyncMonitor.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 Dennis Sheirer + * Copyright (C) 2014-2025 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 @@ -98,7 +98,7 @@ public class FrequencyCorrectionSyncMonitor implements ISyncDetectListener, IFre @Override public void processFrequencyError(long frequencyError) { - mFeedbackDecoder.broadcast(SourceEvent.pllFrequencyMeasurement(frequencyError)); + mFeedbackDecoder.broadcast(SourceEvent.carrierOffsetMeasurement(-frequencyError)); //Only rebroadcast as a frequency error measurement if the sync count is more than 2 if(mSyncCount > 2) diff --git a/src/main/java/io/github/dsheirer/dsp/squelch/PowerMonitor.java b/src/main/java/io/github/dsheirer/dsp/squelch/PowerMonitor.java index 42aa0bd8..ca14acef 100644 --- a/src/main/java/io/github/dsheirer/dsp/squelch/PowerMonitor.java +++ b/src/main/java/io/github/dsheirer/dsp/squelch/PowerMonitor.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2022 Dennis Sheirer + * Copyright (C) 2014-2025 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 @@ -98,7 +98,7 @@ public class PowerMonitor /** * Broadcasts the source event to an optional register listener */ - private void broadcast(SourceEvent event) + public void broadcast(SourceEvent event) { if(mSourceEventListener != null) { diff --git a/src/main/java/io/github/dsheirer/gui/power/ChannelPowerPanel.java b/src/main/java/io/github/dsheirer/gui/power/ChannelPowerPanel.java index 1ee39853..44e473b1 100644 --- a/src/main/java/io/github/dsheirer/gui/power/ChannelPowerPanel.java +++ b/src/main/java/io/github/dsheirer/gui/power/ChannelPowerPanel.java @@ -43,7 +43,6 @@ import java.awt.EventQueue; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.event.MouseEvent; -import java.awt.event.MouseWheelEvent; import java.text.DecimalFormat; import java.text.NumberFormat; import java.util.List; @@ -73,29 +72,25 @@ public class ChannelPowerPanel extends JPanel implements Listener { Number number = mNoiseFloorSpinnerModel.getNumber(); mSpectrumPanel.setSampleSize(number.doubleValue()); }); - mNoiseFloorSpinner = new JSpinner(mNoiseFloorSpinnerModel); - labelPanel.add(mNoiseFloorSpinner); + JSpinner noiseFloorSpinner = new JSpinner(mNoiseFloorSpinnerModel); + labelPanel.add(noiseFloorSpinner); labelPanel.add(new JLabel("Spectral Display Noise Floor")); - mLogIndexesButton = new JButton("Log Settings"); - mLogIndexesButton.addActionListener(e -> { + JButton logIndexesButton = new JButton("Log Settings"); + logIndexesButton.addActionListener(e -> { if(mProcessingChain != null) { Source source = mProcessingChain.getSource(); @@ -229,7 +224,6 @@ public class ChannelPowerPanel extends JPanel implements Listener { - String formattedValue = NumberFormat.getInstance().format(-trackingFrequency); - mPllFrequencyValueLabel.setText(formattedValue + " Hz"); - mPllFrequencyValueLabel.setEnabled(true); - mPllFrequencyLabel.setEnabled(true); + String formattedValue = NumberFormat.getInstance().format(carrierOffsetFrequency); + mEstimatedCarrierOffsetFrequencyValueLabel.setText(formattedValue + " Hz"); + mEstimatedCarrierOffsetFrequencyValueLabel.setEnabled(true); + mEstimatedCarrierOffsetFrequencyLabel.setEnabled(true); }); - mFrequencyOverlayPanel.setPllTrackingFrequency(trackingFrequency); + mFrequencyOverlayPanel.setEstimatedCarrierOffsetFrequency(carrierOffsetFrequency); } /** @@ -349,9 +344,8 @@ public class ChannelPowerPanel extends JPanel implements Listener { @@ -491,30 +483,22 @@ public class ChannelPowerPanel extends JPanel implements Listener { + mPowerMeter.setSquelchThreshold(threshold); + mSquelchLabel.setEnabled(true); + mSquelchValueLabel.setEnabled(true); + mSquelchValueLabel.setText(DECIMAL_FORMAT.format(threshold)); + mSquelchDownButton.setEnabled(true); + mSquelchUpButton.setEnabled(true); }); } - case NOTIFICATION_PLL_FREQUENCY -> + case NOTIFICATION_CARRIER_OFFSET_FREQUENCY -> { - if(sourceEvent.hasValue()) - { - updatePllFrequency(sourceEvent.getValue().longValue()); - } + updateEstimatedCarrierOffsetFrequency(sourceEvent.getValue().longValue()); } case NOTIFICATION_SQUELCH_AUTO_TRACK -> { - boolean autoTrack = sourceEvent.getValue().intValue() == 1 ? true : false; + boolean autoTrack = sourceEvent.getValue().intValue() == 1; setConfigSquelchAutoTrack(autoTrack); EventQueue.invokeLater(() -> { mSquelchAutoTrackCheckBox.setSelected(autoTrack); @@ -549,18 +533,10 @@ public class ChannelPowerPanel extends JPanel implements Listener + * **************************************************************************** + */ + +package io.github.dsheirer.module.carrier; + +import io.github.dsheirer.buffer.FloatAveragingBuffer; +import io.github.dsheirer.dsp.window.WindowFactory; +import io.github.dsheirer.dsp.window.WindowType; +import io.github.dsheirer.sample.complex.ComplexSamples; +import io.github.dsheirer.spectrum.converter.ComplexDecibelConverter; +import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation; +import org.jtransforms.fft.FloatFFT_1D; + +/** + * Calculates the carrier offset from channel center. Processes incoming Complex Sample buffers to detect the carrier + * signal and calculate a carrier offset measurement. + *

+ * Calculates once a second by performing a 128-point FFT on each consecutive complex buffer. Once it detects a + * sequence of 5 consecutive buffers with sufficient SNR (>15 dB), it calculates the average offset across those 5 + * buffers and then averages across the 10 most recent high-SNR sequences. + */ +public class CarrierOffsetProcessor +{ + private static final int FFT_BIN_SIZE = 128; + private static final int MEASUREMENT_COUNT_THRESHOLD = 5; + private static final int SEARCH_WINDOW_MIDDLE = FFT_BIN_SIZE / 2; + private static final long CALCULATION_TIME_INTERVAL_MS = 1000; + private static final float CENTER_INDEX = FFT_BIN_SIZE / 2.0f - 1; + private static final float[] WINDOW = WindowFactory.getWindow(WindowType.BLACKMAN_HARRIS_7, FFT_BIN_SIZE * 2); + private static final FloatFFT_1D FFT = new FloatFFT_1D(FFT_BIN_SIZE); + private final FloatAveragingBuffer mAveragingBuffer = new FloatAveragingBuffer(5); + private final FloatAveragingBuffer mOffsetAverage = new FloatAveragingBuffer(10, 3); + private final StandardDeviation mStandardDeviation = new StandardDeviation(); + private float mCarrierOffset; + private float mResolution; + private int mHalfWidth; + private int mMeasurementCount = 0; + private int mSearchWindowMin; + private int mSearchWindowMax; + private long mEstimatedOffset; + private long mLastCalculationTimestamp = 0; + + /** + * Constructs an instance. + */ + public CarrierOffsetProcessor() + { + setSampleRate(50000.0); + } + + /** + * Update the current channel sample rate. + * @param sampleRate in Hertz + */ + public void setSampleRate(double sampleRate) + { + mResolution = (float)sampleRate / FFT_BIN_SIZE; + mHalfWidth = (int)Math.floor(12500.0 / mResolution / 2.0); //Half of 12.5 kHz signal width + mSearchWindowMin = SEARCH_WINDOW_MIDDLE - mHalfWidth; + mSearchWindowMax = SEARCH_WINDOW_MIDDLE + mHalfWidth; + } + + /** + * Resets this processor whenever the source tuner PPM value is changed or updated, so that we can immediately + * recalculate the carrier offset with the updated settings. + */ + public void reset() + { + mMeasurementCount = 0; + mLastCalculationTimestamp = 0; + mEstimatedOffset = 0; + mAveragingBuffer.reset(); + mStandardDeviation.clear(); + mOffsetAverage.reset(); + } + + /** + * Estimated carrier frequency offset averaged across buffers that have sufficient SNR (>15 dB). + * @return estimated average carrier offset. Note: this method can be accessed after each invocation of process() + * method. + */ + public long getEstimatedOffset() + { + return mEstimatedOffset; + } + + /** + * Indicates if a signal was detected in the most recently processed sample buffer. A value of false indicates the + * signal was either not present, or the SNR was less than 15 dB and therefore this processor could not calculate + * the carrier offest estimation. + * @return true if the most recent buffer had a signal that was also strong enough to process. + */ + public boolean hasCarrier() + { + return mCarrierOffset != Float.MAX_VALUE; + } + + /** + * Processes the complex sample buffer by calculating the detected carrier offset and mixing the buffer samples by + * remove the calculated offset. The current detected carrier offset is available via the getEstimatedOffset() + * method. + * + * @return true if the estimated carrier offset value was updated. + */ + public boolean process(ComplexSamples samples) + { + if(mLastCalculationTimestamp + CALCULATION_TIME_INTERVAL_MS < samples.timestamp()) + { + int bufferOffset = 0; + + while((bufferOffset + FFT_BIN_SIZE) <= samples.length()) + { + mCarrierOffset = calculateCarrierOffset(samples, bufferOffset); + + //Float max value indicates no signal or low SNR - reset the counter so that we can keep trying. + if(mCarrierOffset == Float.MAX_VALUE) + { + mMeasurementCount = 0; + mStandardDeviation.clear(); + } + else + { + mMeasurementCount++; + mAveragingBuffer.add(mCarrierOffset); + } + + if(mMeasurementCount >= MEASUREMENT_COUNT_THRESHOLD) + { + if(mStandardDeviation.getResult() < 5.0) + { + mOffsetAverage.add(mAveragingBuffer.getAverage()); + mLastCalculationTimestamp = samples.timestamp(); + + //Set the buffer offset to the sample length to exit the loop + bufferOffset = samples.length(); + } + + mMeasurementCount = 0; + mStandardDeviation.clear(); + } + + bufferOffset += FFT_BIN_SIZE; + } + + mEstimatedOffset = (long)mOffsetAverage.getAverage(); + return true; + } + + return false; + } + + /** + * Calculate the detected peak signal offset from the sample buffer. + * @param complexSamples to process + * @param offset into the complex samples buffer to calculate + * @return estimated carrier offset from center of the channel. + */ + private float calculateCarrierOffset(ComplexSamples complexSamples, int offset) + { + float[] samples = complexSamples.toInterleaved(offset, FFT_BIN_SIZE).samples(); + WindowFactory.apply(WINDOW, samples); + FFT.complexForward(samples); + float[] magnitudes = ComplexDecibelConverter.convert(samples); + float peakIndex = findPeak(magnitudes); + + //Return float max value for Low SNR or no signal + if(peakIndex == Float.MAX_VALUE) + { + return Float.MAX_VALUE; + } + + mStandardDeviation.increment(peakIndex); + return mResolution * (peakIndex - CENTER_INDEX); + } + + /** + * Finds the FFT bin with the largest magnitude within a window of +/- 12.5 kHz of the center bin. + * @param magnitudes from the FFT measurement. + * @return center bin or Float.MAX_VALUE to indicate no signal or low SNR. + */ + private float findPeak(float[] magnitudes) + { + float peakValue = -150.0f; + int peakIndex = 0; + int min = mSearchWindowMin; + int max = mSearchWindowMax; + int half = mHalfWidth; + + //Find the peak magnitude within twice the search window width of the center indices. + for(int x = min; x <= max; x++) + { + if(magnitudes[x] > peakValue) + { + peakValue = magnitudes[x]; + peakIndex = x; + } + } + + //From there, find the center point that best fits the signal to the search window width. We need a minimum + // SNR for this to work. + int left = peakIndex - half; + int right = peakIndex + half; + + //Do we have enough SNR? + float snr = peakValue - ((magnitudes[left] + magnitudes[right]) / 2.0f); + + if(snr > 15.0f) + { + float minimumSearchThreshold = peakValue - 16.0f; //Stop at bin values 16 db lower. + + while(magnitudes[left] < minimumSearchThreshold && left < peakIndex) + { + left++; + } + + while(magnitudes[right] < minimumSearchThreshold && right > peakIndex) + { + right--; + } + + int binWidth = right - left; + + if(binWidth == 0) + { + return peakIndex; + } + else + { + return left + (binWidth / 2.0f); + } + } + + //No signal or low SNR - return MAX VALUE to indicate no signal. + return Float.MAX_VALUE; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java index 635ef6b9..5c75f405 100644 --- a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * Copyright (C) 2014-2025 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 diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoder.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoder.java index 6eca1747..161c98f3 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoder.java @@ -28,6 +28,7 @@ import io.github.dsheirer.dsp.psk.dqpsk.DQPSKDemodulatorFactory; import io.github.dsheirer.dsp.squelch.PowerMonitor; import io.github.dsheirer.message.IMessage; import io.github.dsheirer.message.SyncLossMessage; +import io.github.dsheirer.module.carrier.CarrierOffsetProcessor; import io.github.dsheirer.module.decode.Decoder; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.module.decode.dmr.audio.DMRAudioModule; @@ -84,6 +85,7 @@ public class DMRDecoder extends Decoder implements IByteBufferProvider, IComplex private static final Logger LOGGER = LoggerFactory.getLogger(DMRDecoder.class); private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#.##"); private static final int SYMBOL_RATE = 4800; + private static final float MAXIMUM_CARRIER_OFFSET = 5000.0f; //Threshold for retuning the signal. private static final Map BASEBAND_FILTERS = new HashMap<>(); private DQPSKDemodulator mDemodulator; private final DMRMessageFramer mMessageFramer; @@ -92,6 +94,7 @@ public class DMRDecoder extends Decoder implements IByteBufferProvider, IComplex private IRealFilter mIBasebandFilter; private IRealFilter mQBasebandFilter; private final PowerMonitor mPowerMonitor = new PowerMonitor(); + private final CarrierOffsetProcessor mCarrierOffsetProcessor = new CarrierOffsetProcessor(); /** * Constructs an instance @@ -137,6 +140,7 @@ public class DMRDecoder extends Decoder implements IByteBufferProvider, IComplex mSymbolProcessor.setSamplesPerSymbol(mDemodulator.getSamplesPerSymbol()); mMessageFramer.setListener(mMessageProcessor); mMessageProcessor.setMessageListener(getMessageListener()); + mCarrierOffsetProcessor.setSampleRate(sampleRate); } /** @@ -147,6 +151,7 @@ public class DMRDecoder extends Decoder implements IByteBufferProvider, IComplex public void receive(ComplexSamples samples) { mMessageFramer.setTimestamp(samples.timestamp()); + float[] i = mIBasebandFilter.filter(samples.i()); float[] q = mQBasebandFilter.filter(samples.q()); @@ -155,6 +160,25 @@ public class DMRDecoder extends Decoder implements IByteBufferProvider, IComplex float[] demodulated = mDemodulator.demodulate(i, q); mSymbolProcessor.receive(demodulated); + + //Estimate carrier offset and broadcast at each update. This value is used in the channel spectral display, + // and it's also processed by the tuner's PPM error monitor to auto-adjust the tuner PPM value. + if(mCarrierOffsetProcessor.process(samples)) + { + //Tuner PPM Monitor - negate the value to indicate channel error from tuner's PPM that's causing the offset + mPowerMonitor.broadcast(SourceEvent.frequencyErrorMeasurement(-mCarrierOffsetProcessor.getEstimatedOffset())); + + //Channel spectral display - when there's a carrier send the estimate, otherwise send a zero to cause the + //display to blank the carrier offset measurement indicator line + if(mCarrierOffsetProcessor.hasCarrier()) + { + mPowerMonitor.broadcast(SourceEvent.carrierOffsetMeasurement(mCarrierOffsetProcessor.getEstimatedOffset())); + } + else + { + mPowerMonitor.broadcast(SourceEvent.carrierOffsetMeasurement(0)); + } + } } /** @@ -262,9 +286,17 @@ public class DMRDecoder extends Decoder implements IByteBufferProvider, IComplex */ private void process(SourceEvent sourceEvent) { - if(sourceEvent != null && sourceEvent.getEvent() == SourceEvent.Event.NOTIFICATION_SAMPLE_RATE_CHANGE) + if(sourceEvent != null) { - setSampleRate(sourceEvent.getValue().doubleValue()); + switch(sourceEvent.getEvent()) + { + case NOTIFICATION_SAMPLE_RATE_CHANGE: + setSampleRate(sourceEvent.getValue().doubleValue()); + break; + case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE: + mCarrierOffsetProcessor.reset(); + break; + } } } diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoderState.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoderState.java index fc23c3ee..22680bf8 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoderState.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoderState.java @@ -223,7 +223,7 @@ public class DMRDecoderState extends TimeslotDecoderState */ private boolean isValid(IMessage message) { - return mIgnoreCRCChecksums || message.isValid(); + return message != null && (mIgnoreCRCChecksums || message.isValid()); } /** diff --git a/src/main/java/io/github/dsheirer/record/wave/ComplexSamplesWaveRecorder.java b/src/main/java/io/github/dsheirer/record/wave/ComplexSamplesWaveRecorder.java index 151b4f03..e8864b37 100644 --- a/src/main/java/io/github/dsheirer/record/wave/ComplexSamplesWaveRecorder.java +++ b/src/main/java/io/github/dsheirer/record/wave/ComplexSamplesWaveRecorder.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 Dennis Sheirer + * Copyright (C) 2014-2025 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 @@ -132,6 +132,39 @@ public class ComplexSamplesWaveRecorder extends Module implements IComplexSample } } + public void flushAndStop() + { + if(mRunning.compareAndSet(true, false)) + { + if(mBufferProcessor != null) + { + mBufferProcessor.flushAndStop(); + mBufferProcessor.setListener(null); + } + + if(mWriter != null) + { + //Thread this operation so that it doesn't tie up the calling thread. The wave writer + //close method will also rename the file and this can sometimes take a few seconds. + ThreadPool.CACHED.submit(new Runnable() + { + @Override + public void run() + { + try + { + mWriter.close(); + } + catch(IOException ioe) + { + mLog.error("Error closing baseband I/Q recorder", ioe); + } + } + }); + } + } + } + @Override public void receive(ComplexSamples buffer) { diff --git a/src/main/java/io/github/dsheirer/sample/complex/ComplexSamples.java b/src/main/java/io/github/dsheirer/sample/complex/ComplexSamples.java index 936d3936..0496e0fb 100644 --- a/src/main/java/io/github/dsheirer/sample/complex/ComplexSamples.java +++ b/src/main/java/io/github/dsheirer/sample/complex/ComplexSamples.java @@ -38,7 +38,42 @@ public record ComplexSamples(float[] i, float[] q, long timestamp) interleaved[2 * x + 1] = q()[x]; } - return new InterleavedComplexSamples(interleaved, System.currentTimeMillis()); + return new InterleavedComplexSamples(interleaved, timestamp()); + } + + /** + * Length of complex samples available in this buffer. + * @return sample count. + */ + public int length() + { + return i().length; + } + + /** + * Extracts an interleaved buffer of specified count of complex samples starting at the specified offset + * @param offset into the I or Q sample array + * @param length of complex samples. + * @return interleaved complex samples with an array length that is twice as long as the requested length. + * @throws ArrayIndexOutOfBoundsException if the requested offset + length exceeds the sample count. + */ + public InterleavedComplexSamples toInterleaved(int offset, int length) + { + if(offset + length > i().length) + { + throw new ArrayIndexOutOfBoundsException("Offset [" + offset + "] length [" + length + + "] out of bounds for length [" + i().length + "]"); + } + + float[] interleaved = new float[length * 2]; + + for(int x = 0; x < length; x++) + { + interleaved[2 * x] = i()[offset + x]; + interleaved[2 * x + 1] = q()[offset + x]; + } + + return new InterleavedComplexSamples(interleaved, timestamp()); } } diff --git a/src/main/java/io/github/dsheirer/source/SourceEvent.java b/src/main/java/io/github/dsheirer/source/SourceEvent.java index 2254b216..77a741f7 100644 --- a/src/main/java/io/github/dsheirer/source/SourceEvent.java +++ b/src/main/java/io/github/dsheirer/source/SourceEvent.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 Dennis Sheirer + * Copyright (C) 2014-2025 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 @@ -41,7 +41,7 @@ public class SourceEvent NOTIFICATION_FREQUENCY_ROTATION_FAILURE, NOTIFICATION_MEASURED_FREQUENCY_ERROR, NOTIFICATION_MEASURED_FREQUENCY_ERROR_SYNC_LOCKED, - NOTIFICATION_PLL_FREQUENCY, + NOTIFICATION_CARRIER_OFFSET_FREQUENCY, NOTIFICATION_RECORDING_FILE_LOADED, NOTIFICATION_SAMPLE_RATE_CHANGE, NOTIFICATION_SQUELCH_THRESHOLD, @@ -245,13 +245,13 @@ public class SourceEvent } /** - * Creates a new PLL frequency error measurement notification event. + * Creates a new carrier offset measurement notification event. * - * @param frequencyError in hertz + * @param carrierOffset in hertz */ - public static SourceEvent pllFrequencyMeasurement(long frequencyError) + public static SourceEvent carrierOffsetMeasurement(long carrierOffset) { - return new SourceEvent(Event.NOTIFICATION_PLL_FREQUENCY, frequencyError); + return new SourceEvent(Event.NOTIFICATION_CARRIER_OFFSET_FREQUENCY, carrierOffset); } /** @@ -318,7 +318,7 @@ public class SourceEvent /** * Creates a new channel count change event * - * @param channelCount + * @param channelCount of channels */ public static SourceEvent channelCountChange(int channelCount) { 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 e1ae1d23..52135976 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/TunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/TunerController.java @@ -596,6 +596,7 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor, public void setMeasuredFrequencyError(int measuredFrequencyError) { mMeasuredFrequencyError = measuredFrequencyError; + getFrequencyErrorCorrectionManager().updatePPM(getPPMFrequencyError()); } /** 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 0faa1171..9e7774f2 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 @@ -201,7 +201,6 @@ public class AirspyTunerController extends USBTunerController { setFrequency(config.getFrequency()); setFrequencyCorrection(config.getFrequencyCorrection()); - getFrequencyErrorCorrectionManager().setEnabled(config.getAutoPPMCorrectionEnabled()); setIFGain(config.getIFGain()); setMixerGain(config.getMixerGain()); diff --git a/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java b/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java index a7b4df26..11c8d900 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java +++ b/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * Copyright (C) 2014-2025 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 @@ -147,9 +147,9 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc case NOTIFICATION_FREQUENCY_CHANGE: case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_LOCKED: case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_UNLOCKED: - case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE: - case NOTIFICATION_PLL_FREQUENCY: + case NOTIFICATION_CARRIER_OFFSET_FREQUENCY: case NOTIFICATION_STOP_SAMPLE_STREAM: + case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE: //no-op break; case NOTIFICATION_SAMPLE_RATE_CHANGE: @@ -198,7 +198,7 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc * Broadcasts the source event to the outbound sample stream consumer. * @param sourceEvent to broadcast */ - protected void broadcastConsumerSourceEvent(SourceEvent sourceEvent) + public void broadcastConsumerSourceEvent(SourceEvent sourceEvent) { if(mConsumerSourceEventListener != null) { diff --git a/src/main/java/io/github/dsheirer/source/tuner/configuration/TunerConfigurationManager.java b/src/main/java/io/github/dsheirer/source/tuner/configuration/TunerConfigurationManager.java index 069d60c0..90613a62 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/configuration/TunerConfigurationManager.java +++ b/src/main/java/io/github/dsheirer/source/tuner/configuration/TunerConfigurationManager.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2022 Dennis Sheirer + * Copyright (C) 2014-2025 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 @@ -179,7 +179,30 @@ public class TunerConfigurationManager implements IDiscoveredTunerStatusListener } } } + } + } + /** + * Updates the tuner configuration with the current tuner PPM setting so that the value can be stored across + * sessions. + * @param discoveredTuner that has an updated PPM value. + */ + public void updateTunerPPM(DiscoveredTuner discoveredTuner) + { + if(discoveredTuner != null) + { + TunerType tunerType = discoveredTuner.getTuner().getTunerType(); + + if(tunerType != TunerType.RECORDING) + { + TunerConfiguration tunerConfiguration = getTunerConfiguration(tunerType, discoveredTuner.getId()); + + if(tunerConfiguration != null) + { + tunerConfiguration.setFrequencyCorrection(discoveredTuner.getTuner().getTunerController().getFrequencyCorrection()); + saveConfigurations(); + } + } } } diff --git a/src/main/java/io/github/dsheirer/source/tuner/manager/FrequencyErrorCorrectionManager.java b/src/main/java/io/github/dsheirer/source/tuner/manager/FrequencyErrorCorrectionManager.java index 58c1f988..de6205f8 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/manager/FrequencyErrorCorrectionManager.java +++ b/src/main/java/io/github/dsheirer/source/tuner/manager/FrequencyErrorCorrectionManager.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2022 Dennis Sheirer + * Copyright (C) 2014-2025 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 @@ -20,11 +20,10 @@ package io.github.dsheirer.source.tuner.manager; import io.github.dsheirer.source.SourceException; import io.github.dsheirer.source.tuner.TunerController; +import java.text.DecimalFormat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.text.DecimalFormat; - /** * Monitors measured frequency error PPM values from the tuner and (when enabled) applies the current * error measurement to the tuner controller to adjust the tuner's PPM and align the frequency value. @@ -35,6 +34,7 @@ public class FrequencyErrorCorrectionManager private static final double FREQUENCY_CORRECTION_ERROR_THRESHOLD = 0.4; private static final long AUTO_CORRECTION_OBSERVATION_PERIOD_MILLISECONDS = 30 * 1000; //30 seconds + private static final long INITIAL_AUTO_CORRECTION_OBSERVATION_PERIOD_MILLISECONDS = 5 * 1000; //5 seconds private long mObservationPeriodStart; private double mPPMRequired; private boolean mEnabled = true; @@ -97,7 +97,6 @@ public class FrequencyErrorCorrectionManager try { mLog.info("Auto-Correcting Tuner PPM to [" + mDecimalFormat.format(frequencyCorrection) + "]"); - mTunerController.setFrequencyCorrection(frequencyCorrection); reset(); } @@ -117,7 +116,8 @@ public class FrequencyErrorCorrectionManager { if(mObservationPeriodStart == 0) { - mObservationPeriodStart = System.currentTimeMillis(); + mObservationPeriodStart = System.currentTimeMillis() - AUTO_CORRECTION_OBSERVATION_PERIOD_MILLISECONDS + + INITIAL_AUTO_CORRECTION_OBSERVATION_PERIOD_MILLISECONDS; mPPMRequired = ppm; } else diff --git a/src/main/java/io/github/dsheirer/source/tuner/manager/TunerManager.java b/src/main/java/io/github/dsheirer/source/tuner/manager/TunerManager.java index ca6d0f63..06cb3594 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/manager/TunerManager.java +++ b/src/main/java/io/github/dsheirer/source/tuner/manager/TunerManager.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * Copyright (C) 2014-2025 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 @@ -70,11 +70,11 @@ import org.usb4java.LibUsb; public class TunerManager implements IDiscoveredTunerStatusListener { private static final Logger mLog = LoggerFactory.getLogger(TunerManager.class); - private UserPreferences mUserPreferences; - private DiscoveredTunerModel mDiscoveredTunerModel = new DiscoveredTunerModel(); - private TunerConfigurationManager mTunerConfigurationManager; - private HotplugEventSupport mHotplugEventSupport = new HotplugEventSupport(); - private Context mLibUsbApplicationContext = new Context(); + private final UserPreferences mUserPreferences; + private final DiscoveredTunerModel mDiscoveredTunerModel; + private final TunerConfigurationManager mTunerConfigurationManager; + private final HotplugEventSupport mHotplugEventSupport = new HotplugEventSupport(); + private final Context mLibUsbApplicationContext = new Context(); private boolean mLibUsbInitialized = false; private SDRplay mSDRplay; @@ -86,6 +86,7 @@ public class TunerManager implements IDiscoveredTunerStatusListener { mUserPreferences = userPreferences; mTunerConfigurationManager = new TunerConfigurationManager(userPreferences); + mDiscoveredTunerModel = new DiscoveredTunerModel(mTunerConfigurationManager); } /** 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 8c7f78a8..1ec80f40 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 @@ -297,18 +297,6 @@ public class RTL2832TunerController extends USBTunerController return mBiasTEnabled; } - /** - * Overrides updates for measured frequency error so that the updates can also be applied to the - * frequency error correction manager for automatic PPM updating. - * @param measuredFrequencyError in hertz averaged over a 5 second interval. - */ - @Override - public void setMeasuredFrequencyError(int measuredFrequencyError) - { - super.setMeasuredFrequencyError(measuredFrequencyError); - getFrequencyErrorCorrectionManager().updatePPM(getPPMFrequencyError()); - } - private void setIFFrequency(int frequency) throws LibUsbException { long ifFrequency = ((long) TWO_TO_22_POWER * (long) frequency) / (long) mOscillatorFrequency * -1; diff --git a/src/main/java/io/github/dsheirer/source/tuner/ui/DiscoveredTunerModel.java b/src/main/java/io/github/dsheirer/source/tuner/ui/DiscoveredTunerModel.java index 1d05ec3c..4984a7cd 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/ui/DiscoveredTunerModel.java +++ b/src/main/java/io/github/dsheirer/source/tuner/ui/DiscoveredTunerModel.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * Copyright (C) 2014-2025 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 @@ -21,6 +21,7 @@ package io.github.dsheirer.source.tuner.ui; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.source.tuner.Tuner; import io.github.dsheirer.source.tuner.TunerEvent; +import io.github.dsheirer.source.tuner.configuration.TunerConfigurationManager; import io.github.dsheirer.source.tuner.manager.DiscoveredTuner; import io.github.dsheirer.source.tuner.manager.DiscoveredUSBTuner; import io.github.dsheirer.source.tuner.manager.IDiscoveredTunerStatusListener; @@ -62,13 +63,15 @@ public class DiscoveredTunerModel extends AbstractTableModel implements Listener private List> mTunerEventListeners = new ArrayList<>(); private DecimalFormat mFrequencyFormat = new DecimalFormat("0.00000"); private Lock mLock = new ReentrantLock(); - + private TunerConfigurationManager mTunerConfigurationManager; /** * Constructs an instance + * @param tunerConfigurationManager to update tuner configurations from tuners. */ - public DiscoveredTunerModel() + public DiscoveredTunerModel(TunerConfigurationManager tunerConfigurationManager) { + mTunerConfigurationManager = tunerConfigurationManager; } /** @@ -425,8 +428,8 @@ public class DiscoveredTunerModel extends AbstractTableModel implements Listener try { - DiscoveredTuner matching = getDiscoveredTuner(event.getTuner()); - int index = mDiscoveredTuners.indexOf(matching); + DiscoveredTuner matchingTuner = getDiscoveredTuner(event.getTuner()); + int index = mDiscoveredTuners.indexOf(matchingTuner); if(index >= 0) { @@ -441,6 +444,12 @@ public class DiscoveredTunerModel extends AbstractTableModel implements Listener case NOTIFICATION_ERROR_STATE: EventQueue.invokeLater(() -> fireTableRowsUpdated(index, index)); break; + case UPDATE_FREQUENCY_ERROR: + if(mTunerConfigurationManager != null) + { + mTunerConfigurationManager.updateTunerPPM(matchingTuner); + } + break; default: break; } diff --git a/src/main/java/io/github/dsheirer/source/tuner/ui/TunerEditor.java b/src/main/java/io/github/dsheirer/source/tuner/ui/TunerEditor.java index 775dc749..db82c6e8 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/ui/TunerEditor.java +++ b/src/main/java/io/github/dsheirer/source/tuner/ui/TunerEditor.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * Copyright (C) 2014-2025 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 @@ -755,9 +755,11 @@ public abstract class TunerEditor @Override public void receive(TunerEvent tunerEvent) { - if(tunerEvent.getEvent().equals(TunerEvent.Event.UPDATE_MEASURED_FREQUENCY_ERROR)) + switch(tunerEvent.getEvent()) { - SwingUtils.run(() -> getFrequencyPanel().updateFrequencyError()); + //Note: called methods are responsible for executing on the swing thread. + case UPDATE_MEASURED_FREQUENCY_ERROR -> getFrequencyPanel().updateFrequencyError(); + case UPDATE_FREQUENCY_ERROR -> getFrequencyPanel().updatePPM(); } } @@ -946,6 +948,18 @@ public abstract class TunerEditor } }); } + + /** + * Updates or refreshes the displayed tuner PPM setting. + */ + public void updatePPM() + { + SwingUtils.run(() -> { + setLoading(true); + getFrequencyCorrectionSpinner().setValue(getTuner().getTunerController().getFrequencyCorrection()); + setLoading(false); + }); + } } /** 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 8f171fd1..990f37e9 100644 --- a/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java +++ b/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 Dennis Sheirer + * Copyright (C) 2014-2025 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 @@ -124,6 +124,19 @@ public class ComplexWaveSource extends Source implements IControllableFileSource // return (int)(getSampleRate() / 20.0d); } + /** + * Audio format for the currently opened or started source file. + */ + public AudioFormat getAudioFormat() + { + if(mInputStream == null) + { + throw new IllegalStateException("Source not opened or started"); + } + + return mInputStream.getFormat(); + } + @Override public void start() { diff --git a/src/main/java/io/github/dsheirer/spectrum/FrequencyOverlayPanel.java b/src/main/java/io/github/dsheirer/spectrum/FrequencyOverlayPanel.java index 34108a45..0eb0aa49 100644 --- a/src/main/java/io/github/dsheirer/spectrum/FrequencyOverlayPanel.java +++ b/src/main/java/io/github/dsheirer/spectrum/FrequencyOverlayPanel.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 Dennis Sheirer + * Copyright (C) 2014-2025 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 @@ -59,7 +59,7 @@ public class FrequencyOverlayPanel extends JPanel implements ISourceEventProcess private int mBandwidth = 0; private int mChannelBandwidth = 0; - private long mPllTrackingFrequency = 0; + private long mEstimatedCarrierOffsetFrequency = 0; private Point mCursorLocation = new Point(0, 0); private boolean mCursorVisible = false; @@ -103,12 +103,12 @@ public class FrequencyOverlayPanel extends JPanel implements ISourceEventProcess } /** - * Sets the tracking frequency of the Phase Locked Loop (PLL) - * @param pllTrackingFrequency current setting. Set to 0 to reset. + * Sets the estimated carrier offset of the channel's signal + * @param estimatedCarrierOffsetFrequency current setting. Set to 0 to reset. */ - public void setPllTrackingFrequency(long pllTrackingFrequency) + public void setEstimatedCarrierOffsetFrequency(long estimatedCarrierOffsetFrequency) { - mPllTrackingFrequency = pllTrackingFrequency; + mEstimatedCarrierOffsetFrequency = estimatedCarrierOffsetFrequency; } /** @@ -212,7 +212,7 @@ public class FrequencyOverlayPanel extends JPanel implements ISourceEventProcess graphics.setRenderingHints(RENDERING_HINTS); drawFrequencies(graphics); drawCursor(graphics); - drawPLL(graphics); + drawEstimatedCarrierOffset(graphics); drawChannelBandwidth(graphics); } @@ -326,16 +326,16 @@ public class FrequencyOverlayPanel extends JPanel implements ISourceEventProcess } /** - * Draws the Phase Locked Loop tracking frequency, offset from channel center + * Draws the estimated carrier offset measured by the CarrierOffsetProcessor */ - private void drawPLL(Graphics2D graphics) + private void drawEstimatedCarrierOffset(Graphics2D graphics) { - if(mPllTrackingFrequency == 0) + if(mEstimatedCarrierOffsetFrequency == 0) { return; } - long frequency = mFrequency - mPllTrackingFrequency; + long frequency = mFrequency + mEstimatedCarrierOffsetFrequency; double xAxis = getAxisFromFrequency(frequency); double height = getSize().getHeight() - mSpectrumInset; @@ -349,7 +349,7 @@ public class FrequencyOverlayPanel extends JPanel implements ISourceEventProcess } /** - * Draws the Phase Locked Loop tracking frequency, offset from channel center + * Draws the channel bandwidth indicators */ private void drawChannelBandwidth(Graphics2D graphics) { diff --git a/src/main/java/io/github/dsheirer/spectrum/converter/ComplexDecibelConverter.java b/src/main/java/io/github/dsheirer/spectrum/converter/ComplexDecibelConverter.java index 5d55e58c..cce8d58e 100644 --- a/src/main/java/io/github/dsheirer/spectrum/converter/ComplexDecibelConverter.java +++ b/src/main/java/io/github/dsheirer/spectrum/converter/ComplexDecibelConverter.java @@ -1,37 +1,37 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2015 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 - ******************************************************************************/ +/* + * ***************************************************************************** + * Copyright (C) 2014-2025 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.spectrum.converter; import org.apache.commons.math3.util.FastMath; /** - * Converts complex DFT output to scaled dB values with a maximum amplitude of - * 0 dB and all values scaled to the minimum dB value which is: + * Converts complex DFT output to scaled dB values with a maximum amplitude of 0 dB and all values scaled to the + * minimum dB value which is: * * Max: 0dB * Min: 20 * log10( 1.0 / ( 2.0 ^ ( bit depth - 1) ) = scaling factor * * Notes: * - * The magnitude of each DFT output bin must be scaled by the number of DFT - * points to normalize the output to a range of ( 0 to 1.0 ). DFT bin magnitude - * can be calculated as: + * The magnitude of each DFT output bin must be scaled by the number of DFT points to normalize the output to a range + * of ( 0 to 1.0 ). DFT bin magnitude can be calculated as: * * 1) sqrt( bin[ x ] * bin[ x ] + bin[ x + 1 ] * bin[ x + 1] ) / DFTSize or, * 2) ( bin[ x ] * bin[ x ] + bin[ x + 1 ] * bin[ x + 1] ) / ( DFTSize * DFTSize ) @@ -42,70 +42,72 @@ import org.apache.commons.math3.util.FastMath; * * Max Value: 20 * log10( 1.0 ) = 0 dB * - * When plotting, these dB bin values should be scaled according to the maximum - * dynamic range supported by the source. Maximum dynamic range for bit depth - * is calculated as: + * When plotting, these dB bin values should be scaled according to the maximum dynamic range supported by the source. + * Maximum dynamic range for bit depth is calculated as: * * 20 * log10( 1 / ( 2 ^ ( bit depth - 1 ) ) * - * A source that provides 12-bit samples can produce values in the range of - * ( 0 to 4096 ) or -2048 to 2047. The smallest observable value would then - * be 1 / 2048. + * A source that provides 12-bit samples can produce values in the range of ( 0 to 4096 ) or -2048 to 2047. The + * smallest observable value would then be 1 / 2048. * * So, the smallest decibel value of a source producing 12 bit samples is: * * 20 * log10( 1 / 2048 ) = -66.23 dB * * and the dynamic range is: (-66.23 to 0.0 dB) - * */ public class ComplexDecibelConverter extends DFTResultsConverter { /** - * Converts the output of the JTransforms FloatFFT_1D.complexForward() - * calculation into the power spectrum in decibels, normalized to the - * sample bit depth. + * Converts the output of the JTransforms FloatFFT_1D.complexForward() calculation into the power spectrum in + * decibels, normalized to the sample bit depth. */ public ComplexDecibelConverter() { } - - @Override - public void receive( float[] results ) - { + + public static float[] convert(float[] results) + { int halfResults = results.length / 2; - -// float dftBinSizeScalor = 1.0f / (float)Math.pow( halfResults, 2.0 ); - float dftBinSizeScalor = 1.0f / (float)halfResults; - - float[] processed = new float[ halfResults ]; - + float dftBinSizeScalor = 1.0f / (float) halfResults; + float[] processed = new float[halfResults]; int middle = processed.length / 2; - - for( int x = 0; x < results.length; x += 2 ) - { - //Calculate the magnitude squared (power) value from each bin's real - //and imaginary value and scale it to the DFT bin size squared. - //Convert the scaled value to decibels. - float decibels = 10.0f * (float) FastMath.log10( - ( ( results[ x ] * results[ x ] ) + - ( results[ x + 1 ] * results[ x + 1 ] ) ) * dftBinSizeScalor ); - // We have to swap the upper and lower halves of the JTransforms - // DFT results for correct display - - int index = x / 2; - - if( index >= middle ) + float temp, decibels; + for(int x = 0; x < results.length; x += 2) + { + //Calculate the magnitude squared (power) value from each bin's real and imaginary value and scale it to the + // DFT bin size squared. Convert the scaled value to decibels. + temp = ((results[x] * results[x]) + (results[x + 1] * results[x + 1])); + + if(temp == 0) { - processed[ index - middle ] = decibels; + decibels = -196.0f; } else { - processed[ index + middle ] = decibels; + decibels = 10.0f * (float)FastMath.log10(temp * dftBinSizeScalor); + } + + // We have to swap the upper and lower halves of the JTransforms DFT results for correct display + int index = x / 2; + + if(index >= middle) + { + processed[index - middle] = decibels; + } + else + { + processed[index + middle] = decibels; } } - - dispatch( processed ); + + return processed; + } + + @Override + public void receive(float[] results) + { + dispatch(convert(results)); } } diff --git a/src/main/java/io/github/dsheirer/spectrum/converter/DFTResultsConverter.java b/src/main/java/io/github/dsheirer/spectrum/converter/DFTResultsConverter.java index bdce5416..a9b3e97d 100644 --- a/src/main/java/io/github/dsheirer/spectrum/converter/DFTResultsConverter.java +++ b/src/main/java/io/github/dsheirer/spectrum/converter/DFTResultsConverter.java @@ -1,15 +1,32 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2025 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.spectrum.converter; import io.github.dsheirer.spectrum.DFTResultsListener; import io.github.dsheirer.spectrum.DFTResultsProvider; - +import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; -public abstract class DFTResultsConverter - implements DFTResultsListener, DFTResultsProvider +public abstract class DFTResultsConverter implements DFTResultsListener, DFTResultsProvider { - private CopyOnWriteArrayList mListeners = - new CopyOnWriteArrayList(); + private final List mListeners = new CopyOnWriteArrayList<>(); /** * DFT Results Converter - for converting the output of the JTransforms @@ -25,22 +42,22 @@ public abstract class DFTResultsConverter } @Override - public void addListener( DFTResultsListener listener ) + public void addListener(DFTResultsListener listener) { - mListeners.add( listener ); + mListeners.add(listener); } @Override - public void removeListener( DFTResultsListener listener ) + public void removeListener(DFTResultsListener listener) { - mListeners.remove( listener ); + mListeners.remove(listener); } - protected void dispatch( float[] results ) + protected void dispatch(float[] results) { - for( DFTResultsListener listener: mListeners ) + for(DFTResultsListener listener: mListeners) { - listener.receive( results ); + listener.receive(results); } } } diff --git a/src/main/java/io/github/dsheirer/util/Dispatcher.java b/src/main/java/io/github/dsheirer/util/Dispatcher.java index bc153f47..3d0a53fe 100644 --- a/src/main/java/io/github/dsheirer/util/Dispatcher.java +++ b/src/main/java/io/github/dsheirer/util/Dispatcher.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * Copyright (C) 2014-2025 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 @@ -158,6 +158,49 @@ public class Dispatcher implements Listener } } + /** + * Stops this buffer processor and flushes the queue to the listener + */ + public void flushAndStop() + { + if(mRunning.compareAndSet(true, false)) + { + if(mScheduledFuture != null) + { + //Note: this has to be false because downstream implementations may have acquired locks and they must + //be able to release those locks or we'll get a deadlock situation. + mScheduledFuture.cancel(false); + mScheduledFuture = null; + } + + if(mExecutorService != null) + { + mExecutorService.shutdown(); + mExecutorService = null; + } + + List elements = new ArrayList<>(); + + mQueue.drainTo(elements); + + for(E element: elements) + { + if(mListener != null) + { + try + { + mListener.receive(element); + } + catch(Throwable t) + { + mLog.error("Error while flusing and dispatching element [" + element.getClass() + "] to listener [" + + mListener.getClass() + "]", t); + } + } + } + } + } + /** * Indicates if this processor is currently running */