Fix audio distortion and improve noise suppression.

- 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>
This commit is contained in:
Storm Dragon
2025-08-20 03:50:14 -04:00
parent df7159bad1
commit b966106727
2 changed files with 78 additions and 29 deletions

View File

@@ -226,28 +226,49 @@ func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) {
if isStereo { if isStereo {
// Process stereo samples as pairs // Process stereo samples as pairs
for i := 0; i < samples*2; i += 2 { for i := 0; i < samples*2; i += 2 {
// Process left channel // Process left channel with saturation protection
sample := packet.AudioBuffer[i] sample := packet.AudioBuffer[i]
if boost > 1 { if boost > 1 {
sample = int16((int32(sample) * int32(boost))) boosted := int32(sample) * int32(boost)
if boosted > 32767 {
sample = 32767
} else if boosted < -32767 {
sample = -32767
} else {
sample = int16(boosted)
}
} }
binary.LittleEndian.PutUint16(raw[rawPtr:], uint16(sample)) binary.LittleEndian.PutUint16(raw[rawPtr:], uint16(sample))
rawPtr += 2 rawPtr += 2
// Process right channel // Process right channel with saturation protection
sample = packet.AudioBuffer[i+1] sample = packet.AudioBuffer[i+1]
if boost > 1 { if boost > 1 {
sample = int16((int32(sample) * int32(boost))) boosted := int32(sample) * int32(boost)
if boosted > 32767 {
sample = 32767
} else if boosted < -32767 {
sample = -32767
} else {
sample = int16(boosted)
}
} }
binary.LittleEndian.PutUint16(raw[rawPtr:], uint16(sample)) binary.LittleEndian.PutUint16(raw[rawPtr:], uint16(sample))
rawPtr += 2 rawPtr += 2
} }
} else { } else {
// Process mono samples // Process mono samples with saturation protection
for i := 0; i < samples; i++ { for i := 0; i < samples; i++ {
sample := packet.AudioBuffer[i] sample := packet.AudioBuffer[i]
if boost > 1 { if boost > 1 {
sample = int16((int32(sample) * int32(boost))) boosted := int32(sample) * int32(boost)
if boosted > 32767 {
sample = 32767
} else if boosted < -32767 {
sample = -32767
} else {
sample = int16(boosted)
}
} }
binary.LittleEndian.PutUint16(raw[rawPtr:], uint16(sample)) binary.LittleEndian.PutUint16(raw[rawPtr:], uint16(sample))
rawPtr += 2 rawPtr += 2

View File

@@ -20,15 +20,23 @@ type Suppressor struct {
prevInput float32 prevInput float32
prevOutput float32 prevOutput float32
alpha float32 alpha float32
// Click detection state
clickThreshold float32
clickDecay float32
recentClickEnergy float32
} }
// NewSuppressor creates a new noise suppressor // NewSuppressor creates a new noise suppressor
func NewSuppressor() *Suppressor { func NewSuppressor() *Suppressor {
return &Suppressor{ return &Suppressor{
enabled: false, enabled: false,
threshold: 0.02, // Noise threshold level threshold: 0.01, // Reduced noise threshold level for less aggressive filtering
gainFactor: 0.8, // Gain reduction for noise gainFactor: 0.9, // Less aggressive gain reduction for noise
alpha: 0.95, // High-pass filter coefficient 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
} }
} }
@@ -60,7 +68,22 @@ func (s *Suppressor) ProcessSamples(samples []int16) {
return return
} }
// Simple noise suppression algorithm // 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 { for i, sample := range samples {
// Convert to float for processing // Convert to float for processing
floatSample := float32(sample) / 32767.0 floatSample := float32(sample) / 32767.0
@@ -68,30 +91,35 @@ func (s *Suppressor) ProcessSamples(samples []int16) {
// Apply high-pass filter for DC removal // Apply high-pass filter for DC removal
filtered := s.highPassFilter(floatSample) filtered := s.highPassFilter(floatSample)
// Calculate signal strength // Calculate signal strength (RMS-like)
strength := float32(math.Abs(float64(filtered))) strength := float32(math.Abs(float64(filtered)))
// Apply noise gate // Apply noise gate with smooth transition
if strength < s.threshold { var gainReduction float32 = 1.0
// Below threshold - reduce gain
filtered *= s.gainFactor // 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 simple spectral subtraction-like effect // Apply gain reduction
// If signal is weak, reduce it further processed := filtered * gainReduction
if strength < s.threshold * 2 {
filtered *= (strength / (s.threshold * 2)) // Convert back to int16 with proper clipping
processedInt := processed * 32767.0
if processedInt > 32767 {
processedInt = 32767
} else if processedInt < -32767 {
processedInt = -32767
} }
// Convert back to int16 samples[i] = int16(processedInt)
processed := filtered * 32767.0
if processed > 32767 {
processed = 32767
} else if processed < -32767 {
processed = -32767
}
samples[i] = int16(processed)
} }
} }