Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

232 行
6.1KB

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