#7 NXDN Decoder support. Redesigns tuner and channel frequency error management. (#2431)

Closes #7 

NXDN Decoder

* Implements: NXDN4800, NXDN9600, Type-C and Type-D decoding and trunk tracking.
* Redesigns tuner and channel frequency error management.
This commit is contained in:
Denny Sheirer
2026-07-08 03:44:08 -04:00
committed by GitHub
parent a053315675
commit 7945f0ce63
445 changed files with 30854 additions and 2053 deletions

View File

@@ -0,0 +1,108 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.source;
import io.github.dsheirer.source.tuner.channel.TunerChannelSource;
/**
* Notification of the current tuner, channel and decoder frequency correction state.
*/
public class ChannelFrequencyCorrectionStatusNotification extends SourceEvent
{
private long mChannelCorrection;
private long mTunerCorrection;
private double mTunerPPM;
private boolean mAutoPPM;
/**
* Constructs an instance
* @param source tuner channel
* @param decoderCorrection from the channel decoder
* @param channelCorrection currently applied frequency correction
* @param tunerPPM current tuner PPM value
* @param tunerCorrection current tuner frequency correction value.
* @param autoPPM indicating if tuner autoPPM is enabled.
*/
private ChannelFrequencyCorrectionStatusNotification(TunerChannelSource source, long decoderCorrection,
long channelCorrection, double tunerPPM, long tunerCorrection,
boolean autoPPM)
{
super(SourceEvent.Event.NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_STATUS, source, decoderCorrection, "Status Report");
mChannelCorrection = channelCorrection;
mTunerCorrection = tunerCorrection;
mTunerPPM = tunerPPM;
mAutoPPM = autoPPM;
}
public boolean isAutoPPM()
{
return mAutoPPM;
}
/**
* Current tuner frequency correction value
*/
public long getTunerCorrection()
{
return mTunerCorrection;
}
/**
* Current tuner PPM setting
*/
public double getTunerPPM()
{
return mTunerPPM;
}
/**
* Current channel correction value
*/
public long getChannelCorrection()
{
return mChannelCorrection;
}
/**
* Correction applied by the decoder which is the requested change (from the decoder)
*/
public long getDecoderCorrection()
{
return getValue().longValue();
}
/**
* Utility method to create an instance
* @param source tuner channel
* @param decoderCorrection from the decoder
* @param channelCorrection currently applied to the channel tuner source
* @param tunerPPM reported by the tuner
* @param tunerCorrection in Hertz reported by the tuner
* @param autoPPM indicating the enabled state of the tuner frequency correction manager
* @return new instance
*/
public static ChannelFrequencyCorrectionStatusNotification create(TunerChannelSource source, long decoderCorrection,
long channelCorrection, double tunerPPM,
long tunerCorrection, boolean autoPPM)
{
return new ChannelFrequencyCorrectionStatusNotification(source, decoderCorrection, channelCorrection,
tunerPPM, tunerCorrection, autoPPM);
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
* Copyright (C) 2014-2026 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
@@ -31,6 +31,7 @@ public class SourceEvent
NOTIFICATION_CHANNEL_COUNT_CHANGE,
NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION,
NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE,
NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_STATUS,
NOTIFICATION_CHANNEL_POWER,
NOTIFICATION_CHANNEL_SAMPLE_RATE_CHANGE,
NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_LOCKED,
@@ -40,7 +41,6 @@ public class SourceEvent
NOTIFICATION_FREQUENCY_ROTATION_SUCCESS,
NOTIFICATION_FREQUENCY_ROTATION_FAILURE,
NOTIFICATION_MEASURED_FREQUENCY_ERROR,
NOTIFICATION_MEASURED_FREQUENCY_ERROR_SYNC_LOCKED,
NOTIFICATION_CARRIER_OFFSET_FREQUENCY,
NOTIFICATION_RECORDING_FILE_LOADED,
NOTIFICATION_SAMPLE_RATE_CHANGE,
@@ -51,6 +51,7 @@ public class SourceEvent
NOTIFICATION_ERROR_STATE,
REQUEST_FREQUENCY_CHANGE,
REQUEST_FREQUENCY_CORRECTION,
REQUEST_FREQUENCY_ROTATION,
REQUEST_FREQUENCY_SELECTION,
REQUEST_CHANGE_SQUELCH_THRESHOLD,
@@ -66,15 +67,15 @@ public class SourceEvent
EnumSet.range(NOTIFICATION_CHANNEL_COUNT_CHANGE, NOTIFICATION_SAMPLE_RATE_CHANGE);
}
private Event mEvent;
private Number mValue;
private final Event mEvent;
private final Number mValue;
private Source mSource;
private String mEventDescription;
private final String mEventDescription;
/**
* Private constructor. Use the static constructor methods to create an event.
*/
private SourceEvent(Event event, Source source, Number value, String eventDescription)
public SourceEvent(Event event, Source source, Number value, String eventDescription)
{
mEvent = event;
mSource = source;
@@ -244,6 +245,16 @@ public class SourceEvent
return new SourceEvent(Event.NOTIFICATION_FREQUENCY_CORRECTION_CHANGE, frequencyCorrection);
}
/**
* Creates a new frequency correction request emitted by the FeedbackDecoder
*
* @param correction in hertz
*/
public static SourceEvent frequencyCorrectionRequest(long correction)
{
return new SourceEvent(Event.REQUEST_FREQUENCY_CORRECTION, correction);
}
/**
* Creates a new carrier offset measurement notification event.
*
@@ -264,18 +275,6 @@ public class SourceEvent
return new SourceEvent(Event.NOTIFICATION_MEASURED_FREQUENCY_ERROR, frequencyError);
}
/**
* Creates a new frequency error measurement notification event. This event is different from
* the raw frequency error measurement and indicates that the current state of the PLL is
* locked and tracking the signal.
*
* @param frequencyError in hertz
*/
public static SourceEvent frequencyErrorMeasurementSyncLocked(long frequencyError, String eventDescription)
{
return new SourceEvent(Event.NOTIFICATION_MEASURED_FREQUENCY_ERROR_SYNC_LOCKED, frequencyError, eventDescription);
}
/**
* Creates a new sample rate change event
*

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2026 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
@@ -40,21 +40,18 @@ public abstract class Tuner implements ISourceEventProcessor, ITunerErrorListene
{
private final static Logger mLog = LoggerFactory.getLogger(Tuner.class);
private Broadcaster<TunerEvent> mTunerEventBroadcaster = new Broadcaster<>();
private final Broadcaster<TunerEvent> mTunerEventBroadcaster = new Broadcaster<>();
private ChannelSourceManager mChannelSourceManager;
private TunerController mTunerController;
private TunerFrequencyErrorMonitor mTunerFrequencyErrorMonitor;
private final TunerController mTunerController;
private ITunerErrorListener mTunerErrorListener;
private AtomicBoolean mRunning = new AtomicBoolean();
private final AtomicBoolean mRunning = new AtomicBoolean();
public Tuner(TunerController tunerController, ITunerErrorListener tunerErrorListener)
{
mTunerController = tunerController;
mTunerErrorListener = tunerErrorListener;
//Register to receive frequency and sample rate change notifications
mTunerController.addListener(this::process);
mTunerFrequencyErrorMonitor = new TunerFrequencyErrorMonitor(this);
mTunerFrequencyErrorMonitor.start();
mTunerController.addListener(this);
}
/**
@@ -129,7 +126,6 @@ public abstract class Tuner implements ISourceEventProcessor, ITunerErrorListene
getTunerController().dispose();
mTunerEventBroadcaster.clear();
mTunerFrequencyErrorMonitor = null;
mTunerErrorListener = null;
}
}
@@ -195,6 +191,9 @@ public abstract class Tuner implements ISourceEventProcessor, ITunerErrorListene
case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
broadcast(new TunerEvent(Tuner.this, Event.UPDATE_FREQUENCY_ERROR));
break;
case NOTIFICATION_MEASURED_FREQUENCY_ERROR:
broadcast(new TunerEvent(Tuner.this, Event.UPDATE_MEASURED_FREQUENCY_ERROR));
break;
case NOTIFICATION_SAMPLE_RATE_CHANGE:
broadcast(new TunerEvent(Tuner.this, Event.UPDATE_SAMPLE_RATE));
break;
@@ -202,9 +201,6 @@ public abstract class Tuner implements ISourceEventProcessor, ITunerErrorListene
case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_UNLOCKED:
broadcast(new TunerEvent(Tuner.this, Event.UPDATE_LOCK_STATE));
break;
case NOTIFICATION_MEASURED_FREQUENCY_ERROR_SYNC_LOCKED:
mTunerFrequencyErrorMonitor.receive(event);
break;
case NOTIFICATION_RECORDING_FILE_LOADED:
//ignore
break;

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2026 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
@@ -35,7 +35,7 @@ import io.github.dsheirer.source.tuner.channel.TunerChannel;
import io.github.dsheirer.source.tuner.configuration.TunerConfiguration;
import io.github.dsheirer.source.tuner.frequency.FrequencyController;
import io.github.dsheirer.source.tuner.frequency.FrequencyController.Tunable;
import io.github.dsheirer.source.tuner.manager.FrequencyErrorCorrectionManager;
import io.github.dsheirer.source.tuner.frequency.TunerFrequencyErrorManager;
import java.text.DecimalFormat;
import java.util.SortedSet;
import java.util.concurrent.locks.ReentrantLock;
@@ -61,8 +61,8 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor,
private SourceEventListenerToProcessorAdapter mSourceEventListener;
private NativeBufferWaveRecorder mRecorder;
private ITunerErrorListener mTunerErrorListener;
private DecimalFormat mFrequencyErrorPPMFormat = new DecimalFormat("0.0");
private FrequencyErrorCorrectionManager mFrequencyErrorCorrectionManager;
private static final DecimalFormat DF = new DecimalFormat("0.000");
protected TunerFrequencyErrorManager mTunerFrequencyErrorManager;
/**
* Abstract tuner controller class. The tuner controller manages frequency bandwidth and currently tuned channels
@@ -74,7 +74,7 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor,
mTunerErrorListener = tunerErrorListener;
mFrequencyController = new FrequencyController(this);
mSourceEventListener = new SourceEventListenerToProcessorAdapter(this);
mFrequencyErrorCorrectionManager = new FrequencyErrorCorrectionManager(this);
mTunerFrequencyErrorManager = new TunerFrequencyErrorManager(this);
}
/**
@@ -82,7 +82,7 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor,
* configuration changes happen atomically. There are at least two threads that can touch this controller:
*
* A: channel processing that changes the center tuned frequency and frequency correction.
* b: user ui thread that can touch any of the frequency correction and gain controls.
* B: user ui thread that can touch any of the frequency correction and gain controls.
*
* Generally, we'll lock on each of the primary configuration change points - getter and setter for:
* Frequency
@@ -97,6 +97,20 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor,
return mLock;
}
/**
* Auto-PPM Tuner frequency error manager. Works in tandem with the ChannelFrequencyErrorManager instance in each
* of the child tuner channel sources to receive feedback from the primary channel decoder of frequency correction
* needed to align the signal within the channel. When enabled, this manager automatically updates the tuner's PPM
* value from the average residual error across each of the allocated channels.
*
* Note: each channel will independently correct any residual error after the tuner makes adjustements.
* @return tuner frequency error manager
*/
public TunerFrequencyErrorManager getTunerFrequencyErrorManager()
{
return mTunerFrequencyErrorManager;
}
/**
* Updates the frequency controller with the new minimum and maximum values.
* @param minimum frequency Hertz
@@ -108,17 +122,9 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor,
mFrequencyController.setMaximumFrequency(maximum);
}
/**
* Frequency correction manager for this tuner controller.
*/
public FrequencyErrorCorrectionManager getFrequencyErrorCorrectionManager()
{
return mFrequencyErrorCorrectionManager;
}
protected void dispose()
{
getFrequencyErrorCorrectionManager().dispose();
mTunerFrequencyErrorManager.dispose();
mNativeBufferBroadcaster.clear();
mFrequencyController.dispose();
mSourceEventListener.dispose();
@@ -224,7 +230,7 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor,
setMaximumFrequency(config.getMaximumFrequency());
}
setFrequencyCorrection(config.getFrequencyCorrection());
getFrequencyErrorCorrectionManager().setEnabled(config.getAutoPPMCorrectionEnabled());
getTunerFrequencyErrorManager().setEnabled(config.getAutoPPMCorrectionEnabled());
}
/**
@@ -349,19 +355,7 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor,
public double getFrequencyCorrection()
{
double correction;
try
{
getLock().lock();
correction = mFrequencyController.getFrequencyCorrection();
}
finally
{
getLock().unlock();
}
return correction;
return mFrequencyController.getFrequencyCorrection();
}
public void setFrequencyCorrection(double correction) throws SourceException
@@ -556,10 +550,10 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor,
if(hasMeasuredFrequencyError())
{
StringBuilder sb = new StringBuilder();
sb.append("Measured Error: ");
sb.append("Average Channel Error: ");
sb.append(getMeasuredFrequencyError());
sb.append("Hz (");
sb.append(mFrequencyErrorPPMFormat.format(getPPMFrequencyError()));
sb.append(" Hz (");
sb.append(DF.format(getPPMFrequencyError()));
sb.append("ppm)");
return sb.toString();
}
@@ -590,13 +584,21 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor,
}
/**
* Sets the measured frequency error average.
* @param measuredFrequencyError in hertz averaged over a 5 second interval.
* Sets the measured frequency error average from the TunerFrequencyErrorManager
* @param measuredFrequencyError in hertz
*/
public void setMeasuredFrequencyError(int measuredFrequencyError)
{
mMeasuredFrequencyError = measuredFrequencyError;
getFrequencyErrorCorrectionManager().updatePPM(getPPMFrequencyError());
try
{
mFrequencyController.broadcast(SourceEvent.frequencyErrorMeasurement(measuredFrequencyError));
}
catch(SourceException e)
{
//Ignore
}
}
/**

View File

@@ -1,135 +0,0 @@
/*
* *****************************************************************************
* 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.source.tuner;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.util.ThreadPool;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Monitors frequency error measurements received from certain decoders (e.g. P25) and averages those
* measurements over an interval period. Averaged measurement values are applied to the tuner controller
* and broadcast as a TunerEvent for visual display.
*/
public class TunerFrequencyErrorMonitor implements Listener<SourceEvent>
{
private final static Logger mLog = LoggerFactory.getLogger(TunerFrequencyErrorMonitor.class);
private static final int PROCESSING_INTERVAL_SECONDS = 5;
private LinkedTransferQueue<Integer> mMeasurementsQueue = new LinkedTransferQueue<>();
private List<Integer> mProcessingMeasurements = new ArrayList();
private ScheduledFuture<?> mTimerHandle;
private Tuner mTuner;
/**
* Constructs a monitor for the tuner argument.
* @param tuner to monitor for frequency error measurements
*/
public TunerFrequencyErrorMonitor(Tuner tuner)
{
mTuner = tuner;
}
/**
* Primary method for receiving frequency error measurements from downstream decoders.
*/
@Override
public void receive(SourceEvent sourceEvent)
{
if(sourceEvent.getEvent() == SourceEvent.Event.NOTIFICATION_MEASURED_FREQUENCY_ERROR_SYNC_LOCKED)
{
mMeasurementsQueue.add(sourceEvent.getValue().intValue());
}
}
/**
* Calculates the average frequency error measurement value from discrete measurements received from
* the decoders over the interval period.
*/
private void process()
{
mMeasurementsQueue.drainTo(mProcessingMeasurements);
if(mProcessingMeasurements.size() > 0)
{
int sum = 0;
for(Integer measurement: mProcessingMeasurements)
{
sum += measurement;
}
//Note: the measurements are the frequency correction being applied to each channel to compensate
//for the error int the channel. So, we report the negated value as the current error measurement
broadcast(-(sum / mProcessingMeasurements.size()));
mProcessingMeasurements.clear();
}
}
private void broadcast(int averageError)
{
mTuner.getTunerController().setMeasuredFrequencyError(averageError);
mTuner.broadcast(new TunerEvent(mTuner, TunerEvent.Event.UPDATE_MEASURED_FREQUENCY_ERROR));
}
/**
* Starts this monitor processing frequency error measurements received from select channel decoders
* and averaging those values over a 1 second interval for transmission as a tuner event.
*/
public void start()
{
if(mTimerHandle == null)
{
mTimerHandle = ThreadPool.SCHEDULED.scheduleAtFixedRate(new Processor(), 0,
PROCESSING_INTERVAL_SECONDS, TimeUnit.SECONDS);
}
}
/**
* Stops this monitor
*/
public void stop()
{
if(mTimerHandle != null)
{
mTimerHandle.cancel(true);
mTimerHandle = null;
broadcast(0);
}
}
/**
* Runnable implementation to invoke the process() method when the timer fires.
*/
public class Processor implements Runnable
{
@Override
public void run()
{
process();
}
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2026 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
@@ -27,6 +27,7 @@ import io.github.dsheirer.dsp.mixer.ComplexMixerFactory;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.source.tuner.frequency.TunerFrequencyErrorManager;
import io.github.dsheirer.util.Dispatcher;
import java.util.Iterator;
import org.slf4j.Logger;
@@ -44,13 +45,15 @@ public class HalfBandTunerChannelSource<T extends INativeBuffer> extends TunerCh
//Maximum number of filled buffers for the blocking queue
private static final int BUFFER_MAX_CAPACITY = 600;
private Dispatcher<T> mBufferDispatcher;
private ComplexMixer mFrequencyCorrectionMixer;
private IRealDecimationFilter mIDecimationFilter;
private IRealDecimationFilter mQDecimationFilter;
private final Dispatcher<T> mBufferDispatcher;
private final ComplexMixer mFrequencyCorrectionMixer;
private final IRealDecimationFilter mIDecimationFilter;
private final IRealDecimationFilter mQDecimationFilter;
private Listener<ComplexSamples> mSamplesListener;
private double mChannelSampleRate;
private long mTunerFrequency;
private final double mChannelSampleRate;
private long mChannelFrequency;
private final long mFrequencyOffset;
private long mFrequencyCorrection;
/**
* Constructs a frequency translating and CIC decimating channel source.
@@ -60,14 +63,16 @@ public class HalfBandTunerChannelSource<T extends INativeBuffer> extends TunerCh
* @param sampleRate of the incoming sample stream
* @param channelSpecification for the requested channel.
* @param threadName for the dispatcher
* @param tunerFrequencyErrorManager to connect with the channel error manager
* @throws FilterDesignException if a final cleanup filter cannot be designed using the remez filter
* designer and the filter parameters.
*/
public HalfBandTunerChannelSource(Listener<SourceEvent> producerSourceEventListener, TunerChannel tunerChannel,
double sampleRate, ChannelSpecification channelSpecification, String threadName)
double sampleRate, ChannelSpecification channelSpecification, String threadName,
TunerFrequencyErrorManager tunerFrequencyErrorManager)
throws FilterDesignException
{
super(producerSourceEventListener, tunerChannel, threadName);
super(producerSourceEventListener, tunerChannel, threadName, tunerFrequencyErrorManager);
int desiredDecimation = (int)(sampleRate / channelSpecification.getMinimumSampleRate());
int decimation = DecimationFilterFactory.getDecimationRate(desiredDecimation);
@@ -81,9 +86,20 @@ public class HalfBandTunerChannelSource<T extends INativeBuffer> extends TunerCh
//Setup the frequency mixer to the current source frequency
mChannelSampleRate = sampleRate / (double)decimation;
mTunerFrequency = tunerChannel.getFrequency();
long frequencyOffset = mTunerFrequency - getTunerChannel().getFrequency();
mFrequencyCorrectionMixer = ComplexMixerFactory.getMixer(frequencyOffset, sampleRate);
mChannelFrequency = tunerChannel.getFrequency();
mFrequencyOffset = mChannelFrequency - getTunerChannel().getFrequency();
mFrequencyCorrectionMixer = ComplexMixerFactory.getMixer(mFrequencyOffset, sampleRate);
}
/**
* Sets and applies the channel decoder requested frequency correction.
* @param correction requested by the downstream processors.
*/
@Override
public void setFrequencyCorrection(long correction)
{
mFrequencyCorrection = correction;
updateMixerFrequencyOffset();
}
@Override
@@ -122,7 +138,7 @@ public class HalfBandTunerChannelSource<T extends INativeBuffer> extends TunerCh
@Override
public void setFrequency(long frequency)
{
mTunerFrequency = frequency;
mChannelFrequency = frequency;
updateMixerFrequencyOffset();
}
@@ -132,7 +148,7 @@ public class HalfBandTunerChannelSource<T extends INativeBuffer> extends TunerCh
*/
private void updateMixerFrequencyOffset()
{
long offset = mTunerFrequency - getTunerChannel().getFrequency();
long offset = mChannelFrequency - getTunerChannel().getFrequency() + mFrequencyCorrection;
mFrequencyCorrectionMixer.setFrequency(offset);
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2026 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
@@ -62,7 +62,7 @@ public class MultiFrequencyTunerChannelSource extends TunerChannelSource
List<Long> frequencies, ChannelSpecification channelSpecification,
String preferredTuner, String threadName)
{
super(null, tunerChannelSource.getTunerChannel(), threadName);
super(null, tunerChannelSource.getTunerChannel(), threadName, null);
mTunerManager = tunerManager;
mTunerChannelSource = tunerChannelSource;
mTunerChannelSource.setSourceEventListener(mConsumerSourceEventAdapter);
@@ -71,6 +71,16 @@ public class MultiFrequencyTunerChannelSource extends TunerChannelSource
mPreferredTuner = preferredTuner;
}
/**
* Applies the channel decoder requested frequency correction.
* @param correction requested by the downstream processors.
*/
@Override
public void setFrequencyCorrection(long correction)
{
mTunerChannelSource.setFrequencyCorrection(correction);
}
/**
* Cycles this source to use the next frequency in the list. If no other frequencies are available,
* because of frequency locking, ignore the rotate request.

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2026 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,6 +20,8 @@
package io.github.dsheirer.source.tuner.channel;
import io.github.dsheirer.buffer.INativeBuffer;
import io.github.dsheirer.dsp.mixer.ComplexMixer;
import io.github.dsheirer.dsp.mixer.ComplexMixerFactory;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.source.ISourceEventListener;
@@ -41,6 +43,7 @@ public class PassThroughChannelSource extends TunerChannelSource implements ISou
private TunerController mTunerController;
private Dispatcher<INativeBuffer> mBufferDispatcher;
private Listener<ComplexSamples> mBufferListener;
private ComplexMixer mFrequencyCorrectionMixer;
/**
* Constructs an instance
@@ -53,10 +56,11 @@ public class PassThroughChannelSource extends TunerChannelSource implements ISou
public PassThroughChannelSource(Listener<SourceEvent> listener, TunerController tunerController,
TunerChannel tunerChannel, String threadName)
{
super(listener, tunerChannel, threadName);
super(listener, tunerChannel, threadName, tunerController.getTunerFrequencyErrorManager());
mTunerController = tunerController;
mBufferDispatcher = new Dispatcher<>(threadName, 50, getHeartbeatManager());
mBufferDispatcher.setListener(new BufferProcessor());
mFrequencyCorrectionMixer = ComplexMixerFactory.getMixer(0,0);
}
@Override
@@ -82,7 +86,13 @@ public class PassThroughChannelSource extends TunerChannelSource implements ISou
@Override
protected void setSampleRate(double sampleRate)
{
mLog.debug("Request to set sample rate: " + sampleRate);
mFrequencyCorrectionMixer.setSampleRate(sampleRate);
}
@Override
public void setFrequencyCorrection(long correction)
{
mFrequencyCorrectionMixer.setFrequency(correction);
}
@Override
@@ -116,7 +126,16 @@ public class PassThroughChannelSource extends TunerChannelSource implements ISou
{
try
{
mBufferListener.receive(iterator.next());
ComplexSamples samples = iterator.next();
if(mFrequencyCorrectionMixer.hasFrequency())
{
mBufferListener.receive(mFrequencyCorrectionMixer.mix(samples));
}
else
{
mBufferListener.receive(samples);
}
}
catch(Throwable t)
{

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
* Copyright (C) 2014-2026 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
@@ -25,6 +25,8 @@ import io.github.dsheirer.source.ISourceEventProcessor;
import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.source.SourceEventListenerToProcessorAdapter;
import io.github.dsheirer.source.SourceException;
import io.github.dsheirer.source.tuner.frequency.ChannelFrequencyErrorManager;
import io.github.dsheirer.source.tuner.frequency.TunerFrequencyErrorManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -32,11 +34,12 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc
{
private final static Logger mLog = LoggerFactory.getLogger(TunerChannelSource.class);
protected static final long HEARTBEAT_INTERVAL_MS = 100;
private SourceEventListenerToProcessorAdapter mConsumerSourceEventListenerAdapter;
private final SourceEventListenerToProcessorAdapter mConsumerSourceEventListenerAdapter;
protected TunerChannel mTunerChannel;
private Listener<SourceEvent> mProducerSourceEventListener;
private Listener<SourceEvent> mConsumerSourceEventListener;
protected String mThreadName;
private final ChannelFrequencyErrorManager mChannelFrequencyErrorManager;
/**
* Tuner Channel Source is a Digital Drop Channel (DDC) abstract class that defines the minimum functionality
@@ -47,14 +50,26 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc
* @param threadName for the channel's dispatcher
*/
public TunerChannelSource(Listener<SourceEvent> producerSourceEventListener, TunerChannel tunerChannel,
String threadName)
String threadName, TunerFrequencyErrorManager tunerErrorManager)
{
mProducerSourceEventListener = producerSourceEventListener;
mTunerChannel = tunerChannel;
mConsumerSourceEventListenerAdapter = new SourceEventListenerToProcessorAdapter(this);
mThreadName = threadName;
mChannelFrequencyErrorManager = new ChannelFrequencyErrorManager(this, tunerErrorManager);
}
/**
* Sets a frequency correction value as requested by the downstream channel consumer. This is normally going to be
* the ChannelFrequencyCorrectionManager using correction measurements provided by the FeedbackDecoder. The
* correction process is implemented separately in each subclass extension to this base class.
*
* Note: this value is in addition to the PPM correction being applied at the tuner level.
*
* @param correction requested by the downstream processors.
*/
public abstract void setFrequencyCorrection(long correction);
@Override
public long getFrequency()
{
@@ -101,6 +116,8 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc
*/
public void start()
{
mChannelFrequencyErrorManager.start();
//Broadcast current frequency and sample rate so consumer can configure correctly
broadcastConsumerSourceEvent(SourceEvent.frequencyChange(this, getFrequency(), "Startup"));
broadcastProducerSourceEvent(SourceEvent.startSampleStreamRequest(this));
@@ -112,6 +129,7 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc
*/
public void stop()
{
mChannelFrequencyErrorManager.stop();
broadcastProducerSourceEvent(SourceEvent.stopSampleStreamRequest(this));
}
@@ -156,14 +174,20 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc
setSampleRate(sourceEvent.getValue().doubleValue());
break;
case NOTIFICATION_MEASURED_FREQUENCY_ERROR:
//Ignore these raw frequency measurement errors. We're only interested in the measurements
//that occur when the channel state indicates that we're sync-locked.
//Ignore
break;
case NOTIFICATION_MEASURED_FREQUENCY_ERROR_SYNC_LOCKED:
case REQUEST_FREQUENCY_CORRECTION:
//Rebroadcast this measurement event so the producer can process it
if(mChannelFrequencyErrorManager != null)
{
mChannelFrequencyErrorManager.receive(sourceEvent);
}
sourceEvent.setSource(this);
broadcastProducerSourceEvent(sourceEvent);
break;
case NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_STATUS:
case NOTIFICATION_FREQUENCY_ROTATION_FAILURE:
case NOTIFICATION_FREQUENCY_ROTATION_SUCCESS:
case NOTIFICATION_CHANNEL_POWER:

View File

@@ -0,0 +1,194 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.source.tuner.frequency;
import io.github.dsheirer.buffer.FloatAveragingBuffer;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.ChannelFrequencyCorrectionStatusNotification;
import io.github.dsheirer.source.ISourceEventListener;
import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.source.tuner.channel.TunerChannelSource;
import io.github.dsheirer.util.ThreadPool;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
/**
* Channel frequency error controller works in tandem with the TunerFrequencyErrorController to track and send error
* reports from the decoder and apply an oscillator to remove the tracked error incrementally over time.
*/
public class ChannelFrequencyErrorManager implements ISourceEventListener, Listener<SourceEvent>
{
private static final long MAXIMUM_CHANNEL_ERROR_CORRECTION_PER_INTERVAL = 10; //Hertz
private static final long TIMER_INTERVAL_MILLISECONDS = 500;
private final FloatAveragingBuffer mRequestedCorrectAveragingBuffer = new FloatAveragingBuffer(6, 2);
private final TunerChannelSource mTunerChannelSource;
private final TunerFrequencyErrorManager mParent;
private ScheduledFuture<?> mScheduledFuture;
private long mAppliedFrequencyCorrection = 0;
private long mAverageRequestedCorrection = 0;
private boolean mCurrentForTunerProcessing = false;
private boolean mCurrentForChannelProcessing = false;
/**
* Constructs an instance.
* @param tunerChannelSource to receive frequency correction requests
* @param parent to register for tuner frequency correction events
*/
public ChannelFrequencyErrorManager(TunerChannelSource tunerChannelSource, TunerFrequencyErrorManager parent)
{
mTunerChannelSource = tunerChannelSource;
mParent = parent;
if(mParent != null)
{
mParent.add(this);
}
}
@Override
public Listener<SourceEvent> getSourceEventListener()
{
return this;
}
/**
* Snapshot of the current applied frequency correction for the channel.
* @return total frequency error in Hertz.
*/
public long getFrequencyError()
{
return mAppliedFrequencyCorrection;
}
/**
* Receive a notification from the parent tuner is updating the PPM and frequency correction change value.
*
* Note: on receiving this change notification, the tuner manager will expose the current PPM and correction values.
* @param change in Hertz resulting from the tuner's PPM update.
*/
public void receive(long change)
{
//The TunerFrequencyErrorManager notifies this channel of the correction applied at the tuner
}
/**
* Receive error measurements from the channel demodulator and average them and update the actual frequency error
* so that when the timer fires, it sends a correction update to the channel source to incrementally drive the
* difference between the actual and corrected frequency error values.
* @param sourceEvent from the FeedbackDecoder.
*/
@Override
public void receive(SourceEvent sourceEvent)
{
if(sourceEvent.getEvent().equals(SourceEvent.Event.REQUEST_FREQUENCY_CORRECTION))
{
long requestedCorrection = sourceEvent.getValue().longValue();
mAverageRequestedCorrection = (long) mRequestedCorrectAveragingBuffer.get(requestedCorrection);
mCurrentForChannelProcessing = true;
}
}
/**
* Indicates if this manager has received an error measurement from the decoder since the last time the current
* flag was cleared, when the last error measurement was read.
* @return true if there is current error value available.
*/
public boolean isCurrentForTunerProcessing()
{
return mCurrentForTunerProcessing;
}
/**
* Clears the current flag once the error value is read
*/
public void clearCurrentFlag()
{
mCurrentForTunerProcessing = false;
}
/**
* Processes the frequency error and sends an updated correction request to the TunerChannelSource. This method
* is invoked on a timer so that it is independent of the decoder and sync pattern rate.
*/
private void process()
{
if(mCurrentForChannelProcessing)
{
long partialCorrection = Math.clamp(mAverageRequestedCorrection, -MAXIMUM_CHANNEL_ERROR_CORRECTION_PER_INTERVAL, MAXIMUM_CHANNEL_ERROR_CORRECTION_PER_INTERVAL);
if(partialCorrection != 0)
{
mAppliedFrequencyCorrection += partialCorrection;
mTunerChannelSource.setFrequencyCorrection(mAppliedFrequencyCorrection);
mRequestedCorrectAveragingBuffer.reset();
}
ChannelFrequencyCorrectionStatusNotification notification = ChannelFrequencyCorrectionStatusNotification
.create(mTunerChannelSource, mAverageRequestedCorrection, mAppliedFrequencyCorrection, mParent.getTunerPPM(),
mParent.getTunerFrequencyCorrection(), mParent.isEnabled());
mTunerChannelSource.broadcastConsumerSourceEvent(notification);
mAverageRequestedCorrection = 0;
mCurrentForTunerProcessing = true;
mCurrentForChannelProcessing = false;
}
else
{
ChannelFrequencyCorrectionStatusNotification notification = ChannelFrequencyCorrectionStatusNotification
.create(mTunerChannelSource, 0, mAppliedFrequencyCorrection, mParent.getTunerPPM(),
mParent.getTunerFrequencyCorrection(), mParent.isEnabled());
mTunerChannelSource.broadcastConsumerSourceEvent(notification);
}
}
/**
* Starts the timer processing of frequency error correction.
*/
public void start()
{
if(mParent != null)
{
mParent.add(this);
if(mScheduledFuture == null)
{
mScheduledFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(this::process, TIMER_INTERVAL_MILLISECONDS,
TIMER_INTERVAL_MILLISECONDS, TimeUnit.MILLISECONDS);
}
}
}
/**
* Cancels the timer processing of frequency error correction.
*/
public void stop()
{
if(mScheduledFuture != null)
{
mScheduledFuture.cancel(true);
}
if(mParent != null)
{
mParent.remove(this);
}
mScheduledFuture = null;
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2026 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
@@ -331,7 +331,7 @@ public class FrequencyController
/**
* Broadcast a frequency error/correction value change
*/
protected void broadcastFrequencyCorrectionChange() throws SourceException
public void broadcastFrequencyCorrectionChange() throws SourceException
{
broadcast(SourceEvent.frequencyCorrectionChange((int)mFrequencyCorrection));
}

View File

@@ -0,0 +1,283 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.source.tuner.frequency;
import io.github.dsheirer.source.ISourceEventProcessor;
import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.source.SourceException;
import io.github.dsheirer.source.tuner.TunerController;
import io.github.dsheirer.util.ThreadPool;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Controls tuner frequency error when Auto-PPM is enabled. Works in tandem with the ChannelFrequencyErrorController
* to receive error measurement feedback from each channel's decoder and calculate an average frequency error across
* all channels and adjusts the tuner's PPM setting to compensate for tuner frequency drift over time.
*/
public class TunerFrequencyErrorManager implements ISourceEventProcessor
{
private static final Logger LOG = LoggerFactory.getLogger(TunerFrequencyErrorManager.class);
private static final double PPM_DIVISOR = 1.0 / 1_000_000.0;
private static final long MAXIMUM_TUNER_ERROR_CORRECTION_PER_INTERVAL_HZ = 10;
private static final long MINIMUM_CORRECTION_THRESHOLD_HZ = 50;
private static final long TIMER_INTERVAL_SECONDS = 5;
private static final DecimalFormat DF = new DecimalFormat("0.00000");
private final List<ChannelFrequencyErrorManager> mChannelManagers = new ArrayList<>();
private final TunerController mTunerController;
private ScheduledFuture<?> mScheduledFuture;
private boolean mEnabled = true;
private boolean mShutdown = false;
private double mTunerPPM;
private long mTunerCorrection;
/**
* Constructs an instance
* @param tunerController to receive automatic PPM adjustments.
*/
public TunerFrequencyErrorManager(TunerController tunerController)
{
mTunerController = tunerController;
//Register to receive frequency and PPM change notifications from the tuner
mTunerController.addListener(this);
mTunerPPM = mTunerController.getFrequencyCorrection();
mTunerCorrection = (long) (mTunerController.getFrequency() * mTunerPPM * PPM_DIVISOR);
}
public void dispose()
{
mShutdown = true;
stop();
}
@Override
public void process(SourceEvent event) throws SourceException
{
//Receive tuner controller frequency change events to detect when the PPM has changed
if(event.getEvent().equals(SourceEvent.Event.NOTIFICATION_FREQUENCY_CHANGE))
{
double ppm = mTunerController.getFrequencyCorrection();
if(ppm != mTunerPPM)
{
double adjustment = ppm - mTunerPPM;
long changeHz = (long)(mTunerController.getFrequency() * adjustment * PPM_DIVISOR);
broadcast(changeHz);
mTunerPPM = ppm;
mTunerCorrection = (long)(mTunerController.getFrequency() * PPM_DIVISOR * mTunerPPM);
}
}
}
/**
* Toggles the enabled state of this manager. When enabled, applies PPM corrections to the tuner controller.
* @param enabled
*/
public void setEnabled(boolean enabled)
{
mEnabled = enabled;
}
/**
* Indicates if tuner Auto-PPM is enabled.
*/
public boolean isEnabled()
{
return mEnabled;
}
/**
* Processes channel frequency error values and updates the tuner to apply a correction.
*/
private void process()
{
mTunerController.getLock().lock();
try
{
if(!mChannelManagers.isEmpty())
{
long requestedChangeHz = 0;
int count = 0;
for(ChannelFrequencyErrorManager manager : mChannelManagers)
{
if(manager.isCurrentForTunerProcessing())
{
requestedChangeHz += manager.getFrequencyError();
count++;
manager.clearCurrentFlag();
}
}
requestedChangeHz /= count;
mTunerController.setMeasuredFrequencyError((int)requestedChangeHz);
if(Math.abs(requestedChangeHz) > MINIMUM_CORRECTION_THRESHOLD_HZ)
{
requestedChangeHz = Math.clamp(requestedChangeHz, -MAXIMUM_TUNER_ERROR_CORRECTION_PER_INTERVAL_HZ, MAXIMUM_TUNER_ERROR_CORRECTION_PER_INTERVAL_HZ);
if(mEnabled)
{
double adjustment = requestedChangeHz / (mTunerController.getFrequency() * PPM_DIVISOR);
try
{
mTunerController.setFrequencyCorrection(mTunerController.getFrequencyCorrection() + adjustment);
}
catch(SourceException e)
{
LOG.error("Error while adjusting PPM value", e);
}
}
}
}
}
finally
{
mTunerController.getLock().unlock();
}
}
/**
* Tuner frequency error correction in parts-per-million (ppm).
*/
public double getTunerPPM()
{
return mTunerController.getFrequencyCorrection();
}
/**
* Current tuner frequency correction value.
* @return correction in Hertz
*/
public long getTunerFrequencyCorrection()
{
return mTunerCorrection;
}
/**
* Notifies each of the channel managers of the amount of frequency change resulting from a PPM automatic adjustment
* or a PPM manual adjustment.
* @param change (ie correction) in Hertz that was applied at the tuner
*/
private void broadcast(long change)
{
mTunerController.getLock().lock();
try
{
for(ChannelFrequencyErrorManager channelManager: mChannelManagers)
{
channelManager.receive(change);
}
}
finally
{
mTunerController.getLock().unlock();
}
}
/**
* Adds the channel manager to receive and provide error updates and automatically starts the timer thread
* if it is not already running.
* @param channelFrequencyErrorManager to add
*/
public void add(ChannelFrequencyErrorManager channelFrequencyErrorManager)
{
boolean startRequired = false;
mTunerController.getLock().lock();
try
{
if(!mChannelManagers.contains(channelFrequencyErrorManager))
{
mChannelManagers.add(channelFrequencyErrorManager);
startRequired = true;
}
}
finally
{
mTunerController.getLock().unlock();
}
if(startRequired)
{
start();
}
}
/**
* Removes the channel error manager and stops the timer thread if all channels are removed.
* @param channelFrequencyErrorManager to remove
*/
public void remove(ChannelFrequencyErrorManager channelFrequencyErrorManager)
{
boolean stopRequired = false;
mTunerController.getLock().lock();
try
{
mChannelManagers.remove(channelFrequencyErrorManager);
stopRequired = mChannelManagers.isEmpty();
}
finally
{
mTunerController.getLock().unlock();
}
if(stopRequired)
{
stop();
}
}
/**
* Starts the timer processing of frequency error correction.
*/
public void start()
{
if(!mShutdown && mScheduledFuture == null)
{
mScheduledFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(this::process, TIMER_INTERVAL_SECONDS,
TIMER_INTERVAL_SECONDS, TimeUnit.SECONDS);
}
}
/**
* Cancels the timer processing of frequency error correction.
*/
public void stop()
{
if(mScheduledFuture != null)
{
mScheduledFuture.cancel(true);
}
mScheduledFuture = null;
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
* Copyright (C) 2014-2026 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
@@ -25,6 +25,7 @@ import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.source.tuner.channel.ChannelSpecification;
import io.github.dsheirer.source.tuner.channel.TunerChannel;
import io.github.dsheirer.source.tuner.channel.TunerChannelSource;
import io.github.dsheirer.source.tuner.frequency.TunerFrequencyErrorManager;
import java.util.SortedSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -90,6 +91,12 @@ public abstract class ChannelSourceManager implements ISourceEventProcessor
*/
public abstract void setErrorMessage(String errorMessage);
/**
* Access the tuner's frequency error manager to insert into each allocated tuner channel source to receive
* feedback from the channel's decoder about measured signal error.
*/
public abstract TunerFrequencyErrorManager getTunerFrequencyErrorManager();
/**
* Adds a listener to receive source events
*/

View File

@@ -1,173 +0,0 @@
/*
* *****************************************************************************
* 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.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;
/**
* 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.
*/
public class FrequencyErrorCorrectionManager
{
private final static Logger mLog = LoggerFactory.getLogger(FrequencyErrorCorrectionManager.class);
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;
private DecimalFormat mDecimalFormat = new DecimalFormat("0.0");
private TunerController mTunerController;
/**
* Constructs a frequency error correction manager for the specified tuner controller.
*/
public FrequencyErrorCorrectionManager(TunerController tunerController)
{
mTunerController = tunerController;
}
/**
* Dispose of this manager instance and nullify references.
*/
public void dispose()
{
setEnabled(false);
mTunerController = null;
}
/**
* Enables or disables automatic frequency PPM correction.
* @param enabled to true to turn on automatic PPM adjustments.
*/
public void setEnabled(boolean enabled)
{
mEnabled = enabled;
}
/**
* Indicates if Auto-PPM is enabled
*/
public boolean isEnabled()
{
return mEnabled;
}
/**
* Resets monitoring
*/
public void reset()
{
mObservationPeriodStart = 0;
mPPMRequired = 0;
}
/**
* Applies a PPM adjustment value to the tuner's current PPM setting, when auto-correction is enabled.
*/
private void applyCorrection()
{
if(mEnabled && mTunerController != null && mTunerController.hasMeasuredFrequencyError())
{
double frequencyCorrection = mTunerController.getFrequencyCorrection();
frequencyCorrection -= mTunerController.getPPMFrequencyError();
try
{
mLog.info("Auto-Correcting Tuner PPM to [" + mDecimalFormat.format(frequencyCorrection) + "]");
mTunerController.setFrequencyCorrection(frequencyCorrection);
reset();
}
catch(SourceException se)
{
mLog.error("Error while applying automatic frequency error PPM correction value: " + frequencyCorrection);
}
}
}
/**
* Updates the monitor with a new PPM frequency error measurement from the tuner.
*/
public void updatePPM(double ppm)
{
if(ppm > FREQUENCY_CORRECTION_ERROR_THRESHOLD)
{
if(mObservationPeriodStart == 0)
{
mObservationPeriodStart = System.currentTimeMillis() - AUTO_CORRECTION_OBSERVATION_PERIOD_MILLISECONDS +
INITIAL_AUTO_CORRECTION_OBSERVATION_PERIOD_MILLISECONDS;
mPPMRequired = ppm;
}
else
{
if(mPPMRequired > 0)
{
mPPMRequired = ppm;
if(mObservationPeriodStart + AUTO_CORRECTION_OBSERVATION_PERIOD_MILLISECONDS < System.currentTimeMillis())
{
applyCorrection();
}
}
else
{
//Opposite polarity ppm value ... restart the timer
mObservationPeriodStart = System.currentTimeMillis();
mPPMRequired = ppm;
}
}
}
else if(ppm < -FREQUENCY_CORRECTION_ERROR_THRESHOLD)
{
if(mObservationPeriodStart == 0)
{
mObservationPeriodStart = System.currentTimeMillis();
mPPMRequired = ppm;
}
else
{
if(mPPMRequired < 0)
{
mPPMRequired = ppm;
if(mObservationPeriodStart + AUTO_CORRECTION_OBSERVATION_PERIOD_MILLISECONDS < System.currentTimeMillis())
{
applyCorrection();
}
}
else
{
//Opposite polarity ppm value ... restart the timer
mObservationPeriodStart = System.currentTimeMillis();
mPPMRequired = ppm;
}
}
}
else
{
reset();
}
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2026 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
@@ -30,6 +30,7 @@ import io.github.dsheirer.source.tuner.channel.ChannelSpecification;
import io.github.dsheirer.source.tuner.channel.HalfBandTunerChannelSource;
import io.github.dsheirer.source.tuner.channel.TunerChannel;
import io.github.dsheirer.source.tuner.channel.TunerChannelSource;
import io.github.dsheirer.source.tuner.frequency.TunerFrequencyErrorManager;
import java.util.ArrayList;
import java.util.List;
import java.util.SortedSet;
@@ -54,12 +55,22 @@ public class HeterodyneChannelSourceManager extends ChannelSourceManager
private NativeSampleDelayBuffer mSampleDelayBuffer;
private boolean mRunning = true;
/**
* Constructs an instance
* @param tunerController for the tuner
*/
public HeterodyneChannelSourceManager(TunerController tunerController)
{
mTunerController = tunerController;
mTunerController.addListener(this);
}
@Override
public TunerFrequencyErrorManager getTunerFrequencyErrorManager()
{
return mTunerController.getTunerFrequencyErrorManager();
}
@Override
public String getStateDescription()
{
@@ -114,40 +125,39 @@ public class HeterodyneChannelSourceManager extends ChannelSourceManager
TunerChannelSource source = null;
mTunerController.getLock().lock();
try
{
mTunerController.getLock().lock();
if(CenterFrequencyCalculator.canTune(tunerChannel, mTunerController, mTunerChannels))
{
try
{
//Attempt to create the channel source first, in case we get a filter design exception
HalfBandTunerChannelSource tunerChannelSource = new HalfBandTunerChannelSource(mChannelSourceEventProcessor,
tunerChannel, mTunerController.getSampleRate(), channelSpecification, threadName);
//Attempt to create the channel source first, in case we get a filter design exception
HalfBandTunerChannelSource<?> tunerChannelSource = new HalfBandTunerChannelSource(mChannelSourceEventProcessor,
tunerChannel, mTunerController.getSampleRate(), channelSpecification, threadName,
getTunerFrequencyErrorManager());
//Add to the list of channel sources so that it will receive the tuner frequency change
mChannelSources.add(tunerChannelSource);
//Add to the list of channel sources so that it will receive the tuner frequency change
mChannelSources.add(tunerChannelSource);
//Set the current tuner frequency
tunerChannelSource.setFrequency(mTunerController.getFrequency());
//Set the current tuner frequency
tunerChannelSource.setFrequency(mTunerController.getFrequency());
//Add to the channel list and update the tuner center frequency as needed
mTunerChannels.add(tunerChannel);
updateTunerFrequency();
//Add to the channel list and update the tuner center frequency as needed
mTunerChannels.add(tunerChannel);
updateTunerFrequency();
//Lock the tuner controller frequency and sample rate
mTunerController.setLockedSampleRate(true);
//Lock the tuner controller frequency and sample rate
mTunerController.setLockedSampleRate(true);
broadcast(SourceEvent.channelCountChange(getTunerChannelCount()));
broadcast(SourceEvent.channelCountChange(getTunerChannelCount()));
source = tunerChannelSource;
}
catch(FilterDesignException fde)
{
mLog.error("Error creating CIC tuner channel source - couldn't design cleanup filter", fde);
}
source = tunerChannelSource;
}
}
catch(FilterDesignException fde)
{
mLog.error("Error creating CIC tuner channel source - couldn't design cleanup filter", fde);
}
finally
{
mTunerController.getLock().unlock();
@@ -216,6 +226,7 @@ public class HeterodyneChannelSourceManager extends ChannelSourceManager
case NOTIFICATION_SAMPLE_RATE_CHANGE:
case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_LOCKED:
case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_UNLOCKED:
case NOTIFICATION_MEASURED_FREQUENCY_ERROR:
//no-op
break;
default:
@@ -328,13 +339,12 @@ public class HeterodyneChannelSourceManager extends ChannelSourceManager
broadcast(SourceEvent.channelCountChange(getTunerChannelCount()));
}
break;
case NOTIFICATION_MEASURED_FREQUENCY_ERROR_SYNC_LOCKED:
//Rebroadcast so that the tuner source can process this event
broadcast(sourceEvent);
break;
case NOTIFICATION_CHANNEL_COUNT_CHANGE:
//Lock the tuner controller frequency & sample rate when we're processing channels
break;
case REQUEST_FREQUENCY_CORRECTION:
//Ignore
break;
default:
mLog.info("Unrecognized Source Event received from channel: " + sourceEvent);
break;

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2026 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
@@ -29,6 +29,7 @@ import io.github.dsheirer.source.tuner.channel.ChannelSpecification;
import io.github.dsheirer.source.tuner.channel.PassThroughChannelSource;
import io.github.dsheirer.source.tuner.channel.TunerChannel;
import io.github.dsheirer.source.tuner.channel.TunerChannelSource;
import io.github.dsheirer.source.tuner.frequency.TunerFrequencyErrorManager;
import java.util.ArrayList;
import java.util.List;
import java.util.SortedSet;
@@ -67,6 +68,12 @@ public class PassThroughSourceManager extends ChannelSourceManager
return sb.toString();
}
@Override
public TunerFrequencyErrorManager getTunerFrequencyErrorManager()
{
return mTunerController.getTunerFrequencyErrorManager();
}
@Override
public void stopAllChannels()
{
@@ -104,9 +111,10 @@ public class PassThroughSourceManager extends ChannelSourceManager
TunerChannelSource source = null;
mTunerController.getLock().lock();
try
{
mTunerController.getLock().lock();
PassThroughChannelSource channelSource = new PassThroughChannelSource(new SourceEventProxy(),
mTunerController, tunerChannel, threadName);

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2026 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
@@ -25,6 +25,7 @@ import io.github.dsheirer.source.tuner.TunerController;
import io.github.dsheirer.source.tuner.channel.ChannelSpecification;
import io.github.dsheirer.source.tuner.channel.TunerChannel;
import io.github.dsheirer.source.tuner.channel.TunerChannelSource;
import io.github.dsheirer.source.tuner.frequency.TunerFrequencyErrorManager;
import java.util.SortedSet;
import org.apache.commons.math3.util.FastMath;
import org.slf4j.Logger;
@@ -38,8 +39,8 @@ import org.slf4j.LoggerFactory;
public class PolyphaseChannelSourceManager extends ChannelSourceManager
{
private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelSourceManager.class);
private PolyphaseChannelManager mPolyphaseChannelManager;
private TunerController mTunerController;
private final PolyphaseChannelManager mPolyphaseChannelManager;
private final TunerController mTunerController;
/**
* Constructs an instance
@@ -55,6 +56,12 @@ public class PolyphaseChannelSourceManager extends ChannelSourceManager
mTunerController.addListener(mPolyphaseChannelManager);
}
@Override
public TunerFrequencyErrorManager getTunerFrequencyErrorManager()
{
return mTunerController.getTunerFrequencyErrorManager();
}
@Override
public String getStateDescription()
{
@@ -429,9 +436,10 @@ public class PolyphaseChannelSourceManager extends ChannelSourceManager
{
TunerChannelSource tunerChannelSource = null;
mTunerController.getLock().lock();
try
{
mTunerController.getLock().lock();
if(isTunable(tunerChannel))
{
//Get a new set of currently tuned channels
@@ -500,11 +508,6 @@ public class PolyphaseChannelSourceManager extends ChannelSourceManager
//when the polyphase manager has channels allocated
mTunerController.setLockedSampleRate(getTunerChannelCount() > 0);
break;
case NOTIFICATION_MEASURED_FREQUENCY_ERROR_SYNC_LOCKED:
//Rebroadcast these frequency measurement errors to the tuner and tuner controller
//for display and/or correction
broadcast(sourceEvent);
break;
default:
mLog.info("Unrecognized source event: " + sourceEvent);
break;

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
* Copyright (C) 2014-2026 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
@@ -246,7 +246,7 @@ public abstract class TunerEditor<T extends Tuner,C extends TunerConfiguration>
if(tuner != null)
{
boolean enabled = getAutoPPMCheckBox().isSelected();
getTuner().getTunerController().getFrequencyErrorCorrectionManager().setEnabled(enabled);
getTuner().getTunerController().getTunerFrequencyErrorManager().setEnabled(enabled);
save();
}
}
@@ -917,7 +917,7 @@ public abstract class TunerEditor<T extends Tuner,C extends TunerConfiguration>
getMinimumFrequencyTextField().setFrequency(tuner.getTunerController().getMinimumFrequency());
getMaximumFrequencyTextField().setFrequency(tuner.getTunerController().getMaximumFrequency());
getFrequencyCorrectionSpinner().setValue(tuner.getTunerController().getFrequencyCorrection());
getAutoPPMCheckBox().setSelected(tuner.getTunerController().getFrequencyErrorCorrectionManager().isEnabled());
getAutoPPMCheckBox().setSelected(tuner.getTunerController().getTunerFrequencyErrorManager().isEnabled());
getFrequencyControl().addListener(getTuner().getTunerController());
getTuner().getTunerController().addListener(getFrequencyControl());
getMeasuredPPMLabel().setText(tuner.getTunerController().getMeasuredErrorStatus());