Synchronize connection-owned stream access

This commit is contained in:
Brandon McGinty (chatgpt)
2026-08-10 11:25:08 -04:00
committed by Brandon McGinty
parent f0aa5ff35a
commit e9ba7f580a
6 changed files with 133 additions and 84 deletions
+17 -9
View File
@@ -97,6 +97,14 @@ type Barnard struct {
// reconnect replaces them. It is intentionally idempotent for repeated // reconnect replaces them. It is intentionally idempotent for repeated
// disconnect notifications. // disconnect notifications.
func (b *Barnard) cleanupConnectionAudio() { func (b *Barnard) cleanupConnectionAudio() {
// Connection audio operations that use both resources take FileStreamMutex
// before connectionMutex, so cleanup follows that order as well.
b.FileStreamMutex.Lock()
if b.FileStream != nil {
_ = b.FileStream.Stop()
b.FileStream = nil
}
b.FileStreamMutex.Unlock()
b.connectionMutex.Lock() b.connectionMutex.Lock()
if b.Stream != nil { if b.Stream != nil {
stream := b.Stream stream := b.Stream
@@ -104,12 +112,6 @@ func (b *Barnard) cleanupConnectionAudio() {
stream.Destroy() stream.Destroy()
} }
b.connectionMutex.Unlock() b.connectionMutex.Unlock()
b.FileStreamMutex.Lock()
if b.FileStream != nil {
_ = b.FileStream.Stop()
b.FileStream = nil
}
b.FileStreamMutex.Unlock()
} }
func (b *Barnard) cleanupToneTestAudio() { func (b *Barnard) cleanupToneTestAudio() {
@@ -266,9 +268,13 @@ func (b *Barnard) TreeItemKeyPress(ui *uiterm.Ui, tree *uiterm.Tree, item uiterm
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) if err := b.UserConfig.ToggleMute(u); err != nil {
b.AddOutputLine("Mute: could not save setting: " + err.Error())
}
} else if !channelWillBeMuted && u.LocallyMuted() { } else if !channelWillBeMuted && u.LocallyMuted() {
b.UserConfig.ToggleMute(u) if err := b.UserConfig.ToggleMute(u); err != nil {
b.AddOutputLine("Mute: could not save setting: " + err.Error())
}
} }
b.updateUserGain(u) b.updateUserGain(u)
@@ -297,7 +303,9 @@ func (b *Barnard) TreeItemKeyPress(ui *uiterm.Ui, tree *uiterm.Tree, item uiterm
if treeItem.User != nil { if treeItem.User != nil {
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) if err := b.UserConfig.ToggleMute(treeItem.User); err != nil {
b.AddOutputLine("Mute: could not save setting: " + err.Error())
}
b.updateUserGain(treeItem.User) b.updateUserGain(treeItem.User)
b.RebuildUserChannelTreePreservingSelection() b.RebuildUserChannelTreePreservingSelection()
b.Ui.Refresh() b.Ui.Refresh()
+5 -5
View File
@@ -47,7 +47,7 @@ func (b *Barnard) exitWithError(err error) {
func (b *Barnard) connect(reconnect bool) bool { func (b *Barnard) connect(reconnect bool) bool {
var err error var err error
_, err = gumble.DialWithDialer(new(net.Dialer), b.Config, &b.TLSConfig) _, err = gumble.DialWithDialer(&net.Dialer{Timeout: 15 * time.Second}, b.Config, &b.TLSConfig)
if err != nil { if err != nil {
if reconnect { if reconnect {
b.Log(err.Error()) b.Log(err.Error())
@@ -295,11 +295,11 @@ func (b *Barnard) OnUserChange(e *gumble.UserChangeEvent) {
if b.isChannelMuted(e.User.Channel.ID) { if b.isChannelMuted(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) if err := b.UserConfig.ToggleMute(e.User); err != nil {
} b.AddOutputLine("Mute: could not save setting: " + err.Error())
if b.Stream != nil { }
b.Stream.UpdateUserGain(e.User)
} }
b.updateUserGain(e.User)
} }
} }
} }
+19 -1
View File
@@ -116,10 +116,28 @@ func DialWithDialer(dialer *net.Dialer, config *Config, tlsConfig *tls.Config) (
} }
start := time.Now() start := time.Now()
conn, err := tls.DialWithDialer(dialer, "tcp", config.Address, tlsConfig) rawConn, err := dialer.Dial("tcp", config.Address)
if err != nil { if err != nil {
return nil, err return nil, err
} }
conn := tls.Client(rawConn, tlsConfig)
// net.Dialer.Timeout covers only the TCP dial. Apply the same bounded
// deadline to TLS negotiation so a peer that accepts but never responds
// cannot block startup indefinitely.
if dialer.Timeout > 0 {
if err := conn.SetDeadline(start.Add(dialer.Timeout)); err != nil {
rawConn.Close()
return nil, err
}
}
if err := conn.Handshake(); err != nil {
rawConn.Close()
return nil, err
}
if err := conn.SetDeadline(time.Time{}); err != nil {
rawConn.Close()
return nil, err
}
client := &Client{ client := &Client{
Conn: NewConn(conn), Conn: NewConn(conn),
+5 -6
View File
@@ -6,6 +6,7 @@ import (
"time" "time"
"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/recording" "git.stormux.org/storm/barnard/recording"
"git.stormux.org/storm/barnard/uiterm" "git.stormux.org/storm/barnard/uiterm"
) )
@@ -179,10 +180,10 @@ func (b *Barnard) finishRecordingStart() {
} }
b.Recorder = recorder b.Recorder = recorder
b.recordingStarting = false b.recordingStarting = false
// Recorder operations take RecordingMutex before connectionMutex. This
// prevents disconnect cleanup from destroying a stream during attachment.
b.withStream(func(stream *gumbleopenal.Stream) { stream.SetRecorder(recorder) })
b.RecordingMutex.Unlock() b.RecordingMutex.Unlock()
if b.Stream != nil {
b.Stream.SetRecorder(recorder)
}
b.AddOutputLine(fmt.Sprintf("Recording started: %s", recorder.Path())) b.AddOutputLine(fmt.Sprintf("Recording started: %s", recorder.Path()))
b.Notify("recordstart", "me", recorder.Path()) b.Notify("recordstart", "me", recorder.Path())
b.renderGeneralStatus() b.renderGeneralStatus()
@@ -205,9 +206,7 @@ func (b *Barnard) detachRecorder() (*recording.Recorder, string, bool) {
} }
b.Recorder = nil b.Recorder = nil
b.recordingStarting = false b.recordingStarting = false
if b.Stream != nil { b.withStream(func(stream *gumbleopenal.Stream) { stream.SetRecorder(nil) })
b.Stream.SetRecorder(nil)
}
return recorder, path, wasPending return recorder, path, wasPending
} }
+52 -31
View File
@@ -129,7 +129,9 @@ func (b *Barnard) OnTimestampToggle(ui *uiterm.Ui, key uiterm.Key) {
func (b *Barnard) OnNoiseSuppressionToggle(ui *uiterm.Ui, key uiterm.Key) { func (b *Barnard) OnNoiseSuppressionToggle(ui *uiterm.Ui, key uiterm.Key) {
enabled := !b.UserConfig.GetNoiseSuppressionEnabled() enabled := !b.UserConfig.GetNoiseSuppressionEnabled()
b.UserConfig.SetNoiseSuppressionEnabled(enabled) if err := b.UserConfig.SetNoiseSuppressionEnabled(enabled); err != nil {
b.AddOutputLine("Noise suppression: could not save setting: " + err.Error())
}
b.NoiseSuppressor.SetEnabled(enabled) b.NoiseSuppressor.SetEnabled(enabled)
if enabled { if enabled {
@@ -198,7 +200,9 @@ func (b *Barnard) CommandMicDown(ui *uiterm.Ui, cmd string) {
func (b *Barnard) CommandNoiseSuppressionToggle(ui *uiterm.Ui, cmd string) { func (b *Barnard) CommandNoiseSuppressionToggle(ui *uiterm.Ui, cmd string) {
enabled := !b.UserConfig.GetNoiseSuppressionEnabled() enabled := !b.UserConfig.GetNoiseSuppressionEnabled()
b.UserConfig.SetNoiseSuppressionEnabled(enabled) if err := b.UserConfig.SetNoiseSuppressionEnabled(enabled); err != nil {
b.AddOutputLine("Noise suppression: could not save setting: " + err.Error())
}
b.NoiseSuppressor.SetEnabled(enabled) b.NoiseSuppressor.SetEnabled(enabled)
if enabled { if enabled {
@@ -266,11 +270,18 @@ func (b *Barnard) CommandPlayFile(ui *uiterm.Ui, cmd string) {
// Enable stereo encoder for file playback // Enable stereo encoder for file playback
b.Client.EnableStereoEncoder() b.Client.EnableStereoEncoder()
// Auto-start transmission if not already transmitting // Auto-start transmission if not already transmitting. FileStreamMutex is
if !b.isTransmitting() && b.Stream != nil { // held here, before withStream's connection mutex, matching cleanup.
err := b.Stream.StartSource(b.UserConfig.GetInputDevice()) if !b.isTransmitting() {
if err != nil { var startErr error
b.AddOutputLine(fmt.Sprintf("Error starting transmission: %s", err.Error())) started := b.withStream(func(stream *gumbleopenal.Stream) {
startErr = stream.StartSource(b.UserConfig.GetInputDevice())
})
if !started {
startErr = errors.New("audio unavailable while reconnecting")
}
if startErr != nil {
b.AddOutputLine(fmt.Sprintf("Error starting transmission: %s", startErr))
b.FileStream.Stop() b.FileStream.Stop()
b.Client.DisableStereoEncoder() b.Client.DisableStereoEncoder()
return return
@@ -327,8 +338,8 @@ func (b *Barnard) setTransmit(ui *uiterm.Ui, val int) {
close(b.toneTestStop) close(b.toneTestStop)
b.toneTestStop = nil b.toneTestStop = nil
} }
} else if b.Stream != nil { } else {
b.Stream.StopSource() b.withStream(func(stream *gumbleopenal.Stream) { _ = stream.StopSource() })
} }
} else if !b.isConnected() { } else if !b.isConnected() {
b.Notify("error", "me", "no tx while disconnected") b.Notify("error", "me", "no tx while disconnected")
@@ -347,20 +358,26 @@ func (b *Barnard) setTransmit(ui *uiterm.Ui, val int) {
b.Notify("micup", "me", "") b.Notify("micup", "me", "")
b.UpdateGeneralStatus(" Tx ", true) b.UpdateGeneralStatus(" Tx ", true)
} else { } else {
err := b.Stream.StartSource(b.UserConfig.GetInputDevice()) started := b.withStream(func(stream *gumbleopenal.Stream) {
if err != nil { err := stream.StartSource(b.UserConfig.GetInputDevice())
b.setTransmitting(false) if err != nil {
if fatalAudioOpenError(err) { b.setTransmitting(false)
// A missing capture device cannot recover through normal if fatalAudioOpenError(err) {
// transmission controls; exit so option 1 reports it on stderr. // A missing capture device cannot recover through normal
b.exitWithError(fmt.Errorf("audio device initialization failed: %w", err)) // transmission controls; exit so option 1 reports it on stderr.
b.exitWithError(fmt.Errorf("audio device initialization failed: %w", err))
return
}
b.Notify("error", "me", err.Error())
b.UpdateGeneralStatus(err.Error(), true)
return return
} }
b.Notify("error", "me", err.Error())
b.UpdateGeneralStatus(err.Error(), true)
} else {
b.Notify("micup", "me", "") b.Notify("micup", "me", "")
b.UpdateGeneralStatus(" Tx ", true) b.UpdateGeneralStatus(" Tx ", true)
})
if !started {
b.setTransmitting(false)
b.UpdateGeneralStatus("audio unavailable while reconnecting", true)
} }
} }
} }
@@ -371,25 +388,29 @@ func fatalAudioOpenError(err error) bool {
} }
func (b *Barnard) OnMicVolumeDown(ui *uiterm.Ui, key uiterm.Key) { func (b *Barnard) OnMicVolumeDown(ui *uiterm.Ui, key uiterm.Key) {
if b.ToneTest || b.Stream == nil { if b.ToneTest {
return return
} }
b.Stream.SetMicVolume(-0.1, true) b.withStream(func(stream *gumbleopenal.Stream) {
b.UserConfig.SetMicVolume(b.Stream.GetMicVolume()) stream.SetMicVolume(-0.1, true)
if err := b.UserConfig.SaveConfig(); err != nil { b.UserConfig.SetMicVolume(stream.GetMicVolume())
b.AddOutputLine("Microphone: could not save volume: " + err.Error()) if err := b.UserConfig.SaveConfig(); err != nil {
} b.AddOutputLine("Microphone: could not save volume: " + err.Error())
}
})
} }
func (b *Barnard) OnMicVolumeUp(ui *uiterm.Ui, key uiterm.Key) { func (b *Barnard) OnMicVolumeUp(ui *uiterm.Ui, key uiterm.Key) {
if b.ToneTest || b.Stream == nil { if b.ToneTest {
return return
} }
b.Stream.SetMicVolume(0.1, true) b.withStream(func(stream *gumbleopenal.Stream) {
b.UserConfig.SetMicVolume(b.Stream.GetMicVolume()) stream.SetMicVolume(0.1, true)
if err := b.UserConfig.SaveConfig(); err != nil { b.UserConfig.SetMicVolume(stream.GetMicVolume())
b.AddOutputLine("Microphone: could not save volume: " + err.Error()) if err := b.UserConfig.SaveConfig(); err != nil {
} b.AddOutputLine("Microphone: could not save volume: " + err.Error())
}
})
} }
func (b *Barnard) OnQuitPress(ui *uiterm.Ui, key uiterm.Key) { func (b *Barnard) OnQuitPress(ui *uiterm.Ui, key uiterm.Key) {
+35 -32
View File
@@ -3,6 +3,7 @@ package main
import ( import (
"fmt" "fmt"
"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/uiterm" "git.stormux.org/storm/barnard/uiterm"
"sort" "sort"
) )
@@ -37,44 +38,46 @@ 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 { b.withStream(func(stream *gumbleopenal.Stream) {
if b.Stream == nil { for _, u := range users {
continue var boost uint16
var ng float32
curboost := float32((u.Boost() - 1)) / 10
ng = u.Volume() + curboost + change
boost = uint16(1)
if ng > 1.0 {
perc := uint16((ng * 10)) - 10
perc += 1
boost = perc
ng = 1.0
}
if ng < 0 {
ng = 0.0
}
u.SetBoost(boost)
u.SetVolume(ng)
stream.UpdateUserGain(u)
b.UserConfig.UpdateConfig(u)
} }
var boost uint16 if err := b.UserConfig.SaveConfig(); err != nil {
var ng float32 b.AddOutputLine("Volume: could not save setting: " + err.Error())
curboost := float32((u.Boost() - 1)) / 10
ng = u.Volume() + curboost + change
boost = uint16(1)
if ng > 1.0 {
perc := uint16((ng * 10)) - 10
perc += 1
boost = perc
ng = 1.0
} }
if ng < 0 { })
ng = 0.0
}
u.SetBoost(boost)
u.SetVolume(ng)
b.Stream.UpdateUserGain(u)
b.UserConfig.UpdateConfig(u)
}
b.UserConfig.SaveConfig()
} }
func (b *Barnard) resetVolume(users []*gumble.User) { func (b *Barnard) resetVolume(users []*gumble.User) {
for _, u := range users { b.withStream(func(stream *gumbleopenal.Stream) {
if b.Stream == nil { for _, u := range users {
continue // Reset to original volume (1.0) and boost (1)
u.SetBoost(uint16(1))
u.SetVolume(1.0)
stream.UpdateUserGain(u)
b.UserConfig.UpdateConfig(u)
} }
// Reset to original volume (1.0) and boost (1) if err := b.UserConfig.SaveConfig(); err != nil {
u.SetBoost(uint16(1)) b.AddOutputLine("Volume: could not save setting: " + err.Error())
u.SetVolume(1.0) }
b.Stream.UpdateUserGain(u) })
b.UserConfig.UpdateConfig(u)
}
b.UserConfig.SaveConfig()
} }
func makeUsersArray(users gumble.Users) []*gumble.User { func makeUsersArray(users gumble.Users) []*gumble.User {