Reverted because of audio problems. Will try again when things are fixed.

This commit is contained in:
Storm Dragon
2026-08-11 20:55:25 -04:00
parent c77d4bac3e
commit d4f8d56c8e
20 changed files with 143 additions and 1255 deletions
-4
View File
@@ -82,10 +82,6 @@ Each event has the following parameters:
* event: the name of the event * event: the name of the event
- join: user has joined the channel you are in - join: user has joined the channel you are in
- leave: user has left the channel you are in - leave: user has left the channel you are in
- mute: a user in your channel has muted themselves
- unmute: a user in your channel has unmuted themselves
- deafen: a user in your channel has deafened themselves
- undeafen: a user in your channel has undeafened themselves
- micup: you have begun transmitting - micup: you have begun transmitting
- micdown: you have stopped transmitting - micdown: you have stopped transmitting
- connect: you have connected to a server - connect: you have connected to a server
+1 -1
View File
@@ -454,7 +454,7 @@ connect() {
barnardArgs+=(-certificate "$certFile") barnardArgs+=(-certificate "$certFile")
fi fi
command barnard "${barnardArgs[@]}" --fifo "$configDir/cmd" --buffers 64 |& log command barnard "${barnardArgs[@]}" --fifo "$configDir/cmd" --buffers 16 |& log
barnardStatus=${PIPESTATUS[0]} barnardStatus=${PIPESTATUS[0]}
if (( barnardStatus != 0 )); then if (( barnardStatus != 0 )); then
msgbox "$(gettext "Barnard exited with status") $barnardStatus. $(gettext "See log:") $logFile" msgbox "$(gettext "Barnard exited with status") $barnardStatus. $(gettext "See log:") $logFile"
+16 -9
View File
@@ -52,9 +52,8 @@ type Barnard struct {
exitMessage string exitMessage string
// Added for channel muting // Added for channel muting
MutedChannels map[uint32]bool MutedChannels map[uint32]bool
userChannels map[uint32]*gumble.Channel userChannels map[uint32]*gumble.Channel
userAudioStates map[uint32]userAudioState
// Added for noise suppression // Added for noise suppression
NoiseSuppressor *noise.Suppressor NoiseSuppressor *noise.Suppressor
@@ -116,14 +115,18 @@ 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.IsLocallyMuted() { if channelWillBeMuted && !u.LocallyMuted {
b.UserConfig.ToggleMute(u) b.UserConfig.ToggleMute(u)
} else if !channelWillBeMuted && u.IsLocallyMuted() { } else if !channelWillBeMuted && u.LocallyMuted {
b.UserConfig.ToggleMute(u) b.UserConfig.ToggleMute(u)
} }
if b.Stream != nil { if u.AudioSource != nil {
b.Stream.SetUserMuteState(u) if u.LocallyMuted {
u.AudioSource.SetGain(0)
} else {
u.AudioSource.SetGain(u.Volume)
}
} }
} }
@@ -156,8 +159,12 @@ 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 b.Stream != nil { if treeItem.User.AudioSource != nil {
b.Stream.SetUserMuteState(treeItem.User) if treeItem.User.LocallyMuted {
treeItem.User.AudioSource.SetGain(0)
} else {
treeItem.User.AudioSource.SetGain(treeItem.User.Volume)
}
} }
b.RebuildUserChannelTreePreservingSelection() b.RebuildUserChannelTreePreservingSelection()
b.Ui.Refresh() b.Ui.Refresh()
+3 -107
View File
@@ -78,7 +78,6 @@ func (b *Barnard) OnConnect(e *gumble.ConnectEvent) {
// Reset muted channels state on connect // Reset muted channels state on connect
b.MutedChannels = make(map[uint32]bool) b.MutedChannels = make(map[uint32]bool)
b.userChannels = make(map[uint32]*gumble.Channel) b.userChannels = make(map[uint32]*gumble.Channel)
b.userAudioStates = make(map[uint32]userAudioState)
b.RecordingMutex.Lock() b.RecordingMutex.Lock()
b.recordingAllowed = nil b.recordingAllowed = nil
b.recordingStarting = false b.recordingStarting = false
@@ -91,7 +90,6 @@ func (b *Barnard) OnConnect(e *gumble.ConnectEvent) {
for _, u := range b.Client.Users { for _, u := range b.Client.Users {
b.UserConfig.UpdateUser(u) b.UserConfig.UpdateUser(u)
b.rememberUserChannel(u) b.rememberUserChannel(u)
b.rememberUserAudioState(u)
} }
b.UpdateInputStatus(fmt.Sprintf("[%s]", e.Client.Self.Channel.Name)) b.UpdateInputStatus(fmt.Sprintf("[%s]", e.Client.Self.Channel.Name))
@@ -121,17 +119,6 @@ func (b *Barnard) OnDisconnect(e *gumble.DisconnectEvent) {
reason = e.String reason = e.String
} }
b.stopRecordingForDisconnect() b.stopRecordingForDisconnect()
b.FileStreamMutex.Lock()
fileStream := b.FileStream
b.FileStream = nil
b.FileStreamMutex.Unlock()
if fileStream != nil && fileStream.IsPlaying() {
_ = fileStream.Stop()
}
if b.Stream != nil {
b.Stream.Destroy()
b.Stream = nil
}
b.Notify("disconnect", "me", reason) b.Notify("disconnect", "me", reason)
if reason == "" { if reason == "" {
b.AddOutputLine("Disconnected") b.AddOutputLine("Disconnected")
@@ -184,30 +171,19 @@ func (b *Barnard) OnTextMessage(e *gumble.TextMessageEvent) {
func (b *Barnard) OnUserChange(e *gumble.UserChangeEvent) { func (b *Barnard) OnUserChange(e *gumble.UserChangeEvent) {
notification, hasNotification := b.userChangeNotification(e) notification, hasNotification := b.userChangeNotification(e)
audioNotification, hasAudioNotification := b.userAudioChangeNotification(e)
if e.User != nil { if e.User != nil {
previousLocalMute, previousLocalMuteGeneration := e.User.LocalMuteState()
previousVolume := e.User.Volume
b.UserConfig.UpdateUser(e.User) b.UserConfig.UpdateUser(e.User)
localMute, localMuteGeneration := e.User.LocalMuteState()
if b.Stream != nil {
if localMute != previousLocalMute || localMuteGeneration != previousLocalMuteGeneration {
b.Stream.SetUserMuteState(e.User)
} else if e.User.Volume != previousVolume {
b.Stream.SetUserGain(e.User, e.User.Volume)
}
}
// Check if user is joining a muted channel // Check if user is joining a muted channel
if e.Type.Has(gumble.UserChangeConnected) || e.Type.Has(gumble.UserChangeChannel) { if e.Type.Has(gumble.UserChangeConnected) || e.Type.Has(gumble.UserChangeChannel) {
// 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.IsLocallyMuted() { if !e.User.LocallyMuted {
b.UserConfig.ToggleMute(e.User) b.UserConfig.ToggleMute(e.User)
} }
if b.Stream != nil { if e.User.AudioSource != nil {
b.Stream.SetUserMuteState(e.User) e.User.AudioSource.SetGain(0)
} }
} }
} }
@@ -222,10 +198,6 @@ func (b *Barnard) OnUserChange(e *gumble.UserChangeEvent) {
b.Notify(notification.event, notification.who, notification.what) b.Notify(notification.event, notification.who, notification.what)
b.AddOutputLine(notification.line) b.AddOutputLine(notification.line)
} }
if hasAudioNotification {
b.Notify(audioNotification.event, audioNotification.who, audioNotification.what)
b.AddOutputLine(audioNotification.line)
}
if e.Type.Has(gumble.UserChangeChannel) && e.User == b.Client.Self { if e.Type.Has(gumble.UserChangeChannel) && e.User == b.Client.Self {
b.UpdateInputStatus(fmt.Sprintf("[%s]", e.User.Channel.Name)) b.UpdateInputStatus(fmt.Sprintf("[%s]", e.User.Channel.Name))
} }
@@ -243,7 +215,6 @@ func (b *Barnard) OnUserChange(e *gumble.UserChangeEvent) {
b.AddOutputLine(formatUserStats(e.User)) b.AddOutputLine(formatUserStats(e.User))
} }
b.updateUserChannel(e) b.updateUserChannel(e)
b.updateUserAudioState(e)
b.RebuildUserChannelTreePreservingSelection() b.RebuildUserChannelTreePreservingSelection()
b.Ui.Refresh() b.Ui.Refresh()
} }
@@ -255,11 +226,6 @@ type userChangeNotification struct {
line string line string
} }
type userAudioState struct {
selfMuted bool
selfDeafened bool
}
func (b *Barnard) userChangeNotification(e *gumble.UserChangeEvent) (userChangeNotification, bool) { func (b *Barnard) userChangeNotification(e *gumble.UserChangeEvent) (userChangeNotification, bool) {
if e == nil || e.User == nil || b.Client == nil || b.Client.Self == nil || b.Client.Self.Channel == nil { if e == nil || e.User == nil || b.Client == nil || b.Client.Self == nil || b.Client.Self.Channel == nil {
return userChangeNotification{}, false return userChangeNotification{}, false
@@ -290,52 +256,6 @@ func (b *Barnard) userChangeNotification(e *gumble.UserChangeEvent) (userChangeN
return userChangeNotification{}, false return userChangeNotification{}, false
} }
func (b *Barnard) userAudioChangeNotification(e *gumble.UserChangeEvent) (userChangeNotification, bool) {
if e == nil || e.User == nil || !e.Type.Has(gumble.UserChangeAudio) || b.Client == nil || b.Client.Self == nil || b.Client.Self.Channel == nil {
return userChangeNotification{}, false
}
previous, known := b.userAudioStates[e.User.Session]
if !known || !sameChannel(e.User.Channel, b.Client.Self.Channel) {
return userChangeNotification{}, false
}
event := ""
verb := ""
if previous.selfDeafened != e.User.SelfDeafened {
if e.User.SelfDeafened {
event = "deafen"
verb = "deafened"
} else {
event = "undeafen"
verb = "undeafened"
}
} else if previous.selfMuted != e.User.SelfMuted {
if e.User.SelfMuted {
event = "mute"
verb = "muted"
} else {
event = "unmute"
verb = "unmuted"
}
}
if event == "" {
return userChangeNotification{}, false
}
who := e.User.Name
line := fmt.Sprintf("%s %s themselves", e.User.Name, verb)
if e.User.Session == b.Client.Self.Session {
who = "me"
line = fmt.Sprintf("You %s yourself", verb)
}
return userChangeNotification{
event: event,
who: who,
what: e.User.Channel.Name,
line: line,
}, true
}
func buildUserChangeNotification(event string, verb string, user *gumble.User, eventChannel *gumble.Channel, currentChannel *gumble.Channel) (userChangeNotification, bool) { func buildUserChangeNotification(event string, verb string, user *gumble.User, eventChannel *gumble.Channel, currentChannel *gumble.Channel) (userChangeNotification, bool) {
if !sameChannel(eventChannel, currentChannel) { if !sameChannel(eventChannel, currentChannel) {
return userChangeNotification{}, false return userChangeNotification{}, false
@@ -382,30 +302,6 @@ func (b *Barnard) updateUserChannel(e *gumble.UserChangeEvent) {
b.rememberUserChannel(e.User) b.rememberUserChannel(e.User)
} }
func (b *Barnard) rememberUserAudioState(user *gumble.User) {
if user == nil {
return
}
if b.userAudioStates == nil {
b.userAudioStates = make(map[uint32]userAudioState)
}
b.userAudioStates[user.Session] = userAudioState{
selfMuted: user.SelfMuted,
selfDeafened: user.SelfDeafened,
}
}
func (b *Barnard) updateUserAudioState(e *gumble.UserChangeEvent) {
if e == nil || e.User == nil {
return
}
if e.Type.Has(gumble.UserChangeDisconnected) {
delete(b.userAudioStates, e.User.Session)
return
}
b.rememberUserAudioState(e.User)
}
func (b *Barnard) OnChannelChange(e *gumble.ChannelChangeEvent) { func (b *Barnard) OnChannelChange(e *gumble.ChannelChangeEvent) {
b.UpdateInputStatus(fmt.Sprintf("[%s]", e.Channel.Name)) b.UpdateInputStatus(fmt.Sprintf("[%s]", e.Channel.Name))
if e.Type.Has(gumble.ChannelChangeDescription) { if e.Type.Has(gumble.ChannelChangeDescription) {
-142
View File
@@ -144,145 +144,3 @@ func TestUpdateUserChannel(t *testing.T) {
t.Fatalf("expected disconnected user channel to be removed, got %#v", got) t.Fatalf("expected disconnected user channel to be removed, got %#v", got)
} }
} }
func TestUserAudioChangeNotification(t *testing.T) {
current := &gumble.Channel{ID: 1, Name: "Current"}
other := &gumble.Channel{ID: 2, Name: "Other"}
self := &gumble.User{Session: 1, Name: "Username", Channel: current}
tests := []struct {
name string
user *gumble.User
previous userAudioState
remember bool
change gumble.UserChangeType
want userChangeNotification
wantOK bool
}{
{
name: "user mutes themselves",
user: &gumble.User{Session: 2, Name: "Guest", Channel: current, SelfMuted: true},
remember: true,
change: gumble.UserChangeAudio,
want: userChangeNotification{
event: "mute",
who: "Guest",
what: "Current",
line: "Guest muted themselves",
},
wantOK: true,
},
{
name: "user unmutes themselves",
user: &gumble.User{Session: 2, Name: "Guest", Channel: current},
previous: userAudioState{selfMuted: true},
remember: true,
change: gumble.UserChangeAudio,
want: userChangeNotification{
event: "unmute",
who: "Guest",
what: "Current",
line: "Guest unmuted themselves",
},
wantOK: true,
},
{
name: "deafen takes precedence over implied mute",
user: &gumble.User{Session: 2, Name: "Guest", Channel: current, SelfMuted: true, SelfDeafened: true},
remember: true,
change: gumble.UserChangeAudio,
want: userChangeNotification{
event: "deafen",
who: "Guest",
what: "Current",
line: "Guest deafened themselves",
},
wantOK: true,
},
{
name: "undeafen takes precedence over implied unmute",
user: &gumble.User{Session: 2, Name: "Guest", Channel: current},
previous: userAudioState{selfMuted: true, selfDeafened: true},
remember: true,
change: gumble.UserChangeAudio,
want: userChangeNotification{
event: "undeafen",
who: "Guest",
what: "Current",
line: "Guest undeafened themselves",
},
wantOK: true,
},
{
name: "self mute uses self wording",
user: &gumble.User{Session: 1, Name: "Username", Channel: current, SelfMuted: true},
remember: true,
change: gumble.UserChangeAudio,
want: userChangeNotification{
event: "mute",
who: "me",
what: "Current",
line: "You muted yourself",
},
wantOK: true,
},
{
name: "other channel is ignored",
user: &gumble.User{Session: 2, Name: "Guest", Channel: other, SelfMuted: true},
remember: true,
change: gumble.UserChangeAudio,
},
{
name: "initial state is remembered without notification",
user: &gumble.User{Session: 2, Name: "Guest", Channel: current, SelfMuted: true},
change: gumble.UserChangeConnected | gumble.UserChangeAudio,
},
{
name: "unrelated user change is ignored",
user: &gumble.User{Session: 2, Name: "Guest", Channel: current, SelfMuted: true},
remember: true,
change: gumble.UserChangeComment,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
b := &Barnard{
Client: &gumble.Client{Self: self},
}
if tt.remember {
b.userAudioStates = map[uint32]userAudioState{tt.user.Session: tt.previous}
}
got, ok := b.userAudioChangeNotification(&gumble.UserChangeEvent{
Client: b.Client,
Type: tt.change,
User: tt.user,
})
if ok != tt.wantOK {
t.Fatalf("expected ok %v, got %v", tt.wantOK, ok)
}
if got != tt.want {
t.Fatalf("expected %#v, got %#v", tt.want, got)
}
})
}
}
func TestUpdateUserAudioState(t *testing.T) {
user := &gumble.User{Session: 2, SelfMuted: true, SelfDeafened: true}
b := &Barnard{}
b.updateUserAudioState(&gumble.UserChangeEvent{User: user})
if got := b.userAudioStates[user.Session]; got != (userAudioState{selfMuted: true, selfDeafened: true}) {
t.Fatalf("unexpected remembered audio state: %#v", got)
}
b.updateUserAudioState(&gumble.UserChangeEvent{
Type: gumble.UserChangeDisconnected,
User: user,
})
if _, ok := b.userAudioStates[user.Session]; ok {
t.Fatal("expected disconnected user audio state to be removed")
}
}
+3 -3
View File
@@ -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.SetLocallyMuted(j.LocallyMuted) u.LocallyMuted = j.LocallyMuted
c.SaveConfig() c.SaveConfig()
} }
@@ -331,7 +331,7 @@ func (c *Config) UpdateUser(u *gumble.User) {
j = c.findUser(uc.Config.Address, u.Name) j = c.findUser(uc.Config.Address, u.Name)
u.Boost = j.Boost u.Boost = j.Boost
u.Volume = j.Volume u.Volume = j.Volume
u.SetLocallyMuted(j.LocallyMuted) u.LocallyMuted = j.LocallyMuted // Update LocallyMuted state from config
if u.Boost < 1 { if u.Boost < 1 {
u.Boost = 1 u.Boost = 1
} }
@@ -343,7 +343,7 @@ func (c *Config) UpdateConfig(u *gumble.User) {
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.IsLocallyMuted() j.LocallyMuted = u.LocallyMuted // Save LocallyMuted state to config
} }
func NewConfig(fn *string) *Config { func NewConfig(fn *string) *Config {
-32
View File
@@ -54,38 +54,6 @@ leave() {
[[ $notify ]] && notify "$2 left the channel." [[ $notify ]] && notify "$2 left the channel."
} }
mute() {
if [[ "$2" == "me" ]]; then
[[ $notify ]] && notify "You muted yourself."
else
[[ $notify ]] && notify "$2 muted."
fi
}
unmute() {
if [[ "$2" == "me" ]]; then
[[ $notify ]] && notify "You unmuted yourself."
else
[[ $notify ]] && notify "$2 unmuted."
fi
}
deafen() {
if [[ "$2" == "me" ]]; then
[[ $notify ]] && notify "You deafened yourself."
else
[[ $notify ]] && notify "$2 deafened."
fi
}
undeafen() {
if [[ "$2" == "me" ]]; then
[[ $notify ]] && notify "You undeafened yourself."
else
[[ $notify ]] && notify "$2 undeafened."
fi
}
micdown() { micdown() {
[[ $sound ]] && play -qnV0 synth .25 sin G6:E5 norm -8 [[ $sound ]] && play -qnV0 synth .25 sin G6:E5 norm -8
[[ $notify ]] && notify "You have stopped transmitting." [[ $notify ]] && notify "You have stopped transmitting."
-7
View File
@@ -85,13 +85,6 @@ type AudioPacket struct {
Client *Client Client *Client
Sender *User Sender *User
Target *VoiceTarget Target *VoiceTarget
Final bool
// Channels is the number of interleaved PCM channels in AudioBuffer.
Channels int
// LocallyMuted and LocalMuteGeneration snapshot the receiver's local mute
// state when the packet entered the playback queue.
LocallyMuted bool
LocalMuteGeneration uint64
AudioBuffer AudioBuffer
-185
View File
@@ -1,185 +0,0 @@
package gumble
import (
"sync"
"testing"
"git.stormux.org/storm/barnard/gumble/go-openal/openal"
)
type testAudioListener struct {
events chan *AudioStreamEvent
}
func (l testAudioListener) OnAudioStream(event *AudioStreamEvent) {
if l.events != nil {
l.events <- event
}
}
func TestAudioBufferCount(t *testing.T) {
tests := []struct {
configured int
want int
}{
{configured: -1, want: AudioMinimumBufferCount},
{configured: 0, want: AudioMinimumBufferCount},
{configured: 2, want: AudioMinimumBufferCount},
{configured: 16, want: 16},
}
for _, tt := range tests {
config := &Config{Buffers: tt.configured}
if got := config.AudioBufferCount(); got != tt.want {
t.Errorf("AudioBufferCount() with %d = %d, want %d", tt.configured, got, tt.want)
}
}
}
func TestDefaultAudioBufferCountPreservesStablePlaybackCapacity(t *testing.T) {
config := NewConfig()
if got := config.AudioBufferCount(); got != 64 {
t.Fatalf("default AudioBufferCount() = %d, want 64", got)
}
}
func TestAudioListenerStreamBufferingAndCleanup(t *testing.T) {
var listeners AudioListeners
events := make(chan *AudioStreamEvent, 1)
detacher := listeners.Attach(testAudioListener{events: events})
user := &User{Session: 7}
packet := &AudioPacket{AudioBuffer: AudioBuffer{1}}
listeners.dispatchAudio(nil, user, packet, 16)
event := <-events
stream := event.C
if got := cap(stream); got != 16 {
t.Fatalf("stream capacity = %d, want 16", got)
}
if got := <-stream; got != packet {
t.Fatal("expected the packet to be delivered to the stream")
}
listeners.dispatchAudio(nil, user, packet, 16)
select {
case <-events:
t.Fatal("expected an existing stream to be reused without another callback")
default:
}
<-stream
listeners.closeUserAudio(user)
if _, ok := <-stream; ok {
t.Fatal("expected user audio stream to be closed")
}
item := detacher.(*audioEventItem)
if _, ok := item.streams[user]; ok {
t.Fatal("expected user audio stream to be removed")
}
}
func TestAudioListenerDetachClosesStreams(t *testing.T) {
var listeners AudioListeners
events := make(chan *AudioStreamEvent, 1)
detacher := listeners.Attach(testAudioListener{events: events})
listeners.dispatchAudio(nil, &User{Session: 9}, &AudioPacket{}, 8)
stream := (<-events).C
<-stream
detacher.Detach()
if _, ok := <-stream; ok {
t.Fatal("expected detach to close its audio streams")
}
if listeners.head != nil || listeners.tail != nil {
t.Fatal("expected detached listener to be unlinked")
}
}
func TestAudioListenerDropsOldestPacketWhenFull(t *testing.T) {
var listeners AudioListeners
events := make(chan *AudioStreamEvent, 1)
listeners.Attach(testAudioListener{events: events})
user := &User{Session: 11}
for sample := int16(1); sample <= 4; sample++ {
listeners.dispatchAudio(nil, user, &AudioPacket{AudioBuffer: AudioBuffer{sample}}, 3)
}
stream := (<-events).C
for _, want := range []int16{2, 3, 4} {
packet := <-stream
if got := packet.AudioBuffer[0]; got != want {
t.Fatalf("queued sample = %d, want %d", got, want)
}
}
}
func TestAudioListenerDetachDuringDelivery(t *testing.T) {
var listeners AudioListeners
detacher := listeners.Attach(testAudioListener{})
user := &User{Session: 13}
start := make(chan struct{})
var workers sync.WaitGroup
workers.Add(1)
go func() {
defer workers.Done()
<-start
for i := 0; i < 1000; i++ {
listeners.dispatchAudio(nil, user, &AudioPacket{}, 3)
}
}()
close(start)
detacher.Detach()
workers.Wait()
}
func TestAudioPacketLength(t *testing.T) {
audioLength, final := parseAudioPacketLength(42 | audioTerminatorFlag)
if audioLength != 42 {
t.Fatalf("audio length = %d, want 42", audioLength)
}
if !final {
t.Fatal("expected terminator flag to mark packet final")
}
audioLength, final = parseAudioPacketLength(42)
if audioLength != 42 || final {
t.Fatalf("non-final packet parsed as length %d, final %v", audioLength, final)
}
}
func TestUserAudioSourceLifecycle(t *testing.T) {
user := &User{}
source := openal.Source(1)
otherSource := openal.Source(2)
user.SetAudioSource(&source)
if !user.HasAudioSource() {
t.Fatal("expected the audio source to be published")
}
user.ClearAudioSource(otherSource)
if !user.HasAudioSource() {
t.Fatal("expected a different source not to clear the active source")
}
user.ClearAudioSource(source)
if user.HasAudioSource() {
t.Fatal("expected the active audio source to be cleared")
}
}
func TestLocalMuteGenerationChangesOnlyOnTransitions(t *testing.T) {
user := &User{}
muted, generation := user.LocalMuteState()
if muted || generation != 0 {
t.Fatalf("initial local mute state = (%v, %d), want (false, 0)", muted, generation)
}
if got := user.SetLocallyMuted(true); got != 1 {
t.Fatalf("mute generation = %d, want 1", got)
}
if got := user.SetLocallyMuted(true); got != 1 {
t.Fatalf("unchanged mute generation = %d, want 1", got)
}
if got := user.SetLocallyMuted(false); got != 2 {
t.Fatalf("unmute generation = %d, want 2", got)
}
}
-1
View File
@@ -50,7 +50,6 @@ type AudioEncoder interface {
// AudioDecoder decodes an encoded byte slice to a chunk of PCM audio samples. // AudioDecoder decodes an encoded byte slice to a chunk of PCM audio samples.
type AudioDecoder interface { type AudioDecoder interface {
ID() int ID() int
Channels() int
Decode(data []byte, frameSize int) ([]int16, error) Decode(data []byte, frameSize int) ([]int16, error)
Reset() Reset()
} }
+2 -114
View File
@@ -1,83 +1,38 @@
package gumble package gumble
import "sync"
type audioEventItem struct { type audioEventItem struct {
parent *AudioListeners parent *AudioListeners
prev, next *audioEventItem prev, next *audioEventItem
listener AudioListener listener AudioListener
streams map[*User]chan *AudioPacket streams map[*User]chan *AudioPacket
attached bool
} }
func (e *audioEventItem) Detach() { func (e *audioEventItem) Detach() {
parent := e.parent
parent.mu.Lock()
defer parent.mu.Unlock()
if !e.attached {
return
}
e.closeAudioStreamsLocked()
if e.prev == nil { if e.prev == nil {
parent.head = e.next e.parent.head = e.next
} else { } else {
e.prev.next = e.next e.prev.next = e.next
} }
if e.next == nil { if e.next == nil {
parent.tail = e.prev e.parent.tail = e.prev
} else { } else {
e.next.prev = e.prev e.next.prev = e.prev
} }
e.attached = false
e.prev = nil
e.next = nil
}
func (e *audioEventItem) closeUserAudioLocked(user *User) {
if stream := e.streams[user]; stream != nil {
for {
select {
case <-stream:
continue
default:
close(stream)
delete(e.streams, user)
return
}
}
}
}
func (e *audioEventItem) closeAudioStreamsLocked() {
for user := range e.streams {
e.closeUserAudioLocked(user)
}
}
type audioStreamCallback struct {
listener AudioListener
event AudioStreamEvent
} }
// AudioListeners is a list of audio listeners. Each attached listener is // AudioListeners is a list of audio listeners. Each attached listener is
// called in sequence when a new user audio stream begins. // called in sequence when a new user audio stream begins.
type AudioListeners struct { type AudioListeners struct {
mu sync.Mutex
head, tail *audioEventItem head, tail *audioEventItem
} }
// Attach adds a new audio listener to the end of the current list of listeners. // Attach adds a new audio listener to the end of the current list of listeners.
func (e *AudioListeners) Attach(listener AudioListener) Detacher { func (e *AudioListeners) Attach(listener AudioListener) Detacher {
e.mu.Lock()
defer e.mu.Unlock()
item := &audioEventItem{ item := &audioEventItem{
parent: e, parent: e,
prev: e.tail, prev: e.tail,
listener: listener, listener: listener,
streams: make(map[*User]chan *AudioPacket), streams: make(map[*User]chan *AudioPacket),
attached: true,
} }
if e.head == nil { if e.head == nil {
e.head = item e.head = item
@@ -86,73 +41,6 @@ func (e *AudioListeners) Attach(listener AudioListener) Detacher {
e.tail = item e.tail = item
} else { } else {
e.tail.next = item e.tail.next = item
e.tail = item
} }
return item return item
} }
func (e *AudioListeners) dispatchAudio(client *Client, user *User, packet *AudioPacket, bufferCount int) {
if bufferCount < 1 {
bufferCount = 1
}
e.mu.Lock()
callbacks := make([]audioStreamCallback, 0)
for item := e.head; item != nil; item = item.next {
stream := item.streams[user]
if stream == nil {
stream = make(chan *AudioPacket, bufferCount)
item.streams[user] = stream
callbacks = append(callbacks, audioStreamCallback{
listener: item.listener,
event: AudioStreamEvent{
Client: client,
User: user,
C: stream,
},
})
}
enqueueLatestAudio(stream, packet)
}
e.mu.Unlock()
for i := range callbacks {
callbacks[i].listener.OnAudioStream(&callbacks[i].event)
}
}
// enqueueLatestAudio keeps packet delivery bounded. If playback falls behind,
// discard the oldest queued packet so the network reader never waits on audio.
func enqueueLatestAudio(stream chan *AudioPacket, packet *AudioPacket) {
select {
case stream <- packet:
return
default:
}
select {
case <-stream:
default:
}
select {
case stream <- packet:
default:
}
}
func (e *AudioListeners) closeUserAudio(user *User) {
e.mu.Lock()
defer e.mu.Unlock()
for item := e.head; item != nil; item = item.next {
item.closeUserAudioLocked(user)
}
}
func (e *AudioListeners) closeAllAudio() {
e.mu.Lock()
defer e.mu.Unlock()
for item := e.head; item != nil; item = item.next {
item.closeAudioStreamsLocked()
}
}
-1
View File
@@ -245,7 +245,6 @@ func (c *Client) readRoutine() {
wasSynced := c.State() == StateSynced wasSynced := c.State() == StateSynced
atomic.StoreUint32(&c.state, uint32(StateDisconnected)) atomic.StoreUint32(&c.state, uint32(StateDisconnected))
close(c.end) close(c.end)
c.Config.AudioListeners.closeAllAudio()
if wasSynced { if wasSynced {
c.Config.Listeners.onDisconnect(&c.disconnectEvent) c.Config.Listeners.onDisconnect(&c.disconnectEvent)
} }
+2 -16
View File
@@ -28,32 +28,18 @@ type Config struct {
// The event listeners used when client events are triggered. // The event listeners used when client events are triggered.
Listeners Listeners Listeners Listeners
AudioListeners AudioListeners AudioListeners AudioListeners
// Buffers is the per-user capacity used between network decoding and audio Buffers int
// playback, and for the OpenAL playback buffer pool.
Buffers int
} }
// AudioMinimumBufferCount is the smallest pool that can provide a short
// playback prebuffer without dropping the current packet.
const AudioMinimumBufferCount = 3
// NewConfig returns a new Config struct with default values set. // NewConfig returns a new Config struct with default values set.
func NewConfig() *Config { func NewConfig() *Config {
return &Config{ return &Config{
Buffers: 64, Buffers: 8,
AudioInterval: AudioDefaultInterval, AudioInterval: AudioDefaultInterval,
AudioDataBytes: AudioDefaultDataBytes, AudioDataBytes: AudioDefaultDataBytes,
} }
} }
// AudioBufferCount returns a safe per-user audio buffer count.
func (c *Config) AudioBufferCount() int {
if c == nil || c.Buffers < AudioMinimumBufferCount {
return AudioMinimumBufferCount
}
return c.Buffers
}
// Attach is an alias of c.Listeners.Attach. // Attach is an alias of c.Listeners.Attach.
func (c *Config) Attach(l EventListener) Detacher { func (c *Config) Attach(l EventListener) Detacher {
return c.Listeners.Attach(l) return c.Listeners.Attach(l)
+24 -23
View File
@@ -22,12 +22,6 @@ var (
errNoCodec = errors.New("gumble: no audio codec") errNoCodec = errors.New("gumble: no audio codec")
) )
const audioTerminatorFlag int64 = 0x2000
func parseAudioPacketLength(length int64) (int, bool) {
return int(length &^ audioTerminatorFlag), length&audioTerminatorFlag != 0
}
var handlers = [...]func(*Client, []byte) error{ var handlers = [...]func(*Client, []byte) error{
(*Client).handleVersion, (*Client).handleVersion,
(*Client).handleUDPTunnel, (*Client).handleUDPTunnel,
@@ -133,28 +127,19 @@ func (c *Client) handleUDPTunnel(buffer []byte) error {
} }
buffer = buffer[n:] buffer = buffer[n:]
// Opus audio packets set the 13th bit in the size field as the terminator. // Opus audio packets set the 13th bit in the size field as the terminator.
audioLength, final := parseAudioPacketLength(length) audioLength := int(length) &^ 0x2000
if audioLength > len(buffer) { if audioLength > len(buffer) {
return errInvalidProtobuf return errInvalidProtobuf
} }
var pcm []int16 pcm, err := decoder.Decode(buffer[:audioLength], AudioMaximumFrameSize)
if audioLength > 0 { if err != nil {
var err error return err
pcm, err = decoder.Decode(buffer[:audioLength], AudioMaximumFrameSize)
if err != nil {
return err
}
} }
locallyMuted, localMuteGeneration := user.LocalMuteState()
event := AudioPacket{ event := AudioPacket{
Client: c, Client: c,
Sender: user, Sender: user,
Final: final,
Channels: decoder.Channels(),
LocallyMuted: locallyMuted,
LocalMuteGeneration: localMuteGeneration,
Target: &VoiceTarget{ Target: &VoiceTarget{
ID: uint32(audioTarget), ID: uint32(audioTarget),
}, },
@@ -171,7 +156,24 @@ func (c *Client) handleUDPTunnel(buffer []byte) error {
event.HasPosition = true event.HasPosition = true
} }
c.Config.AudioListeners.dispatchAudio(c, user, &event, c.Config.AudioBufferCount()) c.volatile.Lock()
for item := c.Config.AudioListeners.head; item != nil; item = item.next {
c.volatile.Unlock()
ch := item.streams[user]
if ch == nil {
ch = make(chan *AudioPacket)
item.streams[user] = ch
event := AudioStreamEvent{
Client: c,
User: user,
C: ch,
}
item.listener.OnAudioStream(&event)
}
ch <- &event
c.volatile.Lock()
}
c.volatile.Unlock()
return nil return nil
} }
@@ -475,7 +477,6 @@ func (c *Client) handleUserRemove(buffer []byte) error {
c.volatile.Unlock() c.volatile.Unlock()
} }
c.Config.AudioListeners.closeUserAudio(event.User)
if c.State() == StateSynced { if c.State() == StateSynced {
c.Config.Listeners.onUserChange(&event) c.Config.Listeners.onUserChange(&event)
+4 -71
View File
@@ -1,8 +1,6 @@
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"
@@ -55,85 +53,20 @@ type User struct {
client *Client client *Client
decoder AudioDecoder decoder AudioDecoder
localMuteMu sync.RWMutex AudioSource *openal.Source
localMuteGeneration uint64 Boost uint16
audioSourceMu sync.Mutex Volume float32
AudioSource *openal.Source
Boost uint16
Volume float32
} }
// 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.IsLocallyMuted() return u.Muted || u.LocallyMuted
} }
func (u *User) GetClient() *Client { func (u *User) GetClient() *Client {
return u.client return u.client
} }
// IsLocallyMuted reports whether this client has locally muted the user.
func (u *User) IsLocallyMuted() bool {
u.localMuteMu.RLock()
defer u.localMuteMu.RUnlock()
return u.LocallyMuted
}
// SetLocallyMuted updates the local mute state and advances its generation
// when the state changes. The generation prevents queued audio from crossing
// a mute or unmute boundary.
func (u *User) SetLocallyMuted(muted bool) uint64 {
u.localMuteMu.Lock()
defer u.localMuteMu.Unlock()
if u.LocallyMuted != muted {
u.LocallyMuted = muted
u.localMuteGeneration++
}
return u.localMuteGeneration
}
// LocalMuteState returns the local mute state and its current generation.
func (u *User) LocalMuteState() (bool, uint64) {
u.localMuteMu.RLock()
defer u.localMuteMu.RUnlock()
return u.LocallyMuted, u.localMuteGeneration
}
// SetAudioSource publishes the OpenAL source used for this user's playback.
func (u *User) SetAudioSource(source *openal.Source) {
u.audioSourceMu.Lock()
defer u.audioSourceMu.Unlock()
u.AudioSource = source
}
// ClearAudioSource removes source if it is still this user's active source.
func (u *User) ClearAudioSource(source openal.Source) {
u.audioSourceMu.Lock()
defer u.audioSourceMu.Unlock()
if u.AudioSource != nil && *u.AudioSource == source {
u.AudioSource = nil
}
}
// SetAudioGain changes the active playback source gain. It returns false when
// the user does not currently have an active source.
func (u *User) SetAudioGain(gain float32) bool {
u.audioSourceMu.Lock()
defer u.audioSourceMu.Unlock()
if u.AudioSource == nil {
return false
}
u.AudioSource.SetGain(gain)
return true
}
// HasAudioSource reports whether the user currently has an active source.
func (u *User) HasAudioSource() bool {
u.audioSourceMu.Lock()
defer u.audioSourceMu.Unlock()
return u.AudioSource != nil
}
// SetTexture sets the user's texture. // SetTexture sets the user's texture.
func (u *User) SetTexture(texture []byte) { func (u *User) SetTexture(texture []byte) {
packet := MumbleProto.UserState{ packet := MumbleProto.UserState{
+75 -377
View File
@@ -32,66 +32,9 @@ type Recorder interface {
const recorderOutgoingSource uint32 = ^uint32(0) const recorderOutgoingSource uint32 = ^uint32(0)
const ( const (
maxBufferSize = 11520 // Max frame size (2880) * bytes per stereo sample (4) maxBufferSize = 11520 // Max frame size (2880) * bytes per stereo sample (4)
playbackPrebufferBuffers = gumble.AudioMinimumBufferCount
playbackBufferWait = 20 * time.Millisecond
) )
func shouldStartPlayback(state openal.State, queued int32, final bool) bool {
return state != openal.Playing && queued > 0 && (queued >= playbackPrebufferBuffers || final)
}
func playbackLayout(packet *gumble.AudioPacket) (openal.Format, int, bool) {
if packet == nil || len(packet.AudioBuffer) == 0 {
return 0, 0, false
}
switch packet.Channels {
case gumble.AudioMonoChannels:
return openal.FormatMono16, len(packet.AudioBuffer), true
case gumble.AudioChannels:
if len(packet.AudioBuffer)%gumble.AudioChannels != 0 {
return 0, 0, false
}
return openal.FormatStereo16, len(packet.AudioBuffer) / gumble.AudioChannels, true
default:
return 0, 0, false
}
}
type playbackCommand struct {
gain float32
muted bool
generation uint64
reset bool
applied chan struct{}
}
type playbackControl struct {
commands chan playbackCommand
cancel chan struct{}
done chan struct{}
}
type playbackState struct {
gain float32
muted bool
generation uint64
}
func (s *playbackState) apply(command playbackCommand) bool {
s.gain = command.gain
if !command.reset {
return false
}
s.muted = command.muted
s.generation = command.generation
return true
}
func (s playbackState) accepts(packet *gumble.AudioPacket) bool {
return packet != nil && !s.muted && !packet.LocallyMuted && packet.LocalMuteGeneration == s.generation
}
var ( var (
ErrState = errors.New("gumbleopenal: invalid state") ErrState = errors.New("gumbleopenal: invalid state")
ErrMic = errors.New("gumbleopenal: microphone disconnected or misconfigured") ErrMic = errors.New("gumbleopenal: microphone disconnected or misconfigured")
@@ -118,10 +61,7 @@ type Stream struct {
sourceChannels int sourceChannels int
sourceFrameSize int sourceFrameSize int
micVolume float32 micVolume float32
sourceStop chan struct{} sourceStop chan bool
sourceMu sync.Mutex
sourceWG sync.WaitGroup
destroyed bool
deviceSink *openal.Device deviceSink *openal.Device
contextSink *openal.Context contextSink *openal.Context
@@ -133,11 +73,6 @@ type Stream struct {
filePlayer FilePlayer filePlayer FilePlayer
recorderMu sync.RWMutex recorderMu sync.RWMutex
recorder Recorder recorder Recorder
playbackMu sync.Mutex
playbackWG sync.WaitGroup
playbackStopped bool
playbackControls map[*gumble.User]*playbackControl
destroyOnce sync.Once
} }
func New(client *gumble.Client, inputDevice *string, outputDevice *string, test bool) (*Stream, error) { func New(client *gumble.Client, inputDevice *string, outputDevice *string, test bool) (*Stream, error) {
@@ -230,182 +165,49 @@ func (s *Stream) getRecorder() Recorder {
return s.recorder return s.recorder
} }
// HasUserAudio reports whether user currently has a playback stream.
func (s *Stream) HasUserAudio(user *gumble.User) bool {
s.playbackMu.Lock()
defer s.playbackMu.Unlock()
return !s.playbackStopped && s.playbackControls[user] != nil
}
// SetUserGain applies gain on the playback goroutine that owns the OpenAL
// source.
func (s *Stream) SetUserGain(user *gumble.User, gain float32) bool {
muted, generation := user.LocalMuteState()
return s.sendPlaybackCommand(user, playbackCommand{
gain: gain,
muted: muted,
generation: generation,
})
}
// SetUserMuteState flushes queued audio and synchronizes the user's current
// local mute state with the playback goroutine.
func (s *Stream) SetUserMuteState(user *gumble.User) bool {
muted, generation := user.LocalMuteState()
return s.sendPlaybackCommand(user, playbackCommand{
gain: user.Volume,
muted: muted,
generation: generation,
reset: true,
})
}
func (s *Stream) sendPlaybackCommand(user *gumble.User, command playbackCommand) bool {
s.playbackMu.Lock()
control := s.playbackControls[user]
stopped := s.playbackStopped
s.playbackMu.Unlock()
if stopped || control == nil {
return false
}
command.applied = make(chan struct{})
select {
case control.commands <- command:
case <-control.cancel:
return false
case <-control.done:
return false
}
select {
case <-command.applied:
return true
case <-control.cancel:
select {
case <-command.applied:
return true
default:
return false
}
case <-control.done:
select {
case <-command.applied:
return true
default:
return false
}
}
}
func (s *Stream) Destroy() { func (s *Stream) Destroy() {
s.destroyOnce.Do(func() { if s.link != nil {
s.playbackMu.Lock() s.link.Detach()
s.playbackStopped = true }
for _, control := range s.playbackControls { if s.deviceSource != nil {
close(control.cancel) s.StopSource()
} s.deviceSource.CaptureCloseDevice()
s.playbackMu.Unlock() s.deviceSource = nil
if s.link != nil { }
s.link.Detach() if s.deviceSink != nil {
s.link = nil s.contextSink.Destroy()
} s.deviceSink.CloseDevice()
s.playbackWG.Wait() s.contextSink = nil
s.deviceSink = nil
s.sourceMu.Lock() }
s.destroyed = true
if s.sourceStop != nil {
_ = s.stopSourceLocked()
}
if s.deviceSource != nil {
s.deviceSource.CaptureCloseDevice()
s.deviceSource = nil
}
s.sourceMu.Unlock()
if s.contextSink != nil {
s.contextSink.Destroy()
s.contextSink = nil
}
if s.deviceSink != nil {
s.deviceSink.CloseDevice()
s.deviceSink = nil
}
})
} }
func (s *Stream) StartSource(inputDevice *string) error { func (s *Stream) StartSource(inputDevice *string) error {
s.sourceMu.Lock()
defer s.sourceMu.Unlock()
if s.destroyed {
return ErrState
}
if s.sourceStop != nil { if s.sourceStop != nil {
return ErrState return ErrState
} }
if s.deviceSource == nil { if s.deviceSource == nil {
return ErrMic return ErrMic
} }
if err := s.configureSourceLocked(inputDevice); err != nil { s.deviceSource.CaptureStart()
return err s.sourceStop = make(chan bool)
} go s.sourceRoutine(inputDevice)
stop := make(chan struct{})
device := s.deviceSource
frameSize := s.sourceFrameSize
sourceChannels := s.sourceChannels
s.sourceStop = stop
device.CaptureStart()
s.sourceWG.Add(1)
go func() {
defer s.sourceWG.Done()
s.sourceRoutine(device, stop, frameSize, sourceChannels)
}()
return nil return nil
} }
func (s *Stream) StopSource() error { func (s *Stream) StopSource() error {
s.sourceMu.Lock()
defer s.sourceMu.Unlock()
return s.stopSourceLocked()
}
func (s *Stream) stopSourceLocked() error {
if s.deviceSource == nil { if s.deviceSource == nil {
return ErrMic return ErrMic
} }
s.deviceSource.CaptureStop()
if s.sourceStop == nil { if s.sourceStop == nil {
return ErrState return ErrState
} }
close(s.sourceStop) close(s.sourceStop)
s.deviceSource.CaptureStop()
s.sourceWG.Wait()
s.sourceStop = nil s.sourceStop = nil
return nil return nil
} }
func (s *Stream) configureSourceLocked(inputDevice *string) error {
frameSize := s.client.Config.AudioFrameSize()
if frameSize == s.sourceFrameSize {
return nil
}
s.deviceSource.CaptureCloseDevice()
s.deviceSource = openal.CaptureOpenDevice(*inputDevice, gumble.AudioSampleRate, s.sourceFormat, uint32(frameSize))
if s.deviceSource == nil && s.sourceFormat == openal.FormatStereo16 {
s.sourceFormat = openal.FormatMono16
s.sourceChannels = 1
s.deviceSource = openal.CaptureOpenDevice(*inputDevice, gumble.AudioSampleRate, s.sourceFormat, uint32(frameSize))
}
if s.deviceSource == nil {
return ErrMic
}
s.sourceFrameSize = frameSize
return nil
}
func (s *Stream) GetMicVolume() float32 { func (s *Stream) GetMicVolume() float32 {
return s.micVolume return s.micVolume
} }
@@ -427,53 +229,22 @@ func (s *Stream) SetMicVolume(change float32, relative bool) {
} }
func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) { func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) {
control := &playbackControl{
commands: make(chan playbackCommand),
cancel: make(chan struct{}),
done: make(chan struct{}),
}
s.playbackMu.Lock()
if s.playbackStopped {
s.playbackMu.Unlock()
return
}
if s.playbackControls == nil {
s.playbackControls = make(map[*gumble.User]*playbackControl)
}
s.playbackControls[e.User] = control
s.playbackWG.Add(1)
s.playbackMu.Unlock()
go func(e *gumble.AudioStreamEvent) { go func(e *gumble.AudioStreamEvent) {
defer func() {
s.playbackMu.Lock()
if s.playbackControls[e.User] == control {
delete(s.playbackControls, e.User)
}
s.playbackMu.Unlock()
close(control.done)
s.playbackWG.Done()
}()
var source = openal.NewSource() var source = openal.NewSource()
e.User.AudioSource = &source
// Set initial gain based on volume and mute state // Set initial gain based on volume and mute state
locallyMuted, localMuteGeneration := e.User.LocalMuteState() if e.User.LocallyMuted {
state := playbackState{ e.User.AudioSource.SetGain(0)
gain: e.User.Volume,
muted: locallyMuted,
generation: localMuteGeneration,
}
if state.muted {
source.SetGain(0)
} else { } else {
source.SetGain(state.gain) e.User.AudioSource.SetGain(e.User.Volume)
} }
e.User.SetAudioSource(&source)
bufferCount := e.Client.Config.AudioBufferCount() bufferCount := e.Client.Config.Buffers
allBufs := openal.NewBuffers(bufferCount) if bufferCount < 64 {
emptyBufs := append(openal.Buffers(nil), allBufs...) bufferCount = 64
}
emptyBufs := openal.NewBuffers(bufferCount)
reclaim := func() { reclaim := func() {
if n := source.BuffersProcessed(); n > 0 { if n := source.BuffersProcessed(); n > 0 {
@@ -483,98 +254,17 @@ func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) {
} }
} }
flushPlayback := func() {
source.Stop()
if n := source.BuffersQueued(); n > 0 {
flushedBufs := make(openal.Buffers, n)
source.UnqueueBuffers(flushedBufs)
emptyBufs = append(emptyBufs, flushedBufs...)
}
}
defer func() {
flushPlayback()
e.User.ClearAudioSource(source)
source.Delete()
allBufs.Delete()
}()
applyCommand := func(command playbackCommand) bool {
reset := state.apply(command)
if reset {
flushPlayback()
}
if state.muted {
source.SetGain(0)
} else {
source.SetGain(state.gain)
}
close(command.applied)
return reset
}
acquireBuffer := func() (openal.Buffer, bool, bool) {
deadline := time.NewTimer(playbackBufferWait)
defer deadline.Stop()
retry := time.NewTicker(time.Millisecond)
defer retry.Stop()
for {
reclaim()
if len(emptyBufs) > 0 {
last := len(emptyBufs) - 1
buffer := emptyBufs[last]
emptyBufs = emptyBufs[:last]
return buffer, true, false
}
if source.State() != openal.Playing && source.BuffersQueued() > 0 {
source.Play()
}
select {
case command := <-control.commands:
if applyCommand(command) {
return 0, false, false
}
case <-control.cancel:
return 0, false, true
case <-deadline.C:
return 0, false, false
case <-retry.C:
continue
}
}
}
var raw [maxBufferSize]byte var raw [maxBufferSize]byte
for { for packet := range e.C {
var packet *gumble.AudioPacket // Skip processing if user is locally muted
select { if e.User.LocallyMuted {
case command := <-control.commands:
applyCommand(command)
continue
case <-control.cancel:
return
case incoming, ok := <-e.C:
if !ok {
return
}
packet = incoming
}
if !state.accepts(packet) {
continue continue
} }
var boost uint16 = uint16(1) var boost uint16 = uint16(1)
if len(packet.AudioBuffer) == 0 { samples := len(packet.AudioBuffer)
if shouldStartPlayback(source.State(), source.BuffersQueued(), packet.Final) { if samples > cap(raw)/2 {
source.Play()
}
continue
}
format, samples, validLayout := playbackLayout(packet)
if !validLayout || len(packet.AudioBuffer) > cap(raw)/2 {
continue continue
} }
@@ -586,7 +276,13 @@ func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) {
recordBuffer = make([]int16, len(packet.AudioBuffer)*gumble.AudioChannels) recordBuffer = make([]int16, len(packet.AudioBuffer)*gumble.AudioChannels)
} }
isStereo := packet.Channels == gumble.AudioChannels // Check if sample count suggests stereo data
isStereo := samples > gumble.AudioDefaultFrameSize && samples%2 == 0
format := openal.FormatMono16
if isStereo {
format = openal.FormatStereo16
samples = samples / 2
}
rawPtr := 0 rawPtr := 0
if isStereo { if isStereo {
@@ -658,30 +354,51 @@ func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) {
recorder.RecordAudioFrame(e.User.Session, recordBuffer[:recordPtr]) recorder.RecordAudioFrame(e.User.Session, recordBuffer[:recordPtr])
} }
buffer, ok, canceled := acquireBuffer() reclaim()
if canceled { if len(emptyBufs) == 0 {
return
}
if !ok {
continue continue
} }
last := len(emptyBufs) - 1
buffer := emptyBufs[last]
emptyBufs = emptyBufs[:last]
buffer.SetData(format, raw[:rawPtr], gumble.AudioSampleRate) buffer.SetData(format, raw[:rawPtr], gumble.AudioSampleRate)
source.QueueBuffer(buffer) source.QueueBuffer(buffer)
if shouldStartPlayback(source.State(), source.BuffersQueued(), packet.Final) { if source.State() != openal.Playing {
source.Play() source.Play()
} }
} }
reclaim()
emptyBufs.Delete()
source.Delete()
}(e) }(e)
} }
func (s *Stream) sourceRoutine(device *openal.CaptureDevice, stop <-chan struct{}, frameSize int, sourceChannels int) { func (s *Stream) sourceRoutine(inputDevice *string) {
interval := s.client.Config.AudioInterval interval := s.client.Config.AudioInterval
frameSize := s.client.Config.AudioFrameSize()
if frameSize != s.sourceFrameSize {
s.deviceSource.CaptureCloseDevice()
s.sourceFrameSize = frameSize
s.deviceSource = openal.CaptureOpenDevice(*inputDevice, gumble.AudioSampleRate, s.sourceFormat, uint32(s.sourceFrameSize))
if s.deviceSource == nil && s.sourceFormat == openal.FormatStereo16 {
s.sourceFormat = openal.FormatMono16
s.sourceChannels = 1
s.deviceSource = openal.CaptureOpenDevice(*inputDevice, gumble.AudioSampleRate, s.sourceFormat, uint32(s.sourceFrameSize))
}
}
if s.deviceSource == nil {
return
}
ticker := time.NewTicker(interval) ticker := time.NewTicker(interval)
defer ticker.Stop() defer ticker.Stop()
stop := s.sourceStop
outgoing := s.client.AudioOutgoing() outgoing := s.client.AudioOutgoing()
defer close(outgoing) defer close(outgoing)
@@ -690,12 +407,12 @@ func (s *Stream) sourceRoutine(device *openal.CaptureDevice, stop <-chan struct{
case <-stop: case <-stop:
return return
case <-ticker.C: case <-ticker.C:
sampleCount := frameSize * sourceChannels sampleCount := frameSize * s.sourceChannels
int16Buffer := make([]int16, sampleCount) int16Buffer := make([]int16, sampleCount)
// Capture microphone if available // Capture microphone if available
hasMicInput := false hasMicInput := false
buff := device.CaptureSamples(uint32(frameSize)) buff := s.deviceSource.CaptureSamples(uint32(frameSize))
if len(buff) == sampleCount*2 { if len(buff) == sampleCount*2 {
hasMicInput = true hasMicInput = true
for i := 0; i < sampleCount; i++ { for i := 0; i < sampleCount; i++ {
@@ -706,7 +423,7 @@ func (s *Stream) sourceRoutine(device *openal.CaptureDevice, stop <-chan struct{
int16Buffer[i] = sample int16Buffer[i] = sample
} }
if sourceChannels == 1 { if s.sourceChannels == 1 {
s.processMonoSamples(int16Buffer) s.processMonoSamples(int16Buffer)
} else { } else {
s.processStereoSamples(int16Buffer, frameSize) s.processStereoSamples(int16Buffer, frameSize)
@@ -726,7 +443,7 @@ func (s *Stream) sourceRoutine(device *openal.CaptureDevice, stop <-chan struct{
outputBuffer = make([]int16, frameSize*2) outputBuffer = make([]int16, frameSize*2)
if hasMicInput { if hasMicInput {
if sourceChannels == 2 { if s.sourceChannels == 2 {
// Mix stereo mic with stereo file // Mix stereo mic with stereo file
for i := 0; i < frameSize; i++ { for i := 0; i < frameSize; i++ {
idx := i * 2 idx := i * 2
@@ -781,17 +498,13 @@ func (s *Stream) sourceRoutine(device *openal.CaptureDevice, stop <-chan struct{
// Determine what to send // Determine what to send
if hasFileAudio { if hasFileAudio {
// Send stereo buffer when file is playing // Send stereo buffer when file is playing
if !sendOutgoingAudio(stop, outgoing, gumble.AudioBuffer(outputBuffer)) { outgoing <- gumble.AudioBuffer(outputBuffer)
return
}
if recorder := s.getRecorder(); recorder != nil { if recorder := s.getRecorder(); recorder != nil {
recorder.RecordAudioFrame(recorderOutgoingSource, outputBuffer) recorder.RecordAudioFrame(recorderOutgoingSource, outputBuffer)
} }
} else if hasMicInput { } else if hasMicInput {
// Send mic when no file is playing // Send mic when no file is playing
if !sendOutgoingAudio(stop, outgoing, gumble.AudioBuffer(int16Buffer)) { outgoing <- gumble.AudioBuffer(int16Buffer)
return
}
if recorder := s.getRecorder(); recorder != nil { if recorder := s.getRecorder(); recorder != nil {
recorder.RecordAudioFrame(recorderOutgoingSource, int16Buffer) recorder.RecordAudioFrame(recorderOutgoingSource, int16Buffer)
} }
@@ -800,21 +513,6 @@ func (s *Stream) sourceRoutine(device *openal.CaptureDevice, stop <-chan struct{
} }
} }
func sendOutgoingAudio(stop <-chan struct{}, outgoing chan<- gumble.AudioBuffer, buffer gumble.AudioBuffer) bool {
select {
case <-stop:
return false
default:
}
select {
case <-stop:
return false
case outgoing <- buffer:
return true
}
}
func scaleForRecording(sample int16, volume float32) int16 { func scaleForRecording(sample int16, volume float32) int16 {
scaled := int32(float32(sample) * volume) scaled := int32(float32(sample) * volume)
if scaled > 32767 { if scaled > 32767 {
-151
View File
@@ -1,151 +0,0 @@
package gumbleopenal
import (
"testing"
"git.stormux.org/storm/barnard/gumble/go-openal/openal"
"git.stormux.org/storm/barnard/gumble/gumble"
)
func TestShouldStartPlayback(t *testing.T) {
tests := []struct {
name string
state openal.State
queued int32
final bool
want bool
}{
{name: "prebuffer not full", state: openal.Initial, queued: playbackPrebufferBuffers - 1},
{name: "prebuffer full", state: openal.Initial, queued: playbackPrebufferBuffers, want: true},
{name: "recover after underrun", state: openal.Stopped, queued: playbackPrebufferBuffers, want: true},
{name: "short final utterance", state: openal.Initial, queued: 1, final: true, want: true},
{name: "empty final packet", state: openal.Initial, final: true},
{name: "already playing", state: openal.Playing, queued: playbackPrebufferBuffers, final: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := shouldStartPlayback(tt.state, tt.queued, tt.final); got != tt.want {
t.Fatalf("shouldStartPlayback() = %v, want %v", got, tt.want)
}
})
}
}
func TestPlaybackLayoutUsesExplicitStereoChannelCount(t *testing.T) {
packet := &gumble.AudioPacket{
Channels: gumble.AudioChannels,
AudioBuffer: make(gumble.AudioBuffer, gumble.AudioDefaultFrameSize),
}
format, frames, ok := playbackLayout(packet)
if !ok {
t.Fatal("expected a valid short stereo packet")
}
if format != openal.FormatStereo16 {
t.Fatalf("format = %v, want stereo", format)
}
if frames != gumble.AudioDefaultFrameSize/gumble.AudioChannels {
t.Fatalf("frames = %d, want %d", frames, gumble.AudioDefaultFrameSize/gumble.AudioChannels)
}
}
func TestSendOutgoingAudioStopsWhileSendIsBlocked(t *testing.T) {
stop := make(chan struct{})
outgoing := make(chan gumble.AudioBuffer)
close(stop)
if sendOutgoingAudio(stop, outgoing, gumble.AudioBuffer{1}) {
t.Fatal("expected a stopped source not to block on outgoing audio")
}
}
func TestSendOutgoingAudioDeliversWhenRunning(t *testing.T) {
stop := make(chan struct{})
outgoing := make(chan gumble.AudioBuffer, 1)
want := gumble.AudioBuffer{1, 2}
if !sendOutgoingAudio(stop, outgoing, want) {
t.Fatal("expected a running source to deliver outgoing audio")
}
got := <-outgoing
if len(got) != len(want) || got[0] != want[0] || got[1] != want[1] {
t.Fatalf("outgoing audio = %v, want %v", got, want)
}
}
func TestPlaybackStateRejectsAudioAcrossMuteBoundary(t *testing.T) {
state := playbackState{gain: 1, generation: 0}
beforeMute := &gumble.AudioPacket{LocalMuteGeneration: 0}
if !state.accepts(beforeMute) {
t.Fatal("expected current-generation audio before mute to be accepted")
}
mute := playbackCommand{gain: 1, muted: true, generation: 1, reset: true}
if !state.apply(mute) {
t.Fatal("expected mute transition to request a playback flush")
}
duringMute := &gumble.AudioPacket{LocallyMuted: true, LocalMuteGeneration: 1}
if state.accepts(duringMute) {
t.Fatal("expected audio received while muted to be rejected")
}
unmute := playbackCommand{gain: 1, generation: 2, reset: true}
if !state.apply(unmute) {
t.Fatal("expected unmute transition to request a playback flush")
}
if state.accepts(beforeMute) {
t.Fatal("expected partial prebuffer audio from before mute to stay rejected after unmute")
}
if state.accepts(duringMute) {
t.Fatal("expected muted-period audio to stay rejected after unmute")
}
afterUnmute := &gumble.AudioPacket{LocalMuteGeneration: 2}
if !state.accepts(afterUnmute) {
t.Fatal("expected current-generation audio after unmute to be accepted")
}
}
func TestSetUserMuteStateWaitsForPlaybackFlush(t *testing.T) {
user := &gumble.User{Volume: 0.75}
user.SetLocallyMuted(true)
control := &playbackControl{
commands: make(chan playbackCommand),
cancel: make(chan struct{}),
done: make(chan struct{}),
}
stream := &Stream{
playbackControls: map[*gumble.User]*playbackControl{user: control},
}
commands := make(chan playbackCommand, 1)
go func() {
command := <-control.commands
commands <- command
close(command.applied)
}()
if !stream.SetUserMuteState(user) {
t.Fatal("expected mute state command to be applied")
}
command := <-commands
if !command.reset || !command.muted || command.generation != 1 || command.gain != user.Volume {
t.Fatalf("unexpected mute command: %#v", command)
}
}
func TestSetUserMuteStateStopsOnPlaybackCancellation(t *testing.T) {
user := &gumble.User{}
control := &playbackControl{
commands: make(chan playbackCommand),
cancel: make(chan struct{}),
done: make(chan struct{}),
}
stream := &Stream{
playbackControls: map[*gumble.User]*playbackControl{user: control},
}
close(control.cancel)
if stream.SetUserMuteState(user) {
t.Fatal("expected a canceled playback stream to reject the mute command")
}
}
-4
View File
@@ -88,10 +88,6 @@ func (*Decoder) ID() int {
return ID return ID
} }
func (d *Decoder) Channels() int {
return d.channels
}
func (d *Decoder) Decode(data []byte, frameSize int) ([]int16, error) { func (d *Decoder) Decode(data []byte, frameSize int) ([]int16, error) {
// Allocate buffer for stereo - frameSize is per channel // Allocate buffer for stereo - frameSize is per channel
pcm := make([]int16, frameSize*gumble.AudioChannels) pcm := make([]int16, frameSize*gumble.AudioChannels)
+1 -1
View File
@@ -111,7 +111,7 @@ func main() {
fifo := flag.String("fifo", "", "path of a FIFO from which to read commands") fifo := flag.String("fifo", "", "path of a FIFO from which to read commands")
serverSet := false serverSet := false
usernameSet := false usernameSet := false
buffers := flag.Int("buffers", 64, "per-user audio buffer count (minimum 3)") buffers := flag.Int("buffers", 16, "number of audio buffers to use")
profile := flag.Bool("profile", false, "add http server to serve profiles") profile := flag.Bool("profile", false, "add http server to serve profiles")
noiseSuppressionEnabled := flag.Bool("noise-suppression", false, "enable noise suppression for microphone input") noiseSuppressionEnabled := flag.Bool("noise-suppression", false, "enable noise suppression for microphone input")
+12 -6
View File
@@ -9,7 +9,7 @@ import (
func (ti TreeItem) String() string { func (ti TreeItem) String() string {
if ti.User != nil { if ti.User != nil {
if ti.User.IsLocallyMuted() { if ti.User.LocallyMuted {
return "[MUTED] " + ti.User.Name return "[MUTED] " + ti.User.Name
} }
// Calculate total volume as percentage // Calculate total volume as percentage
@@ -35,7 +35,8 @@ 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 {
if b.Stream == nil || !b.Stream.HasUserAudio(u) { au := u.AudioSource
if au == nil {
continue continue
} }
var boost uint16 var boost uint16
@@ -43,7 +44,7 @@ func (b *Barnard) changeVolume(users []*gumble.User, change 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 = u.Volume + curboost cv = au.GetGain() + curboost
ng = cv + change ng = cv + change
boost = uint16(1) boost = uint16(1)
if ng > 1.0 { if ng > 1.0 {
@@ -57,7 +58,9 @@ func (b *Barnard) changeVolume(users []*gumble.User, change float32) {
} }
u.Boost = boost u.Boost = boost
u.Volume = ng u.Volume = ng
b.Stream.SetUserGain(u, ng) if !u.LocallyMuted {
au.SetGain(ng)
}
b.UserConfig.UpdateConfig(u) b.UserConfig.UpdateConfig(u)
} }
b.UserConfig.SaveConfig() b.UserConfig.SaveConfig()
@@ -65,13 +68,16 @@ 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 {
if b.Stream == nil || !b.Stream.HasUserAudio(u) { au := u.AudioSource
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.Boost = uint16(1)
u.Volume = 1.0 u.Volume = 1.0
b.Stream.SetUserGain(u, 1.0) if !u.LocallyMuted {
au.SetGain(1.0)
}
b.UserConfig.UpdateConfig(u) b.UserConfig.UpdateConfig(u)
} }
b.UserConfig.SaveConfig() b.UserConfig.SaveConfig()