Initial noise suppression added.
This commit is contained in:
26
README.md
26
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
|
||||
|
||||
- <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
|
||||
|
@@ -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() {
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -18,4 +18,5 @@ type Hotkeys struct {
|
||||
ScrollDown *uiterm.Key
|
||||
ScrollToTop *uiterm.Key
|
||||
ScrollToBottom *uiterm.Key
|
||||
NoiseSuppressionToggle *uiterm.Key
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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
13
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
|
||||
|
||||
|
112
noise/suppression.go
Normal file
112
noise/suppression.go
Normal 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
26
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)
|
||||
|
Reference in New Issue
Block a user