diff --git a/internal/cwdecode/cwdecode.go b/internal/cwdecode/cwdecode.go index 193b7d1..c628b5c 100644 --- a/internal/cwdecode/cwdecode.go +++ b/internal/cwdecode/cwdecode.go @@ -46,7 +46,8 @@ type Decoder struct { quietHops int // consecutive key-up hops while locked noise float64 // broadband noise estimate (percentile of bins) relockHops int // quiet hops before the lock is released - acqSNR float64 // minimum tone/noise ratio to acquire a lock + acqSNR float64 // tone/noise ratio to acquire after a few stable hops + strongSNR float64 // tone/noise ratio to lock immediately (1 hop) // Adaptive keying envelope, on the LOCKED bin's magnitude. peak, floor float64 @@ -88,9 +89,10 @@ func New(sampleRate int, onChar func(string), onStatus func(Status)) *Decoder { d := &Decoder{ fs: sampleRate, hop: sampleRate / 250, // ~4 ms resolution - win: sampleRate / 62, // ~16 ms Goertzel window + win: sampleRate / 100, // ~10 ms Goertzel window (snappy edges) dotHops: 15, // ~20 WPM seed - acqSNR: 1.8, // mild: just enough to ignore pure noise + acqSNR: 1.5, // mild: ignore pure noise, still catch weak + strongSNR: 2.6, // a clearly-strong tone locks in 1 hop lockIdx: -1, candIdx: -1, statusEvery: 25, // ~10 Hz @@ -172,14 +174,16 @@ func (d *Decoder) analyze() { d.noise = d.nbuf[int(0.4*float64(len(d.nbuf)-1)+0.5)] if d.lockIdx < 0 { - // Acquire: lock when the same bin has been dominant for a few hops and - // is at least mildly above the noise (so we don't lock onto pure noise). if maxIdx == d.candIdx { d.candHops++ } else { d.candIdx, d.candHops = maxIdx, 1 } - if d.candHops >= 5 && maxMag/(d.noise+1e-9) > d.acqSNR { + snr := maxMag / (d.noise + 1e-9) + // Tiered acquisition: a clearly strong tone locks on the FIRST hop (so we + // don't eat the first element of a strong signal), a marginal/weak tone + // locks after a couple of stable hops (so we don't lock onto pure noise). + if snr > d.strongSNR || (d.candHops >= 2 && snr > d.acqSNR) { d.lockIdx = maxIdx d.peak, d.floor = maxMag, d.noise // seed the envelope to this bin d.quietHops = 0 @@ -208,12 +212,12 @@ func (d *Decoder) step() { if m < d.floor { d.floor += (m - d.floor) * 0.4 } else { - d.floor += (m - d.floor) * 0.01 + d.floor += (m - d.floor) * 0.005 // creep up slowly so marks aren't swallowed } span := d.peak - d.floor - if span > d.floor*0.3+1e-9 { - onTh := d.floor + 0.55*span - offTh := d.floor + 0.35*span + if span > d.floor*0.22+1e-9 { + onTh := d.floor + 0.50*span + offTh := d.floor + 0.30*span if d.state { on = m > offTh } else { diff --git a/internal/cwdecode/cwdecode_test.go b/internal/cwdecode/cwdecode_test.go index e91f63a..eae177b 100644 --- a/internal/cwdecode/cwdecode_test.go +++ b/internal/cwdecode/cwdecode_test.go @@ -118,6 +118,26 @@ func TestDecodeWithQRM(t *testing.T) { } } +func TestDecodeFirstCharStrong(t *testing.T) { + const fs = 16000 + var sb strings.Builder + d := New(fs, func(s string) { sb.WriteString(s) }, nil) + // Strong signal: the very first element (T = a dash) must not be eaten by + // lock acquisition. Output should begin with the first character. + samples := keyMessageAmp("TEST DE", fs, 20, 700, 16000) + for i := 0; i < len(samples); i += 200 { + end := i + 200 + if end > len(samples) { + end = len(samples) + } + d.Process(samples[i:end]) + } + got := strings.ToUpper(strings.TrimSpace(sb.String())) + if !strings.HasPrefix(got, "TEST") { + t.Fatalf("first chars lost on a strong signal: decoded %q, want it to start with TEST", got) + } +} + func TestDecodeNumbersAndProsign(t *testing.T) { const fs = 16000 var sb strings.Builder