|
- //go:build soapy
-
- // Package soapysdr provides a pure-Go SoapySDR driver that loads the
- // SoapySDR shared library at runtime via dlopen/LoadLibrary.
- // No CGO required. No C compiler required.
- //
- // Build with: go build -tags soapy
- // Requires: SoapySDR shared library installed on the system.
- // Windows: SoapySDR.dll (via PothosSDR)
- // Linux: libSoapySDR.so (via package manager)
- // macOS: libSoapySDR.dylib (via brew)
- package soapysdr
-
- import (
- "context"
- "fmt"
- "math"
- "sync"
- "sync/atomic"
- "time"
- "unsafe"
-
- "github.com/jan/fm-rds-tx/internal/output"
- "github.com/jan/fm-rds-tx/internal/platform"
- )
-
- // nativeDriver implements platform.SoapyDriver using runtime-loaded SoapySDR.
- type nativeDriver struct {
- mu sync.Mutex
- lib *soapyLib
- cfg platform.SoapyConfig
- dev uintptr // SoapySDRDevice*
- stream uintptr // SoapySDRStream*
- mtu int
-
- started bool
- configured bool
- framesWritten atomic.Uint64
- samplesWritten atomic.Uint64
- underruns atomic.Uint64
- lastError string
- lastErrorAt string
- }
-
- // NewNativeDriver creates an uninitialized SoapySDR native driver.
- func NewNativeDriver() platform.SoapyDriver {
- lib, err := loadSoapyLib()
- if err != nil {
- // Return a driver that will fail on Configure with a clear message
- return &nativeDriver{lastError: fmt.Sprintf("load SoapySDR library: %v", err)}
- }
- return &nativeDriver{lib: lib}
- }
-
- // Enumerate lists available SoapySDR devices.
- func Enumerate() ([]map[string]string, error) {
- lib, err := loadSoapyLib()
- if err != nil {
- return nil, fmt.Errorf("load SoapySDR: %w", err)
- }
- return lib.enumerate()
- }
-
- func (d *nativeDriver) Name() string { return "soapy-native" }
-
- func (d *nativeDriver) Configure(_ context.Context, cfg platform.SoapyConfig) error {
- d.mu.Lock()
- defer d.mu.Unlock()
-
- if d.lib == nil {
- return fmt.Errorf("soapy: library not loaded: %s", d.lastError)
- }
-
- // Close existing
- if d.dev != 0 {
- if d.stream != 0 {
- d.lib.deactivateStream(d.dev, d.stream)
- d.lib.closeStream(d.dev, d.stream)
- d.stream = 0
- }
- d.lib.unmakeDevice(d.dev)
- d.dev = 0
- }
- d.cfg = cfg
-
- // Open device
- dev, err := d.lib.makeDevice(cfg.Driver, cfg.Device, cfg.DeviceArgs)
- if err != nil {
- return err
- }
- d.dev = dev
-
- // Sample rate
- rate := cfg.SampleRateHz
- if rate <= 0 {
- rate = 528000
- }
- if err := d.lib.setSampleRate(d.dev, dirTX, 0, rate); err != nil {
- return err
- }
-
- // Frequency
- if cfg.CenterFreqHz > 0 {
- if err := d.lib.setFrequency(d.dev, dirTX, 0, cfg.CenterFreqHz); err != nil {
- return err
- }
- }
-
- // Gain
- if cfg.GainDB != 0 {
- _ = d.lib.setGain(d.dev, dirTX, 0, cfg.GainDB)
- }
-
- // Setup TX stream (CF32)
- stream, err := d.lib.setupStream(d.dev, dirTX, "CF32", []uint{0})
- if err != nil {
- return err
- }
- d.stream = stream
-
- d.mtu = d.lib.getStreamMTU(d.dev, d.stream)
- if d.mtu <= 0 {
- d.mtu = 4096
- }
-
- d.configured = true
- return nil
- }
-
- func (d *nativeDriver) Capabilities(_ context.Context) (platform.DeviceCaps, error) {
- d.mu.Lock()
- defer d.mu.Unlock()
- if d.dev == 0 || d.lib == nil {
- return platform.DeviceCaps{}, fmt.Errorf("device not opened")
- }
- gMin, gMax := d.lib.getGainRange(d.dev, dirTX, 0)
- return platform.DeviceCaps{
- MinSampleRate: 521e3, MaxSampleRate: 61.44e6,
- HasGain: true, GainMinDB: gMin, GainMaxDB: gMax,
- Channels: []int{0},
- }, nil
- }
-
- func (d *nativeDriver) Start(_ context.Context) error {
- d.mu.Lock()
- defer d.mu.Unlock()
- if !d.configured || d.dev == 0 || d.stream == 0 {
- return fmt.Errorf("soapy: not configured")
- }
- if d.started {
- return fmt.Errorf("soapy: already started")
- }
- if err := d.lib.activateStream(d.dev, d.stream); err != nil {
- return err
- }
- d.started = true
- return nil
- }
-
- func (d *nativeDriver) Write(_ context.Context, frame *output.CompositeFrame) (int, error) {
- d.mu.Lock()
- lib, dev, stream, started, mtu := d.lib, d.dev, d.stream, d.started, d.mtu
- d.mu.Unlock()
-
- if !started || dev == 0 || stream == 0 {
- return 0, fmt.Errorf("soapy: stream not active")
- }
- if frame == nil || len(frame.Samples) == 0 {
- return 0, nil
- }
-
- total := len(frame.Samples)
- written := 0
- for written < total {
- chunk := total - written
- if chunk > mtu {
- chunk = mtu
- }
- // IQSample is {I float32, Q float32} — contiguous CF32 in memory
- ptr := unsafe.Pointer(&frame.Samples[written])
- n, err := lib.writeStream(dev, stream, ptr, chunk)
- if err != nil {
- d.mu.Lock()
- d.lastError = err.Error()
- d.lastErrorAt = time.Now().UTC().Format(time.RFC3339)
- d.underruns.Add(1)
- d.mu.Unlock()
- return written, err
- }
- written += n
- }
- d.framesWritten.Add(1)
- d.samplesWritten.Add(uint64(written))
- return written, nil
- }
-
- func (d *nativeDriver) Stop(_ context.Context) error {
- d.mu.Lock()
- defer d.mu.Unlock()
- if !d.started {
- return nil
- }
- if d.dev != 0 && d.stream != 0 {
- d.lib.deactivateStream(d.dev, d.stream)
- }
- d.started = false
- return nil
- }
-
- func (d *nativeDriver) Flush(_ context.Context) error { return nil }
-
- func (d *nativeDriver) Tune(_ context.Context, freqHz float64) error {
- d.mu.Lock()
- defer d.mu.Unlock()
- if d.dev == 0 || d.lib == nil {
- return fmt.Errorf("soapy: not configured")
- }
- return d.lib.setFrequency(d.dev, dirTX, 0, freqHz)
- }
-
- func (d *nativeDriver) Close(_ context.Context) error {
- d.mu.Lock()
- defer d.mu.Unlock()
- if d.stream != 0 && d.dev != 0 {
- if d.started {
- d.lib.deactivateStream(d.dev, d.stream)
- d.started = false
- }
- d.lib.closeStream(d.dev, d.stream)
- d.stream = 0
- }
- if d.dev != 0 {
- d.lib.unmakeDevice(d.dev)
- d.dev = 0
- }
- d.configured = false
- return nil
- }
-
- func (d *nativeDriver) Stats() platform.RuntimeStats {
- d.mu.Lock()
- defer d.mu.Unlock()
- return platform.RuntimeStats{
- TXEnabled: d.started, StreamActive: d.started,
- FramesWritten: d.framesWritten.Load(), SamplesWritten: d.samplesWritten.Load(),
- Underruns: d.underruns.Load(), LastError: d.lastError, LastErrorAt: d.lastErrorAt,
- EffectiveRate: d.cfg.SampleRateHz,
- }
- }
-
- // --- helper constants ---
- const dirTX = 1 // SOAPY_SDR_TX
-
- // float64 from raw bits
- func f64FromPtr(p uintptr) float64 {
- return math.Float64frombits(uint64(p))
- }
|