#2212 Introduce carrier offset measurement processor to monitor a 12.5 kHz channel and provide estimated carrier offset measurements that allow the tuner's frequency error monitor to correct for the average error reported from each channel. Uses a 15 dB threshold where the signal must be at least 15 dB above the noise floor before it will calculate and broadcast the estimate. Stores the updated PPM in the tuner configuration after each update to ensure the value persists across sessions. When selected in the tuner panel, updates the displayed PPM value after each PPM autocorrection. (#2218)

Co-authored-by: Dennis Sheirer <dsheirer@github.com>
This commit is contained in:
Denny Sheirer
2025-04-20 06:55:43 -04:00
committed by GitHub
parent afadb227db
commit 2dcd994eb2
26 changed files with 816 additions and 275 deletions

View File

@@ -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 <http://www.gnu.org/licenses/>
******************************************************************************/
/*
* *****************************************************************************
* 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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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;
}
}

View File

@@ -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<TunerChannelSource> channels = new ArrayList<>(mChannelSources);
for(TunerChannelSource channel: channels)
{
channel.broadcastConsumerSourceEvent(sourceEvent);
}
break;
default:
mLog.info("Unrecognized source event: " + sourceEvent);
break;

View File

@@ -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)

View File

@@ -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)
{

View File

@@ -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<ProcessingChai
private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("0.0");
private static final DecimalFormat FREQUENCY_FORMAT = new DecimalFormat("0.00000");
private static final String NOT_AVAILABLE = "Not Available";
private PlaylistManager mPlaylistManager;
private final PlaylistManager mPlaylistManager;
private ProcessingChain mProcessingChain;
private ComplexSamplesToNativeBufferModule mSampleStreamTapModule = new ComplexSamplesToNativeBufferModule();
private ComplexDftProcessor mComplexDftProcessor = new ComplexDftProcessor();
private DFTResultsConverter mDFTResultsConverter = new ComplexDecibelConverter();
private JLayeredPane mLayeredPanel;
private final ComplexSamplesToNativeBufferModule mSampleStreamTapModule = new ComplexSamplesToNativeBufferModule();
private final ComplexDftProcessor mComplexDftProcessor = new ComplexDftProcessor();
private SpectrumPanel mSpectrumPanel;
private FrequencyOverlayPanel mFrequencyOverlayPanel;
private DbPowerMeter mPowerMeter = new DbPowerMeter();
private PeakMonitor mPeakMonitor = new PeakMonitor(DbPowerMeter.DEFAULT_MINIMUM_POWER);
private SourceEventProcessor mSourceEventProcessor = new SourceEventProcessor();
private SpinnerNumberModel mNoiseFloorSpinnerModel;
private JSpinner mNoiseFloorSpinner;
private JLabel mPowerLabel;
private JLabel mPeakLabel;
private JLabel mSquelchLabel;
private JLabel mSquelchValueLabel;
private JLabel mPllFrequencyLabel;
private JLabel mPllFrequencyValueLabel;
private JButton mSquelchUpButton;
private JButton mSquelchDownButton;
private JButton mLogIndexesButton;
private JCheckBox mSquelchAutoTrackCheckBox;
private final FrequencyOverlayPanel mFrequencyOverlayPanel;
private final DbPowerMeter mPowerMeter = new DbPowerMeter();
private final PeakMonitor mPeakMonitor = new PeakMonitor(DbPowerMeter.DEFAULT_MINIMUM_POWER);
private final SourceEventProcessor mSourceEventProcessor = new SourceEventProcessor();
private final SpinnerNumberModel mNoiseFloorSpinnerModel;
private final JLabel mPowerLabel;
private final JLabel mPeakLabel;
private final JLabel mSquelchLabel;
private final JLabel mSquelchValueLabel;
private final JLabel mEstimatedCarrierOffsetFrequencyLabel;
private final JLabel mEstimatedCarrierOffsetFrequencyValueLabel;
private final JButton mSquelchUpButton;
private final JButton mSquelchDownButton;
private final JCheckBox mSquelchAutoTrackCheckBox;
private double mSquelchThreshold;
private boolean mPanelVisible = false;
private boolean mDftProcessing = false;
@@ -165,27 +160,27 @@ public class ChannelPowerPanel extends JPanel implements Listener<ProcessingChai
fftPanel.setLayout(new MigLayout("insets 0", "[grow,fill]", "[][grow,fill]"));
JPanel labelPanel = new JPanel();
labelPanel.setLayout(new MigLayout("insets 0", "[grow,fill][right][grow,fill][right][grow,fill][]", ""));
labelPanel.setLayout(new MigLayout("insets 0", "[grow,fill][right][grow,fill][right][][]", ""));
labelPanel.add(new JLabel("Channel Spectrum"));
mPllFrequencyLabel = new JLabel("PLL:");
mPllFrequencyLabel.setEnabled(false);
labelPanel.add(mPllFrequencyLabel);
mPllFrequencyValueLabel = new JLabel("0 Hz");
mPllFrequencyValueLabel.setEnabled(false);
labelPanel.add(mPllFrequencyValueLabel);
mEstimatedCarrierOffsetFrequencyLabel = new JLabel("Estimated Carrier Offset:");
mEstimatedCarrierOffsetFrequencyLabel.setEnabled(false);
labelPanel.add(mEstimatedCarrierOffsetFrequencyLabel);
mEstimatedCarrierOffsetFrequencyValueLabel = new JLabel("0 Hz");
mEstimatedCarrierOffsetFrequencyValueLabel.setEnabled(false);
labelPanel.add(mEstimatedCarrierOffsetFrequencyValueLabel);
mNoiseFloorSpinnerModel = new SpinnerNumberModel(18, 8, 36, 1);
mNoiseFloorSpinnerModel.addChangeListener(e -> {
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<ProcessingChai
fftPanel.add(labelPanel, "wrap");
mFrequencyOverlayPanel = new FrequencyOverlayPanel(settingsManager);
mSpectrumPanel = new SpectrumPanel(settingsManager);
mSpectrumPanel.setSampleSize(18.0);
@@ -237,8 +231,8 @@ public class ChannelPowerPanel extends JPanel implements Listener<ProcessingChai
* The layered pane holds the overlapping spectrum and channel panels
* and manages the sizing of each panel with the resize listener
*/
mLayeredPanel = new JLayeredPane();
mLayeredPanel.addComponentListener(new ResizeListener());
JLayeredPane layeredPanel = new JLayeredPane();
layeredPanel.addComponentListener(new ResizeListener());
/**
* Create a mouse adapter to handle mouse events over the spectrum
@@ -251,15 +245,16 @@ public class ChannelPowerPanel extends JPanel implements Listener<ProcessingChai
mFrequencyOverlayPanel.addMouseWheelListener(mouser);
//Add the spectrum and channel panels to the layered panel
mLayeredPanel.add(mSpectrumPanel, 0, 0);
mLayeredPanel.add(mFrequencyOverlayPanel, 1, 0);
layeredPanel.add(mSpectrumPanel, 0, 0);
layeredPanel.add(mFrequencyOverlayPanel, 1, 0);
fftPanel.add(mLayeredPanel);
fftPanel.add(layeredPanel);
add(fftPanel);
mSampleStreamTapModule.setListener(mComplexDftProcessor);
mComplexDftProcessor.addConverter(mDFTResultsConverter);
mDFTResultsConverter.addListener(mSpectrumPanel);
DFTResultsConverter DFTResultsConverter = new ComplexDecibelConverter();
mComplexDftProcessor.addConverter(DFTResultsConverter);
DFTResultsConverter.addListener(mSpectrumPanel);
mSpectrumPanel.clearSpectrum();
}
@@ -323,21 +318,21 @@ public class ChannelPowerPanel extends JPanel implements Listener<ProcessingChai
}
/**
* Updates the decoder's PLL frequency on the swing dispatch thread
* @param trackingFrequency that is currently measured.
* Updates the CarrierOffsetProcessor's current carrier offset tracking frequency
* @param carrierOffsetFrequency that is currently measured/estimated.
*/
private void updatePllFrequency(long trackingFrequency)
private void updateEstimatedCarrierOffsetFrequency(long carrierOffsetFrequency)
{
//Note: we flip the sign on the error measurement because the value represents the amount of offset the PLL
//has to apply to move the signal to center/baseband
EventQueue.invokeLater(() -> {
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<ProcessingChai
{
Channel channel = mPlaylistManager.getChannelProcessingManager().getChannel(mProcessingChain);
if(channel != null && channel.getDecodeConfiguration() instanceof ISquelchConfiguration)
if(channel != null && channel.getDecodeConfiguration() instanceof ISquelchConfiguration configuration)
{
ISquelchConfiguration configuration = (ISquelchConfiguration)channel.getDecodeConfiguration();
configuration.setSquelchThreshold(threshold);
mPlaylistManager.schedulePlaylistSave();
}
@@ -366,9 +360,8 @@ public class ChannelPowerPanel extends JPanel implements Listener<ProcessingChai
{
Channel channel = mPlaylistManager.getChannelProcessingManager().getChannel(mProcessingChain);
if(channel != null && channel.getDecodeConfiguration() instanceof ISquelchConfiguration)
if(channel != null && channel.getDecodeConfiguration() instanceof ISquelchConfiguration configuration)
{
ISquelchConfiguration configuration = (ISquelchConfiguration)channel.getDecodeConfiguration();
configuration.setSquelchAutoTrack(autoTrack);
mPlaylistManager.schedulePlaylistSave();
}
@@ -393,12 +386,12 @@ public class ChannelPowerPanel extends JPanel implements Listener<ProcessingChai
mPeakLabel.setText("0");
mPowerLabel.setText("0");
mPllFrequencyLabel.setEnabled(false);
mPllFrequencyValueLabel.setText("0 Hz");
mPllFrequencyValueLabel.setEnabled(false);
mEstimatedCarrierOffsetFrequencyLabel.setEnabled(false);
mEstimatedCarrierOffsetFrequencyValueLabel.setText("0 Hz");
mEstimatedCarrierOffsetFrequencyValueLabel.setEnabled(false);
mFrequencyOverlayPanel.process(SourceEvent.frequencyChange(null, 0));
mFrequencyOverlayPanel.process(SourceEvent.sampleRateChange(0));
mFrequencyOverlayPanel.setPllTrackingFrequency(0);
mFrequencyOverlayPanel.setEstimatedCarrierOffsetFrequency(0);
mFrequencyOverlayPanel.setChannelBandwidth(0);
mSquelchLabel.setEnabled(false);
@@ -449,7 +442,7 @@ public class ChannelPowerPanel extends JPanel implements Listener<ProcessingChai
if(!tunerChannels.isEmpty())
{
mFrequencyOverlayPanel.setChannelBandwidth(tunerChannels.get(0).getBandwidth());
mFrequencyOverlayPanel.setChannelBandwidth(tunerChannels.getFirst().getBandwidth());
}
}
}
@@ -483,7 +476,6 @@ public class ChannelPowerPanel extends JPanel implements Listener<ProcessingChai
mPowerMeter.setPeak(peak);
mPeakLabel.setText(DECIMAL_FORMAT.format(peak));
});
}
case NOTIFICATION_SQUELCH_THRESHOLD ->
{
@@ -491,30 +483,22 @@ public class ChannelPowerPanel extends JPanel implements Listener<ProcessingChai
mSquelchThreshold = threshold;
setConfigSquelchThreshold((int)threshold);
EventQueue.invokeLater(new Runnable()
{
@Override
public void run()
{
mPowerMeter.setSquelchThreshold(threshold);
mSquelchLabel.setEnabled(true);
mSquelchValueLabel.setEnabled(true);
mSquelchValueLabel.setText(DECIMAL_FORMAT.format(threshold));
mSquelchDownButton.setEnabled(true);
mSquelchUpButton.setEnabled(true);
}
EventQueue.invokeLater(() -> {
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<ProcessingChai
*/
public class MouseEventProcessor extends MouseInputAdapter
{
public MouseEventProcessor()
{
}
@Override public void mouseWheelMoved(MouseWheelEvent e) {}
@Override public void mouseMoved(MouseEvent event)
{
update(event);
}
@Override public void mouseDragged(MouseEvent event) {}
@Override public void mousePressed(MouseEvent e) {}
@Override public void mouseClicked(MouseEvent event) {}
/**
* Updates the cursor display while the mouse is performing actions

View File

@@ -0,0 +1,252 @@
/*
* *****************************************************************************
* 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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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.
* <p>
* 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;
}
}

View File

@@ -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

View File

@@ -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<Double,float[]> 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;
}
}
}

View File

@@ -223,7 +223,7 @@ public class DMRDecoderState extends TimeslotDecoderState
*/
private boolean isValid(IMessage message)
{
return mIgnoreCRCChecksums || message.isValid();
return message != null && (mIgnoreCRCChecksums || message.isValid());
}
/**

View File

@@ -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)
{

View File

@@ -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());
}
}

View File

@@ -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)
{

View File

@@ -596,6 +596,7 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor,
public void setMeasuredFrequencyError(int measuredFrequencyError)
{
mMeasuredFrequencyError = measuredFrequencyError;
getFrequencyErrorCorrectionManager().updatePPM(getPPMFrequencyError());
}
/**

View File

@@ -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());

View File

@@ -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)
{

View File

@@ -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();
}
}
}
}

View File

@@ -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

View File

@@ -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);
}
/**

View File

@@ -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;

View File

@@ -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<Listener<TunerEvent>> 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;
}

View File

@@ -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<T extends Tuner,C extends TunerConfiguration>
@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<T extends Tuner,C extends TunerConfiguration>
}
});
}
/**
* Updates or refreshes the displayed tuner PPM setting.
*/
public void updatePPM()
{
SwingUtils.run(() -> {
setLoading(true);
getFrequencyCorrectionSpinner().setValue(getTuner().getTunerController().getFrequencyCorrection());
setLoading(false);
});
}
}
/**

View File

@@ -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()
{

View File

@@ -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)
{

View File

@@ -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 <http://www.gnu.org/licenses/>
******************************************************************************/
/*
* *****************************************************************************
* 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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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));
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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<DFTResultsListener> mListeners =
new CopyOnWriteArrayList<DFTResultsListener>();
private final List<DFTResultsListener> 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);
}
}
}

View File

@@ -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<E> implements Listener<E>
}
}
/**
* 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<E> 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
*/