Wideband autonomous SDR analysis engine forked from sdr-visual-suite
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

1062 行
33KB

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