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.

142 lines
3.7KB

  1. package recorder
  2. import (
  3. "errors"
  4. "log"
  5. "math"
  6. "path/filepath"
  7. "sdr-visual-suite/internal/classifier"
  8. "sdr-visual-suite/internal/demod"
  9. "sdr-visual-suite/internal/demod/gpudemod"
  10. "sdr-visual-suite/internal/detector"
  11. "sdr-visual-suite/internal/dsp"
  12. )
  13. func (m *Manager) demodAndWrite(dir string, ev detector.Event, iq []complex64, files map[string]any) error {
  14. if ev.Class == nil {
  15. return nil
  16. }
  17. name := mapClassToDemod(ev.Class.ModType)
  18. if name == "" {
  19. return nil
  20. }
  21. d := demod.Get(name)
  22. if d == nil {
  23. return errors.New("demodulator not found")
  24. }
  25. bw := ev.Bandwidth
  26. offset := ev.CenterHz - m.centerHz
  27. var audio []float32
  28. var inputRate int
  29. gpu := m.gpuEngine()
  30. if gpu != nil {
  31. var gpuMode gpudemod.DemodType
  32. var useGPU bool
  33. switch name {
  34. case "NFM":
  35. gpuMode, useGPU = gpudemod.DemodNFM, true
  36. case "WFM":
  37. gpuMode, useGPU = gpudemod.DemodWFM, true
  38. case "AM":
  39. gpuMode, useGPU = gpudemod.DemodAM, true
  40. case "USB":
  41. gpuMode, useGPU = gpudemod.DemodUSB, true
  42. case "LSB":
  43. gpuMode, useGPU = gpudemod.DemodLSB, true
  44. case "CW":
  45. gpuMode, useGPU = gpudemod.DemodCW, true
  46. }
  47. if useGPU {
  48. if gpuAudio, gpuRate, err := gpu.DemodFused(iq, offset, bw, gpuMode); err == nil {
  49. audio = gpuAudio
  50. inputRate = gpuRate
  51. if gpu.LastDemodUsedGPU() {
  52. log.Printf("gpudemod: fused GPU demod used for event %d (%s)", ev.ID, name)
  53. }
  54. } else {
  55. log.Printf("gpudemod: fused GPU demod failed for event %d (%s): %v", ev.ID, name, err)
  56. if gpuAudio, gpuRate, err := gpu.Demod(iq, offset, bw, gpuMode); err == nil {
  57. audio = gpuAudio
  58. inputRate = gpuRate
  59. if gpu.LastDemodUsedGPU() {
  60. log.Printf("gpudemod: legacy GPU demod used for event %d (%s)", ev.ID, name)
  61. }
  62. } else {
  63. log.Printf("gpudemod: legacy GPU demod failed for event %d (%s): %v", ev.ID, name, err)
  64. }
  65. }
  66. }
  67. }
  68. if audio == nil {
  69. if name == "WFM_STEREO" {
  70. log.Printf("gpudemod: WFM_STEREO using CPU stereo/RDS post-process for event %d", ev.ID)
  71. } else {
  72. log.Printf("gpudemod: CPU demod fallback used for event %d (%s)", ev.ID, name)
  73. }
  74. shifted := dsp.FreqShift(iq, m.sampleRate, offset)
  75. cutoff := bw / 2
  76. if cutoff < 200 {
  77. cutoff = 200
  78. }
  79. taps := dsp.LowpassFIR(cutoff, m.sampleRate, 101)
  80. filtered := dsp.ApplyFIR(shifted, taps)
  81. decim := int(math.Round(float64(m.sampleRate) / float64(d.OutputSampleRate())))
  82. if decim < 1 {
  83. decim = 1
  84. }
  85. dec := dsp.Decimate(filtered, decim)
  86. inputRate = m.sampleRate / decim
  87. audio = d.Demod(dec, inputRate)
  88. }
  89. wav := filepath.Join(dir, "audio.wav")
  90. if err := writeWAV(wav, audio, inputRate, d.Channels()); err != nil {
  91. return err
  92. }
  93. files["audio"] = "audio.wav"
  94. files["audio_sample_rate"] = inputRate
  95. files["audio_channels"] = d.Channels()
  96. files["audio_demod"] = name
  97. if name == "WFM_STEREO" {
  98. if rds := demod.RDSBaseband(iq, m.sampleRate); len(rds) > 0 {
  99. rdsPath := filepath.Join(dir, "rds.wav")
  100. _ = writeWAV(rdsPath, rds, 2400, 1)
  101. files["rds_baseband"] = "rds.wav"
  102. files["rds_sample_rate"] = 2400
  103. dec := rdsdecoder{}
  104. res := dec.Decode(rds, 2400)
  105. if res.PI != 0 {
  106. files["rds_pi"] = res.PI
  107. }
  108. if res.PS != "" {
  109. files["rds_ps"] = res.PS
  110. }
  111. if res.RT != "" {
  112. files["rds_rt"] = res.RT
  113. }
  114. }
  115. }
  116. return nil
  117. }
  118. func mapClassToDemod(c classifier.SignalClass) string {
  119. switch c {
  120. case classifier.ClassAM:
  121. return "AM"
  122. case classifier.ClassNFM:
  123. return "NFM"
  124. case classifier.ClassWFM:
  125. return "WFM"
  126. case classifier.ClassSSBUSB:
  127. return "USB"
  128. case classifier.ClassSSBLSB:
  129. return "LSB"
  130. case classifier.ClassCW:
  131. return "CW"
  132. case classifier.ClassFT8, classifier.ClassWSPR, classifier.ClassFSK, classifier.ClassPSK:
  133. return "USB"
  134. default:
  135. return ""
  136. }
  137. }