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. 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 ## 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.
@@ -54,6 +68,7 @@ Current Commands:
* 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. * 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`. * record: Toggle recording. You may also use `record start` or `record stop`.
* exit: Exit Barnard, just like when you press your quit key. * 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>F1</kbd>: toggle voice transmission
- <kbd>F9</kbd>: toggle noise suppression - <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>F11</kbd>: open actions menu for the focused tree item
- <kbd>Ctrl+R</kbd>: toggle recording - <kbd>Ctrl+R</kbd>: toggle recording
- <kbd>Ctrl+L</kbd>: clear chat log - <kbd>Ctrl+L</kbd>: clear chat log
+8 -6
View File
@@ -2,6 +2,7 @@ package audio
import ( import (
"math" "math"
"sync/atomic"
) )
// AGC (Automatic Gain Control) processor for voice normalization // AGC (Automatic Gain Control) processor for voice normalization
@@ -13,14 +14,14 @@ type AGC struct {
releaseTime float32 // Release time coefficient releaseTime float32 // Release time coefficient
currentGain float32 // Current gain value currentGain float32 // Current gain value
envelope float32 // Signal envelope envelope float32 // Signal envelope
enabled bool // Whether AGC is enabled enabled atomic.Bool // Whether AGC is enabled; toggled outside the capture goroutine
compThreshold float32 // Compression threshold compThreshold float32 // Compression threshold
compRatio float32 // Compression ratio compRatio float32 // Compression ratio
} }
// NewAGC creates a new AGC processor with sensible defaults for voice // NewAGC creates a new AGC processor with sensible defaults for voice
func NewAGC() *AGC { func NewAGC() *AGC {
return &AGC{ agc := &AGC{
targetLevel: 0.12, // Target 12% of max amplitude (conservative level) targetLevel: 0.12, // Target 12% of max amplitude (conservative level)
maxGain: 4.0, // Maximum 4x gain (about 12dB) maxGain: 4.0, // Maximum 4x gain (about 12dB)
minGain: 0.25, // Minimum 0.25x gain (-12dB) minGain: 0.25, // Minimum 0.25x gain (-12dB)
@@ -28,15 +29,16 @@ func NewAGC() *AGC {
releaseTime: 0.15, // Slower release (150ms) releaseTime: 0.15, // Slower release (150ms)
currentGain: 1.0, // Start with unity gain currentGain: 1.0, // Start with unity gain
envelope: 0.0, // Start with zero envelope envelope: 0.0, // Start with zero envelope
enabled: true, // Enable by default
compThreshold: 0.85, // Compress signals above 85% compThreshold: 0.85, // Compress signals above 85%
compRatio: 2.0, // 2:1 compression ratio (gentler) compRatio: 2.0, // 2:1 compression ratio (gentler)
} }
agc.enabled.Store(true) // Enable by default
return agc
} }
// ProcessSamples applies AGC processing to audio samples // ProcessSamples applies AGC processing to audio samples
func (agc *AGC) ProcessSamples(samples []int16) { func (agc *AGC) ProcessSamples(samples []int16) {
if !agc.enabled || len(samples) == 0 { if !agc.enabled.Load() || len(samples) == 0 {
return return
} }
@@ -125,12 +127,12 @@ func (agc *AGC) ProcessSamples(samples []int16) {
// SetEnabled enables or disables AGC processing // SetEnabled enables or disables AGC processing
func (agc *AGC) SetEnabled(enabled bool) { func (agc *AGC) SetEnabled(enabled bool) {
agc.enabled = enabled agc.enabled.Store(enabled)
} }
// IsEnabled returns whether AGC is enabled // IsEnabled returns whether AGC is enabled
func (agc *AGC) IsEnabled() bool { func (agc *AGC) IsEnabled() bool {
return agc.enabled return agc.enabled.Load()
} }
// SetTargetLevel sets the target RMS level (0.0-1.0) // 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" serverFile="$configDir/servers.conf"
certFile="$configDir/barnard.pem" certFile="$configDir/barnard.pem"
logFile="$cacheDir/${0##*/}.log" logFile="$cacheDir/${0##*/}.log"
logDir="$HOME/barnard-logs"
logPrefsFile="$configDir/logging.conf"
sessionLogFile=""
saveSessionLogs=0
if ! mkdir -p "$cacheDir" "$configDir"; then if ! mkdir -p "$cacheDir" "$configDir"; then
printf 'Could not create Barnard configuration directories.\n' >&2 printf 'Could not create Barnard configuration directories.\n' >&2
@@ -64,6 +68,9 @@ log() {
local line local line
while IFS= read -r line ; do while IFS= read -r line ; do
printf '%s\n' "$line" >> "$logFile" printf '%s\n' "$line" >> "$logFile"
if [[ -n "$sessionLogFile" ]]; then
printf '%s\n' "$line" >> "$sessionLogFile"
fi
done done
} }
@@ -141,6 +148,12 @@ trim() {
printf '%s' "$value" printf '%s' "$value"
} }
sanitize_filename() {
local value="$1"
value="${value//[^[:alnum:]_.-]/_}"
printf '%s' "$value"
}
field_is_valid() { field_is_valid() {
local value="$1" local value="$1"
[[ "$value" != *$'\n'* && "$value" != *$'\r'* ]] [[ "$value" != *$'\n'* && "$value" != *$'\r'* ]]
@@ -376,6 +389,145 @@ config_has_nonempty_value() {
return 1 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() { add-server() {
local serverName local serverName
local serverAddress local serverAddress
@@ -424,6 +576,7 @@ add-server() {
connect() { connect() {
local serverName local serverName
local barnardStatus local barnardStatus
local safeServerName
local -a names=() local -a names=()
local -a barnardArgs=() local -a barnardArgs=()
@@ -440,6 +593,20 @@ connect() {
require_command barnard barnard 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]}") barnardArgs=(-server "${serverAddresses[$serverName]}:${serverPorts[$serverName]}")
if [[ -n "${serverPasswords[$serverName]}" ]]; then if [[ -n "${serverPasswords[$serverName]}" ]]; then
barnardArgs+=(-password "${serverPasswords[$serverName]}") barnardArgs+=(-password "${serverPasswords[$serverName]}")
@@ -453,9 +620,13 @@ connect() {
if [[ -f "$certFile" ]] && ! config_has_nonempty_value certificate; then if [[ -f "$certFile" ]] && ! config_has_nonempty_value certificate; then
barnardArgs+=(-certificate "$certFile") barnardArgs+=(-certificate "$certFile")
fi fi
if [[ -n "$sessionLogFile" ]]; then
barnardArgs+=(-log debug -logfile "$sessionLogFile")
fi
command barnard "${barnardArgs[@]}" --fifo "$configDir/cmd" --buffers 16 |& log command barnard "${barnardArgs[@]}" --fifo "$configDir/cmd" --buffers 16 |& log
barnardStatus=${PIPESTATUS[0]} barnardStatus=${PIPESTATUS[0]}
sessionLogFile=""
if (( barnardStatus != 0 )); then if (( barnardStatus != 0 )); then
msgbox "$(gettext "Barnard exited with status") $barnardStatus. $(gettext "See log:") $logFile" msgbox "$(gettext "Barnard exited with status") $barnardStatus. $(gettext "See log:") $logFile"
fi fi
@@ -581,14 +752,16 @@ main() {
require_command dialog dialog require_command dialog dialog
load_servers load_servers
load_logging_pref
while : ; do 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 case "$action" in
"$(gettext "Connect")") connect ;; "$(gettext "Connect")") connect ;;
"$(gettext "Add server")") add-server ;; "$(gettext "Add server")") add-server ;;
"$(gettext "Remove server")") remove-server ;; "$(gettext "Remove server")") remove-server ;;
"$(gettext "Manage Certificate")") manage-certificate ;; "$(gettext "Manage Certificate")") manage-certificate ;;
"$(gettext "Logs")") manage-logs ;;
"$(gettext "Exit")"|"") exit 0 ;; "$(gettext "Exit")"|"") exit 0 ;;
esac esac
done done
+1
View File
@@ -90,6 +90,7 @@ func (b *Barnard) connect(reconnect bool) bool {
stream.SetMicVolume(b.UserConfig.GetMicVolume(), false) stream.SetMicVolume(b.UserConfig.GetMicVolume(), false)
stream.AttachStream(b.Client) stream.AttachStream(b.Client)
stream.SetNoiseProcessor(b.NoiseSuppressor) stream.SetNoiseProcessor(b.NoiseSuppressor)
stream.SetAGCEnabled(b.UserConfig.GetAGCEnabled())
stream.SetErrorFunc(func(err error) { stream.SetErrorFunc(func(err error) {
if err != nil { if err != nil {
b.AddOutputLine(fmt.Sprintf("Microphone: %s", err.Error())) b.AddOutputLine(fmt.Sprintf("Microphone: %s", err.Error()))
+1
View File
@@ -21,4 +21,5 @@ type Hotkeys struct {
ScrollToBottom *uiterm.Key ScrollToBottom *uiterm.Key
AdminMenu *uiterm.Key AdminMenu *uiterm.Key
NoiseSuppressionToggle *uiterm.Key NoiseSuppressionToggle *uiterm.Key
AGCToggle *uiterm.Key
} }
+27
View File
@@ -31,6 +31,7 @@ type exportableConfig struct {
Username *string Username *string
NotifyCommand *string NotifyCommand *string
NoiseSuppressionEnabled *bool NoiseSuppressionEnabled *bool
AGCEnabled *bool
Certificate *string Certificate *string
RecordingFormat *string RecordingFormat *string
RecordingDirectory *string RecordingDirectory *string
@@ -109,6 +110,7 @@ func (c *Config) LoadConfig() {
ScrollToBottom: key(uiterm.KeyEnd), ScrollToBottom: key(uiterm.KeyEnd),
AdminMenu: key(uiterm.KeyF11), AdminMenu: key(uiterm.KeyF11),
NoiseSuppressionToggle: key(uiterm.KeyF9), NoiseSuppressionToggle: key(uiterm.KeyF9),
AGCToggle: key(uiterm.KeyF12),
} }
if fileExists(c.fn) { if fileExists(c.fn) {
var data []byte var data []byte
@@ -155,6 +157,11 @@ func (c *Config) LoadConfig() {
enabled := false enabled := false
jc.NoiseSuppressionEnabled = &enabled 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 { if c.config.Certificate == nil {
cert := string("") cert := string("")
jc.Certificate = &cert jc.Certificate = &cert
@@ -190,6 +197,7 @@ func (c *Config) ensureHotkeys() {
ScrollToBottom: key(uiterm.KeyEnd), ScrollToBottom: key(uiterm.KeyEnd),
AdminMenu: key(uiterm.KeyF11), AdminMenu: key(uiterm.KeyF11),
NoiseSuppressionToggle: key(uiterm.KeyF9), NoiseSuppressionToggle: key(uiterm.KeyF9),
AGCToggle: key(uiterm.KeyF12),
} }
hotkeys := c.config.Hotkeys hotkeys := c.config.Hotkeys
if hotkeys.Talk == nil { if hotkeys.Talk == nil {
@@ -240,6 +248,9 @@ func (c *Config) ensureHotkeys() {
if hotkeys.NoiseSuppressionToggle == nil { if hotkeys.NoiseSuppressionToggle == nil {
hotkeys.NoiseSuppressionToggle = defaults.NoiseSuppressionToggle hotkeys.NoiseSuppressionToggle = defaults.NoiseSuppressionToggle
} }
if hotkeys.AGCToggle == nil {
hotkeys.AGCToggle = defaults.AGCToggle
}
} }
func (c *Config) findServer(address string) *server { func (c *Config) findServer(address string) *server {
@@ -365,6 +376,22 @@ func (c *Config) SetNoiseSuppressionEnabled(enabled bool) error {
return c.saveConfigLocked() 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 { func (c *Config) GetRecordingFormat() string {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() 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 // Regression: malformed and IPv6 addresses were split at every colon and
// could panic while merely reading a saved user preference. // could panic while merely reading a saved user preference.
func TestMakeHostPortHandlesIPv6AndMalformedAddress(t *testing.T) { func TestMakeHostPortHandlesIPv6AndMalformedAddress(t *testing.T) {
+59 -1
View File
@@ -53,8 +53,32 @@ const recorderOutgoingSource uint32 = ^uint32(0)
const ( const (
maxBufferSize = 11520 // Max frame size (2880) * bytes per stereo sample (4) maxBufferSize = 11520 // Max frame size (2880) * bytes per stereo sample (4)
jitterMaxPackets = 50 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. // jitterPlaybackReady holds the requested initial playout delay only once.
// Requiring the delay on every packet drains and refills the renderer in bursts. // Requiring the delay on every packet drains and refills the renderer in bursts.
func jitterPlaybackReady(started bool, buffered, target time.Duration) bool { func jitterPlaybackReady(started bool, buffered, target time.Duration) bool {
@@ -264,6 +288,23 @@ func (s *Stream) SetNoiseProcessor(np NoiseProcessor) {
s.noiseProcessorRight = cloneNoiseProcessor(np) 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) { func (s *Stream) SetFilePlayer(fp FilePlayer) {
s.filePlayer = fp s.filePlayer = fp
if player, ok := fp.(interface{ SetLocalPlayback(func([]byte)) }); ok { 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 jitterDuration time.Duration
var jitterNextSeq int64 var jitterNextSeq int64
var jitterInit, jitterStarted bool var jitterInit, jitterStarted bool
var jitterLateRun int
var jitterDrainLogCounter, jitterAnomalyLogCounter int var jitterDrainLogCounter, jitterAnomalyLogCounter int
resetJitter := func() { resetJitter := func() {
jitterBuf = nil jitterBuf = nil
@@ -502,6 +544,7 @@ func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) {
jitterNextSeq = 0 jitterNextSeq = 0
jitterInit = false jitterInit = false
jitterStarted = false jitterStarted = false
jitterLateRun = 0
} }
// insertSorted inserts a packet into the jitter buffer sorted // insertSorted inserts a packet into the jitter buffer sorted
@@ -595,6 +638,17 @@ func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) {
if pkt == nil { if pkt == nil {
if len(jitterBuf) > 0 { if len(jitterBuf) > 0 {
if jitterBuf[0].Sequence < jitterNextSeq { 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 // Late or duplicate: discard so it doesn't
// permanently block the drain loop. // permanently block the drain loop.
jitterAnomalyLogCounter++ jitterAnomalyLogCounter++
@@ -622,6 +676,7 @@ func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) {
} }
break break
} }
jitterLateRun = 0
jitterDrainLogCounter++ jitterDrainLogCounter++
if jitterDrainLogCounter <= 3 || jitterDrainLogCounter%1000 == 0 { if jitterDrainLogCounter <= 3 || jitterDrainLogCounter%1000 == 0 {
log.Debug("jitter: draining seq=%d for %s (buf=%d emptyBufs=%d)", 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() { if noiseProcessor != nil && noiseProcessor.IsEnabled() {
noiseProcessor.ProcessSamples(samples) noiseProcessor.ProcessSamples(samples)
} }
if micAGC != nil { if micAGC != nil && micAGC.IsEnabled() {
micAGC.ProcessSamples(samples) micAGC.ProcessSamples(samples)
} }
} }
@@ -1043,6 +1098,9 @@ func (s *Stream) processChannel(samples []int16, noiseProcessor NoiseProcessor,
func (s *Stream) ensureStereoProcessors() { func (s *Stream) ensureStereoProcessors() {
if s.micAGCRight == nil { if s.micAGCRight == nil {
s.micAGCRight = audio.NewAGC() s.micAGCRight = audio.NewAGC()
if s.micAGC != nil {
s.micAGCRight.SetEnabled(s.micAGC.IsEnabled())
}
} }
if s.noiseProcessorRight == nil { if s.noiseProcessorRight == nil {
s.noiseProcessorRight = cloneNoiseProcessor(s.noiseProcessor) 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) { func TestAudioPacketDurationUsesStereoFrameCount(t *testing.T) {
packet := &gumble.AudioPacket{AudioBuffer: make(gumble.AudioBuffer, 2*gumble.AudioDefaultFrameSize)} packet := &gumble.AudioPacket{AudioBuffer: make(gumble.AudioBuffer, 2*gumble.AudioDefaultFrameSize)}
if got := audioPacketDuration(packet); got != 10*time.Millisecond { 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) { func (b *Barnard) UpdateGeneralStatus(text string, notice bool) {
b.postUI(func() { b.postUI(func() {
b.statusText = text 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) { func (b *Barnard) CommandPlayFile(ui *uiterm.Ui, cmd string) {
// cmd contains just the filename part (everything after "/file ") // cmd contains just the filename part (everything after "/file ")
filename := strings.TrimSpace(cmd) filename := strings.TrimSpace(cmd)
@@ -498,6 +529,8 @@ func (b *Barnard) OnTextInput(ui *uiterm.Ui, textbox *uiterm.Textbox, text strin
b.CommandStatus(ui, cmdArgs) b.CommandStatus(ui, cmdArgs)
case "noise": case "noise":
b.CommandNoiseSuppressionToggle(ui, cmdArgs) b.CommandNoiseSuppressionToggle(ui, cmdArgs)
case "agc":
b.CommandAGCToggle(ui, cmdArgs)
case "record": case "record":
b.CommandRecord(ui, cmdArgs) b.CommandRecord(ui, cmdArgs)
case "admin": case "admin":
@@ -595,6 +628,7 @@ func (b *Barnard) OnUiInitialize(ui *uiterm.Ui) {
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.AddCommandListener(b.CommandNoiseSuppressionToggle, "noise")
b.Ui.AddCommandListener(b.CommandAGCToggle, "agc")
b.Ui.AddCommandListener(b.CommandPlayFile, "file") b.Ui.AddCommandListener(b.CommandPlayFile, "file")
b.Ui.AddCommandListener(b.CommandStopFile, "stop") b.Ui.AddCommandListener(b.CommandStopFile, "stop")
b.Ui.AddCommandListener(b.CommandRecord, "record") 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.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.OnNoiseSuppressionToggle, b.Hotkeys.NoiseSuppressionToggle)
b.Ui.AddKeyListener(b.OnAGCToggle, b.Hotkeys.AGCToggle)
b.Ui.AddKeyListener(b.OnRecordingToggle, b.Hotkeys.RecordToggle) b.Ui.AddKeyListener(b.OnRecordingToggle, b.Hotkeys.RecordToggle)
b.Ui.AddKeyListener(b.OnClearPress, b.Hotkeys.ClearOutput) b.Ui.AddKeyListener(b.OnClearPress, b.Hotkeys.ClearOutput)
b.Ui.AddKeyListener(b.OnQuitPress, b.Hotkeys.Exit) 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) { 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 { for _, u := range users {
var boost uint16 var boost uint16
var ng float32 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()) b.AddOutputLine("Volume: could not save setting: " + err.Error())
} }
}) })
if changed {
b.refreshVolumeDisplay()
}
} }
func (b *Barnard) resetVolume(users []*gumble.User) { func (b *Barnard) resetVolume(users []*gumble.User) {
b.withStream(func(stream *gumbleopenal.Stream) { changed := b.withStream(func(stream *gumbleopenal.Stream) {
for _, u := range users { for _, u := range users {
// Reset to original volume (1.0) and boost (1) // Reset to original volume (1.0) and boost (1)
u.SetBoost(uint16(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()) 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 { func makeUsersArray(users gumble.Users) []*gumble.User {