Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

238 lignes
6.4KB

  1. package platform
  2. import (
  3. "context"
  4. "fmt"
  5. "sync"
  6. "sync/atomic"
  7. "time"
  8. "github.com/jan/fm-rds-tx/internal/output"
  9. )
  10. // -----------------------------------------------------------------------
  11. // Device capabilities and runtime stats
  12. // -----------------------------------------------------------------------
  13. // DeviceCaps describes what a hardware device supports.
  14. type DeviceCaps struct {
  15. MinSampleRate float64
  16. MaxSampleRate float64
  17. SupportedSampleRates []float64
  18. HasGain bool
  19. GainMinDB float64
  20. GainMaxDB float64
  21. Channels []int
  22. }
  23. // RuntimeStats exposes live telemetry from the backend.
  24. type RuntimeStats struct {
  25. TXEnabled bool `json:"txEnabled"`
  26. StreamActive bool `json:"streamActive"`
  27. FramesWritten uint64 `json:"framesWritten"`
  28. SamplesWritten uint64 `json:"samplesWritten"`
  29. Underruns uint64 `json:"underruns"`
  30. Overruns uint64 `json:"overruns"`
  31. SlowWrites uint64 `json:"slowWrites,omitempty"`
  32. SlowFills uint64 `json:"slowFills,omitempty"`
  33. SlowPushes uint64 `json:"slowPushes,omitempty"`
  34. LastError string `json:"lastError,omitempty"`
  35. LastErrorAt string `json:"lastErrorAt,omitempty"`
  36. EffectiveRate float64 `json:"effectiveSampleRateHz"`
  37. MaxWriteMs float64 `json:"maxWriteMs,omitempty"`
  38. MaxFillMs float64 `json:"maxFillMs,omitempty"`
  39. MaxPushMs float64 `json:"maxPushMs,omitempty"`
  40. }
  41. // -----------------------------------------------------------------------
  42. // SoapyConfig
  43. // -----------------------------------------------------------------------
  44. type SoapyConfig struct {
  45. output.BackendConfig
  46. Driver string
  47. Device string
  48. CenterFreqHz float64
  49. GainDB float64
  50. Channels []int
  51. DeviceArgs map[string]string
  52. Simulated bool
  53. SimulationPath string
  54. }
  55. // -----------------------------------------------------------------------
  56. // SoapyDriver interface — extended for real HW
  57. // -----------------------------------------------------------------------
  58. type SoapyDriver interface {
  59. Name() string
  60. Configure(ctx context.Context, cfg SoapyConfig) error
  61. Capabilities(ctx context.Context) (DeviceCaps, error)
  62. Start(ctx context.Context) error
  63. Write(ctx context.Context, frame *output.CompositeFrame) (int, error)
  64. Stop(ctx context.Context) error
  65. Flush(ctx context.Context) error
  66. Close(ctx context.Context) error
  67. Stats() RuntimeStats
  68. // Tune changes the TX center frequency while streaming. Thread-safe.
  69. Tune(ctx context.Context, freqHz float64) error
  70. }
  71. // -----------------------------------------------------------------------
  72. // SoapyBackend wraps driver and exposes output.Backend
  73. // -----------------------------------------------------------------------
  74. type SoapyBackend struct {
  75. mu sync.Mutex
  76. driver SoapyDriver
  77. cfg SoapyConfig
  78. info output.BackendInfo
  79. }
  80. func NewSoapyBackend(cfg SoapyConfig, driver SoapyDriver) *SoapyBackend {
  81. if driver == nil {
  82. driver = NewSimulatedDriver(nil)
  83. }
  84. info := output.BackendInfo{
  85. Name: fmt.Sprintf("soapy/%s", cfg.Driver),
  86. Description: "SoapySDR-friendly backend",
  87. Capabilities: output.BackendCapabilities{
  88. SupportsComposite: true,
  89. FixedRate: cfg.SampleRateHz > 0,
  90. MaxSamplesPerWrite: 8192,
  91. },
  92. }
  93. return &SoapyBackend{driver: driver, cfg: cfg, info: info}
  94. }
  95. func (sb *SoapyBackend) Configure(ctx context.Context, cfg output.BackendConfig) error {
  96. sb.mu.Lock()
  97. sb.cfg.BackendConfig = cfg
  98. sb.mu.Unlock()
  99. return sb.driver.Configure(ctx, sb.cfg)
  100. }
  101. func (sb *SoapyBackend) Write(ctx context.Context, frame *output.CompositeFrame) (int, error) {
  102. return sb.driver.Write(ctx, frame)
  103. }
  104. func (sb *SoapyBackend) Flush(ctx context.Context) error {
  105. return sb.driver.Flush(ctx)
  106. }
  107. func (sb *SoapyBackend) Close(ctx context.Context) error {
  108. return sb.driver.Close(ctx)
  109. }
  110. func (sb *SoapyBackend) Info() output.BackendInfo {
  111. sb.mu.Lock()
  112. defer sb.mu.Unlock()
  113. return sb.info
  114. }
  115. func (sb *SoapyBackend) Driver() SoapyDriver {
  116. return sb.driver
  117. }
  118. // -----------------------------------------------------------------------
  119. // SimulatedDriver — implements full SoapyDriver interface
  120. // -----------------------------------------------------------------------
  121. type SimulatedDriver struct {
  122. mu sync.Mutex
  123. fallback output.Backend
  124. cfg SoapyConfig
  125. started bool
  126. framesWritten atomic.Uint64
  127. samplesWritten atomic.Uint64
  128. lastError string
  129. lastErrorAt string
  130. }
  131. func NewSimulatedDriver(writer output.Backend) *SimulatedDriver {
  132. if writer == nil {
  133. writer = output.NewDummyBackend("simulated-soapy")
  134. }
  135. return &SimulatedDriver{fallback: writer}
  136. }
  137. func (sd *SimulatedDriver) Name() string {
  138. return sd.fallback.Info().Name
  139. }
  140. func (sd *SimulatedDriver) Configure(ctx context.Context, cfg SoapyConfig) error {
  141. sd.mu.Lock()
  142. sd.cfg = cfg
  143. sd.mu.Unlock()
  144. return sd.fallback.Configure(ctx, cfg.BackendConfig)
  145. }
  146. func (sd *SimulatedDriver) Capabilities(_ context.Context) (DeviceCaps, error) {
  147. return DeviceCaps{
  148. MinSampleRate: 48000,
  149. MaxSampleRate: 2400000,
  150. HasGain: true,
  151. GainMinDB: 0,
  152. GainMaxDB: 47,
  153. Channels: []int{0},
  154. }, nil
  155. }
  156. func (sd *SimulatedDriver) Start(_ context.Context) error {
  157. sd.mu.Lock()
  158. defer sd.mu.Unlock()
  159. sd.started = true
  160. return nil
  161. }
  162. func (sd *SimulatedDriver) Write(ctx context.Context, frame *output.CompositeFrame) (int, error) {
  163. n, err := sd.fallback.Write(ctx, frame)
  164. if err != nil {
  165. sd.mu.Lock()
  166. sd.lastError = err.Error()
  167. sd.lastErrorAt = time.Now().UTC().Format(time.RFC3339)
  168. sd.mu.Unlock()
  169. }
  170. if n > 0 {
  171. sd.framesWritten.Add(1)
  172. sd.samplesWritten.Add(uint64(n))
  173. }
  174. return n, err
  175. }
  176. func (sd *SimulatedDriver) Stop(_ context.Context) error {
  177. sd.mu.Lock()
  178. defer sd.mu.Unlock()
  179. sd.started = false
  180. return nil
  181. }
  182. func (sd *SimulatedDriver) Flush(ctx context.Context) error {
  183. return sd.fallback.Flush(ctx)
  184. }
  185. func (sd *SimulatedDriver) Close(ctx context.Context) error {
  186. return sd.fallback.Close(ctx)
  187. }
  188. func (sd *SimulatedDriver) Stats() RuntimeStats {
  189. sd.mu.Lock()
  190. defer sd.mu.Unlock()
  191. return RuntimeStats{
  192. TXEnabled: sd.started,
  193. StreamActive: sd.started,
  194. FramesWritten: sd.framesWritten.Load(),
  195. SamplesWritten: sd.samplesWritten.Load(),
  196. LastError: sd.lastError,
  197. LastErrorAt: sd.lastErrorAt,
  198. EffectiveRate: sd.cfg.SampleRateHz,
  199. }
  200. }
  201. func (sd *SimulatedDriver) Tune(_ context.Context, freqHz float64) error {
  202. sd.mu.Lock()
  203. sd.cfg.CenterFreqHz = freqHz
  204. sd.mu.Unlock()
  205. return nil
  206. }