package platform import ( "context" "fmt" "sync" "github.com/jan/fm-rds-tx/internal/output" ) // SoapyConfig exposes SoapySDR-specific knobs that drive hardware or simulated drivers. type SoapyConfig struct { output.BackendConfig Driver string Device string CenterFreqHz float64 GainDB float64 Channels []int DeviceArgs map[string]string Simulated bool SimulationPath string } // SoapyDriver is the low-level contract for talking to Soapy-style devices. type SoapyDriver interface { Name() string Configure(ctx context.Context, cfg SoapyConfig) error Write(ctx context.Context, frame *output.CompositeFrame) (int, error) Flush(ctx context.Context) error Close(ctx context.Context) error } // SoapyBackend wraps a driver and exposes the output.Backend interface. type SoapyBackend struct { mu sync.Mutex driver SoapyDriver cfg SoapyConfig info output.BackendInfo } // NewSoapyBackend returns an output-aware backend that drives the provided driver. func NewSoapyBackend(cfg SoapyConfig, driver SoapyDriver) *SoapyBackend { if driver == nil { driver = NewSimulatedDriver(nil) } info := output.BackendInfo{ Name: fmt.Sprintf("soapy/%s", cfg.Driver), Description: "SoapySDR-friendly backend", Capabilities: output.BackendCapabilities{ SupportsComposite: true, FixedRate: cfg.SampleRateHz > 0, MaxSamplesPerWrite: 8192, }, } return &SoapyBackend{driver: driver, cfg: cfg, info: info} } // Configure propagates the latest backend config to the driver. func (sb *SoapyBackend) Configure(ctx context.Context, cfg output.BackendConfig) error { sb.mu.Lock() sb.cfg.BackendConfig = cfg sb.mu.Unlock() return sb.driver.Configure(ctx, sb.cfg) } // Write delegates to the driver. func (sb *SoapyBackend) Write(ctx context.Context, frame *output.CompositeFrame) (int, error) { return sb.driver.Write(ctx, frame) } // Flush asks the driver to drain any buffers. func (sb *SoapyBackend) Flush(ctx context.Context) error { return sb.driver.Flush(ctx) } // Close shuts down the driver cleanly. func (sb *SoapyBackend) Close(ctx context.Context) error { return sb.driver.Close(ctx) } // Info reports the configured backend metadata. func (sb *SoapyBackend) Info() output.BackendInfo { sb.mu.Lock() defer sb.mu.Unlock() return sb.info } // SimulatedDriver keeps samples in a downstream backend for testing without hardware. type SimulatedDriver struct { mu sync.Mutex fallback output.Backend cfg SoapyConfig } // NewSimulatedDriver uses the provided backend or falls back to an in-memory dummy. func NewSimulatedDriver(writer output.Backend) *SimulatedDriver { if writer == nil { writer = output.NewDummyBackend("simulated-soapy") } return &SimulatedDriver{fallback: writer} } // Name returns the runtime label of the simulated driver. func (sd *SimulatedDriver) Name() string { return sd.fallback.Info().Name } // Configure pushes the SoapyConfig into the fallback backend. func (sd *SimulatedDriver) Configure(ctx context.Context, cfg SoapyConfig) error { sd.mu.Lock() sd.cfg = cfg sd.mu.Unlock() return sd.fallback.Configure(ctx, cfg.BackendConfig) } // Write simply plants the frame into the fallback pipeline. func (sd *SimulatedDriver) Write(ctx context.Context, frame *output.CompositeFrame) (int, error) { return sd.fallback.Write(ctx, frame) } // Flush is delegated. func (sd *SimulatedDriver) Flush(ctx context.Context) error { return sd.fallback.Flush(ctx) } // Close finalizes the fallback backend. func (sd *SimulatedDriver) Close(ctx context.Context) error { return sd.fallback.Close(ctx) }