package main import ( "log" "sort" "strconv" "time" "sdr-visual-suite/internal/config" "sdr-visual-suite/internal/demod/gpudemod" "sdr-visual-suite/internal/detector" "sdr-visual-suite/internal/dsp" ) func mustParseDuration(raw string, fallback time.Duration) time.Duration { if raw == "" { return fallback } if d, err := time.ParseDuration(raw); err == nil { return d } return fallback } func buildDecoderMap(cfg config.Config) map[string]string { out := map[string]string{} if cfg.Decoder.FT8Cmd != "" { out["FT8"] = cfg.Decoder.FT8Cmd } if cfg.Decoder.WSPRCmd != "" { out["WSPR"] = cfg.Decoder.WSPRCmd } if cfg.Decoder.DMRCmd != "" { out["DMR"] = cfg.Decoder.DMRCmd } if cfg.Decoder.DStarCmd != "" { out["D-STAR"] = cfg.Decoder.DStarCmd } if cfg.Decoder.FSKCmd != "" { out["FSK"] = cfg.Decoder.FSKCmd } if cfg.Decoder.PSKCmd != "" { out["PSK"] = cfg.Decoder.PSKCmd } return out } func decoderKeys(cfg config.Config) []string { m := buildDecoderMap(cfg) keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } sort.Strings(keys) return keys } func (m *extractionManager) reset() { if m == nil { return } m.mu.Lock() defer m.mu.Unlock() if m.runner != nil { m.runner.Close() m.runner = nil } } func (m *extractionManager) get(sampleCount int, sampleRate int) *gpudemod.BatchRunner { if m == nil || sampleCount <= 0 || sampleRate <= 0 || !gpudemod.Available() { return nil } m.mu.Lock() defer m.mu.Unlock() if m.runner == nil { if r, err := gpudemod.NewBatchRunner(sampleCount, sampleRate); err == nil { m.runner = r } else { log.Printf("gpudemod: batch runner init failed: %v", err) } return m.runner } return m.runner } func extractSignalIQ(iq []complex64, sampleRate int, centerHz float64, sigHz float64, bwHz float64) []complex64 { if len(iq) == 0 || sampleRate <= 0 { return nil } results := extractSignalIQBatch(nil, iq, sampleRate, centerHz, []detector.Signal{{CenterHz: sigHz, BWHz: bwHz}}) if len(results) == 0 { return nil } return results[0] } func extractSignalIQBatch(extractMgr *extractionManager, iq []complex64, sampleRate int, centerHz float64, signals []detector.Signal) [][]complex64 { out := make([][]complex64, len(signals)) if len(iq) == 0 || sampleRate <= 0 || len(signals) == 0 { return out } decimTarget := 200000 if decimTarget <= 0 { decimTarget = sampleRate } runner := extractMgr.get(len(iq), sampleRate) if runner != nil { jobs := make([]gpudemod.ExtractJob, len(signals)) for i, sig := range signals { jobs[i] = gpudemod.ExtractJob{OffsetHz: sig.CenterHz - centerHz, BW: sig.BWHz, OutRate: decimTarget} } if gpuOuts, _, err := runner.ShiftFilterDecimateBatch(iq, jobs); err == nil && len(gpuOuts) == len(signals) { log.Printf("gpudemod: batch extraction used for %d signals", len(signals)) for i := range gpuOuts { out[i] = gpuOuts[i] } return out } else if err != nil { log.Printf("gpudemod: batch extraction failed for %d signals: %v", len(signals), err) } } log.Printf("gpudemod: CPU extraction fallback used for %d signals", len(signals)) for i, sig := range signals { offset := sig.CenterHz - centerHz shifted := dsp.FreqShift(iq, sampleRate, offset) cutoff := sig.BWHz / 2 if cutoff < 200 { cutoff = 200 } if cutoff > float64(sampleRate)/2-1 { cutoff = float64(sampleRate)/2 - 1 } taps := dsp.LowpassFIR(cutoff, sampleRate, 101) filtered := dsp.ApplyFIR(shifted, taps) decim := sampleRate / decimTarget if decim < 1 { decim = 1 } out[i] = dsp.Decimate(filtered, decim) } return out } func parseSince(raw string) (time.Time, error) { if raw == "" { return time.Time{}, nil } if ms, err := strconv.ParseInt(raw, 10, 64); err == nil { if ms > 1e12 { return time.UnixMilli(ms), nil } return time.Unix(ms, 0), nil } if t, err := time.Parse(time.RFC3339Nano, raw); err == nil { return t, nil } return time.Parse(time.RFC3339, raw) }