Serialize OpenAL playback on a dedicated thread

This commit is contained in:
Brandon McGinty (chatgpt)
2026-08-09 12:55:55 -04:00
committed by Brandon McGinty
parent 0f2eb4408f
commit 8c0887d562
4 changed files with 105 additions and 62 deletions
+2 -14
View File
@@ -136,13 +136,7 @@ func (b *Barnard) TreeItemKeyPress(ui *uiterm.Ui, tree *uiterm.Tree, item uiterm
b.UserConfig.ToggleMute(u) b.UserConfig.ToggleMute(u)
} }
if au := u.AudioSource(); au != nil { b.Stream.UpdateUserGain(u)
if u.LocallyMuted() {
au.SetGain(0)
} else {
au.SetGain(u.Volume())
}
}
} }
// Update channel mute state // Update channel mute state
@@ -174,13 +168,7 @@ 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 au := treeItem.User.AudioSource(); au != nil { b.Stream.UpdateUserGain(treeItem.User)
if treeItem.User.LocallyMuted() {
au.SetGain(0)
} else {
au.SetGain(treeItem.User.Volume())
}
}
b.RebuildUserChannelTreePreservingSelection() b.RebuildUserChannelTreePreservingSelection()
b.Ui.Refresh() b.Ui.Refresh()
} }
+2 -2
View File
@@ -243,8 +243,8 @@ func (b *Barnard) OnUserChange(e *gumble.UserChangeEvent) {
if !e.User.LocallyMuted() { if !e.User.LocallyMuted() {
b.UserConfig.ToggleMute(e.User) b.UserConfig.ToggleMute(e.User)
} }
if au := e.User.AudioSource(); au != nil { if b.Stream != nil {
au.SetGain(0) b.Stream.UpdateUserGain(e.User)
} }
} }
} }
+75 -11
View File
@@ -5,14 +5,15 @@ import (
"errors" "errors"
"math" "math"
"os/exec" "os/exec"
"runtime"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
"git.stormux.org/storm/barnard/audio" "git.stormux.org/storm/barnard/audio"
"git.stormux.org/storm/barnard/log"
"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" "git.stormux.org/storm/barnard/gumble/gumble"
"git.stormux.org/storm/barnard/log"
"git.stormux.org/storm/barnard/noise" "git.stormux.org/storm/barnard/noise"
) )
@@ -55,6 +56,11 @@ func beep() {
} }
} }
type renderCommand struct {
fn func()
done chan struct{}
}
type Stream struct { type Stream struct {
client *gumble.Client client *gumble.Client
link gumble.Detacher link gumble.Detacher
@@ -68,6 +74,8 @@ type Stream struct {
deviceSink *openal.Device deviceSink *openal.Device
contextSink *openal.Context contextSink *openal.Context
renderCh chan renderCommand
renderDone chan struct{}
noiseProcessor NoiseProcessor noiseProcessor NoiseProcessor
noiseProcessorRight NoiseProcessor noiseProcessorRight NoiseProcessor
@@ -147,11 +155,40 @@ func New(client *gumble.Client, inputDevice *string, outputDevice *string, test
s.Destroy() s.Destroy()
return nil, ErrOutputDevice return nil, ErrOutputDevice
} }
s.contextSink.Activate() // OpenAL contexts are current to an OS thread. Move ownership to one
// dedicated render thread before any source or buffer is created.
openal.NullContext.Activate()
s.startRenderer()
return s, nil return s, nil
} }
func (s *Stream) startRenderer() {
s.renderCh = make(chan renderCommand)
s.renderDone = make(chan struct{})
ready := make(chan struct{})
go func() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
s.contextSink.Activate()
close(ready)
defer close(s.renderDone)
for command := range s.renderCh {
command.fn()
close(command.done)
}
openal.NullContext.Activate()
}()
<-ready
}
// render executes fn on the sole OS thread that owns the OpenAL context.
func (s *Stream) render(fn func()) {
command := renderCommand{fn: fn, done: make(chan struct{})}
s.renderCh <- command
<-command.done
}
func (s *Stream) AttachStream(client *gumble.Client) { func (s *Stream) AttachStream(client *gumble.Client) {
s.link = client.Config.AttachAudio(s) s.link = client.Config.AttachAudio(s)
} }
@@ -169,6 +206,20 @@ func (s *Stream) GetFilePlayer() FilePlayer {
return s.filePlayer return s.filePlayer
} }
// UpdateUserGain applies a user's current mute and volume state on the
// renderer thread.
func (s *Stream) UpdateUserGain(user *gumble.User) {
s.render(func() {
if source := user.AudioSource(); source != nil {
if user.LocallyMuted() {
source.SetGain(0)
} else {
source.SetGain(user.Volume())
}
}
})
}
// SetErrorFunc sets a callback that is invoked when the microphone // SetErrorFunc sets a callback that is invoked when the microphone
// capture device fails to provide audio data. // capture device fails to provide audio data.
func (s *Stream) SetErrorFunc(f func(error)) { func (s *Stream) SetErrorFunc(f func(error)) {
@@ -197,9 +248,13 @@ func (s *Stream) Destroy() {
s.deviceSource = nil s.deviceSource = nil
} }
if s.deviceSink != nil { if s.deviceSink != nil {
s.contextSink.Destroy() if s.contextSink != nil {
s.deviceSink.CloseDevice() s.render(func() { s.contextSink.Destroy() })
close(s.renderCh)
<-s.renderDone
s.contextSink = nil s.contextSink = nil
}
s.deviceSink.CloseDevice()
s.deviceSink = nil s.deviceSink = nil
} }
} }
@@ -257,32 +312,34 @@ 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) {
log.Info("audio stream started for user %s", e.User.Name) log.Info("audio stream started for user %s", e.User.Name)
var source = openal.NewSource() var source openal.Source
var emptyBufs openal.Buffers
var raw [maxBufferSize]byte
s.render(func() {
source = openal.NewSource()
e.User.SetAudioSource(&source) e.User.SetAudioSource(&source)
// Set initial gain based on volume and mute state
if e.User.LocallyMuted() { if e.User.LocallyMuted() {
source.SetGain(0) source.SetGain(0)
} else { } else {
source.SetGain(e.User.Volume()) source.SetGain(e.User.Volume())
} }
bufferCount := e.Client.Config.Buffers bufferCount := e.Client.Config.Buffers
if bufferCount < 64 { if bufferCount < 64 {
bufferCount = 64 bufferCount = 64
} }
emptyBufs := openal.NewBuffers(bufferCount) emptyBufs = openal.NewBuffers(bufferCount)
})
reclaim := func() { reclaim := func() {
s.render(func() {
if n := source.BuffersProcessed(); n > 0 { if n := source.BuffersProcessed(); n > 0 {
reclaimedBufs := make(openal.Buffers, n) reclaimedBufs := make(openal.Buffers, n)
source.UnqueueBuffers(reclaimedBufs) source.UnqueueBuffers(reclaimedBufs)
emptyBufs = append(emptyBufs, reclaimedBufs...) emptyBufs = append(emptyBufs, reclaimedBufs...)
} }
})
} }
var raw [maxBufferSize]byte
// Jitter buffer: collects incoming packets, reorders by // Jitter buffer: collects incoming packets, reorders by
// sequence number, and releases them in order after a small // sequence number, and releases them in order after a small
// initial delay (3-5 packets / 30-50ms). // initial delay (3-5 packets / 30-50ms).
@@ -354,7 +411,9 @@ func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) {
break break
} }
reclaim() reclaim()
s.render(func() {
emptyBufs = s.processAudioPacket(pkt, e.User, &source, emptyBufs, &raw) emptyBufs = s.processAudioPacket(pkt, e.User, &source, emptyBufs, &raw)
})
} }
} }
@@ -368,12 +427,17 @@ func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) {
} }
if pkt != nil { if pkt != nil {
reclaim() reclaim()
s.render(func() {
emptyBufs = s.processAudioPacket(pkt, e.User, &source, emptyBufs, &raw) emptyBufs = s.processAudioPacket(pkt, e.User, &source, emptyBufs, &raw)
})
} }
} }
reclaim() reclaim()
s.render(func() {
emptyBufs.Delete() emptyBufs.Delete()
source.Delete() source.Delete()
e.User.SetAudioSource(nil)
})
log.Debug("audio stream ended for user %s", e.User.Name) log.Debug("audio stream ended for user %s", e.User.Name)
}(e) }(e)
} }
+6 -15
View File
@@ -35,17 +35,13 @@ 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() if b.Stream == nil {
if au == nil {
continue continue
} }
var boost uint16 var boost uint16
var cv float32
var ng float32 var ng float32
var curboost float32 curboost := float32((u.Boost() - 1)) / 10
curboost = float32((u.Boost() - 1)) / 10 ng = u.Volume() + curboost + change
cv = au.GetGain() + curboost
ng = cv + change
boost = uint16(1) boost = uint16(1)
if ng > 1.0 { if ng > 1.0 {
perc := uint16((ng * 10)) - 10 perc := uint16((ng * 10)) - 10
@@ -58,9 +54,7 @@ func (b *Barnard) changeVolume(users []*gumble.User, change float32) {
} }
u.SetBoost(boost) u.SetBoost(boost)
u.SetVolume(ng) u.SetVolume(ng)
if !u.LocallyMuted() { b.Stream.UpdateUserGain(u)
au.SetGain(ng)
}
b.UserConfig.UpdateConfig(u) b.UserConfig.UpdateConfig(u)
} }
b.UserConfig.SaveConfig() b.UserConfig.SaveConfig()
@@ -68,16 +62,13 @@ 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() if b.Stream == 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.SetBoost(uint16(1)) u.SetBoost(uint16(1))
u.SetVolume(1.0) u.SetVolume(1.0)
if !u.LocallyMuted() { b.Stream.UpdateUserGain(u)
au.SetGain(1.0)
}
b.UserConfig.UpdateConfig(u) b.UserConfig.UpdateConfig(u)
} }
b.UserConfig.SaveConfig() b.UserConfig.SaveConfig()