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.

197 lignes
5.0KB

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