From 3db526f42b21d23163ef435d1d465aca9ae6a6c2 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Sat, 21 Feb 2026 02:08:30 -0500 Subject: [PATCH] Noise suppression tweaks. --- README.md | 7 +- config/user_config.go | 19 +++- noise/suppression.go | 230 +++++++++++++++++++++++++++--------------- 3 files changed, 171 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index 2a95226..2965159 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ Barnard includes real-time noise suppression for microphone input to filter out ### Features - **Real-time processing**: Noise suppression is applied during audio capture with minimal latency -- **Configurable threshold**: Adjustable noise gate threshold (default: 0.02) +- **Configurable amount**: Adjustable suppression amount via threshold value (default: `0.08`) - **Persistent settings**: Noise suppression preferences are saved in your configuration file - **Multiple control methods**: Toggle via hotkey, command line flag, or FIFO commands @@ -57,10 +57,11 @@ Barnard includes real-time noise suppression for microphone input to filter out ### Configuration Example ```toml noisesuppressionenabled = true -noisesuppressionthreshold = 0.02 +noisesuppressionthreshold = 0.08 ``` -The noise suppression algorithm uses a combination of high-pass filtering and noise gating to reduce unwanted background sounds while preserving voice quality. +`noisesuppressionthreshold` accepts values from `0.0` to `1.0`, where higher values apply stronger suppression. +The noise suppression algorithm uses adaptive noise-floor tracking, transient suppression, and smoothed gain reduction to reduce background noise while preserving voice quality. ## FIFO Control diff --git a/config/user_config.go b/config/user_config.go index 07c59b9..3a8704a 100644 --- a/config/user_config.go +++ b/config/user_config.go @@ -128,7 +128,7 @@ func (c *Config) LoadConfig() { jc.NoiseSuppressionEnabled = &enabled } if c.config.NoiseSuppressionThreshold == nil { - threshold := float32(0.02) + threshold := float32(0.08) jc.NoiseSuppressionThreshold = &threshold } if c.config.VoiceEffect == nil { @@ -249,12 +249,25 @@ func (c *Config) SetNoiseSuppressionEnabled(enabled bool) { func (c *Config) GetNoiseSuppressionThreshold() float32 { if c.config.NoiseSuppressionThreshold == nil { - return 0.02 + return 0.08 } - return *c.config.NoiseSuppressionThreshold + threshold := *c.config.NoiseSuppressionThreshold + if threshold < 0.0 { + return 0.0 + } + if threshold > 1.0 { + return 1.0 + } + return threshold } func (c *Config) SetNoiseSuppressionThreshold(threshold float32) { + if threshold < 0.0 { + threshold = 0.0 + } + if threshold > 1.0 { + threshold = 1.0 + } c.config.NoiseSuppressionThreshold = &threshold c.SaveConfig() } diff --git a/noise/suppression.go b/noise/suppression.go index 262d3c2..a67ca46 100644 --- a/noise/suppression.go +++ b/noise/suppression.go @@ -2,6 +2,7 @@ package noise import ( "math" + "sync" ) // Ensure Suppressor implements the NoiseProcessor interface @@ -12,129 +13,200 @@ var _ interface { // Suppressor handles noise suppression for audio samples type Suppressor struct { - enabled bool - threshold float32 - gainFactor float32 - - // Simple high-pass filter state for DC removal + mu sync.Mutex + + enabled bool + threshold float32 + + // High-pass filter state for low-frequency rumble/DC removal. prevInput float32 prevOutput float32 - alpha float32 - - // Click detection state - clickThreshold float32 - clickDecay float32 - recentClickEnergy float32 + hpAlpha float32 + + // Adaptive suppression state. + envelope float32 + noiseFloor float32 + suppressionGain float32 + clickEnergy float32 + + // Tunables. + envelopeAttack float32 + envelopeRelease float32 + noiseAttack float32 + noiseRelease float32 + gainAttack float32 + gainRelease float32 + speechRatio float32 + clickDecay float32 + minNoiseFloor float32 } // NewSuppressor creates a new noise suppressor func NewSuppressor() *Suppressor { - return &Suppressor{ - enabled: false, - threshold: 0.01, // Reduced noise threshold level for less aggressive filtering - gainFactor: 0.9, // Less aggressive gain reduction for noise - alpha: 0.98, // More stable high-pass filter coefficient - clickThreshold: 0.15, // Threshold for detecting keyboard clicks - clickDecay: 0.95, // How quickly click energy decays - recentClickEnergy: 0.0, // Tracks recent click activity + s := &Suppressor{ + enabled: false, + threshold: 0.08, + hpAlpha: 0.995, + envelopeAttack: 0.18, + envelopeRelease: 0.02, + noiseAttack: 0.08, + noiseRelease: 0.002, + gainAttack: 0.35, + gainRelease: 0.02, + speechRatio: 4.0, + clickDecay: 0.93, + minNoiseFloor: 0.0008, + suppressionGain: 1.0, } + s.resetStateLocked() + return s } // SetEnabled enables or disables noise suppression func (s *Suppressor) SetEnabled(enabled bool) { + s.mu.Lock() + defer s.mu.Unlock() + if s.enabled == enabled { + return + } s.enabled = enabled + s.resetStateLocked() } // IsEnabled returns whether noise suppression is enabled func (s *Suppressor) IsEnabled() bool { + s.mu.Lock() + defer s.mu.Unlock() return s.enabled } // SetThreshold sets the noise threshold (0.0 to 1.0) func (s *Suppressor) SetThreshold(threshold float32) { - if threshold >= 0.0 && threshold <= 1.0 { - s.threshold = threshold - } + s.mu.Lock() + defer s.mu.Unlock() + s.threshold = clampFloat32(threshold, 0.0, 1.0) } // GetThreshold returns the current noise threshold func (s *Suppressor) GetThreshold() float32 { + s.mu.Lock() + defer s.mu.Unlock() return s.threshold } // ProcessSamples applies noise suppression to audio samples func (s *Suppressor) ProcessSamples(samples []int16) { + s.mu.Lock() + defer s.mu.Unlock() + if !s.enabled || len(samples) == 0 { return } - // Calculate frame energy for click detection - var frameEnergy float32 = 0.0 - for _, sample := range samples { - floatSample := float32(sample) / 32767.0 - frameEnergy += floatSample * floatSample - } - frameEnergy = float32(math.Sqrt(float64(frameEnergy / float32(len(samples))))) - - // Detect sudden energy spikes (likely keyboard clicks) - energySpike := frameEnergy - s.recentClickEnergy - isClick := energySpike > s.clickThreshold && frameEnergy > 0.05 - - // Update recent click energy with decay - s.recentClickEnergy = s.recentClickEnergy*s.clickDecay + frameEnergy*(1.0-s.clickDecay) - - // Improved noise suppression algorithm + intensity := s.thresholdToIntensity() + minGain := 1.0 - (0.92 * intensity) + eps := float32(1e-6) + for i, sample := range samples { - // Convert to float for processing - floatSample := float32(sample) / 32767.0 - - // Apply high-pass filter for DC removal - filtered := s.highPassFilter(floatSample) - - // Calculate signal strength (RMS-like) - strength := float32(math.Abs(float64(filtered))) - - // Apply noise gate with smooth transition - var gainReduction float32 = 1.0 - - // If we detected a click, apply stronger suppression - if isClick { - gainReduction = s.gainFactor * 0.3 // Much stronger reduction for clicks - } else if strength < s.threshold { - // Normal noise gate for low-level sounds - gainReduction = strength / s.threshold - if gainReduction < s.gainFactor { - gainReduction = s.gainFactor - } - } - - // Apply gain reduction - processed := filtered * gainReduction - - // Convert back to int16 with proper clipping - processedInt := processed * 32767.0 - if processedInt > 32767 { - processedInt = 32767 - } else if processedInt < -32767 { - processedInt = -32767 - } - - samples[i] = int16(processedInt) + floatSample := float32(sample) / 32768.0 + filtered := s.highPassFilterLocked(floatSample) + absSample := float32(math.Abs(float64(filtered))) + + s.updateEnvelopeLocked(absSample) + s.updateNoiseFloorLocked() + + snr := s.envelope / (s.noiseFloor + eps) + voicePresence := clampFloat32((snr-1.0)/(s.speechRatio-1.0), 0.0, 1.0) + + targetGain := minGain + ((1.0 - minGain) * voicePresence) + targetGain = s.applyTransientSuppressionLocked(absSample, voicePresence, minGain, targetGain) + + s.applyGainSmoothingLocked(targetGain) + + processed := filtered * s.suppressionGain + processed = clampFloat32(processed, -1.0, 1.0) + samples[i] = int16(processed * 32767.0) } } -// highPassFilter applies a simple high-pass filter to remove DC component -func (s *Suppressor) highPassFilter(input float32) float32 { +func (s *Suppressor) highPassFilterLocked(input float32) float32 { // Simple high-pass filter: y[n] = alpha * (y[n-1] + x[n] - x[n-1]) - output := s.alpha * (s.prevOutput + input - s.prevInput) + output := s.hpAlpha * (s.prevOutput + input - s.prevInput) s.prevInput = input s.prevOutput = output return output } +func (s *Suppressor) thresholdToIntensity() float32 { + // Keep lower legacy threshold values meaningful while allowing up to very aggressive suppression. + return 1.0 - float32(math.Exp(float64(-28.0*clampFloat32(s.threshold, 0.0, 1.0)))) +} + +func (s *Suppressor) updateEnvelopeLocked(absSample float32) { + if absSample > s.envelope { + s.envelope += s.envelopeAttack * (absSample - s.envelope) + } else { + s.envelope += s.envelopeRelease * (absSample - s.envelope) + } + if s.envelope < s.minNoiseFloor { + s.envelope = s.minNoiseFloor + } +} + +func (s *Suppressor) updateNoiseFloorLocked() { + coef := s.noiseRelease + if s.envelope < s.noiseFloor*2.2 { + coef = s.noiseAttack + } + s.noiseFloor += coef * (s.envelope - s.noiseFloor) + if s.noiseFloor < s.minNoiseFloor { + s.noiseFloor = s.minNoiseFloor + } +} + +func (s *Suppressor) applyTransientSuppressionLocked(absSample float32, voicePresence float32, minGain float32, targetGain float32) float32 { + s.clickEnergy = (s.clickEnergy * s.clickDecay) + (absSample * (1.0 - s.clickDecay)) + transient := absSample - s.clickEnergy + transientThreshold := 0.04 + (0.08 * (1.0 - voicePresence)) + if transient > transientThreshold && voicePresence < 0.65 { + clickGain := minGain * 0.55 + if clickGain < targetGain { + targetGain = clickGain + } + } + return clampFloat32(targetGain, 0.02, 1.0) +} + +func (s *Suppressor) applyGainSmoothingLocked(targetGain float32) { + if targetGain < s.suppressionGain { + s.suppressionGain += s.gainAttack * (targetGain - s.suppressionGain) + } else { + s.suppressionGain += s.gainRelease * (targetGain - s.suppressionGain) + } + s.suppressionGain = clampFloat32(s.suppressionGain, 0.02, 1.0) +} + +func (s *Suppressor) resetStateLocked() { + s.prevInput = 0.0 + s.prevOutput = 0.0 + s.envelope = s.minNoiseFloor + s.noiseFloor = s.minNoiseFloor + s.suppressionGain = 1.0 + s.clickEnergy = 0.0 +} + +func clampFloat32(value float32, min float32, max float32) float32 { + if value < min { + return min + } + if value > max { + return max + } + return value +} + // ProcessSamplesAdvanced applies more sophisticated noise suppression -// This is a placeholder for future RNNoise integration +// Placeholder for future RNNoise integration. func (s *Suppressor) ProcessSamplesAdvanced(samples []int16) { - // TODO: Integrate RNNoise or other advanced algorithms s.ProcessSamples(samples) -} \ No newline at end of file +}