Wideband autonomous SDR analysis engine forked from sdr-visual-suite
25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.

1034 satır
32KB

  1. package main
  2. import (
  3. "fmt"
  4. "math"
  5. "strings"
  6. "sync"
  7. "sync/atomic"
  8. "time"
  9. "sdr-wideband-suite/internal/classifier"
  10. "sdr-wideband-suite/internal/config"
  11. "sdr-wideband-suite/internal/demod"
  12. "sdr-wideband-suite/internal/detector"
  13. "sdr-wideband-suite/internal/dsp"
  14. "sdr-wideband-suite/internal/logging"
  15. fftutil "sdr-wideband-suite/internal/fft"
  16. "sdr-wideband-suite/internal/fft/gpufft"
  17. "sdr-wideband-suite/internal/pipeline"
  18. "sdr-wideband-suite/internal/rds"
  19. "sdr-wideband-suite/internal/recorder"
  20. )
  21. type rdsState struct {
  22. dec rds.Decoder
  23. result rds.Result
  24. lastDecode time.Time
  25. busy int32
  26. mu sync.Mutex
  27. }
  28. type dspRuntime struct {
  29. cfg config.Config
  30. det *detector.Detector
  31. derivedDetectors map[string]*derivedDetector
  32. nextDerivedBase int64
  33. window []float64
  34. plan *fftutil.CmplxPlan
  35. detailWindow []float64
  36. detailPlan *fftutil.CmplxPlan
  37. detailFFT int
  38. survWindows map[int][]float64
  39. survPlans map[int]*fftutil.CmplxPlan
  40. survFIR map[int][]float64
  41. dcEnabled bool
  42. iqEnabled bool
  43. useGPU bool
  44. gpuEngine *gpufft.Engine
  45. rdsMap map[int64]*rdsState
  46. streamPhaseState map[int64]*streamExtractState
  47. streamOverlap *streamIQOverlap
  48. arbiter *pipeline.Arbiter
  49. arbitration pipeline.ArbitrationState
  50. gotSamples bool
  51. }
  52. type spectrumArtifacts struct {
  53. allIQ []complex64
  54. surveillanceIQ []complex64
  55. detailIQ []complex64
  56. surveillanceSpectrum []float64
  57. surveillanceSpectra []pipeline.SurveillanceLevelSpectrum
  58. surveillancePlan surveillancePlan
  59. detailSpectrum []float64
  60. finished []detector.Event
  61. detected []detector.Signal
  62. thresholds []float64
  63. noiseFloor float64
  64. now time.Time
  65. }
  66. type derivedDetector struct {
  67. det *detector.Detector
  68. sampleRate int
  69. fftSize int
  70. idBase int64
  71. }
  72. type surveillanceLevelSpec struct {
  73. Level pipeline.AnalysisLevel
  74. Decim int
  75. AllowGPU bool
  76. }
  77. type surveillancePlan struct {
  78. Primary pipeline.AnalysisLevel
  79. Levels []pipeline.AnalysisLevel
  80. LevelSet pipeline.SurveillanceLevelSet
  81. Presentation pipeline.AnalysisLevel
  82. Context pipeline.AnalysisContext
  83. DetectionPolicy pipeline.SurveillanceDetectionPolicy
  84. Specs []surveillanceLevelSpec
  85. }
  86. const derivedIDBlock = int64(1_000_000_000)
  87. func newDSPRuntime(cfg config.Config, det *detector.Detector, window []float64, gpuState *gpuStatus) *dspRuntime {
  88. detailFFT := cfg.Refinement.DetailFFTSize
  89. if detailFFT <= 0 {
  90. detailFFT = cfg.FFTSize
  91. }
  92. rt := &dspRuntime{
  93. cfg: cfg,
  94. det: det,
  95. derivedDetectors: map[string]*derivedDetector{},
  96. nextDerivedBase: -derivedIDBlock,
  97. window: window,
  98. plan: fftutil.NewCmplxPlan(cfg.FFTSize),
  99. detailWindow: fftutil.Hann(detailFFT),
  100. detailPlan: fftutil.NewCmplxPlan(detailFFT),
  101. detailFFT: detailFFT,
  102. survWindows: map[int][]float64{},
  103. survPlans: map[int]*fftutil.CmplxPlan{},
  104. survFIR: map[int][]float64{},
  105. dcEnabled: cfg.DCBlock,
  106. iqEnabled: cfg.IQBalance,
  107. useGPU: cfg.UseGPUFFT,
  108. rdsMap: map[int64]*rdsState{},
  109. streamPhaseState: map[int64]*streamExtractState{},
  110. streamOverlap: &streamIQOverlap{},
  111. arbiter: pipeline.NewArbiter(),
  112. }
  113. if rt.useGPU && gpuState != nil {
  114. snap := gpuState.snapshot()
  115. if snap.Available {
  116. if eng, err := gpufft.New(cfg.FFTSize); err == nil {
  117. rt.gpuEngine = eng
  118. gpuState.set(true, nil)
  119. } else {
  120. gpuState.set(false, err)
  121. rt.useGPU = false
  122. }
  123. }
  124. }
  125. return rt
  126. }
  127. func (rt *dspRuntime) applyUpdate(upd dspUpdate, srcMgr *sourceManager, rec *recorder.Manager, gpuState *gpuStatus) {
  128. prevFFT := rt.cfg.FFTSize
  129. prevSampleRate := rt.cfg.SampleRate
  130. prevUseGPU := rt.useGPU
  131. prevDetailFFT := rt.detailFFT
  132. rt.cfg = upd.cfg
  133. if rec != nil {
  134. rec.Update(rt.cfg.SampleRate, rt.cfg.FFTSize, recorder.Policy{
  135. Enabled: rt.cfg.Recorder.Enabled,
  136. MinSNRDb: rt.cfg.Recorder.MinSNRDb,
  137. MinDuration: mustParseDuration(rt.cfg.Recorder.MinDuration, 1*time.Second),
  138. MaxDuration: mustParseDuration(rt.cfg.Recorder.MaxDuration, 300*time.Second),
  139. PrerollMs: rt.cfg.Recorder.PrerollMs,
  140. RecordIQ: rt.cfg.Recorder.RecordIQ,
  141. RecordAudio: rt.cfg.Recorder.RecordAudio,
  142. AutoDemod: rt.cfg.Recorder.AutoDemod,
  143. AutoDecode: rt.cfg.Recorder.AutoDecode,
  144. MaxDiskMB: rt.cfg.Recorder.MaxDiskMB,
  145. OutputDir: rt.cfg.Recorder.OutputDir,
  146. ClassFilter: rt.cfg.Recorder.ClassFilter,
  147. RingSeconds: rt.cfg.Recorder.RingSeconds,
  148. DeemphasisUs: rt.cfg.Recorder.DeemphasisUs,
  149. ExtractionTaps: rt.cfg.Recorder.ExtractionTaps,
  150. ExtractionBwMult: rt.cfg.Recorder.ExtractionBwMult,
  151. }, rt.cfg.CenterHz, buildDecoderMap(rt.cfg))
  152. }
  153. if upd.det != nil {
  154. rt.det = upd.det
  155. }
  156. if upd.window != nil {
  157. rt.window = upd.window
  158. rt.plan = fftutil.NewCmplxPlan(rt.cfg.FFTSize)
  159. }
  160. detailFFT := rt.cfg.Refinement.DetailFFTSize
  161. if detailFFT <= 0 {
  162. detailFFT = rt.cfg.FFTSize
  163. }
  164. if detailFFT != prevDetailFFT {
  165. rt.detailFFT = detailFFT
  166. rt.detailWindow = fftutil.Hann(detailFFT)
  167. rt.detailPlan = fftutil.NewCmplxPlan(detailFFT)
  168. }
  169. if prevSampleRate != rt.cfg.SampleRate {
  170. rt.survFIR = map[int][]float64{}
  171. }
  172. if prevFFT != rt.cfg.FFTSize {
  173. rt.survWindows = map[int][]float64{}
  174. rt.survPlans = map[int]*fftutil.CmplxPlan{}
  175. }
  176. if upd.det != nil || prevSampleRate != rt.cfg.SampleRate || prevFFT != rt.cfg.FFTSize {
  177. rt.derivedDetectors = map[string]*derivedDetector{}
  178. rt.nextDerivedBase = -derivedIDBlock
  179. }
  180. rt.dcEnabled = upd.dcBlock
  181. rt.iqEnabled = upd.iqBalance
  182. if rt.cfg.FFTSize != prevFFT || rt.cfg.UseGPUFFT != prevUseGPU {
  183. srcMgr.Flush()
  184. rt.gotSamples = false
  185. if rt.gpuEngine != nil {
  186. rt.gpuEngine.Close()
  187. rt.gpuEngine = nil
  188. }
  189. rt.useGPU = rt.cfg.UseGPUFFT
  190. if rt.useGPU && gpuState != nil {
  191. snap := gpuState.snapshot()
  192. if snap.Available {
  193. if eng, err := gpufft.New(rt.cfg.FFTSize); err == nil {
  194. rt.gpuEngine = eng
  195. gpuState.set(true, nil)
  196. } else {
  197. gpuState.set(false, err)
  198. rt.useGPU = false
  199. }
  200. } else {
  201. gpuState.set(false, nil)
  202. rt.useGPU = false
  203. }
  204. } else if gpuState != nil {
  205. gpuState.set(false, nil)
  206. }
  207. }
  208. }
  209. func (rt *dspRuntime) spectrumFromIQ(iq []complex64, gpuState *gpuStatus) []float64 {
  210. return rt.spectrumFromIQWithPlan(iq, rt.window, rt.plan, gpuState, true)
  211. }
  212. func (rt *dspRuntime) spectrumFromIQWithPlan(iq []complex64, window []float64, plan *fftutil.CmplxPlan, gpuState *gpuStatus, allowGPU bool) []float64 {
  213. if len(iq) == 0 {
  214. return nil
  215. }
  216. if allowGPU && rt.useGPU && rt.gpuEngine != nil {
  217. gpuBuf := make([]complex64, len(iq))
  218. if len(window) == len(iq) {
  219. for i := 0; i < len(iq); i++ {
  220. v := iq[i]
  221. w := float32(window[i])
  222. gpuBuf[i] = complex(real(v)*w, imag(v)*w)
  223. }
  224. } else {
  225. copy(gpuBuf, iq)
  226. }
  227. out, err := rt.gpuEngine.Exec(gpuBuf)
  228. if err != nil {
  229. if gpuState != nil {
  230. gpuState.set(false, err)
  231. }
  232. rt.useGPU = false
  233. return fftutil.SpectrumWithPlan(gpuBuf, nil, plan)
  234. }
  235. return fftutil.SpectrumFromFFT(out)
  236. }
  237. return fftutil.SpectrumWithPlan(iq, window, plan)
  238. }
  239. func (rt *dspRuntime) windowForFFT(fftSize int) []float64 {
  240. if fftSize <= 0 {
  241. return nil
  242. }
  243. if fftSize == rt.cfg.FFTSize {
  244. return rt.window
  245. }
  246. if rt.survWindows == nil {
  247. rt.survWindows = map[int][]float64{}
  248. }
  249. if window, ok := rt.survWindows[fftSize]; ok {
  250. return window
  251. }
  252. window := fftutil.Hann(fftSize)
  253. rt.survWindows[fftSize] = window
  254. return window
  255. }
  256. func (rt *dspRuntime) planForFFT(fftSize int) *fftutil.CmplxPlan {
  257. if fftSize <= 0 {
  258. return nil
  259. }
  260. if fftSize == rt.cfg.FFTSize {
  261. return rt.plan
  262. }
  263. if rt.survPlans == nil {
  264. rt.survPlans = map[int]*fftutil.CmplxPlan{}
  265. }
  266. if plan, ok := rt.survPlans[fftSize]; ok {
  267. return plan
  268. }
  269. plan := fftutil.NewCmplxPlan(fftSize)
  270. rt.survPlans[fftSize] = plan
  271. return plan
  272. }
  273. func (rt *dspRuntime) spectrumForLevel(iq []complex64, fftSize int, gpuState *gpuStatus, allowGPU bool) []float64 {
  274. if len(iq) == 0 || fftSize <= 0 {
  275. return nil
  276. }
  277. if len(iq) > fftSize {
  278. iq = iq[len(iq)-fftSize:]
  279. }
  280. window := rt.windowForFFT(fftSize)
  281. plan := rt.planForFFT(fftSize)
  282. return rt.spectrumFromIQWithPlan(iq, window, plan, gpuState, allowGPU)
  283. }
  284. func sanitizeSpectrum(spectrum []float64) {
  285. for i := range spectrum {
  286. if math.IsNaN(spectrum[i]) || math.IsInf(spectrum[i], 0) {
  287. spectrum[i] = -200
  288. }
  289. }
  290. }
  291. func (rt *dspRuntime) decimationTaps(factor int) []float64 {
  292. if factor <= 1 {
  293. return nil
  294. }
  295. if rt.survFIR == nil {
  296. rt.survFIR = map[int][]float64{}
  297. }
  298. if taps, ok := rt.survFIR[factor]; ok {
  299. return taps
  300. }
  301. cutoff := float64(rt.cfg.SampleRate/factor) * 0.5 * 0.8
  302. taps := dsp.LowpassFIR(cutoff, rt.cfg.SampleRate, 101)
  303. rt.survFIR[factor] = taps
  304. return taps
  305. }
  306. func (rt *dspRuntime) decimateSurveillanceIQ(iq []complex64, factor int) []complex64 {
  307. if factor <= 1 {
  308. return iq
  309. }
  310. taps := rt.decimationTaps(factor)
  311. if len(taps) == 0 {
  312. return dsp.Decimate(iq, factor)
  313. }
  314. filtered := dsp.ApplyFIR(iq, taps)
  315. return dsp.Decimate(filtered, factor)
  316. }
  317. func (rt *dspRuntime) captureSpectrum(srcMgr *sourceManager, rec *recorder.Manager, dcBlocker *dsp.DCBlocker, gpuState *gpuStatus) (*spectrumArtifacts, error) {
  318. required := rt.cfg.FFTSize
  319. if rt.detailFFT > required {
  320. required = rt.detailFFT
  321. }
  322. available := required
  323. st := srcMgr.Stats()
  324. if st.BufferSamples > required {
  325. available = (st.BufferSamples / required) * required
  326. if available < required {
  327. available = required
  328. }
  329. }
  330. logging.Debug("capture", "read_iq", "required", required, "available", available, "buf", st.BufferSamples, "reset", st.Resets, "drop", st.Dropped)
  331. allIQ, err := srcMgr.ReadIQ(available)
  332. if err != nil {
  333. return nil, err
  334. }
  335. if rec != nil {
  336. rec.Ingest(time.Now(), allIQ)
  337. }
  338. // Cap allIQ for downstream extraction to prevent buffer bloat.
  339. // Without this cap, buffer accumulation during processing stalls causes
  340. // increasingly large reads → longer extraction → more accumulation
  341. // (positive feedback loop). For audio streaming this creates >150ms
  342. // feed gaps that produce audible clicks.
  343. // The ring buffer (Ingest above) gets the full data; only extraction is capped.
  344. maxStreamSamples := rt.cfg.SampleRate / rt.cfg.FrameRate * 2
  345. if maxStreamSamples < required {
  346. maxStreamSamples = required
  347. }
  348. maxStreamSamples = (maxStreamSamples / required) * required
  349. if len(allIQ) > maxStreamSamples {
  350. allIQ = allIQ[len(allIQ)-maxStreamSamples:]
  351. }
  352. logging.Debug("capture", "iq_len", "len", len(allIQ), "surv_fft", rt.cfg.FFTSize, "detail_fft", rt.detailFFT)
  353. survIQ := allIQ
  354. if len(allIQ) > rt.cfg.FFTSize {
  355. survIQ = allIQ[len(allIQ)-rt.cfg.FFTSize:]
  356. }
  357. detailIQ := survIQ
  358. if rt.detailFFT > 0 && len(allIQ) >= rt.detailFFT {
  359. detailIQ = allIQ[len(allIQ)-rt.detailFFT:]
  360. }
  361. if rt.dcEnabled {
  362. dcBlocker.Apply(allIQ)
  363. }
  364. if rt.iqEnabled {
  365. dsp.IQBalance(survIQ)
  366. if !sameIQBuffer(detailIQ, survIQ) {
  367. detailIQ = append([]complex64(nil), detailIQ...)
  368. dsp.IQBalance(detailIQ)
  369. }
  370. }
  371. survSpectrum := rt.spectrumFromIQ(survIQ, gpuState)
  372. sanitizeSpectrum(survSpectrum)
  373. detailSpectrum := survSpectrum
  374. if !sameIQBuffer(detailIQ, survIQ) {
  375. detailSpectrum = rt.spectrumFromIQWithPlan(detailIQ, rt.detailWindow, rt.detailPlan, gpuState, false)
  376. sanitizeSpectrum(detailSpectrum)
  377. }
  378. policy := pipeline.PolicyFromConfig(rt.cfg)
  379. plan := rt.buildSurveillancePlan(policy)
  380. surveillanceSpectra := make([]pipeline.SurveillanceLevelSpectrum, 0, len(plan.Specs))
  381. for _, spec := range plan.Specs {
  382. if spec.Level.FFTSize <= 0 {
  383. continue
  384. }
  385. var spectrum []float64
  386. if spec.Decim <= 1 {
  387. if spec.Level.FFTSize == len(survSpectrum) {
  388. spectrum = survSpectrum
  389. } else {
  390. spectrum = rt.spectrumForLevel(survIQ, spec.Level.FFTSize, gpuState, spec.AllowGPU)
  391. sanitizeSpectrum(spectrum)
  392. }
  393. } else {
  394. required := spec.Level.FFTSize * spec.Decim
  395. if required > len(survIQ) {
  396. continue
  397. }
  398. src := survIQ
  399. if len(src) > required {
  400. src = src[len(src)-required:]
  401. }
  402. decimated := rt.decimateSurveillanceIQ(src, spec.Decim)
  403. spectrum = rt.spectrumForLevel(decimated, spec.Level.FFTSize, gpuState, false)
  404. sanitizeSpectrum(spectrum)
  405. }
  406. if len(spectrum) == 0 {
  407. continue
  408. }
  409. surveillanceSpectra = append(surveillanceSpectra, pipeline.SurveillanceLevelSpectrum{Level: spec.Level, Spectrum: spectrum})
  410. }
  411. now := time.Now()
  412. finished, detected := rt.det.Process(now, survSpectrum, rt.cfg.CenterHz)
  413. return &spectrumArtifacts{
  414. allIQ: allIQ,
  415. surveillanceIQ: survIQ,
  416. detailIQ: detailIQ,
  417. surveillanceSpectrum: survSpectrum,
  418. surveillanceSpectra: surveillanceSpectra,
  419. surveillancePlan: plan,
  420. detailSpectrum: detailSpectrum,
  421. finished: finished,
  422. detected: detected,
  423. thresholds: rt.det.LastThresholds(),
  424. noiseFloor: rt.det.LastNoiseFloor(),
  425. now: now,
  426. }, nil
  427. }
  428. func (rt *dspRuntime) buildSurveillanceResult(art *spectrumArtifacts) pipeline.SurveillanceResult {
  429. if art == nil {
  430. return pipeline.SurveillanceResult{}
  431. }
  432. policy := pipeline.PolicyFromConfig(rt.cfg)
  433. plan := art.surveillancePlan
  434. if plan.Primary.Name == "" {
  435. plan = rt.buildSurveillancePlan(policy)
  436. }
  437. primaryCandidates := pipeline.CandidatesFromSignalsWithLevel(art.detected, "surveillance-detector", plan.Primary)
  438. derivedCandidates := rt.detectDerivedCandidates(art, plan)
  439. candidates := pipeline.FuseCandidates(primaryCandidates, derivedCandidates)
  440. pipeline.ApplyMonitorWindowMatchesToCandidates(policy, candidates)
  441. scheduled := pipeline.ScheduleCandidates(candidates, policy)
  442. return pipeline.SurveillanceResult{
  443. Level: plan.Primary,
  444. Levels: plan.Levels,
  445. LevelSet: plan.LevelSet,
  446. DetectionPolicy: plan.DetectionPolicy,
  447. DisplayLevel: plan.Presentation,
  448. Context: plan.Context,
  449. Spectra: art.surveillanceSpectra,
  450. Candidates: candidates,
  451. Scheduled: scheduled,
  452. Finished: art.finished,
  453. Signals: art.detected,
  454. NoiseFloor: art.noiseFloor,
  455. Thresholds: art.thresholds,
  456. }
  457. }
  458. func (rt *dspRuntime) detectDerivedCandidates(art *spectrumArtifacts, plan surveillancePlan) []pipeline.Candidate {
  459. if art == nil || len(plan.LevelSet.Derived) == 0 {
  460. return nil
  461. }
  462. spectra := map[string][]float64{}
  463. for _, spec := range art.surveillanceSpectra {
  464. if spec.Level.Name == "" || len(spec.Spectrum) == 0 {
  465. continue
  466. }
  467. spectra[spec.Level.Name] = spec.Spectrum
  468. }
  469. if len(spectra) == 0 {
  470. return nil
  471. }
  472. out := make([]pipeline.Candidate, 0, len(plan.LevelSet.Derived))
  473. for _, level := range plan.LevelSet.Derived {
  474. if level.Name == "" {
  475. continue
  476. }
  477. if !pipeline.IsDetectionLevel(level) {
  478. continue
  479. }
  480. spectrum := spectra[level.Name]
  481. if len(spectrum) == 0 {
  482. continue
  483. }
  484. entry := rt.derivedDetectorForLevel(level)
  485. if entry == nil || entry.det == nil {
  486. continue
  487. }
  488. _, signals := entry.det.Process(art.now, spectrum, level.CenterHz)
  489. if len(signals) == 0 {
  490. continue
  491. }
  492. cands := pipeline.CandidatesFromSignalsWithLevel(signals, "surveillance-derived", level)
  493. for i := range cands {
  494. if cands[i].ID == 0 {
  495. continue
  496. }
  497. cands[i].ID = entry.idBase - cands[i].ID
  498. }
  499. out = append(out, cands...)
  500. }
  501. if len(out) == 0 {
  502. return nil
  503. }
  504. return out
  505. }
  506. func (rt *dspRuntime) derivedDetectorForLevel(level pipeline.AnalysisLevel) *derivedDetector {
  507. if level.SampleRate <= 0 || level.FFTSize <= 0 {
  508. return nil
  509. }
  510. if rt.derivedDetectors == nil {
  511. rt.derivedDetectors = map[string]*derivedDetector{}
  512. }
  513. key := level.Name
  514. if key == "" {
  515. key = fmt.Sprintf("%d:%d", level.SampleRate, level.FFTSize)
  516. }
  517. entry := rt.derivedDetectors[key]
  518. if entry != nil && entry.sampleRate == level.SampleRate && entry.fftSize == level.FFTSize {
  519. return entry
  520. }
  521. if rt.nextDerivedBase == 0 {
  522. rt.nextDerivedBase = -derivedIDBlock
  523. }
  524. entry = &derivedDetector{
  525. det: detector.New(rt.cfg.Detector, level.SampleRate, level.FFTSize),
  526. sampleRate: level.SampleRate,
  527. fftSize: level.FFTSize,
  528. idBase: rt.nextDerivedBase,
  529. }
  530. rt.nextDerivedBase -= derivedIDBlock
  531. rt.derivedDetectors[key] = entry
  532. return entry
  533. }
  534. func (rt *dspRuntime) buildRefinementInput(surv pipeline.SurveillanceResult, now time.Time) pipeline.RefinementInput {
  535. policy := pipeline.PolicyFromConfig(rt.cfg)
  536. baseBudget := pipeline.BudgetModelFromPolicy(policy)
  537. pressure := pipeline.BuildBudgetPressureSummary(baseBudget, rt.arbitration.Refinement, rt.arbitration.Queue)
  538. budget := pipeline.ApplyBudgetRebalance(policy, baseBudget, pressure)
  539. plan := pipeline.BuildRefinementPlanWithBudget(surv.Candidates, policy, budget)
  540. admission := rt.arbiter.AdmitRefinementWithBudget(plan, policy, budget, now)
  541. plan = admission.Plan
  542. workItems := make([]pipeline.RefinementWorkItem, 0, len(admission.WorkItems))
  543. if len(admission.WorkItems) > 0 {
  544. workItems = append(workItems, admission.WorkItems...)
  545. }
  546. scheduled := append([]pipeline.ScheduledCandidate(nil), admission.Admitted...)
  547. workIndex := map[int64]int{}
  548. for i := range workItems {
  549. if workItems[i].Candidate.ID == 0 {
  550. continue
  551. }
  552. workIndex[workItems[i].Candidate.ID] = i
  553. }
  554. windows := make([]pipeline.RefinementWindow, 0, len(scheduled))
  555. for _, sc := range scheduled {
  556. window := pipeline.RefinementWindowForCandidate(policy, sc.Candidate)
  557. windows = append(windows, window)
  558. if idx, ok := workIndex[sc.Candidate.ID]; ok {
  559. workItems[idx].Window = window
  560. }
  561. }
  562. detailFFT := rt.cfg.Refinement.DetailFFTSize
  563. if detailFFT <= 0 {
  564. detailFFT = rt.cfg.FFTSize
  565. }
  566. levelSpan := spanForPolicy(policy, float64(rt.cfg.SampleRate))
  567. if _, maxSpan, ok := windowSpanBounds(windows); ok {
  568. levelSpan = maxSpan
  569. }
  570. level := analysisLevel("refinement", "refinement", "refinement", rt.cfg.SampleRate, detailFFT, rt.cfg.CenterHz, levelSpan, "refinement-window", 1, rt.cfg.SampleRate)
  571. detailLevel := analysisLevel("detail", "detail", "refinement", rt.cfg.SampleRate, detailFFT, rt.cfg.CenterHz, levelSpan, "detail-spectrum", 1, rt.cfg.SampleRate)
  572. if len(workItems) > 0 {
  573. for i := range workItems {
  574. item := &workItems[i]
  575. if item.Window.SpanHz <= 0 {
  576. continue
  577. }
  578. item.Execution = &pipeline.RefinementExecution{
  579. Stage: "refine",
  580. SampleRate: rt.cfg.SampleRate,
  581. FFTSize: detailFFT,
  582. CenterHz: item.Window.CenterHz,
  583. SpanHz: item.Window.SpanHz,
  584. Source: detailLevel.Source,
  585. }
  586. }
  587. }
  588. input := pipeline.RefinementInput{
  589. Level: level,
  590. Detail: detailLevel,
  591. Context: surv.Context,
  592. Request: pipeline.RefinementRequest{Strategy: plan.Strategy, Reason: "surveillance-plan", SpanHintHz: levelSpan},
  593. Budgets: budget,
  594. Admission: admission.Admission,
  595. Candidates: append([]pipeline.Candidate(nil), surv.Candidates...),
  596. Scheduled: scheduled,
  597. WorkItems: workItems,
  598. Plan: plan,
  599. Windows: windows,
  600. SampleRate: rt.cfg.SampleRate,
  601. FFTSize: detailFFT,
  602. CenterHz: rt.cfg.CenterHz,
  603. Source: "surveillance-detector",
  604. }
  605. input.Context.Refinement = level
  606. input.Context.Detail = detailLevel
  607. if !policy.RefinementEnabled {
  608. for i := range input.WorkItems {
  609. item := &input.WorkItems[i]
  610. if item.Status == pipeline.RefinementStatusDropped {
  611. continue
  612. }
  613. item.Status = pipeline.RefinementStatusDropped
  614. item.Reason = pipeline.RefinementReasonDisabled
  615. }
  616. input.Scheduled = nil
  617. input.Request.Reason = pipeline.ReasonAdmissionDisabled
  618. input.Admission.Reason = pipeline.ReasonAdmissionDisabled
  619. input.Admission.Admitted = 0
  620. input.Admission.Skipped = 0
  621. input.Admission.Displaced = 0
  622. input.Plan.Selected = nil
  623. input.Plan.DroppedByBudget = 0
  624. }
  625. rt.setArbitration(policy, input.Budgets, input.Admission, rt.arbitration.Queue)
  626. return input
  627. }
  628. func (rt *dspRuntime) runRefinement(art *spectrumArtifacts, surv pipeline.SurveillanceResult, extractMgr *extractionManager, rec *recorder.Manager) pipeline.RefinementStep {
  629. input := rt.buildRefinementInput(surv, art.now)
  630. markWorkItemsStatus(input.WorkItems, pipeline.RefinementStatusAdmitted, pipeline.RefinementStatusRunning, pipeline.RefinementReasonRunning)
  631. result := rt.refineSignals(art, input, extractMgr, rec)
  632. markWorkItemsCompleted(input.WorkItems, result.Candidates)
  633. return pipeline.RefinementStep{Input: input, Result: result}
  634. }
  635. func (rt *dspRuntime) refineSignals(art *spectrumArtifacts, input pipeline.RefinementInput, extractMgr *extractionManager, rec *recorder.Manager) pipeline.RefinementResult {
  636. if art == nil || len(art.detailIQ) == 0 || len(input.Scheduled) == 0 {
  637. return pipeline.RefinementResult{}
  638. }
  639. policy := pipeline.PolicyFromConfig(rt.cfg)
  640. selectedCandidates := make([]pipeline.Candidate, 0, len(input.Scheduled))
  641. selectedSignals := make([]detector.Signal, 0, len(input.Scheduled))
  642. for _, sc := range input.Scheduled {
  643. selectedCandidates = append(selectedCandidates, sc.Candidate)
  644. selectedSignals = append(selectedSignals, detector.Signal{
  645. ID: sc.Candidate.ID,
  646. FirstBin: sc.Candidate.FirstBin,
  647. LastBin: sc.Candidate.LastBin,
  648. CenterHz: sc.Candidate.CenterHz,
  649. BWHz: sc.Candidate.BandwidthHz,
  650. PeakDb: sc.Candidate.PeakDb,
  651. SNRDb: sc.Candidate.SNRDb,
  652. NoiseDb: sc.Candidate.NoiseDb,
  653. })
  654. }
  655. sampleRate := input.SampleRate
  656. fftSize := input.FFTSize
  657. centerHz := input.CenterHz
  658. if sampleRate <= 0 {
  659. sampleRate = rt.cfg.SampleRate
  660. }
  661. if fftSize <= 0 {
  662. fftSize = rt.cfg.FFTSize
  663. }
  664. if centerHz == 0 {
  665. centerHz = rt.cfg.CenterHz
  666. }
  667. snips, snipRates := extractSignalIQBatch(extractMgr, art.detailIQ, sampleRate, centerHz, selectedSignals)
  668. refined := pipeline.RefineCandidates(selectedCandidates, input.Windows, art.detailSpectrum, sampleRate, fftSize, snips, snipRates, classifier.ClassifierMode(rt.cfg.ClassifierMode))
  669. signals := make([]detector.Signal, 0, len(refined))
  670. decisions := make([]pipeline.SignalDecision, 0, len(refined))
  671. for i, ref := range refined {
  672. sig := ref.Signal
  673. signals = append(signals, sig)
  674. cls := sig.Class
  675. snipRate := ref.SnippetRate
  676. decision := pipeline.DecideSignalAction(policy, ref.Candidate, cls)
  677. decisions = append(decisions, decision)
  678. if cls != nil {
  679. if cls.ModType == classifier.ClassWFM {
  680. cls.ModType = classifier.ClassWFMStereo
  681. signals[i].PlaybackMode = string(classifier.ClassWFMStereo)
  682. signals[i].DemodName = string(classifier.ClassWFMStereo)
  683. signals[i].StereoState = "searching"
  684. }
  685. pll := classifier.PLLResult{}
  686. if i < len(snips) && snips[i] != nil && len(snips[i]) > 256 {
  687. pll = classifier.EstimateExactFrequency(snips[i], snipRate, signals[i].CenterHz, cls.ModType)
  688. cls.PLL = &pll
  689. signals[i].PLL = &pll
  690. if cls.ModType == classifier.ClassWFMStereo {
  691. if pll.Stereo {
  692. signals[i].StereoState = "locked"
  693. } else if signals[i].StereoState == "" {
  694. signals[i].StereoState = "searching"
  695. }
  696. signals[i].PlaybackMode = string(classifier.ClassWFMStereo)
  697. signals[i].DemodName = string(classifier.ClassWFMStereo)
  698. }
  699. }
  700. if cls.ModType == classifier.ClassWFMStereo && rec != nil {
  701. rt.updateRDS(art.now, rec, &signals[i], cls)
  702. if signals[i].PLL != nil && signals[i].PLL.RDSStation != "" {
  703. signals[i].StereoState = "locked"
  704. }
  705. }
  706. }
  707. }
  708. budget := input.Budgets
  709. queueStats := rt.arbiter.ApplyDecisions(decisions, budget, art.now, policy)
  710. rt.setArbitration(policy, budget, input.Admission, queueStats)
  711. summary := summarizeDecisions(decisions)
  712. if rec != nil {
  713. if summary.RecordEnabled > 0 {
  714. rt.cfg.Recorder.Enabled = true
  715. }
  716. if summary.DecodeEnabled > 0 {
  717. rt.cfg.Recorder.AutoDecode = true
  718. }
  719. }
  720. rt.det.UpdateClasses(signals)
  721. return pipeline.RefinementResult{Level: input.Level, Signals: signals, Decisions: decisions, Candidates: selectedCandidates}
  722. }
  723. func (rt *dspRuntime) updateRDS(now time.Time, rec *recorder.Manager, sig *detector.Signal, cls *classifier.Classification) {
  724. if sig == nil || cls == nil {
  725. return
  726. }
  727. keyHz := sig.CenterHz
  728. if sig.PLL != nil && sig.PLL.ExactHz != 0 {
  729. keyHz = sig.PLL.ExactHz
  730. }
  731. key := int64(math.Round(keyHz / 25000.0))
  732. st := rt.rdsMap[key]
  733. if st == nil {
  734. st = &rdsState{}
  735. rt.rdsMap[key] = st
  736. }
  737. if now.Sub(st.lastDecode) >= 4*time.Second && atomic.LoadInt32(&st.busy) == 0 {
  738. st.lastDecode = now
  739. atomic.StoreInt32(&st.busy, 1)
  740. go func(st *rdsState, sigHz float64) {
  741. defer atomic.StoreInt32(&st.busy, 0)
  742. ringIQ, ringSR, ringCenter := rec.SliceRecent(4.0)
  743. if len(ringIQ) < ringSR || ringSR <= 0 {
  744. return
  745. }
  746. offset := sigHz - ringCenter
  747. shifted := dsp.FreqShift(ringIQ, ringSR, offset)
  748. decim1 := ringSR / 1000000
  749. if decim1 < 1 {
  750. decim1 = 1
  751. }
  752. lp1 := dsp.LowpassFIR(float64(ringSR/decim1)/2.0*0.8, ringSR, 51)
  753. f1 := dsp.ApplyFIR(shifted, lp1)
  754. d1 := dsp.Decimate(f1, decim1)
  755. rate1 := ringSR / decim1
  756. decim2 := rate1 / 250000
  757. if decim2 < 1 {
  758. decim2 = 1
  759. }
  760. lp2 := dsp.LowpassFIR(float64(rate1/decim2)/2.0*0.8, rate1, 101)
  761. f2 := dsp.ApplyFIR(d1, lp2)
  762. decimated := dsp.Decimate(f2, decim2)
  763. actualRate := rate1 / decim2
  764. rdsBase := demod.RDSBasebandComplex(decimated, actualRate)
  765. if len(rdsBase.Samples) == 0 {
  766. return
  767. }
  768. st.mu.Lock()
  769. result := st.dec.Decode(rdsBase.Samples, rdsBase.SampleRate)
  770. if result.PS != "" {
  771. st.result = result
  772. }
  773. st.mu.Unlock()
  774. }(st, sig.CenterHz)
  775. }
  776. st.mu.Lock()
  777. ps := st.result.PS
  778. st.mu.Unlock()
  779. if ps != "" && sig.PLL != nil {
  780. sig.PLL.RDSStation = strings.TrimSpace(ps)
  781. cls.PLL = sig.PLL
  782. }
  783. }
  784. func (rt *dspRuntime) maintenance(displaySignals []detector.Signal, rec *recorder.Manager) {
  785. if len(rt.rdsMap) > 0 {
  786. activeIDs := make(map[int64]bool, len(displaySignals))
  787. for _, s := range displaySignals {
  788. keyHz := s.CenterHz
  789. if s.PLL != nil && s.PLL.ExactHz != 0 {
  790. keyHz = s.PLL.ExactHz
  791. }
  792. activeIDs[int64(math.Round(keyHz/25000.0))] = true
  793. }
  794. for id := range rt.rdsMap {
  795. if !activeIDs[id] {
  796. delete(rt.rdsMap, id)
  797. }
  798. }
  799. }
  800. if len(rt.streamPhaseState) > 0 {
  801. sigIDs := make(map[int64]bool, len(displaySignals))
  802. for _, s := range displaySignals {
  803. sigIDs[s.ID] = true
  804. }
  805. for id := range rt.streamPhaseState {
  806. if !sigIDs[id] {
  807. delete(rt.streamPhaseState, id)
  808. }
  809. }
  810. }
  811. if rec != nil && len(displaySignals) > 0 {
  812. aqCfg := extractionConfig{firTaps: rt.cfg.Recorder.ExtractionTaps, bwMult: rt.cfg.Recorder.ExtractionBwMult}
  813. _ = aqCfg
  814. }
  815. }
  816. func spanForPolicy(policy pipeline.Policy, fallback float64) float64 {
  817. if policy.MonitorSpanHz > 0 {
  818. return policy.MonitorSpanHz
  819. }
  820. if len(policy.MonitorWindows) > 0 {
  821. maxSpan := 0.0
  822. for _, w := range policy.MonitorWindows {
  823. if w.SpanHz > maxSpan {
  824. maxSpan = w.SpanHz
  825. }
  826. }
  827. if maxSpan > 0 {
  828. return maxSpan
  829. }
  830. }
  831. if policy.MonitorStartHz != 0 && policy.MonitorEndHz != 0 && policy.MonitorEndHz > policy.MonitorStartHz {
  832. return policy.MonitorEndHz - policy.MonitorStartHz
  833. }
  834. return fallback
  835. }
  836. func windowSpanBounds(windows []pipeline.RefinementWindow) (float64, float64, bool) {
  837. minSpan := 0.0
  838. maxSpan := 0.0
  839. ok := false
  840. for _, w := range windows {
  841. if w.SpanHz <= 0 {
  842. continue
  843. }
  844. if !ok || w.SpanHz < minSpan {
  845. minSpan = w.SpanHz
  846. }
  847. if !ok || w.SpanHz > maxSpan {
  848. maxSpan = w.SpanHz
  849. }
  850. ok = true
  851. }
  852. return minSpan, maxSpan, ok
  853. }
  854. func analysisLevel(name, role, truth string, sampleRate int, fftSize int, centerHz float64, spanHz float64, source string, decimation int, baseRate int) pipeline.AnalysisLevel {
  855. level := pipeline.AnalysisLevel{
  856. Name: name,
  857. Role: role,
  858. Truth: truth,
  859. SampleRate: sampleRate,
  860. FFTSize: fftSize,
  861. CenterHz: centerHz,
  862. SpanHz: spanHz,
  863. Source: source,
  864. }
  865. if level.SampleRate > 0 && level.FFTSize > 0 {
  866. level.BinHz = float64(level.SampleRate) / float64(level.FFTSize)
  867. }
  868. if decimation > 0 {
  869. level.Decimation = decimation
  870. } else if baseRate > 0 && level.SampleRate > 0 && baseRate%level.SampleRate == 0 {
  871. level.Decimation = baseRate / level.SampleRate
  872. }
  873. return level
  874. }
  875. func (rt *dspRuntime) buildSurveillancePlan(policy pipeline.Policy) surveillancePlan {
  876. baseRate := rt.cfg.SampleRate
  877. baseFFT := rt.cfg.Surveillance.AnalysisFFTSize
  878. if baseFFT <= 0 {
  879. baseFFT = rt.cfg.FFTSize
  880. }
  881. span := spanForPolicy(policy, float64(baseRate))
  882. detectionPolicy := pipeline.SurveillanceDetectionPolicyFromPolicy(policy)
  883. primary := analysisLevel("surveillance", pipeline.RoleSurveillancePrimary, "surveillance", baseRate, baseFFT, rt.cfg.CenterHz, span, "baseband", 1, baseRate)
  884. levels := []pipeline.AnalysisLevel{primary}
  885. specs := []surveillanceLevelSpec{{Level: primary, Decim: 1, AllowGPU: true}}
  886. context := pipeline.AnalysisContext{Surveillance: primary}
  887. derivedLevels := make([]pipeline.AnalysisLevel, 0, 2)
  888. supportLevels := make([]pipeline.AnalysisLevel, 0, 2)
  889. strategy := strings.ToLower(strings.TrimSpace(policy.SurveillanceStrategy))
  890. switch strategy {
  891. case "multi-res", "multi-resolution", "multi", "multi_res":
  892. decim := 2
  893. derivedRate := baseRate / decim
  894. derivedFFT := baseFFT / decim
  895. if derivedRate >= 200000 && derivedFFT >= 256 {
  896. derivedSpan := spanForPolicy(policy, float64(derivedRate))
  897. role := pipeline.RoleSurveillanceSupport
  898. if detectionPolicy.DerivedDetectionEnabled {
  899. role = pipeline.RoleSurveillanceDerived
  900. }
  901. derived := analysisLevel("surveillance-lowres", role, "surveillance", derivedRate, derivedFFT, rt.cfg.CenterHz, derivedSpan, "decimated", decim, baseRate)
  902. if detectionPolicy.DerivedDetectionEnabled {
  903. levels = append(levels, derived)
  904. derivedLevels = append(derivedLevels, derived)
  905. } else {
  906. supportLevels = append(supportLevels, derived)
  907. }
  908. specs = append(specs, surveillanceLevelSpec{Level: derived, Decim: decim, AllowGPU: false})
  909. context.Derived = append(context.Derived, derived)
  910. }
  911. }
  912. presentation := analysisLevel("presentation", pipeline.RolePresentation, "presentation", baseRate, rt.cfg.Surveillance.DisplayBins, rt.cfg.CenterHz, span, "display", 1, baseRate)
  913. context.Presentation = presentation
  914. if len(derivedLevels) == 0 && detectionPolicy.DerivedDetectionEnabled {
  915. detectionPolicy.DerivedDetectionEnabled = false
  916. detectionPolicy.DerivedDetectionReason = "levels"
  917. }
  918. switch {
  919. case len(derivedLevels) > 0:
  920. detectionPolicy.DerivedDetectionMode = "detection"
  921. case len(supportLevels) > 0:
  922. detectionPolicy.DerivedDetectionMode = "support"
  923. default:
  924. detectionPolicy.DerivedDetectionMode = "disabled"
  925. }
  926. levelSet := pipeline.SurveillanceLevelSet{
  927. Primary: primary,
  928. Derived: append([]pipeline.AnalysisLevel(nil), derivedLevels...),
  929. Support: append([]pipeline.AnalysisLevel(nil), supportLevels...),
  930. Presentation: presentation,
  931. }
  932. detectionLevels := make([]pipeline.AnalysisLevel, 0, 1+len(derivedLevels))
  933. detectionLevels = append(detectionLevels, primary)
  934. detectionLevels = append(detectionLevels, derivedLevels...)
  935. levelSet.Detection = detectionLevels
  936. allLevels := make([]pipeline.AnalysisLevel, 0, 1+len(derivedLevels)+len(supportLevels)+1)
  937. allLevels = append(allLevels, primary)
  938. allLevels = append(allLevels, derivedLevels...)
  939. allLevels = append(allLevels, supportLevels...)
  940. if presentation.Name != "" {
  941. allLevels = append(allLevels, presentation)
  942. }
  943. levelSet.All = allLevels
  944. return surveillancePlan{
  945. Primary: primary,
  946. Levels: levels,
  947. LevelSet: levelSet,
  948. Presentation: presentation,
  949. Context: context,
  950. DetectionPolicy: detectionPolicy,
  951. Specs: specs,
  952. }
  953. }
  954. func sameIQBuffer(a []complex64, b []complex64) bool {
  955. if len(a) != len(b) {
  956. return false
  957. }
  958. if len(a) == 0 {
  959. return true
  960. }
  961. return &a[0] == &b[0]
  962. }
  963. func markWorkItemsStatus(items []pipeline.RefinementWorkItem, from string, to string, reason string) {
  964. for i := range items {
  965. if items[i].Status != from {
  966. continue
  967. }
  968. items[i].Status = to
  969. if reason != "" {
  970. items[i].Reason = reason
  971. }
  972. }
  973. }
  974. func markWorkItemsCompleted(items []pipeline.RefinementWorkItem, candidates []pipeline.Candidate) {
  975. if len(items) == 0 || len(candidates) == 0 {
  976. return
  977. }
  978. done := map[int64]struct{}{}
  979. for _, cand := range candidates {
  980. if cand.ID != 0 {
  981. done[cand.ID] = struct{}{}
  982. }
  983. }
  984. for i := range items {
  985. if _, ok := done[items[i].Candidate.ID]; !ok {
  986. continue
  987. }
  988. items[i].Status = pipeline.RefinementStatusCompleted
  989. items[i].Reason = pipeline.RefinementReasonCompleted
  990. }
  991. }
  992. func (rt *dspRuntime) setArbitration(policy pipeline.Policy, budget pipeline.BudgetModel, admission pipeline.RefinementAdmission, queue pipeline.DecisionQueueStats) {
  993. rt.arbitration = pipeline.BuildArbitrationState(policy, budget, admission, queue)
  994. }