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.

128 line
2.5KB

  1. package aoiprxkit
  2. import (
  3. "math"
  4. "sync"
  5. "time"
  6. )
  7. type ChannelMeter struct {
  8. RMS float64 `json:"rms"`
  9. Peak float64 `json:"peak"`
  10. Latest float64 `json:"latest"`
  11. }
  12. type MeterSnapshot struct {
  13. UpdatedAt string `json:"updatedAt"`
  14. Source string `json:"source"`
  15. SampleRateHz int `json:"sampleRateHz"`
  16. Channels int `json:"channels"`
  17. Meters []ChannelMeter `json:"meters"`
  18. }
  19. // LiveMeter consumes PCM frames and publishes simple per-channel level data.
  20. type LiveMeter struct {
  21. mu sync.RWMutex
  22. latest MeterSnapshot
  23. subs map[chan MeterSnapshot]struct{}
  24. }
  25. func NewLiveMeter() *LiveMeter {
  26. return &LiveMeter{subs: make(map[chan MeterSnapshot]struct{})}
  27. }
  28. func (m *LiveMeter) Consume(frame PCMFrame) {
  29. if frame.Channels <= 0 || len(frame.Samples) == 0 {
  30. return
  31. }
  32. meters := make([]ChannelMeter, frame.Channels)
  33. fullScale := detectFullScale(frame.Samples)
  34. sums := make([]float64, frame.Channels)
  35. counts := make([]int, frame.Channels)
  36. for i, sample := range frame.Samples {
  37. ch := i % frame.Channels
  38. norm := float64(sample) / fullScale
  39. abs := math.Abs(norm)
  40. if abs > meters[ch].Peak {
  41. meters[ch].Peak = abs
  42. }
  43. meters[ch].Latest = norm
  44. sums[ch] += norm * norm
  45. counts[ch]++
  46. }
  47. for ch := range meters {
  48. if counts[ch] > 0 {
  49. meters[ch].RMS = math.Sqrt(sums[ch] / float64(counts[ch]))
  50. }
  51. }
  52. snap := MeterSnapshot{
  53. UpdatedAt: time.Now().UTC().Format(time.RFC3339Nano),
  54. Source: frame.Source,
  55. SampleRateHz: frame.SampleRateHz,
  56. Channels: frame.Channels,
  57. Meters: meters,
  58. }
  59. m.mu.Lock()
  60. m.latest = snap
  61. subs := make([]chan MeterSnapshot, 0, len(m.subs))
  62. for ch := range m.subs {
  63. subs = append(subs, ch)
  64. }
  65. m.mu.Unlock()
  66. for _, ch := range subs {
  67. select {
  68. case ch <- snap:
  69. default:
  70. }
  71. }
  72. }
  73. func detectFullScale(samples []int32) float64 {
  74. var maxAbs int64
  75. for _, s := range samples {
  76. v := int64(s)
  77. if v < 0 {
  78. v = -v
  79. }
  80. if v > maxAbs {
  81. maxAbs = v
  82. }
  83. }
  84. if maxAbs <= 8388608 {
  85. return 8388608.0
  86. }
  87. return 2147483648.0
  88. }
  89. func (m *LiveMeter) Snapshot() MeterSnapshot {
  90. m.mu.RLock()
  91. defer m.mu.RUnlock()
  92. return m.latest
  93. }
  94. func (m *LiveMeter) Subscribe() (<-chan MeterSnapshot, func()) {
  95. ch := make(chan MeterSnapshot, 8)
  96. m.mu.Lock()
  97. m.subs[ch] = struct{}{}
  98. latest := m.latest
  99. m.mu.Unlock()
  100. if latest.UpdatedAt != "" {
  101. ch <- latest
  102. }
  103. unsubscribe := func() {
  104. m.mu.Lock()
  105. if _, ok := m.subs[ch]; ok {
  106. delete(m.subs, ch)
  107. close(ch)
  108. }
  109. m.mu.Unlock()
  110. }
  111. return ch, unsubscribe
  112. }