mirror of
https://github.com/smittix/intercept.git
synced 2026-06-14 08:43:33 -07:00
Fix Morse decoder silent on real HF signals via AGC and warm-up
Add automatic gain control (AGC) before Goertzel processing to normalize quiet audio from direct sampling mode where the -g gain flag has no effect. Fix broken adaptive threshold bootstrap by adding a 50-block warm-up phase that collects magnitude statistics before seeding noise floor and signal peak. Lower threshold ratio from 50% to 30% for better weak-CW sensitivity. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+44
-7
@@ -95,11 +95,21 @@ class MorseDecoder:
|
||||
self._char_gap = 3.0 * dit_sec / self._block_duration # blocks
|
||||
self._word_gap = 7.0 * dit_sec / self._block_duration # blocks
|
||||
|
||||
# AGC (automatic gain control) for direct sampling / weak signals
|
||||
self._agc_target = 0.3 # target RMS amplitude (0-1 range)
|
||||
self._agc_gain = 1.0 # current AGC multiplier
|
||||
self._agc_alpha = 0.05 # EMA smoothing for gain changes
|
||||
|
||||
# Warm-up phase constants
|
||||
self._WARMUP_BLOCKS = 50 # ~1 second at 50 blocks/sec
|
||||
self._SETTLE_BLOCKS = 200 # blocks for fast→slow EMA transition
|
||||
self._mag_min = float('inf')
|
||||
self._mag_max = 0.0
|
||||
|
||||
# Adaptive threshold via EMA
|
||||
self._noise_floor = 0.0
|
||||
self._signal_peak = 0.0
|
||||
self._threshold = 0.0
|
||||
self._ema_alpha = 0.1 # smoothing factor
|
||||
|
||||
# State machine (counts in blocks, not wall-clock time)
|
||||
self._tone_on = False
|
||||
@@ -136,20 +146,47 @@ class MorseDecoder:
|
||||
|
||||
# Normalize to [-1, 1]
|
||||
normalized = [s / 32768.0 for s in block]
|
||||
|
||||
# AGC: boost quiet signals (e.g. direct sampling mode)
|
||||
rms = math.sqrt(sum(s * s for s in normalized) / len(normalized))
|
||||
if rms > 1e-6:
|
||||
desired_gain = self._agc_target / rms
|
||||
self._agc_gain += self._agc_alpha * (desired_gain - self._agc_gain)
|
||||
self._agc_gain = min(self._agc_gain, 500.0) # cap to prevent runaway
|
||||
normalized = [s * self._agc_gain for s in normalized]
|
||||
|
||||
mag = self._filter.magnitude(normalized)
|
||||
amplitudes.append(mag)
|
||||
|
||||
self._blocks_processed += 1
|
||||
|
||||
# Update adaptive threshold
|
||||
if mag < self._threshold or self._threshold == 0:
|
||||
self._noise_floor += self._ema_alpha * (mag - self._noise_floor)
|
||||
# Warm-up phase: collect statistics, suppress detection
|
||||
if self._blocks_processed <= self._WARMUP_BLOCKS:
|
||||
self._mag_min = min(self._mag_min, mag)
|
||||
self._mag_max = max(self._mag_max, mag)
|
||||
if self._blocks_processed == self._WARMUP_BLOCKS:
|
||||
# Seed thresholds from observed range
|
||||
self._noise_floor = self._mag_min
|
||||
self._signal_peak = max(self._mag_max, self._mag_min * 2)
|
||||
self._threshold = self._noise_floor + 0.3 * (
|
||||
self._signal_peak - self._noise_floor
|
||||
)
|
||||
tone_detected = False
|
||||
else:
|
||||
self._signal_peak += self._ema_alpha * (mag - self._signal_peak)
|
||||
# Adaptive EMA: fast initially, slow in steady state
|
||||
alpha = 0.3 if self._blocks_processed < self._WARMUP_BLOCKS + self._SETTLE_BLOCKS else 0.05
|
||||
|
||||
self._threshold = (self._noise_floor + self._signal_peak) / 2.0
|
||||
if mag < self._threshold:
|
||||
self._noise_floor += alpha * (mag - self._noise_floor)
|
||||
else:
|
||||
self._signal_peak += alpha * (mag - self._signal_peak)
|
||||
|
||||
tone_detected = mag > self._threshold and self._threshold > 0
|
||||
# Threshold at 30% between noise and signal (sensitive to weak CW)
|
||||
self._threshold = self._noise_floor + 0.3 * (
|
||||
self._signal_peak - self._noise_floor
|
||||
)
|
||||
|
||||
tone_detected = mag > self._threshold and self._threshold > 0
|
||||
|
||||
if tone_detected and not self._tone_on:
|
||||
# Tone just started - check silence duration for gaps
|
||||
|
||||
Reference in New Issue
Block a user