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

83 рядки
2.8KB

  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 time is tuned for psychoacoustic burst masking (Bonello, JAES 2007):
  8. // A ~2ms attack lets initial transient peaks pass through to the hard
  9. // clipper, where they're clipped for <5ms. The human auditory system
  10. // cannot resolve distortion in bursts shorter than ~5ms (burst masking
  11. // provides up to 36 dB of additional masking). This gives higher average
  12. // loudness without audible distortion, compared to instant attack which
  13. // suppresses transients that were already inaudible.
  14. //
  15. // Release is smooth (exponential decay) to avoid audible pumping.
  16. type StereoLimiter struct {
  17. ceiling float64
  18. attackCoeff float64
  19. releaseCoeff float64
  20. gainReduction float64
  21. }
  22. // NewStereoLimiter creates a stereo-linked limiter.
  23. // attackMs ~2ms for burst-masking benefit (Bonello). 0 = instant (legacy).
  24. // releaseMs controls how quickly gain recovers after a peak (typ. 50-200ms).
  25. func NewStereoLimiter(ceiling, attackMs, releaseMs, sampleRate float64) *StereoLimiter {
  26. if ceiling <= 0 {
  27. ceiling = 1.0
  28. }
  29. if releaseMs <= 0 {
  30. releaseMs = 100
  31. }
  32. var attackCoeff float64
  33. if attackMs > 0 {
  34. attackSamples := attackMs * sampleRate / 1000
  35. attackCoeff = 1.0 - math.Exp(-1.0/attackSamples)
  36. } else {
  37. attackCoeff = 1.0 // instant: full step in one sample
  38. }
  39. releaseSamples := releaseMs * sampleRate / 1000
  40. return &StereoLimiter{
  41. ceiling: ceiling,
  42. attackCoeff: attackCoeff,
  43. releaseCoeff: 1.0 - math.Exp(-1.0/releaseSamples),
  44. }
  45. }
  46. // Process applies stereo-linked limiting. Both channels receive the
  47. // same gain factor, determined by the louder of the two.
  48. //
  49. // With attackMs > 0: transient peaks that exceed the ceiling are NOT
  50. // instantly suppressed. They pass through to the downstream hard clipper,
  51. // which clips them for a few ms until the limiter catches up. These
  52. // sub-5ms clip artifacts are inaudible due to psychoacoustic burst masking.
  53. func (l *StereoLimiter) Process(left, right float64) (float64, float64) {
  54. peak := math.Max(math.Abs(left), math.Abs(right))
  55. targetReduction := 0.0
  56. if peak > l.ceiling {
  57. targetReduction = 1.0 - l.ceiling/peak
  58. }
  59. if targetReduction > l.gainReduction {
  60. // Attack: smooth ramp toward target (or instant if attackCoeff=1.0)
  61. l.gainReduction += l.attackCoeff * (targetReduction - l.gainReduction)
  62. } else {
  63. // Release: smooth decay back toward zero reduction
  64. l.gainReduction += l.releaseCoeff * (targetReduction - l.gainReduction)
  65. }
  66. gain := 1.0 - l.gainReduction
  67. return left * gain, right * gain
  68. }
  69. // Reset clears the limiter state.
  70. func (l *StereoLimiter) Reset() {
  71. l.gainReduction = 0
  72. }