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)
}
if au := u.AudioSource(); au != nil {
if u.LocallyMuted() {
au.SetGain(0)
} else {
au.SetGain(u.Volume())
}
}
b.Stream.UpdateUserGain(u)
}
// 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 {
// Toggle mute for single user
b.UserConfig.ToggleMute(treeItem.User)
if au := treeItem.User.AudioSource(); au != nil {
if treeItem.User.LocallyMuted() {
au.SetGain(0)
} else {
au.SetGain(treeItem.User.Volume())
}
}
b.Stream.UpdateUserGain(treeItem.User)
b.RebuildUserChannelTreePreservingSelection()
b.Ui.Refresh()
}
+2 -2
View File
@@ -243,8 +243,8 @@ func (b *Barnard) OnUserChange(e *gumble.UserChangeEvent) {
if !e.User.LocallyMuted() {
b.UserConfig.ToggleMute(e.User)
}
if au := e.User.AudioSource(); au != nil {
au.SetGain(0)
if b.Stream != nil {
b.Stream.UpdateUserGain(e.User)
}
}
}
+75 -11
View File
@@ -5,14 +5,15 @@ import (
"errors"
"math"
"os/exec"
"runtime"
"sync"
"sync/atomic"
"time"
"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/gumble"
"git.stormux.org/storm/barnard/log"
"git.stormux.org/storm/barnard/noise"
)
@@ -55,6 +56,11 @@ func beep() {
}
}
type renderCommand struct {
fn func()
done chan struct{}
}
type Stream struct {
client *gumble.Client
link gumble.Detacher
@@ -68,6 +74,8 @@ type Stream struct {
deviceSink *openal.Device
contextSink *openal.Context
renderCh chan renderCommand
renderDone chan struct{}
noiseProcessor NoiseProcessor
noiseProcessorRight NoiseProcessor
@@ -147,11 +155,40 @@ func New(client *gumble.Client, inputDevice *string, outputDevice *string, test
s.Destroy()
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
}
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) {
s.link = client.Config.AttachAudio(s)
}
@@ -169,6 +206,20 @@ func (s *Stream) GetFilePlayer() 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
// capture device fails to provide audio data.
func (s *Stream) SetErrorFunc(f func(error)) {
@@ -197,9 +248,13 @@ func (s *Stream) Destroy() {
s.deviceSource = nil
}
if s.deviceSink != nil {
s.contextSink.Destroy()
s.deviceSink.CloseDevice()
if s.contextSink != nil {
s.render(func() { s.contextSink.Destroy() })
close(s.renderCh)
<-s.renderDone
s.contextSink = nil
}
s.deviceSink.CloseDevice()
s.deviceSink = nil
}
}
@@ -257,32 +312,34 @@ func (s *Stream) SetMicVolume(change float32, relative bool) {
func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) {
go func(e *gumble.AudioStreamEvent) {
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)
// Set initial gain based on volume and mute state
if e.User.LocallyMuted() {
source.SetGain(0)
} else {
source.SetGain(e.User.Volume())
}
bufferCount := e.Client.Config.Buffers
if bufferCount < 64 {
bufferCount = 64
}
emptyBufs := openal.NewBuffers(bufferCount)
emptyBufs = openal.NewBuffers(bufferCount)
})
reclaim := func() {
s.render(func() {
if n := source.BuffersProcessed(); n > 0 {
reclaimedBufs := make(openal.Buffers, n)
source.UnqueueBuffers(reclaimedBufs)
emptyBufs = append(emptyBufs, reclaimedBufs...)
}
})
}
var raw [maxBufferSize]byte
// Jitter buffer: collects incoming packets, reorders by
// sequence number, and releases them in order after a small
// initial delay (3-5 packets / 30-50ms).
@@ -354,7 +411,9 @@ func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) {
break
}
reclaim()
s.render(func() {
emptyBufs = s.processAudioPacket(pkt, e.User, &source, emptyBufs, &raw)
})
}
}
@@ -368,12 +427,17 @@ func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) {
}
if pkt != nil {
reclaim()
s.render(func() {
emptyBufs = s.processAudioPacket(pkt, e.User, &source, emptyBufs, &raw)
})
}
}
reclaim()
s.render(func() {
emptyBufs.Delete()
source.Delete()
e.User.SetAudioSource(nil)
})
log.Debug("audio stream ended for user %s", e.User.Name)
}(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) {
for _, u := range users {
au := u.AudioSource()
if au == nil {
if b.Stream == nil {
continue
}
var boost uint16
var cv float32
var ng float32
var curboost float32
curboost = float32((u.Boost() - 1)) / 10
cv = au.GetGain() + curboost
ng = cv + change
curboost := float32((u.Boost() - 1)) / 10
ng = u.Volume() + curboost + change
boost = uint16(1)
if ng > 1.0 {
perc := uint16((ng * 10)) - 10
@@ -58,9 +54,7 @@ func (b *Barnard) changeVolume(users []*gumble.User, change float32) {
}
u.SetBoost(boost)
u.SetVolume(ng)
if !u.LocallyMuted() {
au.SetGain(ng)
}
b.Stream.UpdateUserGain(u)
b.UserConfig.UpdateConfig(u)
}
b.UserConfig.SaveConfig()
@@ -68,16 +62,13 @@ func (b *Barnard) changeVolume(users []*gumble.User, change float32) {
func (b *Barnard) resetVolume(users []*gumble.User) {
for _, u := range users {
au := u.AudioSource()
if au == nil {
if b.Stream == nil {
continue
}
// Reset to original volume (1.0) and boost (1)
u.SetBoost(uint16(1))
u.SetVolume(1.0)
if !u.LocallyMuted() {
au.SetGain(1.0)
}
b.Stream.UpdateUserGain(u)
b.UserConfig.UpdateConfig(u)
}
b.UserConfig.SaveConfig()