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)