From 9dd013797512ffcd4bde1596803a6facb712885e Mon Sep 17 00:00:00 2001 From: "Brandon McGinty (deepseek)" Date: Sat, 8 Aug 2026 20:58:53 -0400 Subject: [PATCH] fix: add synchronization to User audio fields to prevent data races User.AudioSource, Boost, Volume, and LocallyMuted were accessed from both the OnAudioStream audio goroutine and the UI goroutine without synchronization, a data race under the Go memory model. Replace direct field access with thread-safe getter/setter methods protected by a per-user mutex: - SetAudioSource/GetAudioSource for the OpenAL source pointer - SetBoost/Boost for the audio boost multiplier - SetVolume/Volume for the volume level - SetLocallyMuted/LocallyMuted for the local mute state Update all call sites across config/, barnard.go, client.go, ui_tree.go, and stream.go. --- barnard.go | 20 +++++----- client.go | 6 +-- config/user_config.go | 18 ++++----- gumble/gumble/user.go | 75 +++++++++++++++++++++++++++++++---- gumble/gumbleopenal/stream.go | 20 +++++----- ui_tree.go | 24 +++++------ 6 files changed, 112 insertions(+), 51 deletions(-) diff --git a/barnard.go b/barnard.go index 9c4ded3..c5a4fe8 100644 --- a/barnard.go +++ b/barnard.go @@ -115,17 +115,17 @@ func (b *Barnard) TreeItemKeyPress(ui *uiterm.Ui, tree *uiterm.Tree, item uiterm users := makeUsersArray(treeItem.Channel.Users) for _, u := range users { // Explicitly set user mute state to match channel state - if channelWillBeMuted && !u.LocallyMuted { + if channelWillBeMuted && !u.LocallyMuted() { b.UserConfig.ToggleMute(u) - } else if !channelWillBeMuted && u.LocallyMuted { + } else if !channelWillBeMuted && u.LocallyMuted() { b.UserConfig.ToggleMute(u) } - if u.AudioSource != nil { - if u.LocallyMuted { - u.AudioSource.SetGain(0) + if au := u.AudioSource(); au != nil { + if u.LocallyMuted() { + au.SetGain(0) } else { - u.AudioSource.SetGain(u.Volume) + au.SetGain(u.Volume()) } } } @@ -159,11 +159,11 @@ func (b *Barnard) TreeItemKeyPress(ui *uiterm.Ui, tree *uiterm.Tree, item uiterm if key == *b.Hotkeys.MuteToggle { // Toggle mute for single user b.UserConfig.ToggleMute(treeItem.User) - if treeItem.User.AudioSource != nil { - if treeItem.User.LocallyMuted { - treeItem.User.AudioSource.SetGain(0) + if au := treeItem.User.AudioSource(); au != nil { + if treeItem.User.LocallyMuted() { + au.SetGain(0) } else { - treeItem.User.AudioSource.SetGain(treeItem.User.Volume) + au.SetGain(treeItem.User.Volume()) } } b.RebuildUserChannelTreePreservingSelection() diff --git a/client.go b/client.go index 1d5324d..7b45273 100644 --- a/client.go +++ b/client.go @@ -179,11 +179,11 @@ func (b *Barnard) OnUserChange(e *gumble.UserChangeEvent) { // If the channel is muted, ensure the user is muted if b.MutedChannels[e.User.Channel.ID] { // Only mute if not already muted - if !e.User.LocallyMuted { + if !e.User.LocallyMuted() { b.UserConfig.ToggleMute(e.User) } - if e.User.AudioSource != nil { - e.User.AudioSource.SetGain(0) + if au := e.User.AudioSource(); au != nil { + au.SetGain(0) } } } diff --git a/config/user_config.go b/config/user_config.go index ff026ae..5451565 100644 --- a/config/user_config.go +++ b/config/user_config.go @@ -253,7 +253,7 @@ func (c *Config) findUser(address string, username string) *eUser { func (c *Config) ToggleMute(u *gumble.User) { j := c.findUser(u.GetClient().Config.Address, u.Name) j.LocallyMuted = !j.LocallyMuted - u.LocallyMuted = j.LocallyMuted + u.SetLocallyMuted(j.LocallyMuted) c.SaveConfig() } @@ -329,11 +329,11 @@ func (c *Config) UpdateUser(u *gumble.User) { uc = u.GetClient() if uc != nil { j = c.findUser(uc.Config.Address, u.Name) - u.Boost = j.Boost - u.Volume = j.Volume - u.LocallyMuted = j.LocallyMuted // Update LocallyMuted state from config - if u.Boost < 1 { - u.Boost = 1 + u.SetBoost(j.Boost) + u.SetVolume(j.Volume) + u.SetLocallyMuted(j.LocallyMuted) // Update LocallyMuted state from config + if u.Boost() < 1 { + u.SetBoost(1) } } } @@ -341,9 +341,9 @@ func (c *Config) UpdateUser(u *gumble.User) { func (c *Config) UpdateConfig(u *gumble.User) { var j *eUser j = c.findUser(u.GetClient().Config.Address, u.Name) - j.Boost = u.Boost - j.Volume = u.Volume - j.LocallyMuted = u.LocallyMuted // Save LocallyMuted state to config + j.Boost = u.Boost() + j.Volume = u.Volume() + j.LocallyMuted = u.LocallyMuted() // Save LocallyMuted state to config } func NewConfig(fn *string) *Config { diff --git a/gumble/gumble/user.go b/gumble/gumble/user.go index ef9546c..756f96f 100644 --- a/gumble/gumble/user.go +++ b/gumble/gumble/user.go @@ -1,6 +1,8 @@ package gumble import ( + "sync" + "git.stormux.org/storm/barnard/gumble/go-openal/openal" "git.stormux.org/storm/barnard/gumble/gumble/MumbleProto" "google.golang.org/protobuf/proto" @@ -31,9 +33,6 @@ type User struct { PrioritySpeaker bool // Is the user recording audio? Recording bool - // Has the user been locally muted by the client? - LocallyMuted bool - // The user's comment. Contains the empty string if the user does not have a // comment, or if the comment needs to be requested. Comment string @@ -58,14 +57,76 @@ type User struct { audioSequence int64 audioSequenceValid bool - AudioSource *openal.Source - Boost uint16 - Volume float32 + // audioMu protects audio-related fields accessed from both the + // audio processing goroutine (OnAudioStream) and the UI goroutine. + audioMu sync.Mutex + audioSource *openal.Source + boost uint16 + volume float32 + locallyMuted bool +} + +// SetAudioSource sets the user's OpenAL audio source (thread-safe). +func (u *User) SetAudioSource(src *openal.Source) { + u.audioMu.Lock() + u.audioSource = src + u.audioMu.Unlock() +} + +// AudioSource returns the user's OpenAL audio source (thread-safe). +// The caller must not retain the pointer across unlock boundaries; +// it is only valid while the caller ensures the source is not deleted. +func (u *User) AudioSource() *openal.Source { + u.audioMu.Lock() + defer u.audioMu.Unlock() + return u.audioSource +} + +// SetBoost sets the user's audio boost multiplier (thread-safe). +func (u *User) SetBoost(b uint16) { + u.audioMu.Lock() + u.boost = b + u.audioMu.Unlock() +} + +// Boost returns the user's audio boost multiplier (thread-safe). +func (u *User) Boost() uint16 { + u.audioMu.Lock() + defer u.audioMu.Unlock() + return u.boost +} + +// SetVolume sets the user's volume level (thread-safe). +func (u *User) SetVolume(v float32) { + u.audioMu.Lock() + u.volume = v + u.audioMu.Unlock() +} + +// Volume returns the user's volume level (thread-safe). +func (u *User) Volume() float32 { + u.audioMu.Lock() + defer u.audioMu.Unlock() + return u.volume +} + +// SetLocallyMuted sets whether the user is locally muted (thread-safe). +func (u *User) SetLocallyMuted(m bool) { + u.audioMu.Lock() + u.locallyMuted = m + u.audioMu.Unlock() +} + +// LocallyMuted returns whether the user is locally muted (thread-safe). +func (u *User) LocallyMuted() bool { + u.audioMu.Lock() + defer u.audioMu.Unlock() + return u.locallyMuted } // IsMuted returns true if the user is muted either server-side or locally func (u *User) IsMuted() bool { - return u.Muted || u.LocallyMuted + return u.Muted || u.LocallyMuted() } func (u *User) GetClient() *Client { diff --git a/gumble/gumbleopenal/stream.go b/gumble/gumbleopenal/stream.go index 554f995..4588d32 100644 --- a/gumble/gumbleopenal/stream.go +++ b/gumble/gumbleopenal/stream.go @@ -236,13 +236,13 @@ func (s *Stream) SetMicVolume(change float32, relative bool) { func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) { go func(e *gumble.AudioStreamEvent) { var source = openal.NewSource() - e.User.AudioSource = &source + e.User.SetAudioSource(&source) // Set initial gain based on volume and mute state - if e.User.LocallyMuted { - e.User.AudioSource.SetGain(0) + if e.User.LocallyMuted() { + source.SetGain(0) } else { - e.User.AudioSource.SetGain(e.User.Volume) + source.SetGain(e.User.Volume()) } bufferCount := e.Client.Config.Buffers @@ -263,17 +263,17 @@ func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) { for packet := range e.C { // Skip processing if user is locally muted - if e.User.LocallyMuted { + if e.User.LocallyMuted() { continue } - var boost uint16 = uint16(1) samples := len(packet.AudioBuffer) if samples > cap(raw)/2 { continue } - boost = e.User.Boost + boost := e.User.Boost() + userVolume := e.User.Volume() recorder := s.getRecorder() var recordBuffer []int16 recordPtr := 0 @@ -306,7 +306,7 @@ func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) { } } if recorder != nil { - recordBuffer[recordPtr] = scaleForRecording(sample, e.User.Volume) + recordBuffer[recordPtr] = scaleForRecording(sample, userVolume) recordPtr++ } binary.LittleEndian.PutUint16(raw[rawPtr:], uint16(sample)) @@ -325,7 +325,7 @@ func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) { } } if recorder != nil { - recordBuffer[recordPtr] = scaleForRecording(sample, e.User.Volume) + recordBuffer[recordPtr] = scaleForRecording(sample, userVolume) recordPtr++ } binary.LittleEndian.PutUint16(raw[rawPtr:], uint16(sample)) @@ -346,7 +346,7 @@ func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) { } } if recorder != nil { - recordSample := scaleForRecording(sample, e.User.Volume) + recordSample := scaleForRecording(sample, userVolume) recordBuffer[recordPtr] = recordSample recordBuffer[recordPtr+1] = recordSample recordPtr += 2 diff --git a/ui_tree.go b/ui_tree.go index 6e60b3c..7057777 100644 --- a/ui_tree.go +++ b/ui_tree.go @@ -9,12 +9,12 @@ import ( func (ti TreeItem) String() string { if ti.User != nil { - if ti.User.LocallyMuted { + if ti.User.LocallyMuted() { return "[MUTED] " + ti.User.Name } // Calculate total volume as percentage - boostPercent := float32(ti.User.Boost-1) * 10 - totalVolume := ti.User.Volume*100 + boostPercent + boostPercent := float32(ti.User.Boost()-1) * 10 + totalVolume := ti.User.Volume()*100 + boostPercent return fmt.Sprintf("%s [%.0f%%]", ti.User.Name, totalVolume) } if ti.Channel != nil { @@ -35,7 +35,7 @@ func (ti TreeItem) TreeItemStyle(fg, bg uiterm.Attribute, active bool) (uiterm.A func (b *Barnard) changeVolume(users []*gumble.User, change float32) { for _, u := range users { - au := u.AudioSource + au := u.AudioSource() if au == nil { continue } @@ -43,7 +43,7 @@ func (b *Barnard) changeVolume(users []*gumble.User, change float32) { var cv float32 var ng float32 var curboost float32 - curboost = float32((u.Boost - 1)) / 10 + curboost = float32((u.Boost() - 1)) / 10 cv = au.GetGain() + curboost ng = cv + change boost = uint16(1) @@ -56,9 +56,9 @@ func (b *Barnard) changeVolume(users []*gumble.User, change float32) { if ng < 0 { ng = 0.0 } - u.Boost = boost - u.Volume = ng - if !u.LocallyMuted { + u.SetBoost(boost) + u.SetVolume(ng) + if !u.LocallyMuted() { au.SetGain(ng) } b.UserConfig.UpdateConfig(u) @@ -68,14 +68,14 @@ func (b *Barnard) changeVolume(users []*gumble.User, change float32) { func (b *Barnard) resetVolume(users []*gumble.User) { for _, u := range users { - au := u.AudioSource + au := u.AudioSource() if au == nil { continue } // Reset to original volume (1.0) and boost (1) - u.Boost = uint16(1) - u.Volume = 1.0 - if !u.LocallyMuted { + u.SetBoost(uint16(1)) + u.SetVolume(1.0) + if !u.LocallyMuted() { au.SetGain(1.0) } b.UserConfig.UpdateConfig(u)