Merge latest Barnard fork improvements

Merge the newest logging, jitter recovery, AGC hotkey, and channel tree refresh work from bmmcginty/barnard-stormdragon-ai-fork dev.

Co-authored-by: Brandon McGinty <git@bmcginty.us>

Co-authored-by: Tyler Spivey <tspivey@pcdesk.net>
This commit is contained in:
Storm Dragon
2026-08-14 20:14:46 -04:00
co-authored by Brandon McGinty Tyler Spivey
11 changed files with 402 additions and 28 deletions
+16
View File
@@ -40,6 +40,20 @@ noisesuppressionenabled = true
RNNoise is a required build and runtime dependency.
## Automatic Gain Control
Barnard normalizes the level of your outgoing microphone audio with automatic gain control (AGC), which boosts quiet speech and compresses loud peaks. AGC is enabled by default.
### Controls
- **F12 key**: Toggle AGC on/off (configurable hotkey)
- **FIFO command**: Send `agc` command to toggle during runtime
- **Configuration**: Set `agcenabled` in `~/.barnard.toml`
### Configuration Example
```toml
agcenabled = true
```
## FIFO Control
If you pass the --fifo option to Barnard, a FIFO pipe will be created.
@@ -54,6 +68,7 @@ Current Commands:
* toggle: Toggle your transmission state.
* talk: Synonym for toggle.
* noise: Toggle noise suppression on/off for microphone input.
* agc: Toggle automatic gain control on/off for microphone input.
* record: Toggle recording. You may also use `record start` or `record stop`.
* exit: Exit Barnard, just like when you press your quit key.
@@ -278,6 +293,7 @@ After running the command above, `barnard` will be compiled as `$(go env GOPATH)
- <kbd>F1</kbd>: toggle voice transmission
- <kbd>F9</kbd>: toggle noise suppression
- <kbd>F12</kbd>: toggle automatic gain control
- <kbd>F11</kbd>: open actions menu for the focused tree item
- <kbd>Ctrl+R</kbd>: toggle recording
- <kbd>Ctrl+L</kbd>: clear chat log
+26 -24
View File
@@ -2,41 +2,43 @@ package audio
import (
"math"
"sync/atomic"
)
// AGC (Automatic Gain Control) processor for voice normalization
type AGC struct {
targetLevel float32 // Target RMS level (0.0-1.0)
maxGain float32 // Maximum gain multiplier
minGain float32 // Minimum gain multiplier
attackTime float32 // Attack time coefficient
releaseTime float32 // Release time coefficient
currentGain float32 // Current gain value
envelope float32 // Signal envelope
enabled bool // Whether AGC is enabled
compThreshold float32 // Compression threshold
compRatio float32 // Compression ratio
targetLevel float32 // Target RMS level (0.0-1.0)
maxGain float32 // Maximum gain multiplier
minGain float32 // Minimum gain multiplier
attackTime float32 // Attack time coefficient
releaseTime float32 // Release time coefficient
currentGain float32 // Current gain value
envelope float32 // Signal envelope
enabled atomic.Bool // Whether AGC is enabled; toggled outside the capture goroutine
compThreshold float32 // Compression threshold
compRatio float32 // Compression ratio
}
// NewAGC creates a new AGC processor with sensible defaults for voice
func NewAGC() *AGC {
return &AGC{
targetLevel: 0.12, // Target 12% of max amplitude (conservative level)
maxGain: 4.0, // Maximum 4x gain (about 12dB)
minGain: 0.25, // Minimum 0.25x gain (-12dB)
attackTime: 0.008, // Fast attack (8ms)
releaseTime: 0.15, // Slower release (150ms)
currentGain: 1.0, // Start with unity gain
envelope: 0.0, // Start with zero envelope
enabled: true, // Enable by default
compThreshold: 0.85, // Compress signals above 85%
compRatio: 2.0, // 2:1 compression ratio (gentler)
agc := &AGC{
targetLevel: 0.12, // Target 12% of max amplitude (conservative level)
maxGain: 4.0, // Maximum 4x gain (about 12dB)
minGain: 0.25, // Minimum 0.25x gain (-12dB)
attackTime: 0.008, // Fast attack (8ms)
releaseTime: 0.15, // Slower release (150ms)
currentGain: 1.0, // Start with unity gain
envelope: 0.0, // Start with zero envelope
compThreshold: 0.85, // Compress signals above 85%
compRatio: 2.0, // 2:1 compression ratio (gentler)
}
agc.enabled.Store(true) // Enable by default
return agc
}
// ProcessSamples applies AGC processing to audio samples
func (agc *AGC) ProcessSamples(samples []int16) {
if !agc.enabled || len(samples) == 0 {
if !agc.enabled.Load() || len(samples) == 0 {
return
}
@@ -125,12 +127,12 @@ func (agc *AGC) ProcessSamples(samples []int16) {
// SetEnabled enables or disables AGC processing
func (agc *AGC) SetEnabled(enabled bool) {
agc.enabled = enabled
agc.enabled.Store(enabled)
}
// IsEnabled returns whether AGC is enabled
func (agc *AGC) IsEnabled() bool {
return agc.enabled
return agc.enabled.Load()
}
// SetTargetLevel sets the target RMS level (0.0-1.0)
+174 -1
View File
@@ -38,6 +38,10 @@ configDir="$HOME/.config/barnard"
serverFile="$configDir/servers.conf"
certFile="$configDir/barnard.pem"
logFile="$cacheDir/${0##*/}.log"
logDir="$HOME/barnard-logs"
logPrefsFile="$configDir/logging.conf"
sessionLogFile=""
saveSessionLogs=0
if ! mkdir -p "$cacheDir" "$configDir"; then
printf 'Could not create Barnard configuration directories.\n' >&2
@@ -64,6 +68,9 @@ log() {
local line
while IFS= read -r line ; do
printf '%s\n' "$line" >> "$logFile"
if [[ -n "$sessionLogFile" ]]; then
printf '%s\n' "$line" >> "$sessionLogFile"
fi
done
}
@@ -141,6 +148,12 @@ trim() {
printf '%s' "$value"
}
sanitize_filename() {
local value="$1"
value="${value//[^[:alnum:]_.-]/_}"
printf '%s' "$value"
}
field_is_valid() {
local value="$1"
[[ "$value" != *$'\n'* && "$value" != *$'\r'* ]]
@@ -376,6 +389,145 @@ config_has_nonempty_value() {
return 1
}
load_logging_pref() {
local line
local key
local value
saveSessionLogs=0
[[ -r "$logPrefsFile" ]] || return 0
while IFS= read -r line || [[ -n "$line" ]]; do
line="$(trim "$line")"
[[ -z "$line" || "$line" == \#* || "$line" != *=* ]] && continue
key="${line%%=*}"
key="$(trim "$key")"
key="${key,,}"
value="${line#*=}"
value="$(trim "$value")"
case "$key" in
savesessionlogs)
if [[ "$value" == "1" || "$value" == "true" || "$value" == "yes" ]]; then
saveSessionLogs=1
fi
;;
esac
done < "$logPrefsFile"
}
save_logging_pref() {
local tmpFile="$logPrefsFile.tmp"
if ! printf 'saveSessionLogs=%s\n' "$saveSessionLogs" > "$tmpFile"; then
rm -f "$tmpFile"
msgbox "$(gettext "Could not save logging preference.")"
return 1
fi
chmod 600 "$tmpFile" 2> /dev/null || true
if ! mv "$tmpFile" "$logPrefsFile"; then
rm -f "$tmpFile"
msgbox "$(gettext "Could not save logging preference.")"
return 1
fi
}
toggle-session-logging() {
local question
if (( saveSessionLogs )); then
question="$(gettext "Session logging is currently enabled. Disable it?")"
else
question="$(gettext "Session logging is currently disabled. Enable saving logs to the logs directory?")"
fi
if [[ "$(yesno "$question")" == "Yes" ]]; then
if (( saveSessionLogs )); then
saveSessionLogs=0
else
saveSessionLogs=1
fi
save_logging_pref
fi
}
send-logs() {
local bundle
local outputFile
local code
local wormholePid
local status
local i
local detail
if ! command -v wormhole > /dev/null 2>&1; then
msgbox "$(gettext "Required command not found:") wormhole"
return
fi
if ! command -v tar > /dev/null 2>&1; then
msgbox "$(gettext "Required command not found:") tar"
return
fi
if [[ ! -d "$logDir" ]] || ! compgen -G "$logDir"/*.log > /dev/null 2>&1; then
msgbox "$(gettext "No logs to send. Logs are saved to:") $logDir"
return
fi
bundle="$cacheDir/barnard-logs-$(date +%Y%m%d-%H%M%S).tar.gz"
if ! tar -czf "$bundle" -C "$logDir" . 2> /dev/null; then
msgbox "$(gettext "Could not create log archive.")"
return
fi
outputFile="$cacheDir/wormhole-$$.txt"
wormhole send "$bundle" > "$outputFile" 2>&1 &
wormholePid=$!
code=""
for (( i = 0; i < 40; i++ )); do
sleep 0.25
code="$(grep -Eo '[0-9]+-[a-z]+-[a-z]+' "$outputFile" 2> /dev/null | head -n1)"
[[ -n "$code" ]] && break
kill -0 "$wormholePid" 2> /dev/null || break
done
if [[ -n "$code" ]]; then
msgbox "$(gettext "Wormhole code:") $code"
wait "$wormholePid"
status=$?
if (( status == 0 )); then
msgbox "$(gettext "Logs sent successfully.")"
else
msgbox "$(gettext "Log transfer did not complete successfully.")"
fi
else
kill "$wormholePid" 2> /dev/null || true
wait "$wormholePid" 2> /dev/null || true
detail="$(tail -n 3 "$outputFile" 2> /dev/null)"
if [[ -n "$detail" ]]; then
msgbox "$(gettext "Could not start wormhole transfer:") $detail"
else
msgbox "$(gettext "Could not start wormhole transfer.")"
fi
fi
rm -f "$outputFile" "$bundle"
}
manage-logs() {
local action
local loggingAction
while : ; do
if (( saveSessionLogs )); then
loggingAction="$(gettext "Disable logs")"
else
loggingAction="$(gettext "Enable logs")"
fi
action="$(menulist "$loggingAction" "$(gettext "Send logs with wormhole")" "$(gettext "Go Back")")" || return
case "$action" in
"$loggingAction") toggle-session-logging ;;
"$(gettext "Send logs with wormhole")") send-logs ;;
"$(gettext "Go Back")"|"") return ;;
esac
done
}
add-server() {
local serverName
local serverAddress
@@ -424,6 +576,7 @@ add-server() {
connect() {
local serverName
local barnardStatus
local safeServerName
local -a names=()
local -a barnardArgs=()
@@ -440,6 +593,20 @@ connect() {
require_command barnard barnard
sessionLogFile=""
if (( saveSessionLogs )); then
safeServerName="$(sanitize_filename "$serverName")"
if ! mkdir -p "$logDir"; then
msgbox "$(gettext "Could not create logs directory:") $logDir"
else
sessionLogFile="$logDir/${safeServerName}-$(date +%F).log"
if ! : >> "$sessionLogFile"; then
msgbox "$(gettext "Could not write log file:") $sessionLogFile"
sessionLogFile=""
fi
fi
fi
barnardArgs=(-server "${serverAddresses[$serverName]}:${serverPorts[$serverName]}")
if [[ -n "${serverPasswords[$serverName]}" ]]; then
barnardArgs+=(-password "${serverPasswords[$serverName]}")
@@ -453,9 +620,13 @@ connect() {
if [[ -f "$certFile" ]] && ! config_has_nonempty_value certificate; then
barnardArgs+=(-certificate "$certFile")
fi
if [[ -n "$sessionLogFile" ]]; then
barnardArgs+=(-log debug -logfile "$sessionLogFile")
fi
command barnard "${barnardArgs[@]}" --fifo "$configDir/cmd" --buffers 16 |& log
barnardStatus=${PIPESTATUS[0]}
sessionLogFile=""
if (( barnardStatus != 0 )); then
msgbox "$(gettext "Barnard exited with status") $barnardStatus. $(gettext "See log:") $logFile"
fi
@@ -581,14 +752,16 @@ main() {
require_command dialog dialog
load_servers
load_logging_pref
while : ; do
action="$(menulist "$(gettext "Connect")" "$(gettext "Add server")" "$(gettext "Remove server")" "$(gettext "Manage Certificate")" "$(gettext "Exit")")" || exit 0
action="$(menulist "$(gettext "Connect")" "$(gettext "Add server")" "$(gettext "Remove server")" "$(gettext "Manage Certificate")" "$(gettext "Logs")" "$(gettext "Exit")")" || exit 0
case "$action" in
"$(gettext "Connect")") connect ;;
"$(gettext "Add server")") add-server ;;
"$(gettext "Remove server")") remove-server ;;
"$(gettext "Manage Certificate")") manage-certificate ;;
"$(gettext "Logs")") manage-logs ;;
"$(gettext "Exit")"|"") exit 0 ;;
esac
done
+1
View File
@@ -90,6 +90,7 @@ func (b *Barnard) connect(reconnect bool) bool {
stream.SetMicVolume(b.UserConfig.GetMicVolume(), false)
stream.AttachStream(b.Client)
stream.SetNoiseProcessor(b.NoiseSuppressor)
stream.SetAGCEnabled(b.UserConfig.GetAGCEnabled())
stream.SetErrorFunc(func(err error) {
if err != nil {
b.AddOutputLine(fmt.Sprintf("Microphone: %s", err.Error()))
+1
View File
@@ -21,4 +21,5 @@ type Hotkeys struct {
ScrollToBottom *uiterm.Key
AdminMenu *uiterm.Key
NoiseSuppressionToggle *uiterm.Key
AGCToggle *uiterm.Key
}
+27
View File
@@ -31,6 +31,7 @@ type exportableConfig struct {
Username *string
NotifyCommand *string
NoiseSuppressionEnabled *bool
AGCEnabled *bool
Certificate *string
RecordingFormat *string
RecordingDirectory *string
@@ -109,6 +110,7 @@ func (c *Config) LoadConfig() {
ScrollToBottom: key(uiterm.KeyEnd),
AdminMenu: key(uiterm.KeyF11),
NoiseSuppressionToggle: key(uiterm.KeyF9),
AGCToggle: key(uiterm.KeyF12),
}
if fileExists(c.fn) {
var data []byte
@@ -155,6 +157,11 @@ func (c *Config) LoadConfig() {
enabled := false
jc.NoiseSuppressionEnabled = &enabled
}
if c.config.AGCEnabled == nil {
// AGC has always been active for the microphone, so keep it on by default.
enabled := true
jc.AGCEnabled = &enabled
}
if c.config.Certificate == nil {
cert := string("")
jc.Certificate = &cert
@@ -190,6 +197,7 @@ func (c *Config) ensureHotkeys() {
ScrollToBottom: key(uiterm.KeyEnd),
AdminMenu: key(uiterm.KeyF11),
NoiseSuppressionToggle: key(uiterm.KeyF9),
AGCToggle: key(uiterm.KeyF12),
}
hotkeys := c.config.Hotkeys
if hotkeys.Talk == nil {
@@ -240,6 +248,9 @@ func (c *Config) ensureHotkeys() {
if hotkeys.NoiseSuppressionToggle == nil {
hotkeys.NoiseSuppressionToggle = defaults.NoiseSuppressionToggle
}
if hotkeys.AGCToggle == nil {
hotkeys.AGCToggle = defaults.AGCToggle
}
}
func (c *Config) findServer(address string) *server {
@@ -365,6 +376,22 @@ func (c *Config) SetNoiseSuppressionEnabled(enabled bool) error {
return c.saveConfigLocked()
}
func (c *Config) GetAGCEnabled() bool {
c.mu.Lock()
defer c.mu.Unlock()
if c.config.AGCEnabled == nil {
return true
}
return *c.config.AGCEnabled
}
func (c *Config) SetAGCEnabled(enabled bool) error {
c.mu.Lock()
defer c.mu.Unlock()
c.config.AGCEnabled = &enabled
return c.saveConfigLocked()
}
func (c *Config) GetRecordingFormat() string {
c.mu.Lock()
defer c.mu.Unlock()
+29
View File
@@ -74,6 +74,35 @@ func TestConfigBackfillsRecordingDefaults(t *testing.T) {
}
}
func TestAGCDefaultsOnAndPersists(t *testing.T) {
configPath := filepath.Join(t.TempDir(), "barnard.toml")
if err := os.WriteFile(configPath, []byte("[hotkeys]\ntalk = \"f1\"\n"), 0600); err != nil {
t.Fatal(err)
}
cfg := NewConfig(&configPath)
if !cfg.GetAGCEnabled() {
t.Fatal("expected AGC to default to enabled")
}
if cfg.GetHotkeys().AGCToggle == nil {
t.Fatal("expected AGC toggle hotkey to be backfilled")
}
if got := *cfg.GetHotkeys().AGCToggle; got != uiterm.KeyF12 {
t.Fatalf("expected AGC toggle f12, got %s", got)
}
if err := cfg.SetAGCEnabled(false); err != nil {
t.Fatal(err)
}
reloaded := NewConfig(&configPath)
if reloaded.GetAGCEnabled() {
t.Fatal("expected disabled AGC setting to persist")
}
if got := *reloaded.GetHotkeys().AGCToggle; got != uiterm.KeyF12 {
t.Fatalf("expected saved AGC toggle to reload as f12, got %s", got)
}
}
// Regression: malformed and IPv6 addresses were split at every colon and
// could panic while merely reading a saved user preference.
func TestMakeHostPortHandlesIPv6AndMalformedAddress(t *testing.T) {
+59 -1
View File
@@ -53,8 +53,32 @@ const recorderOutgoingSource uint32 = ^uint32(0)
const (
maxBufferSize = 11520 // Max frame size (2880) * bytes per stereo sample (4)
jitterMaxPackets = 50
// Mumble destroys and recreates AudioInput when the sender switches audio
// devices, which restarts its frame numbering at zero. The destructor
// sends no terminator, so a sender that never unkeys leaves us expecting a
// frame number the new stream will not reach for hours: every packet looks
// permanently late and gets discarded. Detect that and resync.
//
// Two conditions must hold together. A sustained run of late packets
// distinguishes a restarted stream from a clump of reordered packets,
// which is bounded and then recovers on its own. The backwards jump must
// also be too large to be network reordering; a smaller jump needs no
// intervention because the restarted stream climbs back past the stale
// expectation within jitterResyncJump frames anyway.
jitterLateResync = 5
// Frame numbers are Mumble timestamps in 10 ms units, so this is 1 second
// — far beyond any real reordering window.
jitterResyncJump = 100
)
// jitterShouldResync reports whether the sender restarted its frame numbering
// rather than merely delivering a few packets out of order. lateRun is the
// number of consecutive late packets and backJump is how far the current
// packet sits below the expected sequence.
func jitterShouldResync(lateRun int, backJump int64) bool {
return lateRun >= jitterLateResync && backJump >= jitterResyncJump
}
// jitterPlaybackReady holds the requested initial playout delay only once.
// Requiring the delay on every packet drains and refills the renderer in bursts.
func jitterPlaybackReady(started bool, buffered, target time.Duration) bool {
@@ -264,6 +288,23 @@ func (s *Stream) SetNoiseProcessor(np NoiseProcessor) {
s.noiseProcessorRight = cloneNoiseProcessor(np)
}
// SetAGCEnabled turns microphone automatic gain control on or off. The AGC
// objects themselves are created up front, so this only flips their flag and is
// safe to call while capture is running.
func (s *Stream) SetAGCEnabled(enabled bool) {
if s.micAGC != nil {
s.micAGC.SetEnabled(enabled)
}
if s.micAGCRight != nil {
s.micAGCRight.SetEnabled(enabled)
}
}
// IsAGCEnabled reports whether microphone automatic gain control is active.
func (s *Stream) IsAGCEnabled() bool {
return s.micAGC != nil && s.micAGC.IsEnabled()
}
func (s *Stream) SetFilePlayer(fp FilePlayer) {
s.filePlayer = fp
if player, ok := fp.(interface{ SetLocalPlayback(func([]byte)) }); ok {
@@ -495,6 +536,7 @@ func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) {
var jitterDuration time.Duration
var jitterNextSeq int64
var jitterInit, jitterStarted bool
var jitterLateRun int
var jitterDrainLogCounter, jitterAnomalyLogCounter int
resetJitter := func() {
jitterBuf = nil
@@ -502,6 +544,7 @@ func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) {
jitterNextSeq = 0
jitterInit = false
jitterStarted = false
jitterLateRun = 0
}
// insertSorted inserts a packet into the jitter buffer sorted
@@ -595,6 +638,17 @@ func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) {
if pkt == nil {
if len(jitterBuf) > 0 {
if jitterBuf[0].Sequence < jitterNextSeq {
jitterLateRun++
if jitterShouldResync(jitterLateRun, jitterNextSeq-jitterBuf[0].Sequence) {
// The sender restarted its frame numbering
// mid-burst. Follow it instead of discarding
// every remaining packet until it unkeys.
log.Debug("jitter: sequence restart for %s, resyncing from %d to %d",
e.User.Name, jitterNextSeq, jitterBuf[0].Sequence)
jitterNextSeq = jitterBuf[0].Sequence
jitterLateRun = 0
continue
}
// Late or duplicate: discard so it doesn't
// permanently block the drain loop.
jitterAnomalyLogCounter++
@@ -622,6 +676,7 @@ func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) {
}
break
}
jitterLateRun = 0
jitterDrainLogCounter++
if jitterDrainLogCounter <= 3 || jitterDrainLogCounter%1000 == 0 {
log.Debug("jitter: draining seq=%d for %s (buf=%d emptyBufs=%d)",
@@ -1035,7 +1090,7 @@ func (s *Stream) processChannel(samples []int16, noiseProcessor NoiseProcessor,
if noiseProcessor != nil && noiseProcessor.IsEnabled() {
noiseProcessor.ProcessSamples(samples)
}
if micAGC != nil {
if micAGC != nil && micAGC.IsEnabled() {
micAGC.ProcessSamples(samples)
}
}
@@ -1043,6 +1098,9 @@ func (s *Stream) processChannel(samples []int16, noiseProcessor NoiseProcessor,
func (s *Stream) ensureStereoProcessors() {
if s.micAGCRight == nil {
s.micAGCRight = audio.NewAGC()
if s.micAGC != nil {
s.micAGCRight.SetEnabled(s.micAGC.IsEnabled())
}
}
if s.noiseProcessorRight == nil {
s.noiseProcessorRight = cloneNoiseProcessor(s.noiseProcessor)
@@ -62,6 +62,25 @@ func TestJitterPlaybackDelayAppliesOnlyAtStartup(t *testing.T) {
}
}
func TestJitterResyncsAfterSenderRestartsSequence(t *testing.T) {
// Mumble restarts frame numbering at zero when the sender switches audio
// devices mid-burst, and sends no terminator to announce it.
if !jitterShouldResync(jitterLateResync, 52724) {
t.Fatal("jitter did not resync after the sender restarted its frame numbering")
}
if jitterShouldResync(jitterLateResync-1, 52724) {
t.Fatal("jitter resynced before the late run was conclusive")
}
// A clump of reordered packets is bounded and recovers on its own; it must
// not drag the expected sequence backwards.
if jitterShouldResync(jitterLateResync, jitterResyncJump-1) {
t.Fatal("jitter resynced on a backwards jump small enough to be reordering")
}
if jitterShouldResync(1, 52724) {
t.Fatal("jitter resynced on a single late packet")
}
}
func TestAudioPacketDurationUsesStereoFrameCount(t *testing.T) {
packet := &gumble.AudioPacket{AudioBuffer: make(gumble.AudioBuffer, 2*gumble.AudioDefaultFrameSize)}
if got := audioPacketDuration(packet); got != 10*time.Millisecond {
+35
View File
@@ -141,6 +141,29 @@ func (b *Barnard) OnNoiseSuppressionToggle(ui *uiterm.Ui, key uiterm.Key) {
}
}
func (b *Barnard) OnAGCToggle(ui *uiterm.Ui, key uiterm.Key) {
enabled := b.toggleAGC()
if enabled {
b.UpdateGeneralStatus("AGC: ON", false)
} else {
b.UpdateGeneralStatus("AGC: OFF", false)
}
}
// toggleAGC flips the saved AGC preference and applies it to the active
// stream, returning the new state.
func (b *Barnard) toggleAGC() bool {
enabled := !b.UserConfig.GetAGCEnabled()
if err := b.UserConfig.SetAGCEnabled(enabled); err != nil {
b.AddOutputLine("AGC: could not save setting: " + err.Error())
}
b.withStream(func(stream *gumbleopenal.Stream) {
stream.SetAGCEnabled(enabled)
})
return enabled
}
func (b *Barnard) UpdateGeneralStatus(text string, notice bool) {
b.postUI(func() {
b.statusText = text
@@ -212,6 +235,14 @@ func (b *Barnard) CommandNoiseSuppressionToggle(ui *uiterm.Ui, cmd string) {
}
}
func (b *Barnard) CommandAGCToggle(ui *uiterm.Ui, cmd string) {
if b.toggleAGC() {
b.AddOutputLine("AGC enabled")
} else {
b.AddOutputLine("AGC disabled")
}
}
func (b *Barnard) CommandPlayFile(ui *uiterm.Ui, cmd string) {
// cmd contains just the filename part (everything after "/file ")
filename := strings.TrimSpace(cmd)
@@ -498,6 +529,8 @@ func (b *Barnard) OnTextInput(ui *uiterm.Ui, textbox *uiterm.Textbox, text strin
b.CommandStatus(ui, cmdArgs)
case "noise":
b.CommandNoiseSuppressionToggle(ui, cmdArgs)
case "agc":
b.CommandAGCToggle(ui, cmdArgs)
case "record":
b.CommandRecord(ui, cmdArgs)
case "admin":
@@ -595,6 +628,7 @@ func (b *Barnard) OnUiInitialize(ui *uiterm.Ui) {
b.Ui.AddCommandListener(b.CommandExit, "exit")
b.Ui.AddCommandListener(b.CommandStatus, "status")
b.Ui.AddCommandListener(b.CommandNoiseSuppressionToggle, "noise")
b.Ui.AddCommandListener(b.CommandAGCToggle, "agc")
b.Ui.AddCommandListener(b.CommandPlayFile, "file")
b.Ui.AddCommandListener(b.CommandStopFile, "stop")
b.Ui.AddCommandListener(b.CommandRecord, "record")
@@ -604,6 +638,7 @@ func (b *Barnard) OnUiInitialize(ui *uiterm.Ui) {
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.OnAGCToggle, b.Hotkeys.AGCToggle)
b.Ui.AddKeyListener(b.OnRecordingToggle, b.Hotkeys.RecordToggle)
b.Ui.AddKeyListener(b.OnClearPress, b.Hotkeys.ClearOutput)
b.Ui.AddKeyListener(b.OnQuitPress, b.Hotkeys.Exit)
+15 -2
View File
@@ -38,7 +38,7 @@ func (ti TreeItem) TreeItemStyle(fg, bg uiterm.Attribute, active bool) (uiterm.A
}
func (b *Barnard) changeVolume(users []*gumble.User, change float32) {
b.withStream(func(stream *gumbleopenal.Stream) {
changed := b.withStream(func(stream *gumbleopenal.Stream) {
for _, u := range users {
var boost uint16
var ng float32
@@ -63,10 +63,13 @@ func (b *Barnard) changeVolume(users []*gumble.User, change float32) {
b.AddOutputLine("Volume: could not save setting: " + err.Error())
}
})
if changed {
b.refreshVolumeDisplay()
}
}
func (b *Barnard) resetVolume(users []*gumble.User) {
b.withStream(func(stream *gumbleopenal.Stream) {
changed := b.withStream(func(stream *gumbleopenal.Stream) {
for _, u := range users {
// Reset to original volume (1.0) and boost (1)
u.SetBoost(uint16(1))
@@ -78,6 +81,16 @@ func (b *Barnard) resetVolume(users []*gumble.User) {
b.AddOutputLine("Volume: could not save setting: " + err.Error())
}
})
if changed {
b.refreshVolumeDisplay()
}
}
// Tree items render a display string snapshotted at build time, so a volume
// change is only visible after the tree is rebuilt.
func (b *Barnard) refreshVolumeDisplay() {
b.RebuildUserChannelTreePreservingSelection()
b.Ui.Refresh()
}
func makeUsersArray(users gumble.Users) []*gumble.User {