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 }