Automatic gain control was always on with no way to turn it off. Toggle it with F12, the /agc command, or the agc FIFO command, and persist the choice in the configuration file the same way noise suppression does. AgcEnabled defaults to true so existing setups keep their current behavior, and the saved value is applied to the stream on connect. The enabled flag becomes an atomic.Bool because the capture goroutine reads it while the UI goroutine writes it, and the lazily created right channel AGC now inherits the left channel's state. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
161 lines
4.4 KiB
Go
161 lines
4.4 KiB
Go
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
|
|
} |