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.

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