Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

191 行
5.6KB

  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. // BUG-G fix: float64 accumulation over 1200+ chunks can drift slightly
  106. // negative due to rounding. A negative powerSum → negative avgPower →
  107. // math.Sqrt of negative → NaN → gain becomes NaN, silently disabling
  108. // the limiter. Clamp to zero to keep the invariant powerSum >= 0.
  109. if l.powerSum < 0 {
  110. l.powerSum = 0
  111. }
  112. l.bufIdx++
  113. if l.bufIdx >= len(l.powerBuf) {
  114. l.bufIdx = 0
  115. l.bufFull = true
  116. }
  117. // Calculate average power over the window
  118. var count int
  119. if l.bufFull {
  120. count = len(l.powerBuf)
  121. } else {
  122. count = l.bufIdx
  123. }
  124. if count < 1 {
  125. return 1.0
  126. }
  127. avgPower := l.powerSum / float64(count)
  128. // Target gain: bring average audio power to budget
  129. targetGain := 1.0
  130. if avgPower > l.audioBudget && avgPower > 0 {
  131. targetGain = math.Sqrt(l.audioBudget / avgPower)
  132. }
  133. // Smooth gain changes (slow attack, slower release)
  134. if targetGain < l.gain {
  135. l.gain += l.attackCoeff * (targetGain - l.gain)
  136. } else {
  137. l.gain += l.releaseCoeff * (targetGain - l.gain)
  138. }
  139. // Clamp
  140. if l.gain < 0.01 {
  141. l.gain = 0.01
  142. }
  143. if l.gain > 1.0 {
  144. l.gain = 1.0
  145. }
  146. return l.gain
  147. }
  148. // CurrentGain returns the current gain factor (0..1).
  149. // Called at the start of each chunk to get the gain to apply.
  150. func (l *BS412Limiter) CurrentGain() float64 {
  151. return l.gain
  152. }
  153. // CurrentGainDB returns the current gain reduction in dB (negative = reducing).
  154. func (l *BS412Limiter) CurrentGainDB() float64 {
  155. if l.gain <= 0 {
  156. return -100
  157. }
  158. return 20 * math.Log10(l.gain)
  159. }
  160. // Reset clears the power history and restores unity gain.
  161. func (l *BS412Limiter) Reset() {
  162. for i := range l.powerBuf {
  163. l.powerBuf[i] = 0
  164. }
  165. l.bufIdx = 0
  166. l.bufFull = false
  167. l.powerSum = 0
  168. l.gain = 1.0
  169. }