Wideband autonomous SDR analysis engine forked from sdr-visual-suite
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

117 行
3.1KB

  1. package recorder
  2. import (
  3. "bytes"
  4. "errors"
  5. "log"
  6. "math"
  7. "time"
  8. "sdr-wideband-suite/internal/demod"
  9. )
  10. // DemodLive demodulates a recent window and returns WAV bytes.
  11. func (m *Manager) DemodLive(centerHz float64, bw float64, mode string, seconds int) ([]byte, int, error) {
  12. if m == nil || m.ring == nil {
  13. return nil, 0, errors.New("recorder not ready")
  14. }
  15. if seconds <= 0 {
  16. seconds = 2
  17. }
  18. end := time.Now()
  19. start := end.Add(-time.Duration(seconds) * time.Second)
  20. segment := m.ring.Slice(start, end)
  21. if len(segment) == 0 {
  22. return nil, 0, errors.New("no iq in ring")
  23. }
  24. actualDuration := float64(len(segment)) / float64(m.sampleRate)
  25. if actualDuration < float64(seconds)*0.8 {
  26. log.Printf("DEMOD WARNING: requested %ds but ring only has %.2fs of IQ data (ring may be underfilled due to sample drops)", seconds, actualDuration)
  27. }
  28. name := mode
  29. if name == "" {
  30. name = "NFM"
  31. }
  32. switch name {
  33. case "AM", "NFM", "WFM", "WFM_STEREO", "USB", "LSB", "CW":
  34. default:
  35. name = "NFM"
  36. }
  37. d := demod.Get(name)
  38. if d == nil {
  39. return nil, 0, errors.New("demodulator not found")
  40. }
  41. offset := centerHz - m.centerHz
  42. if bw <= 0 {
  43. bw = 12000
  44. }
  45. var audio []float32
  46. var inputRate int
  47. gpu := m.gpuEngine()
  48. if gpu != nil {
  49. gpuMode, useGPU := gpuModeFor(name)
  50. if useGPU {
  51. if gpuAudio, gpuRate, ok := tryGPUAudio(gpu, name, segment, offset, bw, gpuMode); ok {
  52. audio = gpuAudio
  53. inputRate = gpuRate
  54. }
  55. }
  56. }
  57. if audio == nil {
  58. if name == "WFM_STEREO" {
  59. log.Printf("gpudemod: WFM_STEREO live path using CPU stereo/RDS post-process")
  60. } else {
  61. log.Printf("gpudemod: CPU live demod fallback used (%s)", name)
  62. }
  63. audio, inputRate = demodAudioCPU(d, segment, m.sampleRate, offset, bw)
  64. }
  65. log.Printf("DEMOD DIAG: mode=%s iqSamples=%d sampleRate=%d audioSamples=%d inputRate=%d bw=%.0f offset=%.0f",
  66. name, len(segment), m.sampleRate, len(audio), inputRate, bw, offset)
  67. // Resample to 48 kHz for browser-compatible playback.
  68. const browserRate = 48000
  69. channels := d.Channels()
  70. if inputRate > browserRate && len(audio) > 0 {
  71. decim := int(math.Round(float64(inputRate) / float64(browserRate)))
  72. if decim < 1 {
  73. decim = 1
  74. }
  75. if channels > 1 {
  76. nFrames := len(audio) / channels
  77. outFrames := nFrames / decim
  78. if outFrames < 1 {
  79. outFrames = 1
  80. }
  81. resampled := make([]float32, outFrames*channels)
  82. for i := 0; i < outFrames; i++ {
  83. srcIdx := i * decim * channels
  84. for ch := 0; ch < channels; ch++ {
  85. if srcIdx+ch < len(audio) {
  86. resampled[i*channels+ch] = audio[srcIdx+ch]
  87. }
  88. }
  89. }
  90. audio = resampled
  91. } else {
  92. resampled := make([]float32, 0, len(audio)/decim+1)
  93. for i := 0; i < len(audio); i += decim {
  94. resampled = append(resampled, audio[i])
  95. }
  96. audio = resampled
  97. }
  98. inputRate = inputRate / decim
  99. }
  100. log.Printf("DEMOD DIAG: after resample audioSamples=%d finalRate=%d duration=%.2fs",
  101. len(audio), inputRate, float64(len(audio))/float64(inputRate)/float64(channels))
  102. // Use actual sample rate for WAV — don't lie about rate
  103. buf := &bytes.Buffer{}
  104. if err := writeWAVTo(buf, audio, inputRate, channels); err != nil {
  105. return nil, 0, err
  106. }
  107. return buf.Bytes(), inputRate, nil
  108. }