Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

223 行
5.8KB

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