Files
barnard/audio/agc.go
2025-12-10 11:20:08 -05: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.18, // Target 18% of max amplitude (balanced level)
maxGain: 8.0, // Maximum 8x gain (about 18dB)
minGain: 0.1, // Minimum 0.1x gain (-20dB)
attackTime: 0.005, // Fast attack (5ms)
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
} 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.90 {
processed = 0.90 + (processed-0.90)*0.1
} else if processed < -0.90 {
processed = -0.90 + (processed+0.90)*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
}