fix: CW decoder was loosing first caracters

This commit is contained in:
2026-06-20 02:38:02 +02:00
parent 6379e2cd1f
commit e1b3f0faf3
2 changed files with 34 additions and 10 deletions
+14 -10
View File
@@ -46,7 +46,8 @@ type Decoder struct {
quietHops int // consecutive key-up hops while locked quietHops int // consecutive key-up hops while locked
noise float64 // broadband noise estimate (percentile of bins) noise float64 // broadband noise estimate (percentile of bins)
relockHops int // quiet hops before the lock is released 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. // Adaptive keying envelope, on the LOCKED bin's magnitude.
peak, floor float64 peak, floor float64
@@ -88,9 +89,10 @@ func New(sampleRate int, onChar func(string), onStatus func(Status)) *Decoder {
d := &Decoder{ d := &Decoder{
fs: sampleRate, fs: sampleRate,
hop: sampleRate / 250, // ~4 ms resolution 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 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, lockIdx: -1,
candIdx: -1, candIdx: -1,
statusEvery: 25, // ~10 Hz 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)] d.noise = d.nbuf[int(0.4*float64(len(d.nbuf)-1)+0.5)]
if d.lockIdx < 0 { 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 { if maxIdx == d.candIdx {
d.candHops++ d.candHops++
} else { } else {
d.candIdx, d.candHops = maxIdx, 1 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.lockIdx = maxIdx
d.peak, d.floor = maxMag, d.noise // seed the envelope to this bin d.peak, d.floor = maxMag, d.noise // seed the envelope to this bin
d.quietHops = 0 d.quietHops = 0
@@ -208,12 +212,12 @@ func (d *Decoder) step() {
if m < d.floor { if m < d.floor {
d.floor += (m - d.floor) * 0.4 d.floor += (m - d.floor) * 0.4
} else { } 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 span := d.peak - d.floor
if span > d.floor*0.3+1e-9 { if span > d.floor*0.22+1e-9 {
onTh := d.floor + 0.55*span onTh := d.floor + 0.50*span
offTh := d.floor + 0.35*span offTh := d.floor + 0.30*span
if d.state { if d.state {
on = m > offTh on = m > offTh
} else { } else {
+20
View File
@@ -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) { func TestDecodeNumbersAndProsign(t *testing.T) {
const fs = 16000 const fs = 16000
var sb strings.Builder var sb strings.Builder