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.
This commit is contained in:
committed by
Brandon McGinty
parent
1bdd7ac52e
commit
9dd0137975
+10
-10
@@ -115,17 +115,17 @@ func (b *Barnard) TreeItemKeyPress(ui *uiterm.Ui, tree *uiterm.Tree, item uiterm
|
|||||||
users := makeUsersArray(treeItem.Channel.Users)
|
users := makeUsersArray(treeItem.Channel.Users)
|
||||||
for _, u := range users {
|
for _, u := range users {
|
||||||
// Explicitly set user mute state to match channel state
|
// Explicitly set user mute state to match channel state
|
||||||
if channelWillBeMuted && !u.LocallyMuted {
|
if channelWillBeMuted && !u.LocallyMuted() {
|
||||||
b.UserConfig.ToggleMute(u)
|
b.UserConfig.ToggleMute(u)
|
||||||
} else if !channelWillBeMuted && u.LocallyMuted {
|
} else if !channelWillBeMuted && u.LocallyMuted() {
|
||||||
b.UserConfig.ToggleMute(u)
|
b.UserConfig.ToggleMute(u)
|
||||||
}
|
}
|
||||||
|
|
||||||
if u.AudioSource != nil {
|
if au := u.AudioSource(); au != nil {
|
||||||
if u.LocallyMuted {
|
if u.LocallyMuted() {
|
||||||
u.AudioSource.SetGain(0)
|
au.SetGain(0)
|
||||||
} else {
|
} 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 {
|
if key == *b.Hotkeys.MuteToggle {
|
||||||
// Toggle mute for single user
|
// Toggle mute for single user
|
||||||
b.UserConfig.ToggleMute(treeItem.User)
|
b.UserConfig.ToggleMute(treeItem.User)
|
||||||
if treeItem.User.AudioSource != nil {
|
if au := treeItem.User.AudioSource(); au != nil {
|
||||||
if treeItem.User.LocallyMuted {
|
if treeItem.User.LocallyMuted() {
|
||||||
treeItem.User.AudioSource.SetGain(0)
|
au.SetGain(0)
|
||||||
} else {
|
} else {
|
||||||
treeItem.User.AudioSource.SetGain(treeItem.User.Volume)
|
au.SetGain(treeItem.User.Volume())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
b.RebuildUserChannelTreePreservingSelection()
|
b.RebuildUserChannelTreePreservingSelection()
|
||||||
|
|||||||
@@ -179,11 +179,11 @@ func (b *Barnard) OnUserChange(e *gumble.UserChangeEvent) {
|
|||||||
// If the channel is muted, ensure the user is muted
|
// If the channel is muted, ensure the user is muted
|
||||||
if b.MutedChannels[e.User.Channel.ID] {
|
if b.MutedChannels[e.User.Channel.ID] {
|
||||||
// Only mute if not already muted
|
// Only mute if not already muted
|
||||||
if !e.User.LocallyMuted {
|
if !e.User.LocallyMuted() {
|
||||||
b.UserConfig.ToggleMute(e.User)
|
b.UserConfig.ToggleMute(e.User)
|
||||||
}
|
}
|
||||||
if e.User.AudioSource != nil {
|
if au := e.User.AudioSource(); au != nil {
|
||||||
e.User.AudioSource.SetGain(0)
|
au.SetGain(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ func (c *Config) findUser(address string, username string) *eUser {
|
|||||||
func (c *Config) ToggleMute(u *gumble.User) {
|
func (c *Config) ToggleMute(u *gumble.User) {
|
||||||
j := c.findUser(u.GetClient().Config.Address, u.Name)
|
j := c.findUser(u.GetClient().Config.Address, u.Name)
|
||||||
j.LocallyMuted = !j.LocallyMuted
|
j.LocallyMuted = !j.LocallyMuted
|
||||||
u.LocallyMuted = j.LocallyMuted
|
u.SetLocallyMuted(j.LocallyMuted)
|
||||||
c.SaveConfig()
|
c.SaveConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,11 +329,11 @@ func (c *Config) UpdateUser(u *gumble.User) {
|
|||||||
uc = u.GetClient()
|
uc = u.GetClient()
|
||||||
if uc != nil {
|
if uc != nil {
|
||||||
j = c.findUser(uc.Config.Address, u.Name)
|
j = c.findUser(uc.Config.Address, u.Name)
|
||||||
u.Boost = j.Boost
|
u.SetBoost(j.Boost)
|
||||||
u.Volume = j.Volume
|
u.SetVolume(j.Volume)
|
||||||
u.LocallyMuted = j.LocallyMuted // Update LocallyMuted state from config
|
u.SetLocallyMuted(j.LocallyMuted) // Update LocallyMuted state from config
|
||||||
if u.Boost < 1 {
|
if u.Boost() < 1 {
|
||||||
u.Boost = 1
|
u.SetBoost(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -341,9 +341,9 @@ func (c *Config) UpdateUser(u *gumble.User) {
|
|||||||
func (c *Config) UpdateConfig(u *gumble.User) {
|
func (c *Config) UpdateConfig(u *gumble.User) {
|
||||||
var j *eUser
|
var j *eUser
|
||||||
j = c.findUser(u.GetClient().Config.Address, u.Name)
|
j = c.findUser(u.GetClient().Config.Address, u.Name)
|
||||||
j.Boost = u.Boost
|
j.Boost = u.Boost()
|
||||||
j.Volume = u.Volume
|
j.Volume = u.Volume()
|
||||||
j.LocallyMuted = u.LocallyMuted // Save LocallyMuted state to config
|
j.LocallyMuted = u.LocallyMuted() // Save LocallyMuted state to config
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConfig(fn *string) *Config {
|
func NewConfig(fn *string) *Config {
|
||||||
|
|||||||
+68
-7
@@ -1,6 +1,8 @@
|
|||||||
package gumble
|
package gumble
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
"git.stormux.org/storm/barnard/gumble/go-openal/openal"
|
"git.stormux.org/storm/barnard/gumble/go-openal/openal"
|
||||||
"git.stormux.org/storm/barnard/gumble/gumble/MumbleProto"
|
"git.stormux.org/storm/barnard/gumble/gumble/MumbleProto"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
@@ -31,9 +33,6 @@ type User struct {
|
|||||||
PrioritySpeaker bool
|
PrioritySpeaker bool
|
||||||
// Is the user recording audio?
|
// Is the user recording audio?
|
||||||
Recording bool
|
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
|
// 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, or if the comment needs to be requested.
|
||||||
Comment string
|
Comment string
|
||||||
@@ -58,14 +57,76 @@ type User struct {
|
|||||||
audioSequence int64
|
audioSequence int64
|
||||||
audioSequenceValid bool
|
audioSequenceValid bool
|
||||||
|
|
||||||
AudioSource *openal.Source
|
// audioMu protects audio-related fields accessed from both the
|
||||||
Boost uint16
|
// audio processing goroutine (OnAudioStream) and the UI goroutine.
|
||||||
Volume float32
|
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
|
// IsMuted returns true if the user is muted either server-side or locally
|
||||||
func (u *User) IsMuted() bool {
|
func (u *User) IsMuted() bool {
|
||||||
return u.Muted || u.LocallyMuted
|
return u.Muted || u.LocallyMuted()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) GetClient() *Client {
|
func (u *User) GetClient() *Client {
|
||||||
|
|||||||
@@ -236,13 +236,13 @@ func (s *Stream) SetMicVolume(change float32, relative bool) {
|
|||||||
func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) {
|
func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) {
|
||||||
go func(e *gumble.AudioStreamEvent) {
|
go func(e *gumble.AudioStreamEvent) {
|
||||||
var source = openal.NewSource()
|
var source = openal.NewSource()
|
||||||
e.User.AudioSource = &source
|
e.User.SetAudioSource(&source)
|
||||||
|
|
||||||
// Set initial gain based on volume and mute state
|
// Set initial gain based on volume and mute state
|
||||||
if e.User.LocallyMuted {
|
if e.User.LocallyMuted() {
|
||||||
e.User.AudioSource.SetGain(0)
|
source.SetGain(0)
|
||||||
} else {
|
} else {
|
||||||
e.User.AudioSource.SetGain(e.User.Volume)
|
source.SetGain(e.User.Volume())
|
||||||
}
|
}
|
||||||
|
|
||||||
bufferCount := e.Client.Config.Buffers
|
bufferCount := e.Client.Config.Buffers
|
||||||
@@ -263,17 +263,17 @@ func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) {
|
|||||||
|
|
||||||
for packet := range e.C {
|
for packet := range e.C {
|
||||||
// Skip processing if user is locally muted
|
// Skip processing if user is locally muted
|
||||||
if e.User.LocallyMuted {
|
if e.User.LocallyMuted() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var boost uint16 = uint16(1)
|
|
||||||
samples := len(packet.AudioBuffer)
|
samples := len(packet.AudioBuffer)
|
||||||
if samples > cap(raw)/2 {
|
if samples > cap(raw)/2 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
boost = e.User.Boost
|
boost := e.User.Boost()
|
||||||
|
userVolume := e.User.Volume()
|
||||||
recorder := s.getRecorder()
|
recorder := s.getRecorder()
|
||||||
var recordBuffer []int16
|
var recordBuffer []int16
|
||||||
recordPtr := 0
|
recordPtr := 0
|
||||||
@@ -306,7 +306,7 @@ func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if recorder != nil {
|
if recorder != nil {
|
||||||
recordBuffer[recordPtr] = scaleForRecording(sample, e.User.Volume)
|
recordBuffer[recordPtr] = scaleForRecording(sample, userVolume)
|
||||||
recordPtr++
|
recordPtr++
|
||||||
}
|
}
|
||||||
binary.LittleEndian.PutUint16(raw[rawPtr:], uint16(sample))
|
binary.LittleEndian.PutUint16(raw[rawPtr:], uint16(sample))
|
||||||
@@ -325,7 +325,7 @@ func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if recorder != nil {
|
if recorder != nil {
|
||||||
recordBuffer[recordPtr] = scaleForRecording(sample, e.User.Volume)
|
recordBuffer[recordPtr] = scaleForRecording(sample, userVolume)
|
||||||
recordPtr++
|
recordPtr++
|
||||||
}
|
}
|
||||||
binary.LittleEndian.PutUint16(raw[rawPtr:], uint16(sample))
|
binary.LittleEndian.PutUint16(raw[rawPtr:], uint16(sample))
|
||||||
@@ -346,7 +346,7 @@ func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if recorder != nil {
|
if recorder != nil {
|
||||||
recordSample := scaleForRecording(sample, e.User.Volume)
|
recordSample := scaleForRecording(sample, userVolume)
|
||||||
recordBuffer[recordPtr] = recordSample
|
recordBuffer[recordPtr] = recordSample
|
||||||
recordBuffer[recordPtr+1] = recordSample
|
recordBuffer[recordPtr+1] = recordSample
|
||||||
recordPtr += 2
|
recordPtr += 2
|
||||||
|
|||||||
+12
-12
@@ -9,12 +9,12 @@ import (
|
|||||||
|
|
||||||
func (ti TreeItem) String() string {
|
func (ti TreeItem) String() string {
|
||||||
if ti.User != nil {
|
if ti.User != nil {
|
||||||
if ti.User.LocallyMuted {
|
if ti.User.LocallyMuted() {
|
||||||
return "[MUTED] " + ti.User.Name
|
return "[MUTED] " + ti.User.Name
|
||||||
}
|
}
|
||||||
// Calculate total volume as percentage
|
// Calculate total volume as percentage
|
||||||
boostPercent := float32(ti.User.Boost-1) * 10
|
boostPercent := float32(ti.User.Boost()-1) * 10
|
||||||
totalVolume := ti.User.Volume*100 + boostPercent
|
totalVolume := ti.User.Volume()*100 + boostPercent
|
||||||
return fmt.Sprintf("%s [%.0f%%]", ti.User.Name, totalVolume)
|
return fmt.Sprintf("%s [%.0f%%]", ti.User.Name, totalVolume)
|
||||||
}
|
}
|
||||||
if ti.Channel != nil {
|
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) {
|
func (b *Barnard) changeVolume(users []*gumble.User, change float32) {
|
||||||
for _, u := range users {
|
for _, u := range users {
|
||||||
au := u.AudioSource
|
au := u.AudioSource()
|
||||||
if au == nil {
|
if au == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -43,7 +43,7 @@ func (b *Barnard) changeVolume(users []*gumble.User, change float32) {
|
|||||||
var cv float32
|
var cv float32
|
||||||
var ng float32
|
var ng float32
|
||||||
var curboost float32
|
var curboost float32
|
||||||
curboost = float32((u.Boost - 1)) / 10
|
curboost = float32((u.Boost() - 1)) / 10
|
||||||
cv = au.GetGain() + curboost
|
cv = au.GetGain() + curboost
|
||||||
ng = cv + change
|
ng = cv + change
|
||||||
boost = uint16(1)
|
boost = uint16(1)
|
||||||
@@ -56,9 +56,9 @@ func (b *Barnard) changeVolume(users []*gumble.User, change float32) {
|
|||||||
if ng < 0 {
|
if ng < 0 {
|
||||||
ng = 0.0
|
ng = 0.0
|
||||||
}
|
}
|
||||||
u.Boost = boost
|
u.SetBoost(boost)
|
||||||
u.Volume = ng
|
u.SetVolume(ng)
|
||||||
if !u.LocallyMuted {
|
if !u.LocallyMuted() {
|
||||||
au.SetGain(ng)
|
au.SetGain(ng)
|
||||||
}
|
}
|
||||||
b.UserConfig.UpdateConfig(u)
|
b.UserConfig.UpdateConfig(u)
|
||||||
@@ -68,14 +68,14 @@ func (b *Barnard) changeVolume(users []*gumble.User, change float32) {
|
|||||||
|
|
||||||
func (b *Barnard) resetVolume(users []*gumble.User) {
|
func (b *Barnard) resetVolume(users []*gumble.User) {
|
||||||
for _, u := range users {
|
for _, u := range users {
|
||||||
au := u.AudioSource
|
au := u.AudioSource()
|
||||||
if au == nil {
|
if au == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Reset to original volume (1.0) and boost (1)
|
// Reset to original volume (1.0) and boost (1)
|
||||||
u.Boost = uint16(1)
|
u.SetBoost(uint16(1))
|
||||||
u.Volume = 1.0
|
u.SetVolume(1.0)
|
||||||
if !u.LocallyMuted {
|
if !u.LocallyMuted() {
|
||||||
au.SetGain(1.0)
|
au.SetGain(1.0)
|
||||||
}
|
}
|
||||||
b.UserConfig.UpdateConfig(u)
|
b.UserConfig.UpdateConfig(u)
|
||||||
|
|||||||
Reference in New Issue
Block a user