Use RNNoise for noise suppression
This commit is contained in:
@@ -40,11 +40,10 @@ Voice effects are applied to your outgoing audio in real-time, after noise suppr
|
|||||||
|
|
||||||
## Noise Suppression
|
## Noise Suppression
|
||||||
|
|
||||||
Barnard includes real-time noise suppression for microphone input to filter out background noise such as keyboard typing, computer fans, and other environmental sounds.
|
Barnard includes RNNoise-based real-time noise suppression for microphone input to filter out background noise such as keyboard typing, computer fans, and other environmental sounds.
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
- **Real-time processing**: Noise suppression is applied during audio capture with minimal latency
|
- **Real-time processing**: Noise suppression is applied during audio capture with minimal latency
|
||||||
- **Configurable amount**: Adjustable suppression amount via threshold value (default: `0.08`)
|
|
||||||
- **Persistent settings**: Noise suppression preferences are saved in your configuration file
|
- **Persistent settings**: Noise suppression preferences are saved in your configuration file
|
||||||
- **Multiple control methods**: Toggle via hotkey, command line flag, or FIFO commands
|
- **Multiple control methods**: Toggle via hotkey, command line flag, or FIFO commands
|
||||||
|
|
||||||
@@ -52,16 +51,14 @@ Barnard includes real-time noise suppression for microphone input to filter out
|
|||||||
- **F9 key**: Toggle noise suppression on/off (configurable hotkey)
|
- **F9 key**: Toggle noise suppression on/off (configurable hotkey)
|
||||||
- **Command line**: Use `--noise-suppression` flag to enable at startup
|
- **Command line**: Use `--noise-suppression` flag to enable at startup
|
||||||
- **FIFO command**: Send `noise` command to toggle during runtime
|
- **FIFO command**: Send `noise` command to toggle during runtime
|
||||||
- **Configuration**: Set `noisesuppressionenabled` and `noisesuppressionthreshold` in `~/.barnard.toml`
|
- **Configuration**: Set `noisesuppressionenabled` in `~/.barnard.toml`
|
||||||
|
|
||||||
### Configuration Example
|
### Configuration Example
|
||||||
```toml
|
```toml
|
||||||
noisesuppressionenabled = true
|
noisesuppressionenabled = true
|
||||||
noisesuppressionthreshold = 0.08
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`noisesuppressionthreshold` accepts values from `0.0` to `1.0`, where higher values apply stronger suppression.
|
RNNoise is a required build and runtime dependency.
|
||||||
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
|
## FIFO Control
|
||||||
|
|
||||||
|
|||||||
+14
-44
@@ -18,21 +18,20 @@ type Config struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type exportableConfig struct {
|
type exportableConfig struct {
|
||||||
Hotkeys *Hotkeys
|
Hotkeys *Hotkeys
|
||||||
AudioDriver *string
|
AudioDriver *string
|
||||||
MicVolume *float32
|
MicVolume *float32
|
||||||
InputDevice *string
|
InputDevice *string
|
||||||
OutputDevice *string
|
OutputDevice *string
|
||||||
Servers []*server
|
Servers []*server
|
||||||
DefaultServer *string
|
DefaultServer *string
|
||||||
Username *string
|
Username *string
|
||||||
NotifyCommand *string
|
NotifyCommand *string
|
||||||
NoiseSuppressionEnabled *bool
|
NoiseSuppressionEnabled *bool
|
||||||
NoiseSuppressionThreshold *float32
|
VoiceEffect *int
|
||||||
VoiceEffect *int
|
Certificate *string
|
||||||
Certificate *string
|
RecordingFormat *string
|
||||||
RecordingFormat *string
|
RecordingDirectory *string
|
||||||
RecordingDirectory *string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type server struct {
|
type server struct {
|
||||||
@@ -132,10 +131,6 @@ func (c *Config) LoadConfig() {
|
|||||||
enabled := false
|
enabled := false
|
||||||
jc.NoiseSuppressionEnabled = &enabled
|
jc.NoiseSuppressionEnabled = &enabled
|
||||||
}
|
}
|
||||||
if c.config.NoiseSuppressionThreshold == nil {
|
|
||||||
threshold := float32(0.08)
|
|
||||||
jc.NoiseSuppressionThreshold = &threshold
|
|
||||||
}
|
|
||||||
if c.config.VoiceEffect == nil {
|
if c.config.VoiceEffect == nil {
|
||||||
effect := 0 // Default to EffectNone
|
effect := 0 // Default to EffectNone
|
||||||
jc.VoiceEffect = &effect
|
jc.VoiceEffect = &effect
|
||||||
@@ -325,31 +320,6 @@ func (c *Config) SetNoiseSuppressionEnabled(enabled bool) {
|
|||||||
c.SaveConfig()
|
c.SaveConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) GetNoiseSuppressionThreshold() float32 {
|
|
||||||
if c.config.NoiseSuppressionThreshold == nil {
|
|
||||||
return 0.08
|
|
||||||
}
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) GetVoiceEffect() int {
|
func (c *Config) GetVoiceEffect() int {
|
||||||
if c.config.VoiceEffect == nil {
|
if c.config.VoiceEffect == nil {
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -603,9 +603,6 @@ func (s *Stream) syncStereoProcessors() {
|
|||||||
if leftSuppressor.IsEnabled() != rightSuppressor.IsEnabled() {
|
if leftSuppressor.IsEnabled() != rightSuppressor.IsEnabled() {
|
||||||
rightSuppressor.SetEnabled(leftSuppressor.IsEnabled())
|
rightSuppressor.SetEnabled(leftSuppressor.IsEnabled())
|
||||||
}
|
}
|
||||||
if leftSuppressor.GetThreshold() != rightSuppressor.GetThreshold() {
|
|
||||||
rightSuppressor.SetThreshold(leftSuppressor.GetThreshold())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
leftEffects, leftOk := s.effectsProcessor.(*audio.EffectsProcessor)
|
leftEffects, leftOk := s.effectsProcessor.(*audio.EffectsProcessor)
|
||||||
@@ -627,7 +624,6 @@ func cloneNoiseProcessor(np NoiseProcessor) NoiseProcessor {
|
|||||||
if suppressor, ok := np.(*noise.Suppressor); ok {
|
if suppressor, ok := np.(*noise.Suppressor); ok {
|
||||||
clone := noise.NewSuppressor()
|
clone := noise.NewSuppressor()
|
||||||
clone.SetEnabled(suppressor.IsEnabled())
|
clone.SetEnabled(suppressor.IsEnabled())
|
||||||
clone.SetThreshold(suppressor.GetThreshold())
|
|
||||||
return clone
|
return clone
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -195,7 +195,6 @@ func main() {
|
|||||||
b.UserConfig.SetNoiseSuppressionEnabled(true)
|
b.UserConfig.SetNoiseSuppressionEnabled(true)
|
||||||
}
|
}
|
||||||
b.NoiseSuppressor.SetEnabled(enabled)
|
b.NoiseSuppressor.SetEnabled(enabled)
|
||||||
b.NoiseSuppressor.SetThreshold(b.UserConfig.GetNoiseSuppressionThreshold())
|
|
||||||
|
|
||||||
// Configure voice effects
|
// Configure voice effects
|
||||||
b.VoiceEffects.SetEffect(audio.VoiceEffect(b.UserConfig.GetVoiceEffect()))
|
b.VoiceEffects.SetEffect(audio.VoiceEffect(b.UserConfig.GetVoiceEffect()))
|
||||||
|
|||||||
+59
-165
@@ -1,212 +1,106 @@
|
|||||||
package noise
|
package noise
|
||||||
|
|
||||||
|
/*
|
||||||
|
#cgo pkg-config: rnnoise
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <rnnoise.h>
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
"math"
|
||||||
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Ensure Suppressor implements the NoiseProcessor interface
|
// Ensure Suppressor implements the NoiseProcessor interface.
|
||||||
var _ interface {
|
var _ interface {
|
||||||
ProcessSamples(samples []int16)
|
ProcessSamples(samples []int16)
|
||||||
IsEnabled() bool
|
IsEnabled() bool
|
||||||
} = (*Suppressor)(nil)
|
} = (*Suppressor)(nil)
|
||||||
|
|
||||||
// Suppressor handles noise suppression for audio samples
|
// Suppressor handles RNNoise-backed noise suppression for microphone samples.
|
||||||
type Suppressor struct {
|
type Suppressor struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
|
state *C.DenoiseState
|
||||||
enabled bool
|
enabled bool
|
||||||
threshold float32
|
frameSize int
|
||||||
|
|
||||||
// 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
|
// NewSuppressor creates a new RNNoise suppressor.
|
||||||
func NewSuppressor() *Suppressor {
|
func NewSuppressor() *Suppressor {
|
||||||
s := &Suppressor{
|
state := C.rnnoise_create(nil)
|
||||||
enabled: false,
|
if state == nil {
|
||||||
threshold: 0.08,
|
panic("rnnoise: failed to create denoise state")
|
||||||
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()
|
|
||||||
|
s := &Suppressor{
|
||||||
|
state: state,
|
||||||
|
frameSize: int(C.rnnoise_get_frame_size()),
|
||||||
|
}
|
||||||
|
if s.frameSize <= 0 {
|
||||||
|
C.rnnoise_destroy(state)
|
||||||
|
panic("rnnoise: invalid frame size")
|
||||||
|
}
|
||||||
|
runtime.SetFinalizer(s, (*Suppressor).Close)
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetEnabled enables or disables noise suppression
|
// Close releases the RNNoise state.
|
||||||
|
func (s *Suppressor) Close() {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
if s.state == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
C.rnnoise_destroy(s.state)
|
||||||
|
s.state = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetEnabled enables or disables noise suppression.
|
||||||
func (s *Suppressor) SetEnabled(enabled bool) {
|
func (s *Suppressor) SetEnabled(enabled bool) {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
if s.enabled == enabled {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
s.enabled = enabled
|
s.enabled = enabled
|
||||||
s.resetStateLocked()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsEnabled returns whether noise suppression is enabled
|
// IsEnabled returns whether noise suppression is enabled.
|
||||||
func (s *Suppressor) IsEnabled() bool {
|
func (s *Suppressor) IsEnabled() bool {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
return s.enabled
|
return s.enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetThreshold sets the noise threshold (0.0 to 1.0)
|
// ProcessSamples applies RNNoise suppression to complete frames in samples.
|
||||||
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) {
|
func (s *Suppressor) ProcessSamples(samples []int16) {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
if !s.enabled || len(samples) == 0 {
|
if !s.enabled || s.state == nil || len(samples) < s.frameSize {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
intensity := s.thresholdToIntensity()
|
input := make([]C.float, s.frameSize)
|
||||||
minGain := 1.0 - (0.92 * intensity)
|
output := make([]C.float, s.frameSize)
|
||||||
eps := float32(1e-6)
|
for offset := 0; offset+s.frameSize <= len(samples); offset += s.frameSize {
|
||||||
|
frame := samples[offset : offset+s.frameSize]
|
||||||
for i, sample := range samples {
|
for i, sample := range frame {
|
||||||
floatSample := float32(sample) / 32768.0
|
input[i] = C.float(sample)
|
||||||
filtered := s.highPassFilterLocked(floatSample)
|
}
|
||||||
absSample := float32(math.Abs(float64(filtered)))
|
C.rnnoise_process_frame(s.state, &output[0], &input[0])
|
||||||
|
for i, sample := range output {
|
||||||
s.updateEnvelopeLocked(absSample)
|
frame[i] = floatToInt16(float32(sample))
|
||||||
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) {
|
func floatToInt16(sample float32) int16 {
|
||||||
if targetGain < s.suppressionGain {
|
if sample > math.MaxInt16 {
|
||||||
s.suppressionGain += s.gainAttack * (targetGain - s.suppressionGain)
|
return math.MaxInt16
|
||||||
} else {
|
|
||||||
s.suppressionGain += s.gainRelease * (targetGain - s.suppressionGain)
|
|
||||||
}
|
}
|
||||||
s.suppressionGain = clampFloat32(s.suppressionGain, 0.02, 1.0)
|
if sample < math.MinInt16 {
|
||||||
}
|
return math.MinInt16
|
||||||
|
|
||||||
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 int16(sample)
|
||||||
return max
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProcessSamplesAdvanced applies more sophisticated noise suppression
|
|
||||||
// Placeholder for future RNNoise integration.
|
|
||||||
func (s *Suppressor) ProcessSamplesAdvanced(samples []int16) {
|
|
||||||
s.ProcessSamples(samples)
|
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-72
@@ -1,12 +1,11 @@
|
|||||||
package noise
|
package noise
|
||||||
|
|
||||||
import (
|
import "testing"
|
||||||
"math"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSuppressorDisabledBypassesSamples(t *testing.T) {
|
func TestSuppressorDisabledBypassesSamples(t *testing.T) {
|
||||||
suppressor := NewSuppressor()
|
suppressor := NewSuppressor()
|
||||||
|
defer suppressor.Close()
|
||||||
|
|
||||||
samples := []int16{100, -200, 300, -400, 500}
|
samples := []int16{100, -200, 300, -400, 500}
|
||||||
original := append([]int16(nil), samples...)
|
original := append([]int16(nil), samples...)
|
||||||
|
|
||||||
@@ -19,85 +18,41 @@ func TestSuppressorDisabledBypassesSamples(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSuppressorAttenuatesLowLevelNoise(t *testing.T) {
|
func TestSuppressorProcessesCompleteRNNoiseFrame(t *testing.T) {
|
||||||
suppressor := NewSuppressor()
|
suppressor := NewSuppressor()
|
||||||
|
defer suppressor.Close()
|
||||||
suppressor.SetEnabled(true)
|
suppressor.SetEnabled(true)
|
||||||
suppressor.SetThreshold(0.08)
|
|
||||||
|
|
||||||
input := makeSineFrame(600, 700)
|
samples := make([]int16, suppressor.frameSize)
|
||||||
originalRMS := frameRMS(input)
|
for i := range samples {
|
||||||
processed := append([]int16(nil), input...)
|
samples[i] = int16((i % 64) * 128)
|
||||||
suppressor.ProcessSamples(processed)
|
|
||||||
processedRMS := frameRMS(processed)
|
|
||||||
|
|
||||||
if processedRMS >= originalRMS*0.8 {
|
|
||||||
t.Fatalf("expected low-level noise attenuation, got RMS %.2f from %.2f", processedRMS, originalRMS)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suppressor.ProcessSamples(samples)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSuppressorPreservesSpeechLikeSignal(t *testing.T) {
|
func TestSuppressorLeavesIncompleteFrameUnchanged(t *testing.T) {
|
||||||
suppressor := NewSuppressor()
|
suppressor := NewSuppressor()
|
||||||
|
defer suppressor.Close()
|
||||||
suppressor.SetEnabled(true)
|
suppressor.SetEnabled(true)
|
||||||
suppressor.SetThreshold(0.08)
|
|
||||||
|
|
||||||
voice := makeSineFrame(1000, 9000)
|
samples := make([]int16, suppressor.frameSize-1)
|
||||||
originalRMS := frameRMS(voice)
|
for i := range samples {
|
||||||
processed := append([]int16(nil), voice...)
|
samples[i] = int16(i + 1)
|
||||||
suppressor.ProcessSamples(processed)
|
}
|
||||||
processedRMS := frameRMS(processed)
|
original := append([]int16(nil), samples...)
|
||||||
|
|
||||||
if processedRMS <= originalRMS*0.6 {
|
suppressor.ProcessSamples(samples)
|
||||||
t.Fatalf("expected speech-like signal to be mostly preserved, got RMS %.2f from %.2f", processedRMS, originalRMS)
|
|
||||||
|
for i := range samples {
|
||||||
|
if samples[i] != original[i] {
|
||||||
|
t.Fatalf("expected incomplete frame sample %d to remain unchanged, got %d want %d", i, samples[i], original[i])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHigherThresholdAppliesStrongerSuppression(t *testing.T) {
|
func TestSuppressorCloseIsIdempotent(t *testing.T) {
|
||||||
lowSuppressor := NewSuppressor()
|
suppressor := NewSuppressor()
|
||||||
lowSuppressor.SetEnabled(true)
|
suppressor.Close()
|
||||||
lowSuppressor.SetThreshold(0.02)
|
suppressor.Close()
|
||||||
|
|
||||||
highSuppressor := NewSuppressor()
|
|
||||||
highSuppressor.SetEnabled(true)
|
|
||||||
highSuppressor.SetThreshold(0.20)
|
|
||||||
|
|
||||||
noiseFrame := makeSineFrame(500, 700)
|
|
||||||
lowRMS := runFrameWarmup(lowSuppressor, noiseFrame, 8)
|
|
||||||
highRMS := runFrameWarmup(highSuppressor, noiseFrame, 8)
|
|
||||||
|
|
||||||
if highRMS >= lowRMS*0.8 {
|
|
||||||
t.Fatalf("expected stronger suppression at higher threshold, got low %.2f high %.2f", lowRMS, highRMS)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func runFrameWarmup(suppressor *Suppressor, frame []int16, repeats int) float64 {
|
|
||||||
var processed []int16
|
|
||||||
for i := 0; i < repeats; i++ {
|
|
||||||
processed = append([]int16(nil), frame...)
|
|
||||||
suppressor.ProcessSamples(processed)
|
|
||||||
}
|
|
||||||
return frameRMS(processed)
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeSineFrame(frequency float64, amplitude float64) []int16 {
|
|
||||||
const sampleRate = 48000.0
|
|
||||||
const frameSize = 480
|
|
||||||
|
|
||||||
frame := make([]int16, frameSize)
|
|
||||||
for i := 0; i < frameSize; i++ {
|
|
||||||
value := math.Sin((2.0 * math.Pi * frequency * float64(i)) / sampleRate)
|
|
||||||
frame[i] = int16(value * amplitude)
|
|
||||||
}
|
|
||||||
return frame
|
|
||||||
}
|
|
||||||
|
|
||||||
func frameRMS(samples []int16) float64 {
|
|
||||||
if len(samples) == 0 {
|
|
||||||
return 0.0
|
|
||||||
}
|
|
||||||
var sumSquares float64
|
|
||||||
for _, sample := range samples {
|
|
||||||
normalized := float64(sample) / 32768.0
|
|
||||||
sumSquares += normalized * normalized
|
|
||||||
}
|
|
||||||
return math.Sqrt(sumSquares / float64(len(samples)))
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user