150 lines
3.8 KiB
Go
150 lines
3.8 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"hamlog/internal/applog"
|
|
"hamlog/internal/audio"
|
|
"hamlog/internal/cwdecode"
|
|
|
|
wruntime "github.com/wailsapp/wails/v2/pkg/runtime"
|
|
)
|
|
|
|
// CW decoder: taps the RX audio device (the same "From radio" capture the DVK
|
|
// and QSO recorder use) and streams decoded Morse text to the UI. It is started
|
|
// only by the frontend, and only while the entry mode is CW.
|
|
//
|
|
// Pitch targeting: the single-channel decoder is far more reliable when it locks
|
|
// to a KNOWN pitch (a narrow filter at the signal frequency, like a skimmer)
|
|
// instead of auto-searching for the loudest tone. So we follow the radio's CW
|
|
// pitch (FlexRadio cw_pitch) when available — or a manual override — and fall
|
|
// back to auto-search otherwise.
|
|
|
|
// cwTargetPitch returns the pitch (Hz) the decoder should lock to: the manual
|
|
// override if set, else the FlexRadio's CW pitch when it's in CW, else 0 (auto).
|
|
func (a *App) cwTargetPitch() int {
|
|
if a.cwPitchHz > 0 {
|
|
return a.cwPitchHz
|
|
}
|
|
if a.cat != nil {
|
|
if st, ok := a.cat.FlexState(); ok && st.Available {
|
|
// Only trust the radio's pitch when it's actually in CW.
|
|
if st.Mode == "CW" || st.Mode == "CWL" || st.Mode == "CWU" {
|
|
if st.CWPitch > 0 {
|
|
return st.CWPitch
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// StartCWDecoder begins decoding CW from the configured RX audio device. The
|
|
// frontend calls this when the decoder toggle is on AND the mode is CW. Safe to
|
|
// call repeatedly; a second call is a no-op while already running.
|
|
func (a *App) StartCWDecoder() error {
|
|
a.cwMu.Lock()
|
|
defer a.cwMu.Unlock()
|
|
if a.cwStop != nil {
|
|
return nil // already running
|
|
}
|
|
dev := ""
|
|
if a.settings != nil {
|
|
dev, _ = a.settings.Get(a.ctx, keyAudioFromRadio)
|
|
}
|
|
if dev == "" {
|
|
return fmt.Errorf("no RX audio device configured (set \"From radio\" in Audio settings)")
|
|
}
|
|
|
|
dec := cwdecode.New(audio.SampleRate,
|
|
func(text string) {
|
|
if a.ctx != nil {
|
|
wruntime.EventsEmit(a.ctx, "cw:text", text)
|
|
}
|
|
},
|
|
func(st cwdecode.Status) {
|
|
if a.ctx != nil {
|
|
wruntime.EventsEmit(a.ctx, "cw:status", st)
|
|
}
|
|
},
|
|
)
|
|
dec.SetTarget(a.cwTargetPitch())
|
|
a.cwDecoder = dec
|
|
|
|
stop := make(chan struct{})
|
|
a.cwStop = stop
|
|
go func() {
|
|
if err := audio.StreamCapture(dev, stop, dec.Process); err != nil {
|
|
applog.Printf("cw: capture failed: %v", err)
|
|
if a.ctx != nil {
|
|
wruntime.EventsEmit(a.ctx, "cw:error", err.Error())
|
|
}
|
|
}
|
|
a.cwMu.Lock()
|
|
if a.cwStop == stop {
|
|
a.cwStop = nil
|
|
a.cwDecoder = nil
|
|
}
|
|
a.cwMu.Unlock()
|
|
}()
|
|
// Follow the radio's CW pitch live (every second) while this run is active.
|
|
go a.cwFollowPitch(stop, dec)
|
|
return nil
|
|
}
|
|
|
|
// cwFollowPitch keeps the decoder locked to the current target pitch until stop.
|
|
func (a *App) cwFollowPitch(stop <-chan struct{}, dec *cwdecode.Decoder) {
|
|
t := time.NewTicker(time.Second)
|
|
defer t.Stop()
|
|
for {
|
|
select {
|
|
case <-stop:
|
|
return
|
|
case <-t.C:
|
|
dec.SetTarget(a.cwTargetPitch())
|
|
}
|
|
}
|
|
}
|
|
|
|
// StopCWDecoder halts the CW decoder if running.
|
|
func (a *App) StopCWDecoder() {
|
|
a.cwMu.Lock()
|
|
stop := a.cwStop
|
|
a.cwStop = nil
|
|
a.cwDecoder = nil
|
|
a.cwMu.Unlock()
|
|
if stop != nil {
|
|
close(stop)
|
|
}
|
|
}
|
|
|
|
// CWDecoderRunning reports whether the decoder is currently capturing.
|
|
func (a *App) CWDecoderRunning() bool {
|
|
a.cwMu.Lock()
|
|
defer a.cwMu.Unlock()
|
|
return a.cwStop != nil
|
|
}
|
|
|
|
// SetCWDecoderPitch sets a manual decode pitch (Hz); 0 returns to auto (follow
|
|
// the Flex CW pitch, or search). Applies live to a running decoder.
|
|
func (a *App) SetCWDecoderPitch(hz int) {
|
|
if hz < 0 {
|
|
hz = 0
|
|
}
|
|
a.cwMu.Lock()
|
|
a.cwPitchHz = hz
|
|
dec := a.cwDecoder
|
|
a.cwMu.Unlock()
|
|
if dec != nil {
|
|
dec.SetTarget(a.cwTargetPitch())
|
|
}
|
|
}
|
|
|
|
// GetCWDecoderPitch returns the manual override (0 = auto / follow Flex).
|
|
func (a *App) GetCWDecoderPitch() int {
|
|
a.cwMu.Lock()
|
|
defer a.cwMu.Unlock()
|
|
return a.cwPitchHz
|
|
}
|