diff --git a/gumble/gumbleopenal/stream.go b/gumble/gumbleopenal/stream.go index 86b6b34..5d061cb 100644 --- a/gumble/gumbleopenal/stream.go +++ b/gumble/gumbleopenal/stream.go @@ -70,14 +70,16 @@ type Stream struct { client *gumble.Client link gumble.Detacher - deviceSource *openal.CaptureDevice - sourceFormat openal.Format - sourceChannels int - sourceFrameSize int - micVolume atomic.Uint32 // float32 stored as bits - sourceMu sync.Mutex - sourceStop chan bool - sourceDone chan struct{} + deviceSource *openal.CaptureDevice + inputDeviceName string + outputDeviceName string + sourceFormat openal.Format + sourceChannels int + sourceFrameSize int + micVolume atomic.Uint32 // float32 stored as bits + sourceMu sync.Mutex + sourceStop chan bool + sourceDone chan struct{} deviceSink *openal.Device contextSink *openal.Context @@ -154,11 +156,13 @@ func New(client *gumble.Client, inputDevice *string, outputDevice *string, test } s := &Stream{ - client: client, - sourceFormat: inputFormat, - sourceChannels: sourceChannels, - sourceFrameSize: frmsz, - micAGC: audio.NewAGC(), // Always enable AGC for outgoing mic + client: client, + inputDeviceName: devName, + outputDeviceName: outName, + sourceFormat: inputFormat, + sourceChannels: sourceChannels, + sourceFrameSize: frmsz, + micAGC: audio.NewAGC(), // Always enable AGC for outgoing mic } s.micVolume.Store(math.Float32bits(1.0)) if sourceChannels == 2 { @@ -167,21 +171,21 @@ func New(client *gumble.Client, inputDevice *string, outputDevice *string, test s.deviceSource = idev if s.deviceSource == nil { - return nil, ErrInputDevice + return nil, fmt.Errorf("%w: capture device %q is unavailable", ErrInputDevice, deviceName(devName)) } s.deviceSink = odev if s.deviceSink == nil { - return nil, ErrOutputDevice + return nil, fmt.Errorf("%w: playback device %q is unavailable", ErrOutputDevice, deviceName(outName)) } s.contextSink = s.deviceSink.CreateContext() if s.contextSink == nil { err := s.deviceSink.Err() s.Destroy() if err != nil { - return nil, fmt.Errorf("%w: %v", ErrOutputDevice, err) + return nil, fmt.Errorf("%w: creating context for playback device %q: %v", ErrOutputDevice, deviceName(outName), err) } - return nil, ErrOutputDevice + return nil, fmt.Errorf("%w: could not create context for playback device %q", ErrOutputDevice, deviceName(outName)) } // OpenAL contexts are current to an OS thread. Move ownership to one // dedicated render thread before any source or buffer is created. @@ -360,11 +364,11 @@ func (s *Stream) StartSource(inputDevice *string) error { return ErrState } if s.deviceSource == nil { - return ErrMic + return fmt.Errorf("%w: capture device %q is unavailable", ErrMic, deviceName(s.inputDeviceName)) } s.deviceSource.CaptureStart() if err := s.deviceSource.Err(); err != nil { - return fmt.Errorf("%w: %v", ErrMic, err) + return fmt.Errorf("%w: starting capture device %q: %v", ErrMic, deviceName(s.inputDeviceName), err) } stop := make(chan bool) done := make(chan struct{}) @@ -386,11 +390,11 @@ func (s *Stream) StopSource() error { // The routine owns capture access; wait for it before closing/reusing it. <-done if s.deviceSource == nil { - return ErrMic + return fmt.Errorf("%w: capture device %q is unavailable", ErrMic, deviceName(s.inputDeviceName)) } s.deviceSource.CaptureStop() if err := s.deviceSource.Err(); err != nil { - return fmt.Errorf("%w: %v", ErrMic, err) + return fmt.Errorf("%w: stopping capture device %q: %v", ErrMic, deviceName(s.inputDeviceName), err) } return nil } diff --git a/gumble/gumbleopenal/stream_regression_test.go b/gumble/gumbleopenal/stream_regression_test.go index 996f930..9832178 100644 --- a/gumble/gumbleopenal/stream_regression_test.go +++ b/gumble/gumbleopenal/stream_regression_test.go @@ -25,6 +25,15 @@ func TestDeviceOpenErrorsIncludeConfiguredDevice(t *testing.T) { } } +// Regression: later capture start failures also omitted the configured device. +func TestStartSourceUnavailableDeviceIncludesName(t *testing.T) { + s := &Stream{inputDeviceName: "virtual_mic.monitor"} + err := s.StartSource(nil) + if !errors.Is(err, ErrMic) || !strings.Contains(err.Error(), "virtual_mic.monitor") { + t.Fatalf("start error %q", err) + } +} + func TestStopSourceWaitsForWorker(t *testing.T) { stop, done := make(chan bool), make(chan struct{}) s := &Stream{sourceStop: stop, sourceDone: done}