Wideband autonomous SDR analysis engine forked from sdr-visual-suite
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

421 řádky
13KB

  1. package main
  2. import (
  3. "context"
  4. "encoding/json"
  5. "log"
  6. "math"
  7. "os"
  8. "runtime/debug"
  9. "strings"
  10. "sync"
  11. "sync/atomic"
  12. "time"
  13. "sdr-visual-suite/internal/classifier"
  14. "sdr-visual-suite/internal/config"
  15. "sdr-visual-suite/internal/demod"
  16. "sdr-visual-suite/internal/detector"
  17. "sdr-visual-suite/internal/dsp"
  18. fftutil "sdr-visual-suite/internal/fft"
  19. "sdr-visual-suite/internal/fft/gpufft"
  20. "sdr-visual-suite/internal/rds"
  21. "sdr-visual-suite/internal/recorder"
  22. )
  23. func runDSP(ctx context.Context, srcMgr *sourceManager, cfg config.Config, det *detector.Detector, window []float64, h *hub, eventFile *os.File, eventMu *sync.RWMutex, updates <-chan dspUpdate, gpuState *gpuStatus, rec *recorder.Manager, sigSnap *signalSnapshot, extractMgr *extractionManager) {
  24. defer func() {
  25. if r := recover(); r != nil {
  26. log.Printf("FATAL: runDSP goroutine panic: %v\n%s", r, debug.Stack())
  27. }
  28. }()
  29. ticker := time.NewTicker(cfg.FrameInterval())
  30. defer ticker.Stop()
  31. logTicker := time.NewTicker(5 * time.Second)
  32. defer logTicker.Stop()
  33. enc := json.NewEncoder(eventFile)
  34. dcBlocker := dsp.NewDCBlocker(0.995)
  35. dcEnabled := cfg.DCBlock
  36. iqEnabled := cfg.IQBalance
  37. plan := fftutil.NewCmplxPlan(cfg.FFTSize)
  38. useGPU := cfg.UseGPUFFT
  39. // Persistent RDS decoders per signal — async ring-buffer based
  40. type rdsState struct {
  41. dec rds.Decoder
  42. result rds.Result
  43. lastDecode time.Time
  44. busy int32 // atomic: 1 = goroutine running
  45. mu sync.Mutex
  46. }
  47. rdsMap := map[int64]*rdsState{}
  48. // Streaming extraction state: per-signal phase + IQ overlap for FIR halo
  49. streamPhaseState := map[int64]*streamExtractState{}
  50. streamOverlap := &streamIQOverlap{}
  51. var gpuEngine *gpufft.Engine
  52. if useGPU && gpuState != nil {
  53. snap := gpuState.snapshot()
  54. if snap.Available {
  55. if eng, err := gpufft.New(cfg.FFTSize); err == nil {
  56. gpuEngine = eng
  57. gpuState.set(true, nil)
  58. } else {
  59. gpuState.set(false, err)
  60. useGPU = false
  61. }
  62. } else {
  63. gpuState.set(false, nil)
  64. useGPU = false
  65. }
  66. } else if gpuState != nil {
  67. gpuState.set(false, nil)
  68. }
  69. gotSamples := false
  70. for {
  71. select {
  72. case <-ctx.Done():
  73. return
  74. case <-logTicker.C:
  75. st := srcMgr.Stats()
  76. log.Printf("stats: buf=%d drop=%d reset=%d last=%dms", st.BufferSamples, st.Dropped, st.Resets, st.LastSampleAgoMs)
  77. case upd := <-updates:
  78. prevFFT := cfg.FFTSize
  79. prevUseGPU := useGPU
  80. cfg = upd.cfg
  81. if rec != nil {
  82. rec.Update(cfg.SampleRate, cfg.FFTSize, recorder.Policy{
  83. Enabled: cfg.Recorder.Enabled,
  84. MinSNRDb: cfg.Recorder.MinSNRDb,
  85. MinDuration: mustParseDuration(cfg.Recorder.MinDuration, 1*time.Second),
  86. MaxDuration: mustParseDuration(cfg.Recorder.MaxDuration, 300*time.Second),
  87. PrerollMs: cfg.Recorder.PrerollMs,
  88. RecordIQ: cfg.Recorder.RecordIQ,
  89. RecordAudio: cfg.Recorder.RecordAudio,
  90. AutoDemod: cfg.Recorder.AutoDemod,
  91. AutoDecode: cfg.Recorder.AutoDecode,
  92. MaxDiskMB: cfg.Recorder.MaxDiskMB,
  93. OutputDir: cfg.Recorder.OutputDir,
  94. ClassFilter: cfg.Recorder.ClassFilter,
  95. RingSeconds: cfg.Recorder.RingSeconds,
  96. DeemphasisUs: cfg.Recorder.DeemphasisUs,
  97. ExtractionTaps: cfg.Recorder.ExtractionTaps,
  98. ExtractionBwMult: cfg.Recorder.ExtractionBwMult,
  99. }, cfg.CenterHz, buildDecoderMap(cfg))
  100. }
  101. if upd.det != nil {
  102. det = upd.det
  103. }
  104. if upd.window != nil {
  105. window = upd.window
  106. plan = fftutil.NewCmplxPlan(cfg.FFTSize)
  107. }
  108. dcEnabled = upd.dcBlock
  109. iqEnabled = upd.iqBalance
  110. if cfg.FFTSize != prevFFT || cfg.UseGPUFFT != prevUseGPU {
  111. srcMgr.Flush()
  112. gotSamples = false
  113. if gpuEngine != nil {
  114. gpuEngine.Close()
  115. gpuEngine = nil
  116. }
  117. useGPU = cfg.UseGPUFFT
  118. if useGPU && gpuState != nil {
  119. snap := gpuState.snapshot()
  120. if snap.Available {
  121. if eng, err := gpufft.New(cfg.FFTSize); err == nil {
  122. gpuEngine = eng
  123. gpuState.set(true, nil)
  124. } else {
  125. gpuState.set(false, err)
  126. useGPU = false
  127. }
  128. } else {
  129. gpuState.set(false, nil)
  130. useGPU = false
  131. }
  132. } else if gpuState != nil {
  133. gpuState.set(false, nil)
  134. }
  135. }
  136. dcBlocker.Reset()
  137. ticker.Reset(cfg.FrameInterval())
  138. case <-ticker.C:
  139. // Read all available IQ data — not just one FFT block.
  140. // This ensures the ring buffer captures 100% of IQ for recording/demod.
  141. available := cfg.FFTSize
  142. st := srcMgr.Stats()
  143. if st.BufferSamples > cfg.FFTSize {
  144. // Round down to multiple of FFTSize for clean processing
  145. available = (st.BufferSamples / cfg.FFTSize) * cfg.FFTSize
  146. if available < cfg.FFTSize {
  147. available = cfg.FFTSize
  148. }
  149. }
  150. allIQ, err := srcMgr.ReadIQ(available)
  151. if err != nil {
  152. log.Printf("read IQ: %v", err)
  153. if strings.Contains(err.Error(), "timeout") {
  154. if err := srcMgr.Restart(cfg); err != nil {
  155. log.Printf("restart failed: %v", err)
  156. }
  157. }
  158. continue
  159. }
  160. // Ingest ALL IQ data into the ring buffer for recording
  161. if rec != nil {
  162. rec.Ingest(time.Now(), allIQ)
  163. }
  164. // Use only the last FFT block for spectrum display
  165. iq := allIQ
  166. if len(allIQ) > cfg.FFTSize {
  167. iq = allIQ[len(allIQ)-cfg.FFTSize:]
  168. }
  169. if !gotSamples {
  170. log.Printf("received IQ samples")
  171. gotSamples = true
  172. }
  173. if dcEnabled {
  174. dcBlocker.Apply(iq)
  175. }
  176. if iqEnabled {
  177. dsp.IQBalance(iq)
  178. }
  179. var spectrum []float64
  180. if useGPU && gpuEngine != nil {
  181. // GPU FFT: apply window to a COPY — allIQ must stay unmodified
  182. // for extractForStreaming which needs raw IQ for signal extraction.
  183. gpuBuf := make([]complex64, len(iq))
  184. if len(window) == len(iq) {
  185. for i := 0; i < len(iq); i++ {
  186. v := iq[i]
  187. w := float32(window[i])
  188. gpuBuf[i] = complex(real(v)*w, imag(v)*w)
  189. }
  190. } else {
  191. copy(gpuBuf, iq)
  192. }
  193. out, err := gpuEngine.Exec(gpuBuf)
  194. if err != nil {
  195. if gpuState != nil {
  196. gpuState.set(false, err)
  197. }
  198. useGPU = false
  199. spectrum = fftutil.SpectrumWithPlan(gpuBuf, nil, plan)
  200. } else {
  201. spectrum = fftutil.SpectrumFromFFT(out)
  202. }
  203. } else {
  204. spectrum = fftutil.SpectrumWithPlan(iq, window, plan)
  205. }
  206. for i := range spectrum {
  207. if math.IsNaN(spectrum[i]) || math.IsInf(spectrum[i], 0) {
  208. spectrum[i] = -200
  209. }
  210. }
  211. now := time.Now()
  212. finished, signals := det.Process(now, spectrum, cfg.CenterHz)
  213. thresholds := det.LastThresholds()
  214. noiseFloor := det.LastNoiseFloor()
  215. var displaySignals []detector.Signal
  216. if len(iq) > 0 {
  217. snips, snipRates := extractSignalIQBatch(extractMgr, iq, cfg.SampleRate, cfg.CenterHz, signals)
  218. for i := range signals {
  219. var snip []complex64
  220. if i < len(snips) {
  221. snip = snips[i]
  222. }
  223. // Determine actual sample rate of the extracted snippet
  224. snipRate := cfg.SampleRate
  225. if i < len(snipRates) && snipRates[i] > 0 {
  226. snipRate = snipRates[i]
  227. }
  228. cls := classifier.Classify(classifier.SignalInput{FirstBin: signals[i].FirstBin, LastBin: signals[i].LastBin, SNRDb: signals[i].SNRDb, CenterHz: signals[i].CenterHz, BWHz: signals[i].BWHz}, spectrum, cfg.SampleRate, cfg.FFTSize, snip, classifier.ClassifierMode(cfg.ClassifierMode))
  229. signals[i].Class = cls
  230. if cls != nil && snip != nil && len(snip) > 256 {
  231. pll := classifier.EstimateExactFrequency(snip, snipRate, signals[i].CenterHz, cls.ModType)
  232. cls.PLL = &pll
  233. signals[i].PLL = &pll
  234. // Upgrade WFM → WFM_STEREO if stereo pilot detected
  235. if cls.ModType == classifier.ClassWFM && pll.Stereo {
  236. cls.ModType = classifier.ClassWFMStereo
  237. }
  238. // RDS decode for WFM — async, uses ring buffer for continuous IQ
  239. if (cls.ModType == classifier.ClassWFM || cls.ModType == classifier.ClassWFMStereo) && rec != nil {
  240. key := int64(math.Round(signals[i].CenterHz / 500000))
  241. st := rdsMap[key]
  242. if st == nil {
  243. st = &rdsState{}
  244. rdsMap[key] = st
  245. }
  246. // Launch async decode every 4 seconds, skip if previous still running
  247. if now.Sub(st.lastDecode) >= 4*time.Second && atomic.LoadInt32(&st.busy) == 0 {
  248. st.lastDecode = now
  249. atomic.StoreInt32(&st.busy, 1)
  250. go func(st *rdsState, sigHz float64) {
  251. defer atomic.StoreInt32(&st.busy, 0)
  252. ringIQ, ringSR, ringCenter := rec.SliceRecent(4.0)
  253. if len(ringIQ) < ringSR || ringSR <= 0 {
  254. return
  255. }
  256. // Shift FM station to center
  257. offset := sigHz - ringCenter
  258. shifted := dsp.FreqShift(ringIQ, ringSR, offset)
  259. // Two-stage decimation to ~250kHz with proper anti-alias
  260. // Stage 1: 4MHz → 1MHz (decim 4), LP at 400kHz
  261. decim1 := ringSR / 1000000
  262. if decim1 < 1 {
  263. decim1 = 1
  264. }
  265. lp1 := dsp.LowpassFIR(float64(ringSR/decim1)/2.0*0.8, ringSR, 51)
  266. f1 := dsp.ApplyFIR(shifted, lp1)
  267. d1 := dsp.Decimate(f1, decim1)
  268. rate1 := ringSR / decim1
  269. // Stage 2: 1MHz → 250kHz (decim 4), LP at 100kHz
  270. decim2 := rate1 / 250000
  271. if decim2 < 1 {
  272. decim2 = 1
  273. }
  274. lp2 := dsp.LowpassFIR(float64(rate1/decim2)/2.0*0.8, rate1, 101)
  275. f2 := dsp.ApplyFIR(d1, lp2)
  276. decimated := dsp.Decimate(f2, decim2)
  277. actualRate := rate1 / decim2
  278. // RDS baseband extraction on the clean decimated block
  279. rdsBase := demod.RDSBasebandComplex(decimated, actualRate)
  280. if len(rdsBase.Samples) == 0 {
  281. return
  282. }
  283. st.mu.Lock()
  284. result := st.dec.Decode(rdsBase.Samples, rdsBase.SampleRate)
  285. diag := st.dec.LastDiag
  286. if result.PS != "" {
  287. st.result = result
  288. }
  289. st.mu.Unlock()
  290. log.Printf("RDS TRACE: ring decode freq=%.1fMHz decIQ=%d decSR=%d bbLen=%d bbRate=%d PI=%04X PS=%q %s",
  291. sigHz/1e6, len(decimated), actualRate, len(rdsBase.Samples), rdsBase.SampleRate,
  292. result.PI, result.PS, diag)
  293. if result.PS != "" {
  294. log.Printf("RDS decoded: PI=%04X PS=%q RT=%q freq=%.1fMHz", result.PI, result.PS, result.RT, sigHz/1e6)
  295. }
  296. }(st, signals[i].CenterHz)
  297. }
  298. // Read last known result (lock-free for display)
  299. st.mu.Lock()
  300. ps := st.result.PS
  301. st.mu.Unlock()
  302. if ps != "" {
  303. pll.RDSStation = strings.TrimSpace(ps)
  304. cls.PLL = &pll
  305. signals[i].PLL = &pll
  306. }
  307. }
  308. }
  309. }
  310. det.UpdateClasses(signals)
  311. // Cleanup RDS accumulators for signals that no longer exist
  312. if len(rdsMap) > 0 {
  313. activeIDs := make(map[int64]bool, len(signals))
  314. for _, s := range signals {
  315. activeIDs[int64(math.Round(s.CenterHz / 500000))] = true
  316. }
  317. for id := range rdsMap {
  318. if !activeIDs[id] {
  319. delete(rdsMap, id)
  320. }
  321. }
  322. }
  323. // Cleanup streamPhaseState for disappeared signals
  324. if len(streamPhaseState) > 0 {
  325. sigIDs := make(map[int64]bool, len(signals))
  326. for _, s := range signals {
  327. sigIDs[s.ID] = true
  328. }
  329. for id := range streamPhaseState {
  330. if !sigIDs[id] {
  331. delete(streamPhaseState, id)
  332. }
  333. }
  334. }
  335. // GPU-extract signal snippets with phase-continuous FreqShift and
  336. // IQ overlap for FIR halo. Heavy work on GPU, only demod runs async.
  337. displaySignals = det.StableSignals()
  338. if rec != nil && len(displaySignals) > 0 && len(allIQ) > 0 {
  339. aqCfg := extractionConfig{
  340. firTaps: cfg.Recorder.ExtractionTaps,
  341. bwMult: cfg.Recorder.ExtractionBwMult,
  342. }
  343. streamSnips, streamRates := extractForStreaming(extractMgr, allIQ, cfg.SampleRate, cfg.CenterHz, displaySignals, streamPhaseState, streamOverlap, aqCfg)
  344. items := make([]recorder.StreamFeedItem, 0, len(displaySignals))
  345. for j, ds := range displaySignals {
  346. if ds.ID == 0 || ds.Class == nil {
  347. continue
  348. }
  349. if j >= len(streamSnips) || len(streamSnips[j]) == 0 {
  350. continue
  351. }
  352. snipRate := cfg.SampleRate
  353. if j < len(streamRates) && streamRates[j] > 0 {
  354. snipRate = streamRates[j]
  355. }
  356. items = append(items, recorder.StreamFeedItem{
  357. Signal: ds,
  358. Snippet: streamSnips[j],
  359. SnipRate: snipRate,
  360. })
  361. }
  362. if len(items) > 0 {
  363. rec.FeedSnippets(items)
  364. }
  365. }
  366. } else {
  367. // No IQ data this frame — still need displaySignals for broadcast
  368. displaySignals = det.StableSignals()
  369. }
  370. if sigSnap != nil {
  371. sigSnap.set(displaySignals)
  372. }
  373. eventMu.Lock()
  374. for _, ev := range finished {
  375. _ = enc.Encode(ev)
  376. }
  377. eventMu.Unlock()
  378. if rec != nil && len(finished) > 0 {
  379. evCopy := make([]detector.Event, len(finished))
  380. copy(evCopy, finished)
  381. rec.OnEvents(evCopy)
  382. }
  383. var debugInfo *SpectrumDebug
  384. if len(thresholds) > 0 || len(displaySignals) > 0 || noiseFloor != 0 {
  385. scoreDebug := make([]map[string]any, 0, len(displaySignals))
  386. for _, s := range displaySignals {
  387. if s.Class == nil || len(s.Class.Scores) == 0 {
  388. scoreDebug = append(scoreDebug, map[string]any{"center_hz": s.CenterHz, "class": nil})
  389. continue
  390. }
  391. scores := make(map[string]float64, len(s.Class.Scores))
  392. for k, v := range s.Class.Scores {
  393. scores[string(k)] = v
  394. }
  395. scoreDebug = append(scoreDebug, map[string]any{
  396. "center_hz": s.CenterHz,
  397. "mod_type": s.Class.ModType,
  398. "confidence": s.Class.Confidence,
  399. "second_best": s.Class.SecondBest,
  400. "scores": scores,
  401. })
  402. }
  403. debugInfo = &SpectrumDebug{Thresholds: thresholds, NoiseFloor: noiseFloor, Scores: scoreDebug}
  404. }
  405. h.broadcast(SpectrumFrame{Timestamp: now.UnixMilli(), CenterHz: cfg.CenterHz, SampleHz: cfg.SampleRate, FFTSize: cfg.FFTSize, Spectrum: spectrum, Signals: displaySignals, Debug: debugInfo})
  406. }
  407. }
  408. }