Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

231 lignes
6.1KB

  1. package audio
  2. import (
  3. "encoding/binary"
  4. "fmt"
  5. "io"
  6. "sync/atomic"
  7. )
  8. // StreamSource is a lock-free SPSC (single-producer, single-consumer) ring buffer
  9. // for real-time audio streaming. One goroutine writes PCM frames, the DSP
  10. // goroutine reads them via NextFrame(). Returns silence on underrun.
  11. //
  12. // Zero allocations in steady state. No mutex in the read or write path.
  13. type StreamSource struct {
  14. ring []Frame
  15. size int
  16. mask int // size-1, for fast modulo (size must be power of 2)
  17. SampleRate int
  18. writePos atomic.Int64
  19. readPos atomic.Int64
  20. Underruns atomic.Uint64
  21. Overflows atomic.Uint64
  22. Written atomic.Uint64
  23. highWatermark atomic.Int64
  24. }
  25. // NewStreamSource creates a ring buffer with the given capacity (rounded up
  26. // to next power of 2) and input sample rate.
  27. func NewStreamSource(capacity, sampleRate int) *StreamSource {
  28. // Round up to power of 2
  29. size := 1
  30. for size < capacity {
  31. size <<= 1
  32. }
  33. return &StreamSource{
  34. ring: make([]Frame, size),
  35. size: size,
  36. mask: size - 1,
  37. SampleRate: sampleRate,
  38. }
  39. }
  40. // WriteFrame pushes a single frame into the ring buffer.
  41. // Returns false if the buffer is full (overflow).
  42. func (s *StreamSource) WriteFrame(f Frame) bool {
  43. wp := s.writePos.Load()
  44. rp := s.readPos.Load()
  45. if wp-rp >= int64(s.size) {
  46. s.Overflows.Add(1)
  47. return false
  48. }
  49. s.ring[int(wp)&s.mask] = f
  50. s.writePos.Add(1)
  51. s.Written.Add(1)
  52. s.updateHighWatermark()
  53. return true
  54. }
  55. // WritePCM decodes interleaved S16LE stereo PCM bytes and writes frames
  56. // to the ring buffer. Returns the number of frames written.
  57. func (s *StreamSource) WritePCM(data []byte) int {
  58. frames := len(data) / 4 // 2 channels × 2 bytes per sample
  59. written := 0
  60. for i := 0; i < frames; i++ {
  61. off := i * 4
  62. l := int16(binary.LittleEndian.Uint16(data[off:]))
  63. r := int16(binary.LittleEndian.Uint16(data[off+2:]))
  64. f := NewFrame(
  65. Sample(float64(l)/32768.0),
  66. Sample(float64(r)/32768.0),
  67. )
  68. if !s.WriteFrame(f) {
  69. break
  70. }
  71. written++
  72. }
  73. return written
  74. }
  75. // ReadFrame consumes one frame from the ring buffer.
  76. // Returns silence (0,0) on underrun.
  77. func (s *StreamSource) ReadFrame() Frame {
  78. rp := s.readPos.Load()
  79. wp := s.writePos.Load()
  80. if rp >= wp {
  81. s.Underruns.Add(1)
  82. return NewFrame(0, 0)
  83. }
  84. f := s.ring[int(rp)&s.mask]
  85. s.readPos.Add(1)
  86. return f
  87. }
  88. // NextFrame implements the frameSource interface.
  89. func (s *StreamSource) NextFrame() Frame {
  90. return s.ReadFrame()
  91. }
  92. // Available returns the number of frames currently buffered.
  93. func (s *StreamSource) Available() int {
  94. return int(s.writePos.Load() - s.readPos.Load())
  95. }
  96. // Buffered returns the fill ratio (0.0 = empty, 1.0 = full).
  97. func (s *StreamSource) Buffered() float64 {
  98. return float64(s.Available()) / float64(s.size)
  99. }
  100. // Stats returns diagnostic counters.
  101. func (s *StreamSource) Stats() StreamStats {
  102. available := s.Available()
  103. buffered := 0.0
  104. if s.size > 0 {
  105. buffered = float64(available) / float64(s.size)
  106. }
  107. highWatermark := int(s.highWatermark.Load())
  108. return StreamStats{
  109. Available: available,
  110. Capacity: s.size,
  111. Buffered: buffered,
  112. BufferedDurationSeconds: s.bufferedDurationSeconds(available),
  113. HighWatermark: highWatermark,
  114. HighWatermarkDurationSeconds: s.bufferedDurationSeconds(highWatermark),
  115. Written: s.Written.Load(),
  116. Underruns: s.Underruns.Load(),
  117. Overflows: s.Overflows.Load(),
  118. }
  119. }
  120. // StreamStats exposes runtime telemetry for the stream buffer.
  121. type StreamStats struct {
  122. Available int `json:"available"`
  123. Capacity int `json:"capacity"`
  124. Buffered float64 `json:"buffered"`
  125. BufferedDurationSeconds float64 `json:"bufferedDurationSeconds"`
  126. HighWatermark int `json:"highWatermark"`
  127. HighWatermarkDurationSeconds float64 `json:"highWatermarkDurationSeconds"`
  128. Written uint64 `json:"written"`
  129. Underruns uint64 `json:"underruns"`
  130. Overflows uint64 `json:"overflows"`
  131. }
  132. func (s *StreamSource) bufferedDurationSeconds(available int) float64 {
  133. if s.SampleRate <= 0 {
  134. return 0
  135. }
  136. return float64(available) / float64(s.SampleRate)
  137. }
  138. func (s *StreamSource) updateHighWatermark() {
  139. available := s.Available()
  140. for {
  141. prev := s.highWatermark.Load()
  142. if int64(available) <= prev {
  143. return
  144. }
  145. if s.highWatermark.CompareAndSwap(prev, int64(available)) {
  146. return
  147. }
  148. }
  149. }
  150. // --- StreamResampler ---
  151. // StreamResampler wraps a StreamSource and rate-converts from the stream's
  152. // native sample rate to the target output rate using linear interpolation.
  153. // Consumes input frames on demand — no buffering beyond the ring buffer.
  154. type StreamResampler struct {
  155. src *StreamSource
  156. ratio float64 // inputRate / outputRate (< 1 when upsampling)
  157. pos float64
  158. prev Frame
  159. curr Frame
  160. }
  161. // NewStreamResampler creates a streaming resampler.
  162. func NewStreamResampler(src *StreamSource, outputRate float64) *StreamResampler {
  163. if src == nil || outputRate <= 0 || src.SampleRate <= 0 {
  164. return &StreamResampler{src: src, ratio: 1.0}
  165. }
  166. return &StreamResampler{
  167. src: src,
  168. ratio: float64(src.SampleRate) / outputRate,
  169. }
  170. }
  171. // NextFrame returns the next interpolated frame at the output rate.
  172. // Implements the frameSource interface.
  173. func (r *StreamResampler) NextFrame() Frame {
  174. if r.src == nil {
  175. return NewFrame(0, 0)
  176. }
  177. // Consume input samples as the fractional position advances
  178. for r.pos >= 1.0 {
  179. r.prev = r.curr
  180. r.curr = r.src.ReadFrame()
  181. r.pos -= 1.0
  182. }
  183. frac := r.pos
  184. l := float64(r.prev.L)*(1-frac) + float64(r.curr.L)*frac
  185. ri := float64(r.prev.R)*(1-frac) + float64(r.curr.R)*frac
  186. r.pos += r.ratio
  187. return NewFrame(Sample(l), Sample(ri))
  188. }
  189. // --- Ingest helpers ---
  190. // IngestReader continuously reads S16LE stereo PCM from an io.Reader into
  191. // a StreamSource. Blocks until the reader returns an error or io.EOF.
  192. // Designed to run as a goroutine.
  193. func IngestReader(r io.Reader, dst *StreamSource) error {
  194. buf := make([]byte, 16384) // 4096 frames per read (16KB)
  195. for {
  196. n, err := r.Read(buf)
  197. if n > 0 {
  198. dst.WritePCM(buf[:n])
  199. }
  200. if err != nil {
  201. if err == io.EOF {
  202. return nil
  203. }
  204. return fmt.Errorf("audio ingest: %w", err)
  205. }
  206. }
  207. }