diff --git a/barnard.go b/barnard.go index 954089e..8f8f802 100644 --- a/barnard.go +++ b/barnard.go @@ -27,7 +27,7 @@ type Barnard struct { Address string TLSConfig tls.Config - Stream *gumbleopenal.Stream + Stream *gumbleopenal.Stream Tx bool AutoTransmit bool // auto-start transmission on connect Connected bool @@ -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() } diff --git a/client.go b/client.go index 89f6458..a31c197 100644 --- a/client.go +++ b/client.go @@ -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) } } } diff --git a/gumble/gumbleopenal/stream.go b/gumble/gumbleopenal/stream.go index 059dfbe..b5a7872 100644 --- a/gumble/gumbleopenal/stream.go +++ b/gumble/gumbleopenal/stream.go @@ -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() + if s.contextSink != nil { + s.render(func() { s.contextSink.Destroy() }) + close(s.renderCh) + <-s.renderDone + s.contextSink = nil + } s.deviceSink.CloseDevice() - s.contextSink = nil 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() - 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) + var source openal.Source + var emptyBufs openal.Buffers + var raw [maxBufferSize]byte + s.render(func() { + source = openal.NewSource() + e.User.SetAudioSource(&source) + 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) + }) reclaim := func() { - if n := source.BuffersProcessed(); n > 0 { - reclaimedBufs := make(openal.Buffers, n) - source.UnqueueBuffers(reclaimedBufs) - emptyBufs = append(emptyBufs, reclaimedBufs...) - } + 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() - emptyBufs = s.processAudioPacket(pkt, e.User, &source, emptyBufs, &raw) + 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() - emptyBufs = s.processAudioPacket(pkt, e.User, &source, emptyBufs, &raw) + s.render(func() { + emptyBufs = s.processAudioPacket(pkt, e.User, &source, emptyBufs, &raw) + }) } } reclaim() - emptyBufs.Delete() - source.Delete() + s.render(func() { + emptyBufs.Delete() + source.Delete() + e.User.SetAudioSource(nil) + }) log.Debug("audio stream ended for user %s", e.User.Name) }(e) } diff --git a/ui_tree.go b/ui_tree.go index 7057777..7469e7b 100644 --- a/ui_tree.go +++ b/ui_tree.go @@ -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()