autotune

package
v0.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 14, 2026 License: Apache-2.0 Imports: 4 Imported by: 0

Documentation

Overview

Package autotune tracks the per-dongle carrier-frequency error of an SDR source and turns it into a digital tuning correction, ported in concept from trunk-recorder's AutotuneManager (trunk-recorder/autotune.cc).

Every RTL-SDR crystal is a little off, which shifts the received carrier off centre. A demodulator's AFC / carrier-recovery loop measures that residual offset in Hz once it locks (GopherTrunk reads it from internal/radio/p25/phase1/receiver.Receiver.AFCOffsetHz). This package keeps a running average of the last MaxHistory measurements and exposes it as an offset the caller applies digitally — by shifting the channel's down- converter target — so the next recorder/decoder hands its loop a starting frequency that is already close to lock. It never rewrites the dongle's hardware PPM; it only reports a suggested config value the operator can bake in by hand.

A Manager is per physical dongle (keyed by serial in a Registry): a control SDR and a voice SDR have independent crystal errors and independent corrections.

Index

Constants

View Source
const (
	// MaxHistory is how many recent error measurements feed the running
	// average. Matches trunk-recorder's MAX_HISTORY.
	MaxHistory = 20
	// PPMThreshold is the correction magnitude (in PPM, relative to the
	// channel centre frequency) above which the source is flagged as
	// mis-configured. Matches trunk-recorder's PPM_THRESHOLD.
	PPMThreshold = 3.5
	// SuggestedErrorRounding rounds the suggested config value to the
	// nearest N Hz. Matches trunk-recorder's SUGGESTED_ERROR_ROUNDING.
	SuggestedErrorRounding = 10
	// WarmupSamples is how many measurements must accumulate before the
	// running average is trusted enough to apply. Until then Ready() is
	// false and callers apply no correction, so a single noisy first
	// measurement can't yank tuning by hundreds of Hz before the average
	// has had a chance to settle.
	WarmupSamples = 3
	// MaxAbsErrorHz bounds a single measurement's plausible magnitude when
	// the channel centre is unknown (the voice path passes centre 0). A
	// real crystal error is a few PPM — single-digit Hz on a disciplined
	// USRP, low hundreds on a loose RTL crystal at VHF/UHF. A per-call AFC
	// residual far beyond that is a measurement glitch (an unconverged loop,
	// a data-dependent bias snapshot at end-of-call), not drift, and folding
	// it in produces the runaway kHz corrections this guard prevents. ~1.5 kHz
	// is ≈3.3 PPM at 450 MHz — comfortably above any correction worth making.
	MaxAbsErrorHz = 1500
)

Variables

This section is empty.

Functions

This section is empty.

Types

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

Manager accumulates carrier-error measurements for one SDR source and serves the running-average correction. It is safe for concurrent use: the control decoder and the voice composer both call into the same Manager.

func NewManager

func NewManager(serial string, log *slog.Logger) *Manager

NewManager returns a Manager labelled with the dongle serial for logging. A nil logger is tolerated (logging becomes a no-op).

func (*Manager) AddErrorMeasurement

func (m *Manager) AddErrorMeasurement(observedHz, currentOffsetHz int, centerFreqHz uint32)

AddErrorMeasurement folds one observation into the running average. observedHz is the loop's freshly-measured residual offset; currentOffsetHz is the autotune offset that was already applied while that residual was observed, so total = observed + applied is the source's true error. centerFreqHz is the channel centre the measurement was taken at, used only for the PPM sanity warning. Port of AutotuneManager::add_error_measurement.

func (*Manager) AverageError

func (m *Manager) AverageError() int

AverageError returns the cached running-average correction in Hz. Callers apply it digitally as targetHz - AverageError(). Port of get_average_error. This is the raw average regardless of warm-up; callers that drive tuning should gate on Ready() so an unsettled early average is not applied.

func (*Manager) Correction added in v0.5.1

func (m *Manager) Correction() int

Correction returns the average correction to apply, or 0 until warm-up completes. It is the value tuning call sites should use.

func (*Manager) Ready added in v0.5.1

func (m *Manager) Ready() bool

Ready reports whether enough measurements have accumulated (>= WarmupSamples) for the running average to be trusted as an applied tuning correction. Before that, callers should apply no correction.

func (*Manager) Reset

func (m *Manager) Reset()

Reset clears the measurement history. Port of AutotuneManager::reset. The cached average is intentionally left in place so an in-flight correction is not yanked to zero mid-call (matches trunk-recorder, which only clears the deque).

func (*Manager) StatusString

func (m *Manager) StatusString(centerFreqHz uint32, initialErrorHz int) string

StatusString renders the live correction and a suggested config value, given the channel centre frequency and the operator's currently-configured error in Hz (derive from the dongle's configured ppm: round(ppm * centerHz/1e6)). Port of AutotuneManager::get_status_string, adapted to GopherTrunk's ppm config: it also reports the suggested value as a ppm so it can be pasted into the device block.

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry hands out per-serial Managers. When disabled, Get returns nil so every call site collapses to the pre-autotune behaviour at zero cost.

func NewRegistry

func NewRegistry(enabled bool, log *slog.Logger) *Registry

NewRegistry builds a Registry. When enabled is false, Get always returns nil.

func (*Registry) Enabled

func (r *Registry) Enabled() bool

Enabled reports whether autotune is on.

func (*Registry) Get

func (r *Registry) Get(serial string) *Manager

Get returns the shared Manager for a dongle serial, creating it on first use. Returns nil when the Registry is nil or disabled, so callers can write `if m := reg.Get(serial); m != nil { ... }`.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL