- Add saturation protection to prevent crackling at high volume boost levels - Implement keyboard click detection using energy spike analysis - Reduce aggressive noise suppression parameters to prevent audio artifacts - Apply stronger suppression specifically for detected keyboard clicks while preserving voice quality 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
140 lines
4.0 KiB
Go
140 lines
4.0 KiB
Go
package noise
|
|
|
|
import (
|
|
"math"
|
|
)
|
|
|
|
// Ensure Suppressor implements the NoiseProcessor interface
|
|
var _ interface {
|
|
ProcessSamples(samples []int16)
|
|
IsEnabled() bool
|
|
} = (*Suppressor)(nil)
|
|
|
|
// Suppressor handles noise suppression for audio samples
|
|
type Suppressor struct {
|
|
enabled bool
|
|
threshold float32
|
|
gainFactor float32
|
|
|
|
// Simple high-pass filter state for DC removal
|
|
prevInput float32
|
|
prevOutput float32
|
|
alpha float32
|
|
|
|
// Click detection state
|
|
clickThreshold float32
|
|
clickDecay float32
|
|
recentClickEnergy 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
|
|
}
|
|
}
|
|
|
|
// SetEnabled enables or disables noise suppression
|
|
func (s *Suppressor) SetEnabled(enabled bool) {
|
|
s.enabled = enabled
|
|
}
|
|
|
|
// IsEnabled returns whether noise suppression is enabled
|
|
func (s *Suppressor) IsEnabled() bool {
|
|
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
|
|
}
|
|
}
|
|
|
|
// GetThreshold returns the current noise threshold
|
|
func (s *Suppressor) GetThreshold() float32 {
|
|
return s.threshold
|
|
}
|
|
|
|
// ProcessSamples applies noise suppression to audio samples
|
|
func (s *Suppressor) ProcessSamples(samples []int16) {
|
|
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
|
|
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)
|
|
}
|
|
}
|
|
|
|
// highPassFilter applies a simple high-pass filter to remove DC component
|
|
func (s *Suppressor) highPassFilter(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)
|
|
s.prevInput = input
|
|
s.prevOutput = output
|
|
return output
|
|
}
|
|
|
|
// ProcessSamplesAdvanced applies more sophisticated noise suppression
|
|
// This is a placeholder for future RNNoise integration
|
|
func (s *Suppressor) ProcessSamplesAdvanced(samples []int16) {
|
|
// TODO: Integrate RNNoise or other advanced algorithms
|
|
s.ProcessSamples(samples)
|
|
} |