Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

134 строки
2.9KB

  1. package httpraw
  2. import (
  3. "context"
  4. "encoding/binary"
  5. "fmt"
  6. "sync/atomic"
  7. "time"
  8. "github.com/jan/fm-rds-tx/internal/ingest"
  9. )
  10. type Source struct {
  11. id string
  12. sampleRate int
  13. channels int
  14. chunks chan ingest.PCMChunk
  15. errs chan error
  16. sequence atomic.Uint64
  17. state atomic.Value // string
  18. chunksIn atomic.Uint64
  19. samplesIn atomic.Uint64
  20. discontinuities atomic.Uint64
  21. lastChunkAtUnix atomic.Int64
  22. lastError atomic.Value // string
  23. }
  24. func New(id string, sampleRate, channels int) *Source {
  25. if id == "" {
  26. id = "http-raw"
  27. }
  28. if sampleRate <= 0 {
  29. sampleRate = 44100
  30. }
  31. if channels <= 0 {
  32. channels = 2
  33. }
  34. s := &Source{
  35. id: id,
  36. sampleRate: sampleRate,
  37. channels: channels,
  38. chunks: make(chan ingest.PCMChunk, 32),
  39. errs: make(chan error, 8),
  40. }
  41. s.state.Store("idle")
  42. return s
  43. }
  44. func (s *Source) Descriptor() ingest.SourceDescriptor {
  45. return ingest.SourceDescriptor{
  46. ID: s.id,
  47. Kind: "http-raw",
  48. Family: "raw",
  49. Transport: "http",
  50. Codec: "pcm_s16le",
  51. Channels: s.channels,
  52. SampleRateHz: s.sampleRate,
  53. Detail: "HTTP push /audio/stream",
  54. }
  55. }
  56. func (s *Source) Start(_ context.Context) error {
  57. s.state.Store("running")
  58. return nil
  59. }
  60. func (s *Source) Stop() error {
  61. s.state.Store("stopped")
  62. return nil
  63. }
  64. func (s *Source) Chunks() <-chan ingest.PCMChunk { return s.chunks }
  65. func (s *Source) Errors() <-chan error { return s.errs }
  66. func (s *Source) Stats() ingest.SourceStats {
  67. state, _ := s.state.Load().(string)
  68. last := s.lastChunkAtUnix.Load()
  69. errStr, _ := s.lastError.Load().(string)
  70. var lastChunkAt time.Time
  71. if last > 0 {
  72. lastChunkAt = time.Unix(0, last)
  73. }
  74. return ingest.SourceStats{
  75. State: state,
  76. Connected: state == "running",
  77. LastChunkAt: lastChunkAt,
  78. ChunksIn: s.chunksIn.Load(),
  79. SamplesIn: s.samplesIn.Load(),
  80. Discontinuities: s.discontinuities.Load(),
  81. LastError: errStr,
  82. }
  83. }
  84. func (s *Source) WritePCM16(data []byte) (int, error) {
  85. if s.channels != 1 && s.channels != 2 {
  86. return 0, fmt.Errorf("unsupported configured channels: %d", s.channels)
  87. }
  88. if len(data) == 0 {
  89. return 0, nil
  90. }
  91. frameBytes := s.channels * 2
  92. usable := len(data) - (len(data) % frameBytes)
  93. if usable == 0 {
  94. return 0, nil
  95. }
  96. samples := make([]int32, 0, usable/2)
  97. for i := 0; i+1 < usable; i += 2 {
  98. v := int16(binary.LittleEndian.Uint16(data[i : i+2]))
  99. samples = append(samples, int32(v)<<16)
  100. }
  101. seq := s.sequence.Add(1) - 1
  102. chunk := ingest.PCMChunk{
  103. Samples: samples,
  104. Channels: s.channels,
  105. SampleRateHz: s.sampleRate,
  106. Sequence: seq,
  107. Timestamp: time.Now(),
  108. SourceID: s.id,
  109. }
  110. select {
  111. case s.chunks <- chunk:
  112. default:
  113. s.discontinuities.Add(1)
  114. return 0, fmt.Errorf("http raw ingress overflow")
  115. }
  116. frames := usable / frameBytes
  117. s.chunksIn.Add(1)
  118. s.samplesIn.Add(uint64(len(samples)))
  119. s.lastChunkAtUnix.Store(time.Now().UnixNano())
  120. return frames, nil
  121. }