Wideband autonomous SDR analysis engine forked from sdr-visual-suite
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

138 line
4.0KB

  1. package main
  2. import (
  3. "math"
  4. "sdr-wideband-suite/internal/demod/gpudemod"
  5. "sdr-wideband-suite/internal/detector"
  6. "sdr-wideband-suite/internal/telemetry"
  7. )
  8. const useStreamingOraclePath = false // temporarily disable oracle during bring-up to isolate production-path runtime behavior
  9. const useStreamingProductionPath = true // route top-level extraction through the new production path during bring-up/validation
  10. var streamingOracleRunner *gpudemod.CPUOracleRunner
  11. func buildStreamingJobs(sampleRate int, centerHz float64, signals []detector.Signal, aqCfg extractionConfig) ([]gpudemod.StreamingExtractJob, error) {
  12. jobs := make([]gpudemod.StreamingExtractJob, len(signals))
  13. bwMult := aqCfg.bwMult
  14. if bwMult <= 0 {
  15. bwMult = 1.0
  16. }
  17. firTaps := aqCfg.firTaps
  18. if firTaps <= 0 {
  19. firTaps = 101
  20. }
  21. for i, sig := range signals {
  22. bw := sig.BWHz * bwMult
  23. sigMHz := sig.CenterHz / 1e6
  24. isWFM := (sigMHz >= 87.5 && sigMHz <= 108.0) ||
  25. (sig.Class != nil && (sig.Class.ModType == "WFM" || sig.Class.ModType == "WFM_STEREO"))
  26. var outRate int
  27. if isWFM {
  28. outRate = wfmStreamOutRate
  29. if bw < wfmStreamMinBW {
  30. bw = wfmStreamMinBW
  31. }
  32. } else {
  33. // Non-WFM target: must be an exact integer divisor of sampleRate.
  34. // The old hardcoded 200000 fails for common SDR rates (e.g. 4096000/200000=20.48).
  35. // Find the nearest valid rate >= 128000 (enough for NFM/AM/SSB).
  36. outRate = nearestExactDecimationRate(sampleRate, 200000, 128000)
  37. if bw < 20000 {
  38. bw = 20000
  39. }
  40. }
  41. if _, err := gpudemod.ExactIntegerDecimation(sampleRate, outRate); err != nil {
  42. return nil, err
  43. }
  44. offset := sig.CenterHz - centerHz
  45. jobs[i] = gpudemod.StreamingExtractJob{
  46. SignalID: sig.ID,
  47. OffsetHz: offset,
  48. Bandwidth: bw,
  49. OutRate: outRate,
  50. NumTaps: firTaps,
  51. ConfigHash: gpudemod.StreamingConfigHash(sig.ID, offset, bw, outRate, firTaps, sampleRate),
  52. }
  53. }
  54. return jobs, nil
  55. }
  56. func resetStreamingOracleRunner() {
  57. if streamingOracleRunner != nil {
  58. streamingOracleRunner.ResetAllStates()
  59. }
  60. }
  61. func extractForStreamingOracle(
  62. allIQ []complex64,
  63. sampleRate int,
  64. centerHz float64,
  65. signals []detector.Signal,
  66. aqCfg extractionConfig,
  67. coll *telemetry.Collector,
  68. ) ([][]complex64, []int, error) {
  69. out := make([][]complex64, len(signals))
  70. rates := make([]int, len(signals))
  71. jobs, err := buildStreamingJobs(sampleRate, centerHz, signals, aqCfg)
  72. if err != nil {
  73. return nil, nil, err
  74. }
  75. if streamingOracleRunner == nil || streamingOracleRunner.SampleRate != sampleRate {
  76. streamingOracleRunner = gpudemod.NewCPUOracleRunner(sampleRate)
  77. }
  78. results, err := streamingOracleRunner.StreamingExtract(allIQ, jobs)
  79. if err != nil {
  80. return nil, nil, err
  81. }
  82. for i, res := range results {
  83. out[i] = res.IQ
  84. rates[i] = res.Rate
  85. observeStreamingResult(coll, "streaming.oracle", res)
  86. }
  87. return out, rates, nil
  88. }
  89. func phaseIncForOffset(sampleRate int, offsetHz float64) float64 {
  90. return -2.0 * math.Pi * offsetHz / float64(sampleRate)
  91. }
  92. // nearestExactDecimationRate finds the output rate closest to targetRate
  93. // (but not below minRate) that is an exact integer divisor of sampleRate.
  94. // This avoids the ExactIntegerDecimation check failing for rates like
  95. // 4096000/200000=20.48 which silently killed the entire streaming batch.
  96. func nearestExactDecimationRate(sampleRate int, targetRate int, minRate int) int {
  97. if sampleRate <= 0 || targetRate <= 0 {
  98. return targetRate
  99. }
  100. if sampleRate%targetRate == 0 {
  101. return targetRate // already exact
  102. }
  103. // Try decimation factors near the target
  104. targetDecim := sampleRate / targetRate // floor
  105. bestRate := 0
  106. bestDist := sampleRate // impossibly large
  107. for d := max(1, targetDecim-2); d <= targetDecim+2; d++ {
  108. rate := sampleRate / d
  109. if rate < minRate {
  110. continue
  111. }
  112. if sampleRate%rate != 0 {
  113. continue // not exact (shouldn't happen since rate = sampleRate/d, but guard)
  114. }
  115. dist := targetRate - rate
  116. if dist < 0 {
  117. dist = -dist
  118. }
  119. if dist < bestDist {
  120. bestDist = dist
  121. bestRate = rate
  122. }
  123. }
  124. if bestRate > 0 {
  125. return bestRate
  126. }
  127. return targetRate // fallback — will fail ExactIntegerDecimation and surface the error
  128. }