From 7fdb2e67b614ac46de6323dee89c28cb4ff39f37 Mon Sep 17 00:00:00 2001 From: Jan Svabenik Date: Sun, 22 Mar 2026 04:38:17 +0100 Subject: [PATCH] dsp: gate lowres levels and split detail spectrum path --- cmd/sdrd/pipeline_runtime.go | 90 +++++++++++++++++++------------ cmd/sdrd/pipeline_runtime_test.go | 15 ++++++ 2 files changed, 70 insertions(+), 35 deletions(-) diff --git a/cmd/sdrd/pipeline_runtime.go b/cmd/sdrd/pipeline_runtime.go index 2d10d89..2850d01 100644 --- a/cmd/sdrd/pipeline_runtime.go +++ b/cmd/sdrd/pipeline_runtime.go @@ -146,6 +146,34 @@ func (rt *dspRuntime) applyUpdate(upd dspUpdate, srcMgr *sourceManager, rec *rec } } +func (rt *dspRuntime) spectrumFromIQ(iq []complex64, gpuState *gpuStatus) []float64 { + if len(iq) == 0 { + return nil + } + if rt.useGPU && rt.gpuEngine != nil { + gpuBuf := make([]complex64, len(iq)) + if len(rt.window) == len(iq) { + for i := 0; i < len(iq); i++ { + v := iq[i] + w := float32(rt.window[i]) + gpuBuf[i] = complex(real(v)*w, imag(v)*w) + } + } else { + copy(gpuBuf, iq) + } + out, err := rt.gpuEngine.Exec(gpuBuf) + if err != nil { + if gpuState != nil { + gpuState.set(false, err) + } + rt.useGPU = false + return fftutil.SpectrumWithPlan(gpuBuf, nil, rt.plan) + } + return fftutil.SpectrumFromFFT(out) + } + return fftutil.SpectrumWithPlan(iq, rt.window, rt.plan) +} + func (rt *dspRuntime) captureSpectrum(srcMgr *sourceManager, rec *recorder.Manager, dcBlocker *dsp.DCBlocker, gpuState *gpuStatus) (*spectrumArtifacts, error) { available := rt.cfg.FFTSize st := srcMgr.Stats() @@ -172,44 +200,30 @@ func (rt *dspRuntime) captureSpectrum(srcMgr *sourceManager, rec *recorder.Manag if rt.iqEnabled { dsp.IQBalance(survIQ) } - var spectrum []float64 - if rt.useGPU && rt.gpuEngine != nil { - gpuBuf := make([]complex64, len(survIQ)) - if len(rt.window) == len(survIQ) { - for i := 0; i < len(survIQ); i++ { - v := survIQ[i] - w := float32(rt.window[i]) - gpuBuf[i] = complex(real(v)*w, imag(v)*w) - } - } else { - copy(gpuBuf, survIQ) + survSpectrum := rt.spectrumFromIQ(survIQ, gpuState) + for i := range survSpectrum { + if math.IsNaN(survSpectrum[i]) || math.IsInf(survSpectrum[i], 0) { + survSpectrum[i] = -200 } - out, err := rt.gpuEngine.Exec(gpuBuf) - if err != nil { - if gpuState != nil { - gpuState.set(false, err) - } - rt.useGPU = false - spectrum = fftutil.SpectrumWithPlan(gpuBuf, nil, rt.plan) - } else { - spectrum = fftutil.SpectrumFromFFT(out) - } - } else { - spectrum = fftutil.SpectrumWithPlan(survIQ, rt.window, rt.plan) } - for i := range spectrum { - if math.IsNaN(spectrum[i]) || math.IsInf(spectrum[i], 0) { - spectrum[i] = -200 + detailIQ := survIQ + detailSpectrum := survSpectrum + if !sameIQBuffer(detailIQ, survIQ) { + detailSpectrum = rt.spectrumFromIQ(detailIQ, gpuState) + for i := range detailSpectrum { + if math.IsNaN(detailSpectrum[i]) || math.IsInf(detailSpectrum[i], 0) { + detailSpectrum[i] = -200 + } } } now := time.Now() - finished, detected := rt.det.Process(now, spectrum, rt.cfg.CenterHz) + finished, detected := rt.det.Process(now, survSpectrum, rt.cfg.CenterHz) return &spectrumArtifacts{ allIQ: allIQ, surveillanceIQ: survIQ, - detailIQ: survIQ, - surveillanceSpectrum: spectrum, - detailSpectrum: spectrum, + detailIQ: detailIQ, + surveillanceSpectrum: survSpectrum, + detailSpectrum: detailSpectrum, finished: finished, detected: detected, thresholds: rt.det.LastThresholds(), @@ -498,14 +512,20 @@ func surveillanceLevels(policy pipeline.Policy, primary pipeline.AnalysisLevel, levels := []pipeline.AnalysisLevel{primary} strategy := strings.ToLower(strings.TrimSpace(policy.SurveillanceStrategy)) switch strategy { - case "", "single-resolution": - if secondary.SampleRate != primary.SampleRate || secondary.FFTSize != primary.FFTSize { - levels = append(levels, secondary) - } - default: + case "multi-res", "multi-resolution", "multi", "multi_res": if secondary.SampleRate != primary.SampleRate || secondary.FFTSize != primary.FFTSize { levels = append(levels, secondary) } } return levels } + +func sameIQBuffer(a []complex64, b []complex64) bool { + if len(a) != len(b) { + return false + } + if len(a) == 0 { + return true + } + return &a[0] == &b[0] +} diff --git a/cmd/sdrd/pipeline_runtime_test.go b/cmd/sdrd/pipeline_runtime_test.go index 8ff9b77..0fded23 100644 --- a/cmd/sdrd/pipeline_runtime_test.go +++ b/cmd/sdrd/pipeline_runtime_test.go @@ -42,3 +42,18 @@ func TestScheduledCandidateSelectionUsesPolicy(t *testing.T) { t.Fatalf("expected highest priority candidate, got %d", got[0].Candidate.ID) } } + +func TestSurveillanceLevelsRespectStrategy(t *testing.T) { + policy := pipeline.Policy{SurveillanceStrategy: "single-resolution"} + primary := pipeline.AnalysisLevel{Name: "primary", SampleRate: 2000000, FFTSize: 2048} + secondary := pipeline.AnalysisLevel{Name: "secondary", SampleRate: 1000000, FFTSize: 1024} + levels := surveillanceLevels(policy, primary, secondary) + if len(levels) != 1 { + t.Fatalf("expected single level for single-resolution, got %d", len(levels)) + } + policy.SurveillanceStrategy = "multi-res" + levels = surveillanceLevels(policy, primary, secondary) + if len(levels) != 2 { + t.Fatalf("expected secondary level for multi-res, got %d", len(levels)) + } +}