Add F12 hotkey to toggle microphone AGC

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>
This commit is contained in:
Tyler Spivey
2026-08-14 13:16:44 -07:00
co-authored by Claude Opus 5
parent 132df6863a
commit 4393739ffa
8 changed files with 156 additions and 25 deletions
+26 -24
View File
@@ -2,41 +2,43 @@ 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 bool // Whether AGC is enabled
compThreshold float32 // Compression threshold
compRatio float32 // Compression ratio
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 {
return &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
enabled: true, // Enable by default
compThreshold: 0.85, // Compress signals above 85%
compRatio: 2.0, // 2:1 compression ratio (gentler)
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 || len(samples) == 0 {
if !agc.enabled.Load() || len(samples) == 0 {
return
}
@@ -125,12 +127,12 @@ func (agc *AGC) ProcessSamples(samples []int16) {
// SetEnabled enables or disables AGC processing
func (agc *AGC) SetEnabled(enabled bool) {
agc.enabled = enabled
agc.enabled.Store(enabled)
}
// IsEnabled returns whether AGC is enabled
func (agc *AGC) IsEnabled() bool {
return agc.enabled
return agc.enabled.Load()
}
// SetTargetLevel sets the target RMS level (0.0-1.0)