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 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.
|
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
|
## FIFO Control
|
||||||
|
|
||||||
If you pass the --fifo option to Barnard, a FIFO pipe will be created.
|
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.
|
* micdown: Stop transmitting, just like when you release your talk key. Does nothing if you are not already transmitting.
|
||||||
* toggle: Toggle your transmission state.
|
* toggle: Toggle your transmission state.
|
||||||
* talk: Synonym for toggle.
|
* talk: Synonym for toggle.
|
||||||
|
* noise: Toggle noise suppression on/off for microphone input.
|
||||||
* exit: Exit Barnard, just like when you press your quit key.
|
* exit: Exit Barnard, just like when you press your quit key.
|
||||||
|
|
||||||
## Event Notification
|
## Event Notification
|
||||||
@@ -165,6 +190,7 @@ After running the command above, `barnard` will be compiled as `$(go env GOPATH)
|
|||||||
### Key bindings
|
### Key bindings
|
||||||
|
|
||||||
- <kbd>F1</kbd>: toggle voice transmission
|
- <kbd>F1</kbd>: toggle voice transmission
|
||||||
|
- <kbd>F9</kbd>: toggle noise suppression
|
||||||
- <kbd>Ctrl+L</kbd>: clear chat log
|
- <kbd>Ctrl+L</kbd>: clear chat log
|
||||||
- <kbd>Tab</kbd>: toggle focus between chat and user tree
|
- <kbd>Tab</kbd>: toggle focus between chat and user tree
|
||||||
- <kbd>Page Up</kbd>: scroll chat up
|
- <kbd>Page Up</kbd>: scroll chat up
|
||||||
|
@@ -6,6 +6,7 @@ import (
|
|||||||
"git.stormux.org/storm/barnard/config"
|
"git.stormux.org/storm/barnard/config"
|
||||||
"git.stormux.org/storm/barnard/gumble/gumble"
|
"git.stormux.org/storm/barnard/gumble/gumble"
|
||||||
"git.stormux.org/storm/barnard/gumble/gumbleopenal"
|
"git.stormux.org/storm/barnard/gumble/gumbleopenal"
|
||||||
|
"git.stormux.org/storm/barnard/noise"
|
||||||
"git.stormux.org/storm/barnard/uiterm"
|
"git.stormux.org/storm/barnard/uiterm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -43,6 +44,9 @@ type Barnard struct {
|
|||||||
|
|
||||||
// Added for channel muting
|
// Added for channel muting
|
||||||
MutedChannels map[uint32]bool
|
MutedChannels map[uint32]bool
|
||||||
|
|
||||||
|
// Added for noise suppression
|
||||||
|
NoiseSuppressor *noise.Suppressor
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) StopTransmission() {
|
func (b *Barnard) StopTransmission() {
|
||||||
|
@@ -49,6 +49,7 @@ func (b *Barnard) connect(reconnect bool) bool {
|
|||||||
}
|
}
|
||||||
b.Stream = stream
|
b.Stream = stream
|
||||||
b.Stream.AttachStream(b.Client)
|
b.Stream.AttachStream(b.Client)
|
||||||
|
b.Stream.SetNoiseProcessor(b.NoiseSuppressor)
|
||||||
b.Connected = true
|
b.Connected = true
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@@ -18,4 +18,5 @@ type Hotkeys struct {
|
|||||||
ScrollDown *uiterm.Key
|
ScrollDown *uiterm.Key
|
||||||
ScrollToTop *uiterm.Key
|
ScrollToTop *uiterm.Key
|
||||||
ScrollToBottom *uiterm.Key
|
ScrollToBottom *uiterm.Key
|
||||||
|
NoiseSuppressionToggle *uiterm.Key
|
||||||
}
|
}
|
||||||
|
@@ -26,6 +26,8 @@ type exportableConfig struct {
|
|||||||
DefaultServer *string
|
DefaultServer *string
|
||||||
Username *string
|
Username *string
|
||||||
NotifyCommand *string
|
NotifyCommand *string
|
||||||
|
NoiseSuppressionEnabled *bool
|
||||||
|
NoiseSuppressionThreshold *float32
|
||||||
}
|
}
|
||||||
|
|
||||||
type server struct {
|
type server struct {
|
||||||
@@ -75,6 +77,7 @@ func (c *Config) LoadConfig() {
|
|||||||
SwitchViews: key(uiterm.KeyTab),
|
SwitchViews: key(uiterm.KeyTab),
|
||||||
ScrollUp: key(uiterm.KeyPgup),
|
ScrollUp: key(uiterm.KeyPgup),
|
||||||
ScrollDown: key(uiterm.KeyPgdn),
|
ScrollDown: key(uiterm.KeyPgdn),
|
||||||
|
NoiseSuppressionToggle: key(uiterm.KeyF9),
|
||||||
}
|
}
|
||||||
if fileExists(c.fn) {
|
if fileExists(c.fn) {
|
||||||
var data []byte
|
var data []byte
|
||||||
@@ -112,6 +115,14 @@ func (c *Config) LoadConfig() {
|
|||||||
ncmd := string("")
|
ncmd := string("")
|
||||||
jc.NotifyCommand = &ncmd
|
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 {
|
func (c *Config) findServer(address string) *server {
|
||||||
@@ -197,6 +208,30 @@ func (c *Config) GetUsername() *string {
|
|||||||
return c.config.Username
|
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) {
|
func (c *Config) UpdateUser(u *gumble.User) {
|
||||||
var j *eUser
|
var j *eUser
|
||||||
var uc *gumble.Client
|
var uc *gumble.Client
|
||||||
|
@@ -10,6 +10,12 @@ import (
|
|||||||
"git.stormux.org/storm/barnard/gumble/go-openal/openal"
|
"git.stormux.org/storm/barnard/gumble/go-openal/openal"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// NoiseProcessor interface for noise suppression
|
||||||
|
type NoiseProcessor interface {
|
||||||
|
ProcessSamples(samples []int16)
|
||||||
|
IsEnabled() bool
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
maxBufferSize = 11520 // Max frame size (2880) * bytes per stereo sample (4)
|
maxBufferSize = 11520 // Max frame size (2880) * bytes per stereo sample (4)
|
||||||
)
|
)
|
||||||
@@ -42,6 +48,8 @@ type Stream struct {
|
|||||||
|
|
||||||
deviceSink *openal.Device
|
deviceSink *openal.Device
|
||||||
contextSink *openal.Context
|
contextSink *openal.Context
|
||||||
|
|
||||||
|
noiseProcessor NoiseProcessor
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(client *gumble.Client, inputDevice *string, outputDevice *string, test bool) (*Stream, error) {
|
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)
|
s.link = client.Config.AttachAudio(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Stream) SetNoiseProcessor(np NoiseProcessor) {
|
||||||
|
s.noiseProcessor = np
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Stream) Destroy() {
|
func (s *Stream) Destroy() {
|
||||||
if s.link != nil {
|
if s.link != nil {
|
||||||
s.link.Detach()
|
s.link.Detach()
|
||||||
@@ -300,6 +312,12 @@ func (s *Stream) sourceRoutine(inputDevice *string) {
|
|||||||
}
|
}
|
||||||
int16Buffer[i] = sample
|
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)
|
outgoing <- gumble.AudioBuffer(int16Buffer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
13
main.go
13
main.go
@@ -17,6 +17,7 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"github.com/alessio/shellescape"
|
"github.com/alessio/shellescape"
|
||||||
"git.stormux.org/storm/barnard/config"
|
"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/gumble"
|
||||||
_ "git.stormux.org/storm/barnard/gumble/opus"
|
_ "git.stormux.org/storm/barnard/gumble/opus"
|
||||||
@@ -113,6 +114,7 @@ func main() {
|
|||||||
usernameSet := false
|
usernameSet := false
|
||||||
buffers := flag.Int("buffers", 16, "number of audio buffers to use")
|
buffers := flag.Int("buffers", 16, "number of audio buffers to use")
|
||||||
profile := flag.Bool("profile", false, "add http server to serve profiles")
|
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()
|
flag.Parse()
|
||||||
|
|
||||||
@@ -159,11 +161,22 @@ func main() {
|
|||||||
UserConfig: userConfig,
|
UserConfig: userConfig,
|
||||||
Address: *server,
|
Address: *server,
|
||||||
MutedChannels: make(map[uint32]bool),
|
MutedChannels: make(map[uint32]bool),
|
||||||
|
NoiseSuppressor: noise.NewSuppressor(),
|
||||||
}
|
}
|
||||||
b.Config.Buffers = *buffers
|
b.Config.Buffers = *buffers
|
||||||
|
|
||||||
b.Hotkeys = b.UserConfig.GetHotkeys()
|
b.Hotkeys = b.UserConfig.GetHotkeys()
|
||||||
b.UserConfig.SaveConfig()
|
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.Username = *username
|
||||||
b.Config.Password = *password
|
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()
|
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) {
|
func (b *Barnard) UpdateGeneralStatus(text string, notice bool) {
|
||||||
if notice {
|
if notice {
|
||||||
b.UiStatus.Fg = uiterm.ColorWhite | uiterm.AttrBold
|
b.UiStatus.Fg = uiterm.ColorWhite | uiterm.AttrBold
|
||||||
@@ -127,6 +139,18 @@ func (b *Barnard) CommandMicDown(ui *uiterm.Ui, cmd string) {
|
|||||||
b.setTransmit(ui, 0)
|
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) {
|
func (b *Barnard) setTransmit(ui *uiterm.Ui, val int) {
|
||||||
if b.Tx && val == 1 {
|
if b.Tx && val == 1 {
|
||||||
return
|
return
|
||||||
@@ -292,9 +316,11 @@ func (b *Barnard) OnUiInitialize(ui *uiterm.Ui) {
|
|||||||
b.Ui.AddCommandListener(b.CommandTalk, "talk")
|
b.Ui.AddCommandListener(b.CommandTalk, "talk")
|
||||||
b.Ui.AddCommandListener(b.CommandExit, "exit")
|
b.Ui.AddCommandListener(b.CommandExit, "exit")
|
||||||
b.Ui.AddCommandListener(b.CommandStatus, "status")
|
b.Ui.AddCommandListener(b.CommandStatus, "status")
|
||||||
|
b.Ui.AddCommandListener(b.CommandNoiseSuppressionToggle, "noise")
|
||||||
b.Ui.AddKeyListener(b.OnFocusPress, b.Hotkeys.SwitchViews)
|
b.Ui.AddKeyListener(b.OnFocusPress, b.Hotkeys.SwitchViews)
|
||||||
b.Ui.AddKeyListener(b.OnVoiceToggle, b.Hotkeys.Talk)
|
b.Ui.AddKeyListener(b.OnVoiceToggle, b.Hotkeys.Talk)
|
||||||
b.Ui.AddKeyListener(b.OnTimestampToggle, b.Hotkeys.ToggleTimestamps)
|
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.OnQuitPress, b.Hotkeys.Exit)
|
||||||
b.Ui.AddKeyListener(b.OnScrollOutputUp, b.Hotkeys.ScrollUp)
|
b.Ui.AddKeyListener(b.OnScrollOutputUp, b.Hotkeys.ScrollUp)
|
||||||
b.Ui.AddKeyListener(b.OnScrollOutputDown, b.Hotkeys.ScrollDown)
|
b.Ui.AddKeyListener(b.OnScrollOutputDown, b.Hotkeys.ScrollDown)
|
||||||
|
Reference in New Issue
Block a user