From df7159bad127deb6b7fbe01776557c1e22edbcc0 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Sat, 16 Aug 2025 21:29:52 -0400 Subject: [PATCH] Initial noise suppression added. --- README.md | 26 ++++++++ barnard.go | 4 ++ client.go | 1 + config/hotkey_config.go | 1 + config/user_config.go | 35 +++++++++++ gumble/gumbleopenal/stream.go | 18 ++++++ main.go | 13 ++++ noise/suppression.go | 112 ++++++++++++++++++++++++++++++++++ ui.go | 26 ++++++++ 9 files changed, 236 insertions(+) create mode 100644 noise/suppression.go diff --git a/README.md b/README.md index dbd1278..cb42173 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,30 @@ If a user is too soft to hear, you can boost their audio. The audio should drastically increase once you have hit the VolumeUp key over 10 times (from the silent/0 position). The boost setting is saved per user, just like per user volume. +## 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. + +### Features +- **Real-time processing**: Noise suppression is applied during audio capture with minimal latency +- **Configurable threshold**: Adjustable noise gate threshold (default: 0.02) +- **Persistent settings**: Noise suppression preferences are saved in your configuration file +- **Multiple control methods**: Toggle via hotkey, command line flag, or FIFO commands + +### Controls +- **F9 key**: Toggle noise suppression on/off (configurable hotkey) +- **Command line**: Use `--noise-suppression` flag to enable at startup +- **FIFO command**: Send `noise` command to toggle during runtime +- **Configuration**: Set `noisesuppressionenabled` and `noisesuppressionthreshold` in `~/.barnard.yaml` + +### Configuration Example +```yaml +noisesuppressionenabled: true +noisesuppressionthreshold: 0.02 +``` + +The noise suppression algorithm uses a combination of high-pass filtering and noise gating to reduce unwanted background sounds while preserving voice quality. + ## FIFO Control If you pass the --fifo option to Barnard, a FIFO pipe will be created. @@ -31,6 +55,7 @@ Current Commands: * micdown: Stop transmitting, just like when you release your talk key. Does nothing if you are not already transmitting. * toggle: Toggle your transmission state. * talk: Synonym for toggle. +* noise: Toggle noise suppression on/off for microphone input. * exit: Exit Barnard, just like when you press your quit key. ## Event Notification @@ -165,6 +190,7 @@ After running the command above, `barnard` will be compiled as `$(go env GOPATH) ### Key bindings - F1: toggle voice transmission +- F9: toggle noise suppression - Ctrl+L: clear chat log - Tab: toggle focus between chat and user tree - Page Up: scroll chat up diff --git a/barnard.go b/barnard.go index 42da32f..a465058 100644 --- a/barnard.go +++ b/barnard.go @@ -6,6 +6,7 @@ import ( "git.stormux.org/storm/barnard/config" "git.stormux.org/storm/barnard/gumble/gumble" "git.stormux.org/storm/barnard/gumble/gumbleopenal" + "git.stormux.org/storm/barnard/noise" "git.stormux.org/storm/barnard/uiterm" ) @@ -43,6 +44,9 @@ type Barnard struct { // Added for channel muting MutedChannels map[uint32]bool + + // Added for noise suppression + NoiseSuppressor *noise.Suppressor } func (b *Barnard) StopTransmission() { diff --git a/client.go b/client.go index b4e0dbd..7a7d92f 100644 --- a/client.go +++ b/client.go @@ -49,6 +49,7 @@ func (b *Barnard) connect(reconnect bool) bool { } b.Stream = stream b.Stream.AttachStream(b.Client) + b.Stream.SetNoiseProcessor(b.NoiseSuppressor) b.Connected = true return true } diff --git a/config/hotkey_config.go b/config/hotkey_config.go index 13b19eb..1a592b7 100644 --- a/config/hotkey_config.go +++ b/config/hotkey_config.go @@ -18,4 +18,5 @@ type Hotkeys struct { ScrollDown *uiterm.Key ScrollToTop *uiterm.Key ScrollToBottom *uiterm.Key + NoiseSuppressionToggle *uiterm.Key } diff --git a/config/user_config.go b/config/user_config.go index 2102622..bbbead0 100644 --- a/config/user_config.go +++ b/config/user_config.go @@ -26,6 +26,8 @@ type exportableConfig struct { DefaultServer *string Username *string NotifyCommand *string + NoiseSuppressionEnabled *bool + NoiseSuppressionThreshold *float32 } type server struct { @@ -75,6 +77,7 @@ func (c *Config) LoadConfig() { SwitchViews: key(uiterm.KeyTab), ScrollUp: key(uiterm.KeyPgup), ScrollDown: key(uiterm.KeyPgdn), + NoiseSuppressionToggle: key(uiterm.KeyF9), } if fileExists(c.fn) { var data []byte @@ -112,6 +115,14 @@ func (c *Config) LoadConfig() { ncmd := string("") jc.NotifyCommand = &ncmd } + if c.config.NoiseSuppressionEnabled == nil { + enabled := false + jc.NoiseSuppressionEnabled = &enabled + } + if c.config.NoiseSuppressionThreshold == nil { + threshold := float32(0.02) + jc.NoiseSuppressionThreshold = &threshold + } } func (c *Config) findServer(address string) *server { @@ -197,6 +208,30 @@ func (c *Config) GetUsername() *string { return c.config.Username } +func (c *Config) GetNoiseSuppressionEnabled() bool { + if c.config.NoiseSuppressionEnabled == nil { + return false + } + return *c.config.NoiseSuppressionEnabled +} + +func (c *Config) SetNoiseSuppressionEnabled(enabled bool) { + c.config.NoiseSuppressionEnabled = &enabled + c.SaveConfig() +} + +func (c *Config) GetNoiseSuppressionThreshold() float32 { + if c.config.NoiseSuppressionThreshold == nil { + return 0.02 + } + return *c.config.NoiseSuppressionThreshold +} + +func (c *Config) SetNoiseSuppressionThreshold(threshold float32) { + c.config.NoiseSuppressionThreshold = &threshold + c.SaveConfig() +} + func (c *Config) UpdateUser(u *gumble.User) { var j *eUser var uc *gumble.Client diff --git a/gumble/gumbleopenal/stream.go b/gumble/gumbleopenal/stream.go index cfc21d0..756e608 100644 --- a/gumble/gumbleopenal/stream.go +++ b/gumble/gumbleopenal/stream.go @@ -10,6 +10,12 @@ import ( "git.stormux.org/storm/barnard/gumble/go-openal/openal" ) +// NoiseProcessor interface for noise suppression +type NoiseProcessor interface { + ProcessSamples(samples []int16) + IsEnabled() bool +} + const ( maxBufferSize = 11520 // Max frame size (2880) * bytes per stereo sample (4) ) @@ -42,6 +48,8 @@ type Stream struct { deviceSink *openal.Device contextSink *openal.Context + + noiseProcessor NoiseProcessor } func New(client *gumble.Client, inputDevice *string, outputDevice *string, test bool) (*Stream, error) { @@ -97,6 +105,10 @@ func (s *Stream) AttachStream(client *gumble.Client) { s.link = client.Config.AttachAudio(s) } +func (s *Stream) SetNoiseProcessor(np NoiseProcessor) { + s.noiseProcessor = np +} + func (s *Stream) Destroy() { if s.link != nil { s.link.Detach() @@ -300,6 +312,12 @@ func (s *Stream) sourceRoutine(inputDevice *string) { } int16Buffer[i] = sample } + + // Apply noise suppression if available and enabled + if s.noiseProcessor != nil && s.noiseProcessor.IsEnabled() { + s.noiseProcessor.ProcessSamples(int16Buffer) + } + outgoing <- gumble.AudioBuffer(int16Buffer) } } diff --git a/main.go b/main.go index bb4bae0..fea7ff3 100644 --- a/main.go +++ b/main.go @@ -17,6 +17,7 @@ import ( "flag" "github.com/alessio/shellescape" "git.stormux.org/storm/barnard/config" + "git.stormux.org/storm/barnard/noise" "git.stormux.org/storm/barnard/gumble/gumble" _ "git.stormux.org/storm/barnard/gumble/opus" @@ -113,6 +114,7 @@ func main() { usernameSet := false buffers := flag.Int("buffers", 16, "number of audio buffers to use") profile := flag.Bool("profile", false, "add http server to serve profiles") + noiseSuppressionEnabled := flag.Bool("noise-suppression", false, "enable noise suppression for microphone input") flag.Parse() @@ -159,11 +161,22 @@ func main() { UserConfig: userConfig, Address: *server, MutedChannels: make(map[uint32]bool), + NoiseSuppressor: noise.NewSuppressor(), } b.Config.Buffers = *buffers b.Hotkeys = b.UserConfig.GetHotkeys() b.UserConfig.SaveConfig() + + // Configure noise suppression + enabled := b.UserConfig.GetNoiseSuppressionEnabled() + if *noiseSuppressionEnabled { + enabled = true + b.UserConfig.SetNoiseSuppressionEnabled(true) + } + b.NoiseSuppressor.SetEnabled(enabled) + b.NoiseSuppressor.SetThreshold(b.UserConfig.GetNoiseSuppressionThreshold()) + b.Config.Username = *username b.Config.Password = *password diff --git a/noise/suppression.go b/noise/suppression.go new file mode 100644 index 0000000..2c3de2f --- /dev/null +++ b/noise/suppression.go @@ -0,0 +1,112 @@ +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 +} + +// NewSuppressor creates a new noise suppressor +func NewSuppressor() *Suppressor { + return &Suppressor{ + enabled: false, + threshold: 0.02, // Noise threshold level + gainFactor: 0.8, // Gain reduction for noise + alpha: 0.95, // High-pass filter coefficient + } +} + +// 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 + } + + // Simple 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 + strength := float32(math.Abs(float64(filtered))) + + // Apply noise gate + if strength < s.threshold { + // Below threshold - reduce gain + filtered *= s.gainFactor + } + + // Apply simple spectral subtraction-like effect + // If signal is weak, reduce it further + if strength < s.threshold * 2 { + filtered *= (strength / (s.threshold * 2)) + } + + // Convert back to int16 + processed := filtered * 32767.0 + if processed > 32767 { + processed = 32767 + } else if processed < -32767 { + processed = -32767 + } + + samples[i] = int16(processed) + } +} + +// 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) +} \ No newline at end of file diff --git a/ui.go b/ui.go index 2792ab8..6037756 100644 --- a/ui.go +++ b/ui.go @@ -95,6 +95,18 @@ func (b *Barnard) OnTimestampToggle(ui *uiterm.Ui, key uiterm.Key) { b.UiOutput.ToggleTimestamps() } +func (b *Barnard) OnNoiseSuppressionToggle(ui *uiterm.Ui, key uiterm.Key) { + enabled := !b.UserConfig.GetNoiseSuppressionEnabled() + b.UserConfig.SetNoiseSuppressionEnabled(enabled) + b.NoiseSuppressor.SetEnabled(enabled) + + if enabled { + b.UpdateGeneralStatus("Noise suppression: ON", false) + } else { + b.UpdateGeneralStatus("Noise suppression: OFF", false) + } +} + func (b *Barnard) UpdateGeneralStatus(text string, notice bool) { if notice { b.UiStatus.Fg = uiterm.ColorWhite | uiterm.AttrBold @@ -127,6 +139,18 @@ func (b *Barnard) CommandMicDown(ui *uiterm.Ui, cmd string) { b.setTransmit(ui, 0) } +func (b *Barnard) CommandNoiseSuppressionToggle(ui *uiterm.Ui, cmd string) { + enabled := !b.UserConfig.GetNoiseSuppressionEnabled() + b.UserConfig.SetNoiseSuppressionEnabled(enabled) + b.NoiseSuppressor.SetEnabled(enabled) + + if enabled { + b.AddOutputLine("Noise suppression enabled") + } else { + b.AddOutputLine("Noise suppression disabled") + } +} + func (b *Barnard) setTransmit(ui *uiterm.Ui, val int) { if b.Tx && val == 1 { return @@ -292,9 +316,11 @@ func (b *Barnard) OnUiInitialize(ui *uiterm.Ui) { b.Ui.AddCommandListener(b.CommandTalk, "talk") b.Ui.AddCommandListener(b.CommandExit, "exit") b.Ui.AddCommandListener(b.CommandStatus, "status") + b.Ui.AddCommandListener(b.CommandNoiseSuppressionToggle, "noise") b.Ui.AddKeyListener(b.OnFocusPress, b.Hotkeys.SwitchViews) b.Ui.AddKeyListener(b.OnVoiceToggle, b.Hotkeys.Talk) b.Ui.AddKeyListener(b.OnTimestampToggle, b.Hotkeys.ToggleTimestamps) + b.Ui.AddKeyListener(b.OnNoiseSuppressionToggle, b.Hotkeys.NoiseSuppressionToggle) b.Ui.AddKeyListener(b.OnQuitPress, b.Hotkeys.Exit) b.Ui.AddKeyListener(b.OnScrollOutputUp, b.Hotkeys.ScrollUp) b.Ui.AddKeyListener(b.OnScrollOutputDown, b.Hotkeys.ScrollDown)