Files
barnard/noise/suppression.go
2026-02-21 02:08:30 -05:00

213 lines
5.4 KiB
Go

package noise
import (
"math"
"sync"
)
// 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 {
mu sync.Mutex
enabled bool
threshold float32
// High-pass filter state for low-frequency rumble/DC removal.
prevInput float32
prevOutput 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 {
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) {
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
}
intensity := s.thresholdToIntensity()
minGain := 1.0 - (0.92 * intensity)
eps := float32(1e-6)
for i, sample := range samples {
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)
}
}
func (s *Suppressor) highPassFilterLocked(input float32) float32 {
// Simple high-pass filter: y[n] = alpha * (y[n-1] + x[n] - x[n-1])
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
// Placeholder for future RNNoise integration.
func (s *Suppressor) ProcessSamplesAdvanced(samples []int16) {
s.ProcessSamples(samples)
}