ソースを参照

feat: add phase 1 CUDA demod scaffold

master
Jan Svabenik 2日前
コミット
ddf2c1d7d5
5個のファイルの変更190行の追加12行の削除
  1. +104
    -0
      internal/demod/gpudemod/gpudemod.go
  2. +35
    -0
      internal/demod/gpudemod/gpudemod_stub.go
  3. +9
    -0
      internal/demod/gpudemod/gpudemod_test.go
  4. +23
    -12
      internal/recorder/demod.go
  5. +19
    -0
      internal/recorder/recorder.go

+ 104
- 0
internal/demod/gpudemod/gpudemod.go ファイルの表示

@@ -0,0 +1,104 @@
//go:build cufft

package gpudemod

/*
#cgo windows LDFLAGS: -lcufft64_12 -lcudart64_13
#include <cuda_runtime.h>
#include <cufft.h>
*/
import "C"

import (
"errors"
"math"

"sdr-visual-suite/internal/demod"
"sdr-visual-suite/internal/dsp"
)

type DemodType int

const (
DemodNFM DemodType = iota
DemodWFM
DemodAM
DemodUSB
DemodLSB
DemodCW
)

type Engine struct {
maxSamples int
sampleRate int
phase float64
bfoPhase float64
firTaps []float32
}

func Available() bool { return true }

func New(maxSamples int, sampleRate int) (*Engine, error) {
if maxSamples <= 0 {
return nil, errors.New("invalid maxSamples")
}
if sampleRate <= 0 {
return nil, errors.New("invalid sampleRate")
}
return &Engine{maxSamples: maxSamples, sampleRate: sampleRate}, nil
}

func (e *Engine) SetFIR(taps []float32) {
if len(taps) == 0 {
e.firTaps = nil
return
}
e.firTaps = append(e.firTaps[:0], taps...)
}

func (e *Engine) Demod(iq []complex64, offsetHz float64, bw float64, mode DemodType) ([]float32, int, error) {
if e == nil {
return nil, 0, errors.New("nil CUDA demod engine")
}
if len(iq) == 0 {
return nil, 0, nil
}
if len(iq) > e.maxSamples {
return nil, 0, errors.New("sample count exceeds engine capacity")
}
if mode != DemodNFM {
return nil, 0, errors.New("CUDA demod phase 1 currently supports NFM only")
}

// Phase 1 conservative scaffold:
// Keep build/tag/CUDA-specific package boundaries now, but use the existing
// CPU DSP implementation as the processing backend until the actual CUDA
// kernels are introduced in later phases.
shifted := dsp.FreqShift(iq, e.sampleRate, offsetHz)
cutoff := bw / 2
if cutoff < 200 {
cutoff = 200
}
taps := e.firTaps
if len(taps) == 0 {
base := dsp.LowpassFIR(cutoff, e.sampleRate, 101)
taps = append(make([]float32, 0, len(base)), base...)
}
filtered := dsp.ApplyFIR(shifted, taps)
outRate := demod.NFM{}.OutputSampleRate()
decim := int(math.Round(float64(e.sampleRate) / float64(outRate)))
if decim < 1 {
decim = 1
}
dec := dsp.Decimate(filtered, decim)
inputRate := e.sampleRate / decim
audio := demod.NFM{}.Demod(dec, inputRate)
return audio, inputRate, nil
}

func (e *Engine) Close() {
if e == nil {
return
}
e.firTaps = nil
}

+ 35
- 0
internal/demod/gpudemod/gpudemod_stub.go ファイルの表示

@@ -0,0 +1,35 @@
//go:build !cufft

package gpudemod

import "errors"

type DemodType int

const (
DemodNFM DemodType = iota
DemodWFM
DemodAM
DemodUSB
DemodLSB
DemodCW
)

type Engine struct {
maxSamples int
sampleRate int
}

func Available() bool { return false }

func New(maxSamples int, sampleRate int) (*Engine, error) {
return nil, errors.New("CUDA demod not available: cufft build tag not enabled")
}

func (e *Engine) SetFIR(taps []float32) {}

func (e *Engine) Demod(iq []complex64, offsetHz float64, bw float64, mode DemodType) ([]float32, int, error) {
return nil, 0, errors.New("CUDA demod not available: cufft build tag not enabled")
}

func (e *Engine) Close() {}

+ 9
- 0
internal/demod/gpudemod/gpudemod_test.go ファイルの表示

@@ -0,0 +1,9 @@
package gpudemod

import "testing"

func TestStubAvailableFalseWithoutCufft(t *testing.T) {
if Available() {
t.Fatal("expected CUDA demod to be unavailable without cufft build tag")
}
}

+ 23
- 12
internal/recorder/demod.go ファイルの表示

@@ -7,6 +7,7 @@ import (

"sdr-visual-suite/internal/classifier"
"sdr-visual-suite/internal/demod"
"sdr-visual-suite/internal/demod/gpudemod"
"sdr-visual-suite/internal/detector"
"sdr-visual-suite/internal/dsp"
)
@@ -26,20 +27,30 @@ func (m *Manager) demodAndWrite(dir string, ev detector.Event, iq []complex64, f
// band-extract around signal
bw := ev.Bandwidth
offset := ev.CenterHz - m.centerHz
shifted := dsp.FreqShift(iq, m.sampleRate, offset)
cutoff := bw / 2
if cutoff < 200 {
cutoff = 200
var audio []float32
var inputRate int
if m.gpuDemod != nil && name == "NFM" {
if gpuAudio, gpuRate, err := m.gpuDemod.Demod(iq, offset, bw, gpudemod.DemodNFM); err == nil {
audio = gpuAudio
inputRate = gpuRate
}
}
taps := dsp.LowpassFIR(cutoff, m.sampleRate, 101)
filtered := dsp.ApplyFIR(shifted, taps)
decim := int(math.Round(float64(m.sampleRate) / float64(d.OutputSampleRate())))
if decim < 1 {
decim = 1
if audio == nil {
shifted := dsp.FreqShift(iq, m.sampleRate, offset)
cutoff := bw / 2
if cutoff < 200 {
cutoff = 200
}
taps := dsp.LowpassFIR(cutoff, m.sampleRate, 101)
filtered := dsp.ApplyFIR(shifted, taps)
decim := int(math.Round(float64(m.sampleRate) / float64(d.OutputSampleRate())))
if decim < 1 {
decim = 1
}
dec := dsp.Decimate(filtered, decim)
inputRate = m.sampleRate / decim
audio = d.Demod(dec, inputRate)
}
dec := dsp.Decimate(filtered, decim)
inputRate := m.sampleRate / decim
audio := d.Demod(dec, inputRate)
wav := filepath.Join(dir, "audio.wav")
if err := writeWAV(wav, audio, inputRate, d.Channels()); err != nil {
return err


+ 19
- 0
internal/recorder/recorder.go ファイルの表示

@@ -8,6 +8,7 @@ import (
"strings"
"time"

"sdr-visual-suite/internal/demod/gpudemod"
"sdr-visual-suite/internal/detector"
)

@@ -35,6 +36,7 @@ type Manager struct {
centerHz float64
decodeCommands map[string]string
queue chan detector.Event
gpuDemod *gpudemod.Engine
}

func New(sampleRate int, blockSize int, policy Policy, centerHz float64, decodeCommands map[string]string) *Manager {
@@ -45,6 +47,7 @@ func New(sampleRate int, blockSize int, policy Policy, centerHz float64, decodeC
policy.RingSeconds = 8
}
m := &Manager{policy: policy, ring: NewRing(sampleRate, blockSize, policy.RingSeconds), sampleRate: sampleRate, blockSize: blockSize, centerHz: centerHz, decodeCommands: decodeCommands, queue: make(chan detector.Event, 64)}
m.initGPUDemod(sampleRate, blockSize)
go m.worker()
return m
}
@@ -55,6 +58,7 @@ func (m *Manager) Update(sampleRate int, blockSize int, policy Policy, centerHz
m.blockSize = blockSize
m.centerHz = centerHz
m.decodeCommands = decodeCommands
m.initGPUDemod(sampleRate, blockSize)
if m.ring == nil {
m.ring = NewRing(sampleRate, blockSize, policy.RingSeconds)
return
@@ -88,6 +92,21 @@ func (m *Manager) worker() {
}
}

func (m *Manager) initGPUDemod(sampleRate int, blockSize int) {
if m.gpuDemod != nil {
m.gpuDemod.Close()
m.gpuDemod = nil
}
if !gpudemod.Available() {
return
}
eng, err := gpudemod.New(blockSize, sampleRate)
if err != nil {
return
}
m.gpuDemod = eng
}

func (m *Manager) recordEvent(ev detector.Event) error {
if !m.policy.Enabled {
return nil


読み込み中…
キャンセル
保存