Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

155 lines
4.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. // ProcessChunk measures the audio power of a chunk and returns the
  65. // gain factor to apply to the audio composite for BS.412 compliance.
  66. // Call once per chunk with the average audio power of that chunk.
  67. //
  68. // audioPower = (1/N) × Σ sample² over the chunk's audio composite samples.
  69. func (l *BS412Limiter) ProcessChunk(audioPower float64) float64 {
  70. if !l.enabled {
  71. return 1.0
  72. }
  73. // Update rolling 60-second power average
  74. old := l.powerBuf[l.bufIdx]
  75. l.powerBuf[l.bufIdx] = audioPower
  76. l.powerSum += audioPower - old
  77. l.bufIdx++
  78. if l.bufIdx >= len(l.powerBuf) {
  79. l.bufIdx = 0
  80. l.bufFull = true
  81. }
  82. // Calculate average power over the window
  83. var count int
  84. if l.bufFull {
  85. count = len(l.powerBuf)
  86. } else {
  87. count = l.bufIdx
  88. }
  89. if count < 1 {
  90. return 1.0
  91. }
  92. avgPower := l.powerSum / float64(count)
  93. // Target gain: bring average audio power to budget
  94. targetGain := 1.0
  95. if avgPower > l.audioBudget && avgPower > 0 {
  96. targetGain = math.Sqrt(l.audioBudget / avgPower)
  97. }
  98. // Smooth gain changes (slow attack, slower release)
  99. if targetGain < l.gain {
  100. l.gain += l.attackCoeff * (targetGain - l.gain)
  101. } else {
  102. l.gain += l.releaseCoeff * (targetGain - l.gain)
  103. }
  104. // Clamp
  105. if l.gain < 0.01 {
  106. l.gain = 0.01
  107. }
  108. if l.gain > 1.0 {
  109. l.gain = 1.0
  110. }
  111. return l.gain
  112. }
  113. // CurrentGain returns the current gain factor (0..1).
  114. // Called at the start of each chunk to get the gain to apply.
  115. func (l *BS412Limiter) CurrentGain() float64 {
  116. return l.gain
  117. }
  118. // CurrentGainDB returns the current gain reduction in dB (negative = reducing).
  119. func (l *BS412Limiter) CurrentGainDB() float64 {
  120. if l.gain <= 0 {
  121. return -100
  122. }
  123. return 20 * math.Log10(l.gain)
  124. }
  125. // Reset clears the power history and restores unity gain.
  126. func (l *BS412Limiter) Reset() {
  127. for i := range l.powerBuf {
  128. l.powerBuf[i] = 0
  129. }
  130. l.bufIdx = 0
  131. l.bufFull = false
  132. l.powerSum = 0
  133. l.gain = 1.0
  134. }