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:
@@ -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
|
||||||
|
@@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user