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.

185 line
3.8KB

  1. package stdinpcm
  2. import (
  3. "context"
  4. "encoding/binary"
  5. "fmt"
  6. "io"
  7. "sync"
  8. "sync/atomic"
  9. "time"
  10. "github.com/jan/fm-rds-tx/internal/ingest"
  11. )
  12. type Source struct {
  13. id string
  14. reader io.Reader
  15. sampleRate int
  16. channels int
  17. chunkFrames int
  18. chunks chan ingest.PCMChunk
  19. errs chan error
  20. cancel context.CancelFunc
  21. wg sync.WaitGroup
  22. state atomic.Value // string
  23. chunksIn atomic.Uint64
  24. samplesIn atomic.Uint64
  25. discontinuities atomic.Uint64
  26. lastChunkAtUnix atomic.Int64
  27. lastError atomic.Value // string
  28. }
  29. func New(id string, reader io.Reader, sampleRate, channels, chunkFrames int) *Source {
  30. if id == "" {
  31. id = "stdin"
  32. }
  33. if sampleRate <= 0 {
  34. sampleRate = 44100
  35. }
  36. if channels <= 0 {
  37. channels = 2
  38. }
  39. if chunkFrames <= 0 {
  40. chunkFrames = 1024
  41. }
  42. s := &Source{
  43. id: id,
  44. reader: reader,
  45. sampleRate: sampleRate,
  46. channels: channels,
  47. chunkFrames: chunkFrames,
  48. chunks: make(chan ingest.PCMChunk, 8),
  49. errs: make(chan error, 4),
  50. }
  51. s.state.Store("idle")
  52. return s
  53. }
  54. func (s *Source) Descriptor() ingest.SourceDescriptor {
  55. return ingest.SourceDescriptor{
  56. ID: s.id,
  57. Kind: "stdin-pcm",
  58. Family: "raw",
  59. Transport: "stdin",
  60. Codec: "pcm_s16le",
  61. Channels: s.channels,
  62. SampleRateHz: s.sampleRate,
  63. Detail: "S16LE interleaved PCM via stdin",
  64. }
  65. }
  66. func (s *Source) Start(ctx context.Context) error {
  67. if s.reader == nil {
  68. return fmt.Errorf("stdin source reader is nil")
  69. }
  70. // BUG-2 fix: recreate channels — readLoop() closes them on exit.
  71. s.chunks = make(chan ingest.PCMChunk, 8)
  72. s.errs = make(chan error, 4)
  73. runCtx, cancel := context.WithCancel(ctx)
  74. s.cancel = cancel
  75. s.state.Store("running")
  76. s.wg.Add(1)
  77. go s.readLoop(runCtx)
  78. return nil
  79. }
  80. func (s *Source) Stop() error {
  81. if s.cancel != nil {
  82. s.cancel()
  83. }
  84. s.wg.Wait()
  85. s.state.Store("stopped")
  86. return nil
  87. }
  88. func (s *Source) Chunks() <-chan ingest.PCMChunk { return s.chunks }
  89. func (s *Source) Errors() <-chan error { return s.errs }
  90. func (s *Source) Stats() ingest.SourceStats {
  91. state, _ := s.state.Load().(string)
  92. last := s.lastChunkAtUnix.Load()
  93. errStr, _ := s.lastError.Load().(string)
  94. var lastChunkAt time.Time
  95. if last > 0 {
  96. lastChunkAt = time.Unix(0, last)
  97. }
  98. return ingest.SourceStats{
  99. State: state,
  100. Connected: state == "running",
  101. LastChunkAt: lastChunkAt,
  102. ChunksIn: s.chunksIn.Load(),
  103. SamplesIn: s.samplesIn.Load(),
  104. Discontinuities: s.discontinuities.Load(),
  105. LastError: errStr,
  106. }
  107. }
  108. func (s *Source) readLoop(ctx context.Context) {
  109. defer s.wg.Done()
  110. defer close(s.errs)
  111. defer close(s.chunks)
  112. frameBytes := s.channels * 2
  113. buf := make([]byte, s.chunkFrames*frameBytes)
  114. seq := uint64(0)
  115. for {
  116. select {
  117. case <-ctx.Done():
  118. return
  119. default:
  120. }
  121. n, err := io.ReadAtLeast(s.reader, buf, frameBytes)
  122. if err != nil {
  123. if err == io.EOF || err == io.ErrUnexpectedEOF {
  124. if n > 0 {
  125. s.emitChunk(buf[:n], seq)
  126. }
  127. s.state.Store("stopped")
  128. return
  129. }
  130. wrapped := fmt.Errorf("stdin read: %w", err)
  131. s.lastError.Store(wrapped.Error())
  132. s.state.Store("failed")
  133. select {
  134. case s.errs <- wrapped:
  135. default:
  136. }
  137. return
  138. }
  139. s.emitChunk(buf[:n], seq)
  140. seq++
  141. }
  142. }
  143. func (s *Source) emitChunk(data []byte, seq uint64) {
  144. samples := make([]int32, 0, len(data)/2)
  145. for i := 0; i+1 < len(data); i += 2 {
  146. v := int16(binary.LittleEndian.Uint16(data[i : i+2]))
  147. samples = append(samples, int32(v)<<16)
  148. }
  149. chunk := ingest.PCMChunk{
  150. Samples: samples,
  151. Channels: s.channels,
  152. SampleRateHz: s.sampleRate,
  153. Sequence: seq,
  154. Timestamp: time.Now(),
  155. SourceID: s.id,
  156. }
  157. s.chunksIn.Add(1)
  158. s.samplesIn.Add(uint64(len(samples)))
  159. s.lastChunkAtUnix.Store(time.Now().UnixNano())
  160. select {
  161. case s.chunks <- chunk:
  162. default:
  163. s.discontinuities.Add(1)
  164. }
  165. }