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.

181 wiersze
3.7KB

  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. runCtx, cancel := context.WithCancel(ctx)
  71. s.cancel = cancel
  72. s.state.Store("running")
  73. s.wg.Add(1)
  74. go s.readLoop(runCtx)
  75. return nil
  76. }
  77. func (s *Source) Stop() error {
  78. if s.cancel != nil {
  79. s.cancel()
  80. }
  81. s.wg.Wait()
  82. s.state.Store("stopped")
  83. return nil
  84. }
  85. func (s *Source) Chunks() <-chan ingest.PCMChunk { return s.chunks }
  86. func (s *Source) Errors() <-chan error { return s.errs }
  87. func (s *Source) Stats() ingest.SourceStats {
  88. state, _ := s.state.Load().(string)
  89. last := s.lastChunkAtUnix.Load()
  90. errStr, _ := s.lastError.Load().(string)
  91. var lastChunkAt time.Time
  92. if last > 0 {
  93. lastChunkAt = time.Unix(0, last)
  94. }
  95. return ingest.SourceStats{
  96. State: state,
  97. Connected: state == "running",
  98. LastChunkAt: lastChunkAt,
  99. ChunksIn: s.chunksIn.Load(),
  100. SamplesIn: s.samplesIn.Load(),
  101. Discontinuities: s.discontinuities.Load(),
  102. LastError: errStr,
  103. }
  104. }
  105. func (s *Source) readLoop(ctx context.Context) {
  106. defer s.wg.Done()
  107. defer close(s.chunks)
  108. frameBytes := s.channels * 2
  109. buf := make([]byte, s.chunkFrames*frameBytes)
  110. seq := uint64(0)
  111. for {
  112. select {
  113. case <-ctx.Done():
  114. return
  115. default:
  116. }
  117. n, err := io.ReadAtLeast(s.reader, buf, frameBytes)
  118. if err != nil {
  119. if err == io.EOF || err == io.ErrUnexpectedEOF {
  120. if n > 0 {
  121. s.emitChunk(buf[:n], seq)
  122. }
  123. s.state.Store("stopped")
  124. return
  125. }
  126. wrapped := fmt.Errorf("stdin read: %w", err)
  127. s.lastError.Store(wrapped.Error())
  128. s.state.Store("failed")
  129. select {
  130. case s.errs <- wrapped:
  131. default:
  132. }
  133. return
  134. }
  135. s.emitChunk(buf[:n], seq)
  136. seq++
  137. }
  138. }
  139. func (s *Source) emitChunk(data []byte, seq uint64) {
  140. samples := make([]int32, 0, len(data)/2)
  141. for i := 0; i+1 < len(data); i += 2 {
  142. v := int16(binary.LittleEndian.Uint16(data[i : i+2]))
  143. samples = append(samples, int32(v)<<16)
  144. }
  145. chunk := ingest.PCMChunk{
  146. Samples: samples,
  147. Channels: s.channels,
  148. SampleRateHz: s.sampleRate,
  149. Sequence: seq,
  150. Timestamp: time.Now(),
  151. SourceID: s.id,
  152. }
  153. s.chunksIn.Add(1)
  154. s.samplesIn.Add(uint64(len(samples)))
  155. s.lastChunkAtUnix.Store(time.Now().UnixNano())
  156. select {
  157. case s.chunks <- chunk:
  158. default:
  159. s.discontinuities.Add(1)
  160. }
  161. }