fix: CW decoder was loosing first caracters
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user