package audio import ( "math" "sync/atomic" ) // AGC (Automatic Gain Control) processor for voice normalization type AGC struct { targetLevel float32 // Target RMS level (0.0-1.0) maxGain float32 // Maximum gain multiplier minGain float32 // Minimum gain multiplier attackTime float32 // Attack time coefficient releaseTime float32 // Release time coefficient currentGain float32 // Current gain value envelope float32 // Signal envelope enabled atomic.Bool // Whether AGC is enabled; toggled outside the capture goroutine compThreshold float32 // Compression threshold compRatio float32 // Compression ratio } // NewAGC creates a new AGC processor with sensible defaults for voice func NewAGC() *AGC { agc := &AGC{ targetLevel: 0.12, // Target 12% of max amplitude (conservative level) maxGain: 4.0, // Maximum 4x gain (about 12dB) minGain: 0.25, // Minimum 0.25x gain (-12dB) attackTime: 0.008, // Fast attack (8ms) releaseTime: 0.15, // Slower release (150ms) currentGain: 1.0, // Start with unity gain envelope: 0.0, // Start with zero envelope compThreshold: 0.85, // Compress signals above 85% compRatio: 2.0, // 2:1 compression ratio (gentler) } agc.enabled.Store(true) // Enable by default return agc } // ProcessSamples applies AGC processing to audio samples func (agc *AGC) ProcessSamples(samples []int16) { if !agc.enabled.Load() || len(samples) == 0 { return } // Convert samples to float32 for processing floatSamples := make([]float32, len(samples)) for i, sample := range samples { floatSamples[i] = float32(sample) / 32768.0 } // Calculate RMS level for gain control rmsSum := float32(0.0) for _, sample := range floatSamples { rmsSum += sample * sample } rms := float32(math.Sqrt(float64(rmsSum / float32(len(floatSamples))))) // Update envelope with peak detection for compression peak := float32(0.0) for _, sample := range floatSamples { absample := float32(math.Abs(float64(sample))) if absample > peak { peak = absample } } // Envelope following if peak > agc.envelope { agc.envelope += (peak - agc.envelope) * agc.attackTime } else { agc.envelope += (peak - agc.envelope) * agc.releaseTime } // Calculate desired gain based on RMS var desiredGain float32 if rms > 0.001 { // Avoid division by zero for very quiet signals desiredGain = agc.targetLevel / rms } else { desiredGain = agc.maxGain // Boost very quiet signals } // Apply gain limits if desiredGain > agc.maxGain { desiredGain = agc.maxGain } if desiredGain < agc.minGain { desiredGain = agc.minGain } // Smooth gain changes if desiredGain > agc.currentGain { agc.currentGain += (desiredGain - agc.currentGain) * agc.attackTime } else { agc.currentGain += (desiredGain - agc.currentGain) * agc.releaseTime } // Apply AGC gain to samples for i, sample := range floatSamples { processed := sample * agc.currentGain // Apply compression for loud signals if agc.envelope > agc.compThreshold { // Calculate compression amount overage := agc.envelope - agc.compThreshold compAmount := overage / agc.compRatio compGain := (agc.compThreshold + compAmount) / agc.envelope processed *= compGain } // Soft limiting to prevent clipping if processed > 0.95 { processed = 0.95 + (processed-0.95)*0.2 } else if processed < -0.95 { processed = -0.95 + (processed+0.95)*0.2 } // Convert back to int16 intSample := int32(processed * 32767.0) if intSample > 32767 { intSample = 32767 } else if intSample < -32767 { intSample = -32767 } samples[i] = int16(intSample) } } // SetEnabled enables or disables AGC processing func (agc *AGC) SetEnabled(enabled bool) { agc.enabled.Store(enabled) } // IsEnabled returns whether AGC is enabled func (agc *AGC) IsEnabled() bool { return agc.enabled.Load() } // SetTargetLevel sets the target RMS level (0.0-1.0) func (agc *AGC) SetTargetLevel(level float32) { if level > 0.0 && level < 1.0 { agc.targetLevel = level } } // SetMaxGain sets the maximum gain multiplier func (agc *AGC) SetMaxGain(gain float32) { if gain > 1.0 && gain <= 10.0 { agc.maxGain = gain } } // GetCurrentGain returns the current gain being applied func (agc *AGC) GetCurrentGain() float32 { return agc.currentGain } // Reset resets the AGC state func (agc *AGC) Reset() { agc.currentGain = 1.0 agc.envelope = 0.0 }