Wideband autonomous SDR analysis engine forked from sdr-visual-suite
Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.

601 wiersze
18KB

  1. package main
  2. import (
  3. "math"
  4. "strings"
  5. "sync"
  6. "sync/atomic"
  7. "time"
  8. "sdr-wideband-suite/internal/classifier"
  9. "sdr-wideband-suite/internal/config"
  10. "sdr-wideband-suite/internal/demod"
  11. "sdr-wideband-suite/internal/detector"
  12. "sdr-wideband-suite/internal/dsp"
  13. fftutil "sdr-wideband-suite/internal/fft"
  14. "sdr-wideband-suite/internal/fft/gpufft"
  15. "sdr-wideband-suite/internal/pipeline"
  16. "sdr-wideband-suite/internal/rds"
  17. "sdr-wideband-suite/internal/recorder"
  18. )
  19. type rdsState struct {
  20. dec rds.Decoder
  21. result rds.Result
  22. lastDecode time.Time
  23. busy int32
  24. mu sync.Mutex
  25. }
  26. type dspRuntime struct {
  27. cfg config.Config
  28. det *detector.Detector
  29. window []float64
  30. plan *fftutil.CmplxPlan
  31. dcEnabled bool
  32. iqEnabled bool
  33. useGPU bool
  34. gpuEngine *gpufft.Engine
  35. rdsMap map[int64]*rdsState
  36. streamPhaseState map[int64]*streamExtractState
  37. streamOverlap *streamIQOverlap
  38. decisionQueues *decisionQueues
  39. queueStats decisionQueueStats
  40. gotSamples bool
  41. }
  42. type spectrumArtifacts struct {
  43. allIQ []complex64
  44. surveillanceIQ []complex64
  45. detailIQ []complex64
  46. surveillanceSpectrum []float64
  47. detailSpectrum []float64
  48. finished []detector.Event
  49. detected []detector.Signal
  50. thresholds []float64
  51. noiseFloor float64
  52. now time.Time
  53. }
  54. func newDSPRuntime(cfg config.Config, det *detector.Detector, window []float64, gpuState *gpuStatus) *dspRuntime {
  55. rt := &dspRuntime{
  56. cfg: cfg,
  57. det: det,
  58. window: window,
  59. plan: fftutil.NewCmplxPlan(cfg.FFTSize),
  60. dcEnabled: cfg.DCBlock,
  61. iqEnabled: cfg.IQBalance,
  62. useGPU: cfg.UseGPUFFT,
  63. rdsMap: map[int64]*rdsState{},
  64. streamPhaseState: map[int64]*streamExtractState{},
  65. streamOverlap: &streamIQOverlap{},
  66. }
  67. if rt.useGPU && gpuState != nil {
  68. snap := gpuState.snapshot()
  69. if snap.Available {
  70. if eng, err := gpufft.New(cfg.FFTSize); err == nil {
  71. rt.gpuEngine = eng
  72. gpuState.set(true, nil)
  73. } else {
  74. gpuState.set(false, err)
  75. rt.useGPU = false
  76. }
  77. }
  78. }
  79. return rt
  80. }
  81. func (rt *dspRuntime) applyUpdate(upd dspUpdate, srcMgr *sourceManager, rec *recorder.Manager, gpuState *gpuStatus) {
  82. prevFFT := rt.cfg.FFTSize
  83. prevUseGPU := rt.useGPU
  84. rt.cfg = upd.cfg
  85. if rec != nil {
  86. rec.Update(rt.cfg.SampleRate, rt.cfg.FFTSize, recorder.Policy{
  87. Enabled: rt.cfg.Recorder.Enabled,
  88. MinSNRDb: rt.cfg.Recorder.MinSNRDb,
  89. MinDuration: mustParseDuration(rt.cfg.Recorder.MinDuration, 1*time.Second),
  90. MaxDuration: mustParseDuration(rt.cfg.Recorder.MaxDuration, 300*time.Second),
  91. PrerollMs: rt.cfg.Recorder.PrerollMs,
  92. RecordIQ: rt.cfg.Recorder.RecordIQ,
  93. RecordAudio: rt.cfg.Recorder.RecordAudio,
  94. AutoDemod: rt.cfg.Recorder.AutoDemod,
  95. AutoDecode: rt.cfg.Recorder.AutoDecode,
  96. MaxDiskMB: rt.cfg.Recorder.MaxDiskMB,
  97. OutputDir: rt.cfg.Recorder.OutputDir,
  98. ClassFilter: rt.cfg.Recorder.ClassFilter,
  99. RingSeconds: rt.cfg.Recorder.RingSeconds,
  100. DeemphasisUs: rt.cfg.Recorder.DeemphasisUs,
  101. ExtractionTaps: rt.cfg.Recorder.ExtractionTaps,
  102. ExtractionBwMult: rt.cfg.Recorder.ExtractionBwMult,
  103. }, rt.cfg.CenterHz, buildDecoderMap(rt.cfg))
  104. }
  105. if upd.det != nil {
  106. rt.det = upd.det
  107. }
  108. if upd.window != nil {
  109. rt.window = upd.window
  110. rt.plan = fftutil.NewCmplxPlan(rt.cfg.FFTSize)
  111. }
  112. rt.dcEnabled = upd.dcBlock
  113. rt.iqEnabled = upd.iqBalance
  114. if rt.cfg.FFTSize != prevFFT || rt.cfg.UseGPUFFT != prevUseGPU {
  115. srcMgr.Flush()
  116. rt.gotSamples = false
  117. if rt.gpuEngine != nil {
  118. rt.gpuEngine.Close()
  119. rt.gpuEngine = nil
  120. }
  121. rt.useGPU = rt.cfg.UseGPUFFT
  122. if rt.useGPU && gpuState != nil {
  123. snap := gpuState.snapshot()
  124. if snap.Available {
  125. if eng, err := gpufft.New(rt.cfg.FFTSize); err == nil {
  126. rt.gpuEngine = eng
  127. gpuState.set(true, nil)
  128. } else {
  129. gpuState.set(false, err)
  130. rt.useGPU = false
  131. }
  132. } else {
  133. gpuState.set(false, nil)
  134. rt.useGPU = false
  135. }
  136. } else if gpuState != nil {
  137. gpuState.set(false, nil)
  138. }
  139. }
  140. }
  141. func (rt *dspRuntime) spectrumFromIQ(iq []complex64, gpuState *gpuStatus) []float64 {
  142. if len(iq) == 0 {
  143. return nil
  144. }
  145. if rt.useGPU && rt.gpuEngine != nil {
  146. gpuBuf := make([]complex64, len(iq))
  147. if len(rt.window) == len(iq) {
  148. for i := 0; i < len(iq); i++ {
  149. v := iq[i]
  150. w := float32(rt.window[i])
  151. gpuBuf[i] = complex(real(v)*w, imag(v)*w)
  152. }
  153. } else {
  154. copy(gpuBuf, iq)
  155. }
  156. out, err := rt.gpuEngine.Exec(gpuBuf)
  157. if err != nil {
  158. if gpuState != nil {
  159. gpuState.set(false, err)
  160. }
  161. rt.useGPU = false
  162. return fftutil.SpectrumWithPlan(gpuBuf, nil, rt.plan)
  163. }
  164. return fftutil.SpectrumFromFFT(out)
  165. }
  166. return fftutil.SpectrumWithPlan(iq, rt.window, rt.plan)
  167. }
  168. func (rt *dspRuntime) captureSpectrum(srcMgr *sourceManager, rec *recorder.Manager, dcBlocker *dsp.DCBlocker, gpuState *gpuStatus) (*spectrumArtifacts, error) {
  169. available := rt.cfg.FFTSize
  170. st := srcMgr.Stats()
  171. if st.BufferSamples > rt.cfg.FFTSize {
  172. available = (st.BufferSamples / rt.cfg.FFTSize) * rt.cfg.FFTSize
  173. if available < rt.cfg.FFTSize {
  174. available = rt.cfg.FFTSize
  175. }
  176. }
  177. allIQ, err := srcMgr.ReadIQ(available)
  178. if err != nil {
  179. return nil, err
  180. }
  181. if rec != nil {
  182. rec.Ingest(time.Now(), allIQ)
  183. }
  184. survIQ := allIQ
  185. if len(allIQ) > rt.cfg.FFTSize {
  186. survIQ = allIQ[len(allIQ)-rt.cfg.FFTSize:]
  187. }
  188. if rt.dcEnabled {
  189. dcBlocker.Apply(survIQ)
  190. }
  191. if rt.iqEnabled {
  192. dsp.IQBalance(survIQ)
  193. }
  194. survSpectrum := rt.spectrumFromIQ(survIQ, gpuState)
  195. for i := range survSpectrum {
  196. if math.IsNaN(survSpectrum[i]) || math.IsInf(survSpectrum[i], 0) {
  197. survSpectrum[i] = -200
  198. }
  199. }
  200. detailIQ := survIQ
  201. detailSpectrum := survSpectrum
  202. if !sameIQBuffer(detailIQ, survIQ) {
  203. detailSpectrum = rt.spectrumFromIQ(detailIQ, gpuState)
  204. for i := range detailSpectrum {
  205. if math.IsNaN(detailSpectrum[i]) || math.IsInf(detailSpectrum[i], 0) {
  206. detailSpectrum[i] = -200
  207. }
  208. }
  209. }
  210. now := time.Now()
  211. finished, detected := rt.det.Process(now, survSpectrum, rt.cfg.CenterHz)
  212. return &spectrumArtifacts{
  213. allIQ: allIQ,
  214. surveillanceIQ: survIQ,
  215. detailIQ: detailIQ,
  216. surveillanceSpectrum: survSpectrum,
  217. detailSpectrum: detailSpectrum,
  218. finished: finished,
  219. detected: detected,
  220. thresholds: rt.det.LastThresholds(),
  221. noiseFloor: rt.det.LastNoiseFloor(),
  222. now: now,
  223. }, nil
  224. }
  225. func (rt *dspRuntime) buildSurveillanceResult(art *spectrumArtifacts) pipeline.SurveillanceResult {
  226. if art == nil {
  227. return pipeline.SurveillanceResult{}
  228. }
  229. policy := pipeline.PolicyFromConfig(rt.cfg)
  230. candidates := pipeline.CandidatesFromSignals(art.detected, "surveillance-detector")
  231. scheduled := pipeline.ScheduleCandidates(candidates, policy)
  232. level := pipeline.AnalysisLevel{
  233. Name: "surveillance",
  234. Role: "surveillance",
  235. Truth: "surveillance",
  236. SampleRate: rt.cfg.SampleRate,
  237. FFTSize: rt.cfg.Surveillance.AnalysisFFTSize,
  238. CenterHz: rt.cfg.CenterHz,
  239. SpanHz: spanForPolicy(policy, float64(rt.cfg.SampleRate)),
  240. Source: "baseband",
  241. }
  242. lowRate := rt.cfg.SampleRate / 2
  243. lowFFT := rt.cfg.Surveillance.AnalysisFFTSize / 2
  244. if lowRate < 200000 {
  245. lowRate = rt.cfg.SampleRate
  246. }
  247. if lowFFT < 256 {
  248. lowFFT = rt.cfg.Surveillance.AnalysisFFTSize
  249. }
  250. lowLevel := pipeline.AnalysisLevel{
  251. Name: "surveillance-lowres",
  252. Role: "surveillance-lowres",
  253. Truth: "surveillance",
  254. SampleRate: lowRate,
  255. FFTSize: lowFFT,
  256. CenterHz: rt.cfg.CenterHz,
  257. SpanHz: spanForPolicy(policy, float64(lowRate)),
  258. Source: "downsampled",
  259. }
  260. displayLevel := pipeline.AnalysisLevel{
  261. Name: "presentation",
  262. Role: "presentation",
  263. Truth: "presentation",
  264. SampleRate: rt.cfg.SampleRate,
  265. FFTSize: rt.cfg.Surveillance.DisplayBins,
  266. CenterHz: rt.cfg.CenterHz,
  267. SpanHz: spanForPolicy(policy, float64(rt.cfg.SampleRate)),
  268. Source: "display",
  269. }
  270. levels, context := surveillanceLevels(policy, level, lowLevel, displayLevel)
  271. return pipeline.SurveillanceResult{
  272. Level: level,
  273. Levels: levels,
  274. DisplayLevel: displayLevel,
  275. Context: context,
  276. Candidates: candidates,
  277. Scheduled: scheduled,
  278. Finished: art.finished,
  279. Signals: art.detected,
  280. NoiseFloor: art.noiseFloor,
  281. Thresholds: art.thresholds,
  282. }
  283. }
  284. func (rt *dspRuntime) buildRefinementInput(surv pipeline.SurveillanceResult) pipeline.RefinementInput {
  285. policy := pipeline.PolicyFromConfig(rt.cfg)
  286. plan := pipeline.BuildRefinementPlan(surv.Candidates, policy)
  287. scheduled := append([]pipeline.ScheduledCandidate(nil), surv.Scheduled...)
  288. if len(scheduled) == 0 && len(plan.Selected) > 0 {
  289. scheduled = append([]pipeline.ScheduledCandidate(nil), plan.Selected...)
  290. }
  291. workItems := make([]pipeline.RefinementWorkItem, 0, len(plan.WorkItems))
  292. if len(plan.WorkItems) > 0 {
  293. workItems = append(workItems, plan.WorkItems...)
  294. }
  295. workIndex := map[int64]int{}
  296. for i := range workItems {
  297. if workItems[i].Candidate.ID == 0 {
  298. continue
  299. }
  300. workIndex[workItems[i].Candidate.ID] = i
  301. }
  302. windows := make([]pipeline.RefinementWindow, 0, len(scheduled))
  303. for _, sc := range scheduled {
  304. window := pipeline.RefinementWindowForCandidate(policy, sc.Candidate)
  305. windows = append(windows, window)
  306. if idx, ok := workIndex[sc.Candidate.ID]; ok {
  307. workItems[idx].Window = window
  308. }
  309. }
  310. levelSpan := spanForPolicy(policy, float64(rt.cfg.SampleRate))
  311. if _, maxSpan, ok := windowSpanBounds(windows); ok {
  312. levelSpan = maxSpan
  313. }
  314. level := pipeline.AnalysisLevel{
  315. Name: "refinement",
  316. Role: "refinement",
  317. Truth: "refinement",
  318. SampleRate: rt.cfg.SampleRate,
  319. FFTSize: rt.cfg.FFTSize,
  320. CenterHz: rt.cfg.CenterHz,
  321. SpanHz: levelSpan,
  322. Source: "refinement-window",
  323. }
  324. detailLevel := pipeline.AnalysisLevel{
  325. Name: "detail",
  326. Role: "detail",
  327. Truth: "refinement",
  328. SampleRate: rt.cfg.SampleRate,
  329. FFTSize: rt.cfg.FFTSize,
  330. CenterHz: rt.cfg.CenterHz,
  331. SpanHz: levelSpan,
  332. Source: "detail-spectrum",
  333. }
  334. input := pipeline.RefinementInput{
  335. Level: level,
  336. Detail: detailLevel,
  337. Context: surv.Context,
  338. Request: pipeline.RefinementRequest{Strategy: plan.Strategy, Reason: "surveillance-plan", SpanHintHz: levelSpan},
  339. Budgets: pipeline.BudgetModelFromPolicy(policy),
  340. Candidates: append([]pipeline.Candidate(nil), surv.Candidates...),
  341. Scheduled: scheduled,
  342. WorkItems: workItems,
  343. Plan: plan,
  344. Windows: windows,
  345. SampleRate: rt.cfg.SampleRate,
  346. FFTSize: rt.cfg.FFTSize,
  347. CenterHz: rt.cfg.CenterHz,
  348. Source: "surveillance-detector",
  349. }
  350. input.Context.Refinement = level
  351. input.Context.Detail = detailLevel
  352. if !policy.RefinementEnabled {
  353. input.Scheduled = nil
  354. input.WorkItems = nil
  355. input.Request.Reason = pipeline.RefinementReasonDisabled
  356. }
  357. return input
  358. }
  359. func (rt *dspRuntime) runRefinement(art *spectrumArtifacts, surv pipeline.SurveillanceResult, extractMgr *extractionManager, rec *recorder.Manager) pipeline.RefinementStep {
  360. input := rt.buildRefinementInput(surv)
  361. result := rt.refineSignals(art, input, extractMgr, rec)
  362. return pipeline.RefinementStep{Input: input, Result: result}
  363. }
  364. func (rt *dspRuntime) refineSignals(art *spectrumArtifacts, input pipeline.RefinementInput, extractMgr *extractionManager, rec *recorder.Manager) pipeline.RefinementResult {
  365. if art == nil || len(art.detailIQ) == 0 || len(input.Scheduled) == 0 {
  366. return pipeline.RefinementResult{}
  367. }
  368. policy := pipeline.PolicyFromConfig(rt.cfg)
  369. selectedCandidates := make([]pipeline.Candidate, 0, len(input.Scheduled))
  370. selectedSignals := make([]detector.Signal, 0, len(input.Scheduled))
  371. for _, sc := range input.Scheduled {
  372. selectedCandidates = append(selectedCandidates, sc.Candidate)
  373. selectedSignals = append(selectedSignals, detector.Signal{
  374. ID: sc.Candidate.ID,
  375. FirstBin: sc.Candidate.FirstBin,
  376. LastBin: sc.Candidate.LastBin,
  377. CenterHz: sc.Candidate.CenterHz,
  378. BWHz: sc.Candidate.BandwidthHz,
  379. PeakDb: sc.Candidate.PeakDb,
  380. SNRDb: sc.Candidate.SNRDb,
  381. NoiseDb: sc.Candidate.NoiseDb,
  382. })
  383. }
  384. sampleRate := input.SampleRate
  385. fftSize := input.FFTSize
  386. centerHz := input.CenterHz
  387. if sampleRate <= 0 {
  388. sampleRate = rt.cfg.SampleRate
  389. }
  390. if fftSize <= 0 {
  391. fftSize = rt.cfg.FFTSize
  392. }
  393. if centerHz == 0 {
  394. centerHz = rt.cfg.CenterHz
  395. }
  396. snips, snipRates := extractSignalIQBatch(extractMgr, art.detailIQ, sampleRate, centerHz, selectedSignals)
  397. refined := pipeline.RefineCandidates(selectedCandidates, input.Windows, art.detailSpectrum, sampleRate, fftSize, snips, snipRates, classifier.ClassifierMode(rt.cfg.ClassifierMode))
  398. signals := make([]detector.Signal, 0, len(refined))
  399. decisions := make([]pipeline.SignalDecision, 0, len(refined))
  400. for i, ref := range refined {
  401. sig := ref.Signal
  402. signals = append(signals, sig)
  403. cls := sig.Class
  404. snipRate := ref.SnippetRate
  405. decision := pipeline.DecideSignalAction(policy, ref.Candidate, cls)
  406. decisions = append(decisions, decision)
  407. if cls != nil {
  408. pll := classifier.PLLResult{}
  409. if i < len(snips) && snips[i] != nil && len(snips[i]) > 256 {
  410. pll = classifier.EstimateExactFrequency(snips[i], snipRate, signals[i].CenterHz, cls.ModType)
  411. cls.PLL = &pll
  412. signals[i].PLL = &pll
  413. if cls.ModType == classifier.ClassWFM && pll.Stereo {
  414. cls.ModType = classifier.ClassWFMStereo
  415. }
  416. }
  417. if (cls.ModType == classifier.ClassWFM || cls.ModType == classifier.ClassWFMStereo) && rec != nil {
  418. rt.updateRDS(art.now, rec, &signals[i], cls)
  419. }
  420. }
  421. }
  422. budget := pipeline.BudgetModelFromPolicy(policy)
  423. queueStats := rt.decisionQueues.Apply(decisions, budget, art.now, policy)
  424. rt.queueStats = queueStats
  425. summary := summarizeDecisions(decisions)
  426. if rec != nil {
  427. if summary.RecordEnabled > 0 {
  428. rt.cfg.Recorder.Enabled = true
  429. }
  430. if summary.DecodeEnabled > 0 {
  431. rt.cfg.Recorder.AutoDecode = true
  432. }
  433. }
  434. rt.det.UpdateClasses(signals)
  435. return pipeline.RefinementResult{Level: input.Level, Signals: signals, Decisions: decisions, Candidates: selectedCandidates}
  436. }
  437. func (rt *dspRuntime) updateRDS(now time.Time, rec *recorder.Manager, sig *detector.Signal, cls *classifier.Classification) {
  438. if sig == nil || cls == nil {
  439. return
  440. }
  441. keyHz := sig.CenterHz
  442. if sig.PLL != nil && sig.PLL.ExactHz != 0 {
  443. keyHz = sig.PLL.ExactHz
  444. }
  445. key := int64(math.Round(keyHz / 25000.0))
  446. st := rt.rdsMap[key]
  447. if st == nil {
  448. st = &rdsState{}
  449. rt.rdsMap[key] = st
  450. }
  451. if now.Sub(st.lastDecode) >= 4*time.Second && atomic.LoadInt32(&st.busy) == 0 {
  452. st.lastDecode = now
  453. atomic.StoreInt32(&st.busy, 1)
  454. go func(st *rdsState, sigHz float64) {
  455. defer atomic.StoreInt32(&st.busy, 0)
  456. ringIQ, ringSR, ringCenter := rec.SliceRecent(4.0)
  457. if len(ringIQ) < ringSR || ringSR <= 0 {
  458. return
  459. }
  460. offset := sigHz - ringCenter
  461. shifted := dsp.FreqShift(ringIQ, ringSR, offset)
  462. decim1 := ringSR / 1000000
  463. if decim1 < 1 {
  464. decim1 = 1
  465. }
  466. lp1 := dsp.LowpassFIR(float64(ringSR/decim1)/2.0*0.8, ringSR, 51)
  467. f1 := dsp.ApplyFIR(shifted, lp1)
  468. d1 := dsp.Decimate(f1, decim1)
  469. rate1 := ringSR / decim1
  470. decim2 := rate1 / 250000
  471. if decim2 < 1 {
  472. decim2 = 1
  473. }
  474. lp2 := dsp.LowpassFIR(float64(rate1/decim2)/2.0*0.8, rate1, 101)
  475. f2 := dsp.ApplyFIR(d1, lp2)
  476. decimated := dsp.Decimate(f2, decim2)
  477. actualRate := rate1 / decim2
  478. rdsBase := demod.RDSBasebandComplex(decimated, actualRate)
  479. if len(rdsBase.Samples) == 0 {
  480. return
  481. }
  482. st.mu.Lock()
  483. result := st.dec.Decode(rdsBase.Samples, rdsBase.SampleRate)
  484. if result.PS != "" {
  485. st.result = result
  486. }
  487. st.mu.Unlock()
  488. }(st, sig.CenterHz)
  489. }
  490. st.mu.Lock()
  491. ps := st.result.PS
  492. st.mu.Unlock()
  493. if ps != "" && sig.PLL != nil {
  494. sig.PLL.RDSStation = strings.TrimSpace(ps)
  495. cls.PLL = sig.PLL
  496. }
  497. }
  498. func (rt *dspRuntime) maintenance(displaySignals []detector.Signal, rec *recorder.Manager) {
  499. if len(rt.rdsMap) > 0 {
  500. activeIDs := make(map[int64]bool, len(displaySignals))
  501. for _, s := range displaySignals {
  502. keyHz := s.CenterHz
  503. if s.PLL != nil && s.PLL.ExactHz != 0 {
  504. keyHz = s.PLL.ExactHz
  505. }
  506. activeIDs[int64(math.Round(keyHz/25000.0))] = true
  507. }
  508. for id := range rt.rdsMap {
  509. if !activeIDs[id] {
  510. delete(rt.rdsMap, id)
  511. }
  512. }
  513. }
  514. if len(rt.streamPhaseState) > 0 {
  515. sigIDs := make(map[int64]bool, len(displaySignals))
  516. for _, s := range displaySignals {
  517. sigIDs[s.ID] = true
  518. }
  519. for id := range rt.streamPhaseState {
  520. if !sigIDs[id] {
  521. delete(rt.streamPhaseState, id)
  522. }
  523. }
  524. }
  525. if rec != nil && len(displaySignals) > 0 {
  526. aqCfg := extractionConfig{firTaps: rt.cfg.Recorder.ExtractionTaps, bwMult: rt.cfg.Recorder.ExtractionBwMult}
  527. _ = aqCfg
  528. }
  529. }
  530. func spanForPolicy(policy pipeline.Policy, fallback float64) float64 {
  531. if policy.MonitorSpanHz > 0 {
  532. return policy.MonitorSpanHz
  533. }
  534. if policy.MonitorStartHz != 0 && policy.MonitorEndHz != 0 && policy.MonitorEndHz > policy.MonitorStartHz {
  535. return policy.MonitorEndHz - policy.MonitorStartHz
  536. }
  537. return fallback
  538. }
  539. func windowSpanBounds(windows []pipeline.RefinementWindow) (float64, float64, bool) {
  540. minSpan := 0.0
  541. maxSpan := 0.0
  542. ok := false
  543. for _, w := range windows {
  544. if w.SpanHz <= 0 {
  545. continue
  546. }
  547. if !ok || w.SpanHz < minSpan {
  548. minSpan = w.SpanHz
  549. }
  550. if !ok || w.SpanHz > maxSpan {
  551. maxSpan = w.SpanHz
  552. }
  553. ok = true
  554. }
  555. return minSpan, maxSpan, ok
  556. }
  557. func surveillanceLevels(policy pipeline.Policy, primary pipeline.AnalysisLevel, secondary pipeline.AnalysisLevel, presentation pipeline.AnalysisLevel) ([]pipeline.AnalysisLevel, pipeline.AnalysisContext) {
  558. levels := []pipeline.AnalysisLevel{primary}
  559. context := pipeline.AnalysisContext{
  560. Surveillance: primary,
  561. Presentation: presentation,
  562. }
  563. strategy := strings.ToLower(strings.TrimSpace(policy.SurveillanceStrategy))
  564. switch strategy {
  565. case "multi-res", "multi-resolution", "multi", "multi_res":
  566. if secondary.SampleRate != primary.SampleRate || secondary.FFTSize != primary.FFTSize {
  567. levels = append(levels, secondary)
  568. context.Derived = append(context.Derived, secondary)
  569. }
  570. }
  571. return levels, context
  572. }
  573. func sameIQBuffer(a []complex64, b []complex64) bool {
  574. if len(a) != len(b) {
  575. return false
  576. }
  577. if len(a) == 0 {
  578. return true
  579. }
  580. return &a[0] == &b[0]
  581. }