Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.

486 wiersze
10KB

  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. // Propagate the actual decoded sample rate to the sink and pacer the
  195. // first time (or whenever) it differs from our working rate. This fixes
  196. // the two-part rate-mismatch bug that appears when a native decoder
  197. // (e.g. go-mp3) decodes a 48000 Hz stream while the StreamSource and
  198. // StreamResampler were initialised assuming 44100 Hz:
  199. //
  200. // 1. The pacer (pacedDrainLimitLocked) was draining at the wrong rate,
  201. // causing the work buffer to overflow → glitches.
  202. // 2. The StreamResampler ratio (inputRate/outputRate) was computed from
  203. // the stale sink.SampleRate, so every frame was played at the wrong
  204. // pitch → audio too slow (44100/48000 ≈ 91.9 % speed).
  205. //
  206. // SetSampleRate writes atomically, so the StreamResampler's NextFrame()
  207. // picks up the corrected ratio without any additional locking.
  208. if chunk.SampleRateHz > 0 && chunk.SampleRateHz != r.workSampleRate {
  209. r.workSampleRate = chunk.SampleRateHz
  210. if r.sink != nil {
  211. r.sink.SetSampleRate(chunk.SampleRateHz)
  212. }
  213. }
  214. r.mu.Unlock()
  215. frames, err := ChunkToFrames(chunk)
  216. if err != nil {
  217. r.mu.Lock()
  218. r.stats.ConvertErrors++
  219. r.stats.State = "degraded"
  220. r.mu.Unlock()
  221. return
  222. }
  223. dropped := uint64(0)
  224. for _, frame := range frames {
  225. if !r.work.push(frame) {
  226. dropped++
  227. }
  228. }
  229. r.mu.Lock()
  230. if chunk.SampleRateHz > 0 {
  231. r.active.SampleRateHz = chunk.SampleRateHz
  232. }
  233. if chunk.Channels > 0 {
  234. r.active.Channels = chunk.Channels
  235. }
  236. r.stats.LastChunkAt = time.Now()
  237. r.stats.DroppedFrames += dropped
  238. if dropped > 0 {
  239. r.stats.State = "degraded"
  240. }
  241. r.updateBufferedStatsLocked()
  242. r.mu.Unlock()
  243. r.drainWorkingBuffer()
  244. }
  245. func (r *Runtime) drainWorkingBuffer() {
  246. r.mu.Lock()
  247. defer r.mu.Unlock()
  248. now := time.Now()
  249. if r.sink == nil {
  250. r.resetDrainPacerLocked(now)
  251. r.updateBufferedStatsLocked()
  252. return
  253. }
  254. bufferedFrames := r.work.available()
  255. if !r.gateOpen {
  256. switch {
  257. case bufferedFrames == 0:
  258. if r.stats.State == "degraded" {
  259. // Keep degraded visible until fresh audio recovers runtime.
  260. } else if !r.seenChunk {
  261. r.stats.State = "starting"
  262. } else if r.stats.State != "degraded" {
  263. r.stats.State = "running"
  264. }
  265. r.stats.Prebuffering = false
  266. r.stats.WriteBlocked = false
  267. r.resetDrainPacerLocked(now)
  268. r.updateBufferedStatsLocked()
  269. return
  270. case r.prebufferFrames > 0 && bufferedFrames < r.prebufferFrames:
  271. r.stats.State = "prebuffering"
  272. r.stats.Prebuffering = true
  273. r.stats.WriteBlocked = false
  274. r.resetDrainPacerLocked(now)
  275. r.updateBufferedStatsLocked()
  276. return
  277. default:
  278. r.gateOpen = true
  279. r.resetDrainPacerLocked(now)
  280. }
  281. }
  282. writeBlocked := false
  283. limit := r.pacedDrainLimitLocked(now, bufferedFrames)
  284. written := 0
  285. for written < limit && r.work.available() > 0 {
  286. frame, ok := r.work.peek()
  287. if !ok {
  288. break
  289. }
  290. if !r.sink.WriteFrame(frame) {
  291. writeBlocked = true
  292. break
  293. }
  294. r.work.pop()
  295. written++
  296. }
  297. if written > 0 {
  298. r.drainAllowance -= float64(written)
  299. if r.drainAllowance < 0 {
  300. r.drainAllowance = 0
  301. }
  302. }
  303. if r.work.available() == 0 && r.prebufferFrames > 0 {
  304. // Re-arm the gate after dry-out to rebuild margin before resuming.
  305. r.gateOpen = false
  306. r.resetDrainPacerLocked(now)
  307. }
  308. r.stats.Prebuffering = false
  309. r.stats.WriteBlocked = writeBlocked
  310. if writeBlocked {
  311. r.stats.State = "degraded"
  312. } else {
  313. r.stats.State = "running"
  314. }
  315. r.updateBufferedStatsLocked()
  316. }
  317. func (r *Runtime) pacedDrainLimitLocked(now time.Time, bufferedFrames int) int {
  318. if bufferedFrames <= 0 {
  319. return 0
  320. }
  321. // Use workSampleRate which is kept in sync with sink.SampleRate via
  322. // handleChunk. This ensures the pacer drains at the actual decoded rate
  323. // rather than the initial (potentially wrong) configured rate.
  324. rate := r.workSampleRate
  325. if r.sink != nil && r.sink.GetSampleRate() > 0 {
  326. rate = r.sink.GetSampleRate()
  327. }
  328. if rate <= 0 {
  329. return bufferedFrames
  330. }
  331. if !r.lastDrainAt.IsZero() {
  332. elapsed := now.Sub(r.lastDrainAt)
  333. if elapsed > 0 {
  334. r.drainAllowance += elapsed.Seconds() * float64(rate)
  335. }
  336. }
  337. r.lastDrainAt = now
  338. maxAllowance := maxInt(1, rate/5) // cap accumulated credit at 200 ms
  339. if r.drainAllowance > float64(maxAllowance) {
  340. r.drainAllowance = float64(maxAllowance)
  341. }
  342. limit := int(r.drainAllowance)
  343. if limit <= 0 {
  344. return 0
  345. }
  346. maxBurst := maxInt(1, rate/50) // max 20 ms worth of frames per drain call
  347. if limit > maxBurst {
  348. limit = maxBurst
  349. }
  350. sinkStats := r.sink.Stats()
  351. headroom := sinkStats.Capacity - sinkStats.Available
  352. if headroom < 0 {
  353. headroom = 0
  354. }
  355. if limit > headroom {
  356. limit = headroom
  357. }
  358. if limit > bufferedFrames {
  359. limit = bufferedFrames
  360. }
  361. return limit
  362. }
  363. func (r *Runtime) resetDrainPacerLocked(now time.Time) {
  364. r.lastDrainAt = now
  365. r.drainAllowance = 0
  366. }
  367. func maxInt(a, b int) int {
  368. if a > b {
  369. return a
  370. }
  371. return b
  372. }
  373. func (r *Runtime) updateBufferedStatsLocked() {
  374. available := r.work.available()
  375. capacity := r.work.capacity()
  376. buffered := 0.0
  377. if capacity > 0 {
  378. buffered = float64(available) / float64(capacity)
  379. }
  380. bufferedSeconds := 0.0
  381. if r.workSampleRate > 0 {
  382. bufferedSeconds = float64(available) / float64(r.workSampleRate)
  383. }
  384. r.stats.Buffered = buffered
  385. r.stats.BufferedSeconds = bufferedSeconds
  386. }
  387. func (r *Runtime) Stats() Stats {
  388. r.mu.RLock()
  389. runtimeStats := r.stats
  390. active := r.active
  391. r.mu.RUnlock()
  392. sourceStats := SourceStats{}
  393. if r.source != nil {
  394. sourceStats = r.source.Stats()
  395. }
  396. if sourceStats.BufferedSeconds < runtimeStats.BufferedSeconds {
  397. sourceStats.BufferedSeconds = runtimeStats.BufferedSeconds
  398. }
  399. return Stats{
  400. Active: active,
  401. Source: sourceStats,
  402. Runtime: runtimeStats,
  403. }
  404. }
  405. type frameBuffer struct {
  406. frames []audio.Frame
  407. head int
  408. len int
  409. }
  410. func newFrameBuffer(capacity int) *frameBuffer {
  411. if capacity < 1 {
  412. capacity = 1
  413. }
  414. return &frameBuffer{frames: make([]audio.Frame, capacity)}
  415. }
  416. func (b *frameBuffer) capacity() int {
  417. return len(b.frames)
  418. }
  419. func (b *frameBuffer) available() int {
  420. return b.len
  421. }
  422. func (b *frameBuffer) reset() {
  423. b.head = 0
  424. b.len = 0
  425. }
  426. func (b *frameBuffer) push(frame audio.Frame) bool {
  427. if b.len >= len(b.frames) {
  428. return false
  429. }
  430. idx := (b.head + b.len) % len(b.frames)
  431. b.frames[idx] = frame
  432. b.len++
  433. return true
  434. }
  435. func (b *frameBuffer) peek() (audio.Frame, bool) {
  436. if b.len == 0 {
  437. return audio.Frame{}, false
  438. }
  439. return b.frames[b.head], true
  440. }
  441. func (b *frameBuffer) pop() (audio.Frame, bool) {
  442. if b.len == 0 {
  443. return audio.Frame{}, false
  444. }
  445. frame := b.frames[b.head]
  446. b.head = (b.head + 1) % len(b.frames)
  447. b.len--
  448. return frame, true
  449. }