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

184 рядки
5.3KB

  1. package dsp
  2. import "math"
  3. // BS412Limiter implements ITU-R BS.412 MPX power limiting.
  4. // Measures the rolling 60-second average power of the composite signal
  5. // and reduces audio gain when the power exceeds the threshold.
  6. //
  7. // The threshold is specified in dBr where 0 dBr is the reference power
  8. // of a fully modulated mono signal (composite peak = 1.0, power = 0.5).
  9. //
  10. // Pilot and RDS power are accounted for: the audio power budget is
  11. // reduced by their constant contribution so the total stays within limits.
  12. type BS412Limiter struct {
  13. enabled bool
  14. thresholdPow float64 // linear power threshold for total MPX
  15. audioBudget float64 // = thresholdPow - pilotPow - rdsPow
  16. // Rolling 60-second power integrator
  17. powerBuf []float64 // per-chunk average power values
  18. bufIdx int
  19. bufFull bool // true once the buffer has wrapped at least once
  20. powerSum float64
  21. // Slow gain controller
  22. gain float64 // current output gain (0..1)
  23. attackCoeff float64 // gain reduction speed
  24. releaseCoeff float64 // gain recovery speed
  25. }
  26. // NewBS412Limiter creates a BS.412 MPX power limiter.
  27. //
  28. // Parameters:
  29. // - thresholdDBr: power limit in dBr (0 = standard, +3 = relaxed)
  30. // - pilotLevel: pilot amplitude in composite (e.g. 0.09)
  31. // - rdsInjection: RDS amplitude in composite (e.g. 0.04)
  32. // - chunkDurationSec: duration of each processing chunk (e.g. 0.05 for 50ms)
  33. func NewBS412Limiter(thresholdDBr, pilotLevel, rdsInjection, chunkDurationSec float64) *BS412Limiter {
  34. // Reference power: 0 dBr = power of mono sine at peak=1.0 = 0.5
  35. refPower := 0.5
  36. thresholdPow := refPower * math.Pow(10, thresholdDBr/10)
  37. // Constant power contributions from pilot and RDS
  38. pilotPow := pilotLevel * pilotLevel / 2 // sine wave RMS²
  39. rdsPow := rdsInjection * rdsInjection / 4 // BPSK has ~half the power of a sine
  40. audioBudget := thresholdPow - pilotPow - rdsPow
  41. if audioBudget < 0.01 {
  42. audioBudget = 0.01
  43. }
  44. // 60-second window in chunks
  45. windowSec := 60.0
  46. bufLen := int(math.Ceil(windowSec / chunkDurationSec))
  47. if bufLen < 10 {
  48. bufLen = 10
  49. }
  50. // Attack: ~2 seconds (slow, avoids pumping)
  51. // Release: ~5 seconds (very slow, smooth recovery)
  52. attackTC := 2.0 / chunkDurationSec // time constant in chunks
  53. releaseTC := 5.0 / chunkDurationSec
  54. return &BS412Limiter{
  55. enabled: true,
  56. thresholdPow: thresholdPow,
  57. audioBudget: audioBudget,
  58. powerBuf: make([]float64, bufLen),
  59. gain: 1.0,
  60. attackCoeff: 1.0 - math.Exp(-1.0/attackTC),
  61. releaseCoeff: 1.0 - math.Exp(-1.0/releaseTC),
  62. }
  63. }
  64. // UpdateChunkDuration reconfigures the limiter for a new chunk size.
  65. // Call this from GenerateFrame when the actual chunk duration is known
  66. // (computed as samples/sampleRate) to avoid calibration errors if the
  67. // engine's chunk duration differs from the value passed to NewBS412Limiter.
  68. // Safe to call on every chunk; no-ops when duration has not changed.
  69. func (l *BS412Limiter) UpdateChunkDuration(chunkSec float64) {
  70. if chunkSec <= 0 {
  71. return
  72. }
  73. windowSec := 60.0
  74. newBufLen := int(math.Ceil(windowSec / chunkSec))
  75. if newBufLen < 10 {
  76. newBufLen = 10
  77. }
  78. if newBufLen == len(l.powerBuf) {
  79. return // no change
  80. }
  81. // Resize buffer — drop history to avoid stale power readings from the
  82. // old window size distorting the rolling average.
  83. l.powerBuf = make([]float64, newBufLen)
  84. l.bufIdx = 0
  85. l.bufFull = false
  86. l.powerSum = 0
  87. attackTC := 2.0 / chunkSec
  88. releaseTC := 5.0 / chunkSec
  89. l.attackCoeff = 1.0 - math.Exp(-1.0/attackTC)
  90. l.releaseCoeff = 1.0 - math.Exp(-1.0/releaseTC)
  91. }
  92. // ProcessChunk measures the audio power of a chunk and returns the
  93. // gain factor to apply to the audio composite for BS.412 compliance.
  94. // Call once per chunk with the average audio power of that chunk.
  95. //
  96. // audioPower = (1/N) × Σ sample² over the chunk's audio composite samples.
  97. func (l *BS412Limiter) ProcessChunk(audioPower float64) float64 {
  98. if !l.enabled {
  99. return 1.0
  100. }
  101. // Update rolling 60-second power average
  102. old := l.powerBuf[l.bufIdx]
  103. l.powerBuf[l.bufIdx] = audioPower
  104. l.powerSum += audioPower - old
  105. l.bufIdx++
  106. if l.bufIdx >= len(l.powerBuf) {
  107. l.bufIdx = 0
  108. l.bufFull = true
  109. }
  110. // Calculate average power over the window
  111. var count int
  112. if l.bufFull {
  113. count = len(l.powerBuf)
  114. } else {
  115. count = l.bufIdx
  116. }
  117. if count < 1 {
  118. return 1.0
  119. }
  120. avgPower := l.powerSum / float64(count)
  121. // Target gain: bring average audio power to budget
  122. targetGain := 1.0
  123. if avgPower > l.audioBudget && avgPower > 0 {
  124. targetGain = math.Sqrt(l.audioBudget / avgPower)
  125. }
  126. // Smooth gain changes (slow attack, slower release)
  127. if targetGain < l.gain {
  128. l.gain += l.attackCoeff * (targetGain - l.gain)
  129. } else {
  130. l.gain += l.releaseCoeff * (targetGain - l.gain)
  131. }
  132. // Clamp
  133. if l.gain < 0.01 {
  134. l.gain = 0.01
  135. }
  136. if l.gain > 1.0 {
  137. l.gain = 1.0
  138. }
  139. return l.gain
  140. }
  141. // CurrentGain returns the current gain factor (0..1).
  142. // Called at the start of each chunk to get the gain to apply.
  143. func (l *BS412Limiter) CurrentGain() float64 {
  144. return l.gain
  145. }
  146. // CurrentGainDB returns the current gain reduction in dB (negative = reducing).
  147. func (l *BS412Limiter) CurrentGainDB() float64 {
  148. if l.gain <= 0 {
  149. return -100
  150. }
  151. return 20 * math.Log10(l.gain)
  152. }
  153. // Reset clears the power history and restores unity gain.
  154. func (l *BS412Limiter) Reset() {
  155. for i := range l.powerBuf {
  156. l.powerBuf[i] = 0
  157. }
  158. l.bufIdx = 0
  159. l.bufFull = false
  160. l.powerSum = 0
  161. l.gain = 1.0
  162. }