Files
Look4Sat-mckero/core
mckero 9798107d37 feat(cw): optionally shift out-of-window CW tones into the model's range
DeepCW only analyses 400-1200 Hz - its input tensor is 65 bins wide, fixed at
training time - so a CW note outside that range is invisible to the decoder.
This adds an opt-in preprocessing step that moves such a tone to 800 Hz, the
window centre, extending the usable pitch range without touching the model.

Single-sideband mixing via a 63-tap Hilbert transformer. Plain real mixing was
measured and rejected: shifting 1500 Hz to 800 Hz left a fold-back image at
1000 Hz at 0.999 of the wanted amplitude, inside the window. Zero-stuff
upsampling plus lowpass handled downward shifts but left a 0.996 image when
shifting 300 Hz upward. The Hilbert approach measures clean on nine tones from
150 to 1550 Hz: one peak at the target, nothing above 0.3 relative amplitude.
In-window energy for a 1500 Hz input goes from 6.8% to 94.6%.

Only out-of-range audio is processed. A tone already inside 400-1200 Hz is
returned untouched (same array instance, no copy), and with the setting off the
audio path is exactly what it was before.

CwToneShifter.Streaming carries the Hilbert filter history and mixer phase
across capture chunks. Shifting each chunk in isolation left 62 of every 320
samples convolving against zeros, inflating envelope ripple to 8.7x the
whole-buffer baseline. A residual difference in the last ~3 samples of each
chunk is causal and documented: those output samples would need input that has
not been captured yet.

Detection pools chunks rather than gating on one. A capture chunk is 4410
samples at 44.1 kHz but only 320 after resampling to 3200 Hz, so requiring
1280 samples in a single chunk would have made the feature dead code - the two
independent audits both found this before it shipped. Detection now runs on a
pooled 0.4 s window, at most every 2 s.

Toggling the setting or a change in the detected shift drops the buffered
audio: the 20 s window would otherwise keep decoding samples moved by the old
amount, and the pitch readout could only be correct for one of them. The
readout itself subtracts the active shift so it shows the pitch on the radio,
not the shifted one.

Settings: OtherSettings.cwToneShiftEnabled, off by default, persisted and read
back in SettingsRepo, toggled from the Other card in Settings with a help line
explaining the 400-1200 Hz limit. Strings added to all nine locales. The
decoder reads the flag per chunk, so the toggle applies without restarting
capture.

Debug: the enabled-state transition, each detection verdict (no tone / inside
window / shifting by N Hz), and every shift change are logged, with the noisy
paths throttled to the 2 s detection interval. CwProbe records shift changes
only, keeping well inside its 1 MiB cap.

Tests: 8 shifter tests (detection sweep, noise rejection, pass-through
identity, image-free shifting across 8 tones, end-to-end spectrogram energy),
8 streaming tests (chunk continuity, history retention, reset semantics, chunk
sizes above and below the history window), and 6 gate tests including a
regression guard that a 320-sample chunk must be able to reach the detection
threshold. All 53 CW tests pass, golden vectors included.
2026-08-22 01:07:38 +00:00
..