Files
barnard/audio/agc.go
Storm Dragon 82b308000d Add automatic gain control for outgoing microphone audio.
This addresses low microphone volume issues by automatically normalizing
outgoing audio levels with dynamic range compression and soft limiting.
The AGC is always enabled and applies voice-optimized parameters to
ensure consistent audio levels are sent to other users while preserving
manual volume control for incoming audio.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-31 15:33:18 -04:00

159 lines
4.3 KiB
Go

package audio
import (
"math"
)
// 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 bool // Whether AGC is enabled
compThreshold float32 // Compression threshold
compRatio float32 // Compression ratio
}
// NewAGC creates a new AGC processor with sensible defaults for voice
func NewAGC() *AGC {
return &AGC{
targetLevel: 0.15, // Target 15% of max amplitude
maxGain: 6.0, // Maximum 6x gain (about 15.5dB)
minGain: 0.1, // Minimum 0.1x gain (-20dB)
attackTime: 0.001, // Fast attack (1ms)
releaseTime: 0.1, // Slower release (100ms)
currentGain: 1.0, // Start with unity gain
envelope: 0.0, // Start with zero envelope
enabled: true, // Enable by default
compThreshold: 0.7, // Compress signals above 70%
compRatio: 3.0, // 3:1 compression ratio
}
}
// ProcessSamples applies AGC processing to audio samples
func (agc *AGC) ProcessSamples(samples []int16) {
if !agc.enabled || 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 * 0.1
} 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.1
} else if processed < -0.95 {
processed = -0.95 + (processed+0.95)*0.1
}
// 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 = enabled
}
// IsEnabled returns whether AGC is enabled
func (agc *AGC) IsEnabled() bool {
return agc.enabled
}
// 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
}