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.

69 lines
2.2KB

  1. package dsp
  2. import "math"
  3. // StereoLimiter applies identical gain reduction to L and R channels,
  4. // driven by the peak of max(|L|, |R|). This preserves the stereo image
  5. // while preventing either channel from exceeding the ceiling.
  6. //
  7. // Attack is INSTANTANEOUS — gain is reduced in the same sample that
  8. // exceeds the ceiling. This avoids overshoot entirely, which is critical
  9. // because overshoot causes composite clipping that destroys pilot/RDS.
  10. // Unlike hard clipping, gain scaling preserves the waveform shape and
  11. // does not create harmonics.
  12. //
  13. // Release is smooth (exponential decay) to avoid audible pumping.
  14. type StereoLimiter struct {
  15. ceiling float64
  16. releaseCoeff float64
  17. gainReduction float64
  18. }
  19. // NewStereoLimiter creates a stereo-linked limiter with instant attack.
  20. // releaseMs controls how quickly gain recovers after a peak (typ. 50-200ms).
  21. func NewStereoLimiter(ceiling, attackMs, releaseMs, sampleRate float64) *StereoLimiter {
  22. if ceiling <= 0 {
  23. ceiling = 1.0
  24. }
  25. if releaseMs <= 0 {
  26. releaseMs = 100
  27. }
  28. releaseSamples := releaseMs * sampleRate / 1000
  29. return &StereoLimiter{
  30. ceiling: ceiling,
  31. releaseCoeff: 1.0 - math.Exp(-1.0/releaseSamples),
  32. }
  33. }
  34. // Process applies stereo-linked limiting. Both channels receive the
  35. // same gain factor, determined by the louder of the two.
  36. //
  37. // If the peak exceeds ceiling, gain is INSTANTLY reduced (zero overshoot).
  38. // When the signal drops below ceiling, gain recovers smoothly via release.
  39. func (l *StereoLimiter) Process(left, right float64) (float64, float64) {
  40. peak := math.Max(math.Abs(left), math.Abs(right))
  41. // Target: how much gain reduction do we need right now?
  42. targetReduction := 0.0
  43. if peak > l.ceiling {
  44. targetReduction = 1.0 - l.ceiling/peak
  45. }
  46. // Instant attack: if we need MORE reduction, apply it NOW.
  47. // Smooth release: if we need LESS reduction, decay slowly.
  48. if targetReduction > l.gainReduction {
  49. l.gainReduction = targetReduction // instant
  50. } else {
  51. l.gainReduction += l.releaseCoeff * (targetReduction - l.gainReduction) // smooth
  52. }
  53. gain := 1.0 - l.gainReduction
  54. return left * gain, right * gain
  55. }
  56. // Reset clears the limiter state.
  57. func (l *StereoLimiter) Reset() {
  58. l.gainReduction = 0
  59. }