Initial noise suppression added.

This commit is contained in:
Storm Dragon
2025-08-16 21:29:52 -04:00
parent 657ff1dbef
commit df7159bad1
9 changed files with 236 additions and 0 deletions

View File

@@ -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
- <kbd>F1</kbd>: toggle voice transmission
- <kbd>F9</kbd>: toggle noise suppression
- <kbd>Ctrl+L</kbd>: clear chat log
- <kbd>Tab</kbd>: toggle focus between chat and user tree
- <kbd>Page Up</kbd>: scroll chat up

View File

@@ -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() {

View File

@@ -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
}

View File

@@ -18,4 +18,5 @@ type Hotkeys struct {
ScrollDown *uiterm.Key
ScrollToTop *uiterm.Key
ScrollToBottom *uiterm.Key
NoiseSuppressionToggle *uiterm.Key
}

View File

@@ -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

View File

@@ -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)
}
}

13
main.go
View File

@@ -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

112
noise/suppression.go Normal file
View File

@@ -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)
}

26
ui.go
View File

@@ -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)