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.

386 lines
7.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. onTitle func(string)
  14. prebuffer time.Duration
  15. ctx context.Context
  16. cancel context.CancelFunc
  17. wg sync.WaitGroup
  18. work *frameBuffer
  19. workSampleRate int
  20. prebufferFrames int
  21. gateOpen bool
  22. seenChunk bool
  23. mu sync.RWMutex
  24. active SourceDescriptor
  25. stats RuntimeStats
  26. }
  27. type RuntimeOption func(*Runtime)
  28. func WithStreamTitleHandler(handler func(string)) RuntimeOption {
  29. return func(r *Runtime) {
  30. r.onTitle = handler
  31. }
  32. }
  33. func WithPrebuffer(d time.Duration) RuntimeOption {
  34. return func(r *Runtime) {
  35. if d < 0 {
  36. d = 0
  37. }
  38. r.prebuffer = d
  39. }
  40. }
  41. func WithPrebufferMs(ms int) RuntimeOption {
  42. return func(r *Runtime) {
  43. if ms < 0 {
  44. ms = 0
  45. }
  46. r.prebuffer = time.Duration(ms) * time.Millisecond
  47. }
  48. }
  49. func NewRuntime(sink *audio.StreamSource, src Source, opts ...RuntimeOption) *Runtime {
  50. sampleRate := 44100
  51. capacity := 1024
  52. if sink != nil {
  53. if sink.SampleRate > 0 {
  54. sampleRate = sink.SampleRate
  55. }
  56. if sinkCap := sink.Stats().Capacity; sinkCap > 0 {
  57. capacity = sinkCap * 2
  58. }
  59. }
  60. r := &Runtime{
  61. sink: sink,
  62. source: src,
  63. work: newFrameBuffer(capacity),
  64. workSampleRate: sampleRate,
  65. stats: RuntimeStats{
  66. State: "idle",
  67. },
  68. }
  69. for _, opt := range opts {
  70. if opt != nil {
  71. opt(r)
  72. }
  73. }
  74. if r.workSampleRate > 0 && r.prebuffer > 0 {
  75. r.prebufferFrames = int(r.prebuffer.Seconds() * float64(r.workSampleRate))
  76. }
  77. minCapacity := 256
  78. if r.prebufferFrames > 0 && minCapacity < r.prebufferFrames*2 {
  79. minCapacity = r.prebufferFrames * 2
  80. }
  81. if r.work == nil || r.work.capacity() < minCapacity {
  82. r.work = newFrameBuffer(minCapacity)
  83. }
  84. r.updateBufferedStatsLocked()
  85. return r
  86. }
  87. func (r *Runtime) Start(ctx context.Context) error {
  88. if r.sink == nil {
  89. r.mu.Lock()
  90. r.stats.State = "failed"
  91. r.mu.Unlock()
  92. return nil
  93. }
  94. if r.source == nil {
  95. r.mu.Lock()
  96. r.stats.State = "idle"
  97. r.mu.Unlock()
  98. return nil
  99. }
  100. if !r.started.CompareAndSwap(false, true) {
  101. return nil
  102. }
  103. r.ctx, r.cancel = context.WithCancel(ctx)
  104. r.mu.Lock()
  105. r.active = r.source.Descriptor()
  106. r.stats.State = "starting"
  107. r.stats.Prebuffering = false
  108. r.stats.WriteBlocked = false
  109. r.gateOpen = false
  110. r.seenChunk = false
  111. r.work.reset()
  112. r.updateBufferedStatsLocked()
  113. r.mu.Unlock()
  114. if err := r.source.Start(r.ctx); err != nil {
  115. r.started.Store(false)
  116. r.mu.Lock()
  117. r.stats.State = "failed"
  118. r.mu.Unlock()
  119. return err
  120. }
  121. r.wg.Add(1)
  122. go r.run()
  123. return nil
  124. }
  125. func (r *Runtime) Stop() error {
  126. if !r.started.CompareAndSwap(true, false) {
  127. return nil
  128. }
  129. if r.cancel != nil {
  130. r.cancel()
  131. }
  132. if r.source != nil {
  133. _ = r.source.Stop()
  134. }
  135. r.wg.Wait()
  136. r.mu.Lock()
  137. r.stats.State = "stopped"
  138. r.mu.Unlock()
  139. return nil
  140. }
  141. func (r *Runtime) run() {
  142. defer r.wg.Done()
  143. ch := r.source.Chunks()
  144. errCh := r.source.Errors()
  145. ticker := time.NewTicker(10 * time.Millisecond)
  146. defer ticker.Stop()
  147. var titleCh <-chan string
  148. if src, ok := r.source.(StreamTitleSource); ok && r.onTitle != nil {
  149. titleCh = src.StreamTitleUpdates()
  150. }
  151. for {
  152. select {
  153. case <-r.ctx.Done():
  154. return
  155. case err, ok := <-errCh:
  156. if !ok {
  157. errCh = nil
  158. continue
  159. }
  160. if err == nil {
  161. continue
  162. }
  163. r.mu.Lock()
  164. r.stats.State = "degraded"
  165. r.stats.Prebuffering = false
  166. r.mu.Unlock()
  167. case chunk, ok := <-ch:
  168. if !ok {
  169. r.mu.Lock()
  170. r.stats.State = "stopped"
  171. r.stats.Prebuffering = false
  172. r.mu.Unlock()
  173. return
  174. }
  175. r.handleChunk(chunk)
  176. case <-ticker.C:
  177. r.drainWorkingBuffer()
  178. case title, ok := <-titleCh:
  179. if !ok {
  180. titleCh = nil
  181. continue
  182. }
  183. r.onTitle(title)
  184. }
  185. }
  186. }
  187. func (r *Runtime) handleChunk(chunk PCMChunk) {
  188. r.mu.Lock()
  189. r.seenChunk = true
  190. r.mu.Unlock()
  191. frames, err := ChunkToFrames(chunk)
  192. if err != nil {
  193. r.mu.Lock()
  194. r.stats.ConvertErrors++
  195. r.stats.State = "degraded"
  196. r.mu.Unlock()
  197. return
  198. }
  199. dropped := uint64(0)
  200. for _, frame := range frames {
  201. if !r.work.push(frame) {
  202. dropped++
  203. }
  204. }
  205. r.mu.Lock()
  206. if chunk.SampleRateHz > 0 {
  207. r.active.SampleRateHz = chunk.SampleRateHz
  208. }
  209. if chunk.Channels > 0 {
  210. r.active.Channels = chunk.Channels
  211. }
  212. r.stats.LastChunkAt = time.Now()
  213. r.stats.DroppedFrames += dropped
  214. if dropped > 0 {
  215. r.stats.State = "degraded"
  216. }
  217. r.updateBufferedStatsLocked()
  218. r.mu.Unlock()
  219. r.drainWorkingBuffer()
  220. }
  221. func (r *Runtime) drainWorkingBuffer() {
  222. r.mu.Lock()
  223. defer r.mu.Unlock()
  224. if r.sink == nil {
  225. r.updateBufferedStatsLocked()
  226. return
  227. }
  228. bufferedFrames := r.work.available()
  229. if !r.gateOpen {
  230. switch {
  231. case bufferedFrames == 0:
  232. if r.stats.State == "degraded" {
  233. // Keep degraded visible until fresh audio recovers runtime.
  234. } else if !r.seenChunk {
  235. r.stats.State = "starting"
  236. } else if r.stats.State != "degraded" {
  237. r.stats.State = "running"
  238. }
  239. r.stats.Prebuffering = false
  240. r.stats.WriteBlocked = false
  241. r.updateBufferedStatsLocked()
  242. return
  243. case r.prebufferFrames > 0 && bufferedFrames < r.prebufferFrames:
  244. r.stats.State = "prebuffering"
  245. r.stats.Prebuffering = true
  246. r.stats.WriteBlocked = false
  247. r.updateBufferedStatsLocked()
  248. return
  249. default:
  250. r.gateOpen = true
  251. }
  252. }
  253. writeBlocked := false
  254. for r.work.available() > 0 {
  255. frame, ok := r.work.peek()
  256. if !ok {
  257. break
  258. }
  259. if !r.sink.WriteFrame(frame) {
  260. writeBlocked = true
  261. break
  262. }
  263. r.work.pop()
  264. }
  265. if r.work.available() == 0 && r.prebufferFrames > 0 {
  266. // Re-arm the gate after dry-out to rebuild margin before resuming.
  267. r.gateOpen = false
  268. }
  269. r.stats.Prebuffering = false
  270. r.stats.WriteBlocked = writeBlocked
  271. if writeBlocked {
  272. r.stats.State = "degraded"
  273. } else {
  274. r.stats.State = "running"
  275. }
  276. r.updateBufferedStatsLocked()
  277. }
  278. func (r *Runtime) updateBufferedStatsLocked() {
  279. available := r.work.available()
  280. capacity := r.work.capacity()
  281. buffered := 0.0
  282. if capacity > 0 {
  283. buffered = float64(available) / float64(capacity)
  284. }
  285. bufferedSeconds := 0.0
  286. if r.workSampleRate > 0 {
  287. bufferedSeconds = float64(available) / float64(r.workSampleRate)
  288. }
  289. r.stats.Buffered = buffered
  290. r.stats.BufferedSeconds = bufferedSeconds
  291. }
  292. func (r *Runtime) Stats() Stats {
  293. r.mu.RLock()
  294. runtimeStats := r.stats
  295. active := r.active
  296. r.mu.RUnlock()
  297. sourceStats := SourceStats{}
  298. if r.source != nil {
  299. sourceStats = r.source.Stats()
  300. }
  301. if sourceStats.BufferedSeconds < runtimeStats.BufferedSeconds {
  302. sourceStats.BufferedSeconds = runtimeStats.BufferedSeconds
  303. }
  304. return Stats{
  305. Active: active,
  306. Source: sourceStats,
  307. Runtime: runtimeStats,
  308. }
  309. }
  310. type frameBuffer struct {
  311. frames []audio.Frame
  312. head int
  313. len int
  314. }
  315. func newFrameBuffer(capacity int) *frameBuffer {
  316. if capacity < 1 {
  317. capacity = 1
  318. }
  319. return &frameBuffer{frames: make([]audio.Frame, capacity)}
  320. }
  321. func (b *frameBuffer) capacity() int {
  322. return len(b.frames)
  323. }
  324. func (b *frameBuffer) available() int {
  325. return b.len
  326. }
  327. func (b *frameBuffer) reset() {
  328. b.head = 0
  329. b.len = 0
  330. }
  331. func (b *frameBuffer) push(frame audio.Frame) bool {
  332. if b.len >= len(b.frames) {
  333. return false
  334. }
  335. idx := (b.head + b.len) % len(b.frames)
  336. b.frames[idx] = frame
  337. b.len++
  338. return true
  339. }
  340. func (b *frameBuffer) peek() (audio.Frame, bool) {
  341. if b.len == 0 {
  342. return audio.Frame{}, false
  343. }
  344. return b.frames[b.head], true
  345. }
  346. func (b *frameBuffer) pop() (audio.Frame, bool) {
  347. if b.len == 0 {
  348. return audio.Frame{}, false
  349. }
  350. frame := b.frames[b.head]
  351. b.head = (b.head + 1) % len(b.frames)
  352. b.len--
  353. return frame, true
  354. }