Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
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.

130 lines
3.6KB

  1. package platform
  2. import (
  3. "context"
  4. "fmt"
  5. "sync"
  6. "github.com/jan/fm-rds-tx/internal/output"
  7. )
  8. // SoapyConfig exposes SoapySDR-specific knobs that drive hardware or simulated drivers.
  9. type SoapyConfig struct {
  10. output.BackendConfig
  11. Driver string
  12. Device string
  13. CenterFreqHz float64
  14. GainDB float64
  15. Channels []int
  16. DeviceArgs map[string]string
  17. Simulated bool
  18. SimulationPath string
  19. }
  20. // SoapyDriver is the low-level contract for talking to Soapy-style devices.
  21. type SoapyDriver interface {
  22. Name() string
  23. Configure(ctx context.Context, cfg SoapyConfig) error
  24. Write(ctx context.Context, frame *output.CompositeFrame) (int, error)
  25. Flush(ctx context.Context) error
  26. Close(ctx context.Context) error
  27. }
  28. // SoapyBackend wraps a driver and exposes the output.Backend interface.
  29. type SoapyBackend struct {
  30. mu sync.Mutex
  31. driver SoapyDriver
  32. cfg SoapyConfig
  33. info output.BackendInfo
  34. }
  35. // NewSoapyBackend returns an output-aware backend that drives the provided driver.
  36. func NewSoapyBackend(cfg SoapyConfig, driver SoapyDriver) *SoapyBackend {
  37. if driver == nil {
  38. driver = NewSimulatedDriver(nil)
  39. }
  40. info := output.BackendInfo{
  41. Name: fmt.Sprintf("soapy/%s", cfg.Driver),
  42. Description: "SoapySDR-friendly backend",
  43. Capabilities: output.BackendCapabilities{
  44. SupportsComposite: true,
  45. FixedRate: cfg.SampleRateHz > 0,
  46. MaxSamplesPerWrite: 8192,
  47. },
  48. }
  49. return &SoapyBackend{driver: driver, cfg: cfg, info: info}
  50. }
  51. // Configure propagates the latest backend config to the driver.
  52. func (sb *SoapyBackend) Configure(ctx context.Context, cfg output.BackendConfig) error {
  53. sb.mu.Lock()
  54. sb.cfg.BackendConfig = cfg
  55. sb.mu.Unlock()
  56. return sb.driver.Configure(ctx, sb.cfg)
  57. }
  58. // Write delegates to the driver.
  59. func (sb *SoapyBackend) Write(ctx context.Context, frame *output.CompositeFrame) (int, error) {
  60. return sb.driver.Write(ctx, frame)
  61. }
  62. // Flush asks the driver to drain any buffers.
  63. func (sb *SoapyBackend) Flush(ctx context.Context) error {
  64. return sb.driver.Flush(ctx)
  65. }
  66. // Close shuts down the driver cleanly.
  67. func (sb *SoapyBackend) Close(ctx context.Context) error {
  68. return sb.driver.Close(ctx)
  69. }
  70. // Info reports the configured backend metadata.
  71. func (sb *SoapyBackend) Info() output.BackendInfo {
  72. sb.mu.Lock()
  73. defer sb.mu.Unlock()
  74. return sb.info
  75. }
  76. // SimulatedDriver keeps samples in a downstream backend for testing without hardware.
  77. type SimulatedDriver struct {
  78. mu sync.Mutex
  79. fallback output.Backend
  80. cfg SoapyConfig
  81. }
  82. // NewSimulatedDriver uses the provided backend or falls back to an in-memory dummy.
  83. func NewSimulatedDriver(writer output.Backend) *SimulatedDriver {
  84. if writer == nil {
  85. writer = output.NewDummyBackend("simulated-soapy")
  86. }
  87. return &SimulatedDriver{fallback: writer}
  88. }
  89. // Name returns the runtime label of the simulated driver.
  90. func (sd *SimulatedDriver) Name() string {
  91. return sd.fallback.Info().Name
  92. }
  93. // Configure pushes the SoapyConfig into the fallback backend.
  94. func (sd *SimulatedDriver) Configure(ctx context.Context, cfg SoapyConfig) error {
  95. sd.mu.Lock()
  96. sd.cfg = cfg
  97. sd.mu.Unlock()
  98. return sd.fallback.Configure(ctx, cfg.BackendConfig)
  99. }
  100. // Write simply plants the frame into the fallback pipeline.
  101. func (sd *SimulatedDriver) Write(ctx context.Context, frame *output.CompositeFrame) (int, error) {
  102. return sd.fallback.Write(ctx, frame)
  103. }
  104. // Flush is delegated.
  105. func (sd *SimulatedDriver) Flush(ctx context.Context) error {
  106. return sd.fallback.Flush(ctx)
  107. }
  108. // Close finalizes the fallback backend.
  109. func (sd *SimulatedDriver) Close(ctx context.Context) error {
  110. return sd.fallback.Close(ctx)
  111. }