Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

148 строки
2.4KB

  1. package ingest
  2. import (
  3. "context"
  4. "sync"
  5. "sync/atomic"
  6. "time"
  7. "github.com/jan/fm-rds-tx/internal/audio"
  8. )
  9. type Runtime struct {
  10. sink *audio.StreamSource
  11. source Source
  12. started atomic.Bool
  13. ctx context.Context
  14. cancel context.CancelFunc
  15. wg sync.WaitGroup
  16. mu sync.RWMutex
  17. active SourceDescriptor
  18. stats RuntimeStats
  19. }
  20. func NewRuntime(sink *audio.StreamSource, src Source) *Runtime {
  21. return &Runtime{
  22. sink: sink,
  23. source: src,
  24. stats: RuntimeStats{
  25. State: "idle",
  26. },
  27. }
  28. }
  29. func (r *Runtime) Start(ctx context.Context) error {
  30. if r.source == nil {
  31. r.mu.Lock()
  32. r.stats.State = "idle"
  33. r.mu.Unlock()
  34. return nil
  35. }
  36. if !r.started.CompareAndSwap(false, true) {
  37. return nil
  38. }
  39. r.ctx, r.cancel = context.WithCancel(ctx)
  40. r.mu.Lock()
  41. r.active = r.source.Descriptor()
  42. r.stats.State = "starting"
  43. r.mu.Unlock()
  44. if err := r.source.Start(r.ctx); err != nil {
  45. r.started.Store(false)
  46. r.mu.Lock()
  47. r.stats.State = "failed"
  48. r.mu.Unlock()
  49. return err
  50. }
  51. r.wg.Add(1)
  52. go r.run()
  53. return nil
  54. }
  55. func (r *Runtime) Stop() error {
  56. if !r.started.CompareAndSwap(true, false) {
  57. return nil
  58. }
  59. if r.cancel != nil {
  60. r.cancel()
  61. }
  62. if r.source != nil {
  63. _ = r.source.Stop()
  64. }
  65. r.wg.Wait()
  66. r.mu.Lock()
  67. r.stats.State = "stopped"
  68. r.mu.Unlock()
  69. return nil
  70. }
  71. func (r *Runtime) run() {
  72. defer r.wg.Done()
  73. r.mu.Lock()
  74. r.stats.State = "running"
  75. r.mu.Unlock()
  76. ch := r.source.Chunks()
  77. errCh := r.source.Errors()
  78. for {
  79. select {
  80. case <-r.ctx.Done():
  81. return
  82. case err := <-errCh:
  83. if err == nil {
  84. continue
  85. }
  86. r.mu.Lock()
  87. r.stats.State = "degraded"
  88. r.mu.Unlock()
  89. case chunk, ok := <-ch:
  90. if !ok {
  91. return
  92. }
  93. r.handleChunk(chunk)
  94. }
  95. }
  96. }
  97. func (r *Runtime) handleChunk(chunk PCMChunk) {
  98. frames, err := ChunkToFrames(chunk)
  99. if err != nil {
  100. r.mu.Lock()
  101. r.stats.ConvertErrors++
  102. r.stats.State = "degraded"
  103. r.mu.Unlock()
  104. return
  105. }
  106. dropped := uint64(0)
  107. for _, frame := range frames {
  108. if !r.sink.WriteFrame(frame) {
  109. dropped++
  110. }
  111. }
  112. r.mu.Lock()
  113. r.stats.LastChunkAt = time.Now()
  114. r.stats.DroppedFrames += dropped
  115. r.stats.WriteBlocked = dropped > 0
  116. r.mu.Unlock()
  117. }
  118. func (r *Runtime) Stats() Stats {
  119. r.mu.RLock()
  120. runtimeStats := r.stats
  121. active := r.active
  122. r.mu.RUnlock()
  123. sourceStats := SourceStats{}
  124. if r.source != nil {
  125. sourceStats = r.source.Stats()
  126. }
  127. return Stats{
  128. Active: active,
  129. Source: sourceStats,
  130. Runtime: runtimeStats,
  131. }
  132. }