Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

461 lines
9.1KB

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