diff --git a/client.go b/client.go index b1ad5ec..c130c9c 100644 --- a/client.go +++ b/client.go @@ -108,7 +108,7 @@ func (b *Barnard) connect(reconnect bool) bool { } }) - // Initialize stereo encoder for file playback, reusing the one built for + // Initialize stereo encoder for two-channel capture and file playback, reusing the one built for // the previous connection rather than allocating another. if b.stereoEncoder == nil { b.stereoEncoder = opus.NewStereoEncoder() diff --git a/gumble/gumble/audio.go b/gumble/gumble/audio.go index b99ad67..0547f1b 100644 --- a/gumble/gumble/audio.go +++ b/gumble/gumble/audio.go @@ -52,15 +52,22 @@ type AudioStreamEvent struct { // AudioBuffer is a slice of PCM audio samples. type AudioBuffer []int16 +// encoderForAudioBuffer is called with volatile held. +func (client *Client) encoderForAudioBuffer(a AudioBuffer) AudioEncoder { + frameSize := client.Config.AudioFrameSize() + if client.AudioEncoderStereo != nil && (len(a) == frameSize*AudioChannels || + (client.useStereoEncoder && len(a) != frameSize)) { + return client.AudioEncoderStereo + } + return client.AudioEncoder +} + func (a AudioBuffer) writeAudio(client *Client, seq int64, final bool) error { // Encoding shares mutable codec state with server-configuration and file // playback changes. Keep the client read lock through Encode and Reset so a // stereo encoder cannot be replaced or reset while it is in use. client.volatile.RLock() - encoder := client.AudioEncoder - if client.useStereoEncoder && client.AudioEncoderStereo != nil { - encoder = client.AudioEncoderStereo - } + encoder := client.encoderForAudioBuffer(a) if encoder == nil { client.volatile.RUnlock() return nil diff --git a/gumble/gumble/audio_stereo_test.go b/gumble/gumble/audio_stereo_test.go new file mode 100644 index 0000000..fbe2e3f --- /dev/null +++ b/gumble/gumble/audio_stereo_test.go @@ -0,0 +1,34 @@ +package gumble + +import ( + "testing" + "time" +) + +type testAudioEncoder struct{ channels int } + +func (*testAudioEncoder) ID() int { return 4 } +func (*testAudioEncoder) Encode([]int16, int, int) ([]byte, error) { return nil, nil } +func (*testAudioEncoder) Reset() {} + +func TestEncoderForAudioBufferPreservesStereoMicrophone(t *testing.T) { + for _, interval := range []time.Duration{10 * time.Millisecond, 20 * time.Millisecond, 40 * time.Millisecond, 60 * time.Millisecond} { + config := NewConfig() + config.AudioInterval = interval + mono, stereo := &testAudioEncoder{channels: 1}, &testAudioEncoder{channels: 2} + client := &Client{Config: config, AudioEncoder: mono, AudioEncoderStereo: stereo} + frameSize := config.AudioFrameSize() + + if got := client.encoderForAudioBuffer(make(AudioBuffer, frameSize*AudioChannels)); got != stereo { + t.Errorf("%v: stereo microphone selected mono encoder", interval) + } + client.useStereoEncoder = true + if got := client.encoderForAudioBuffer(make(AudioBuffer, frameSize)); got != mono { + t.Errorf("%v: mono microphone selected stereo encoder during file playback", interval) + } + client.useStereoEncoder = false + if got := client.encoderForAudioBuffer(make(AudioBuffer, frameSize)); got != mono { + t.Errorf("%v: mono microphone selected stereo encoder", interval) + } + } +} diff --git a/gumble/gumble/client.go b/gumble/gumble/client.go index 6b7705f..f49b341 100644 --- a/gumble/gumble/client.go +++ b/gumble/gumble/client.go @@ -399,7 +399,7 @@ func (c *Client) Send(message Message) { message.writeMessage(c) } -// SetStereoEncoder installs the encoder used for stereo file playback. +// SetStereoEncoder installs the encoder used for stereo capture and file playback. func (c *Client) SetStereoEncoder(encoder AudioEncoder) { c.volatile.Lock() defer c.volatile.Unlock() @@ -458,9 +458,8 @@ func (c *Client) UDPActive() bool { return c.udpActive } -// DisableStereoEncoder switches back to mono encoding for voice and -// resets the stereo encoder so stale state does not bleed into the -// next file playback. +// DisableStereoEncoder clears file playback mode and resets the stereo +// encoder. Stereo microphone frames still select it by channel count. func (c *Client) DisableStereoEncoder() { c.volatile.Lock() defer c.volatile.Unlock() diff --git a/gumble/gumbleopenal/stream.go b/gumble/gumbleopenal/stream.go index b00ba24..f91f132 100644 --- a/gumble/gumbleopenal/stream.go +++ b/gumble/gumbleopenal/stream.go @@ -1064,21 +1064,10 @@ func (s *Stream) sourceRoutine(inputDevice *string, stop chan bool, done chan st recorder.RecordAudioFrame(recorderOutgoingSource, outputBuffer, true) } } else if hasMicInput { - // Send mic when no file is playing. If the microphone is - // stereo, downmix to mono since Mumble voice transmission - // uses mono Opus encoding. - outBuf := int16Buffer - if s.sourceChannels == 2 { - monoBuf := make([]int16, frameSize) - for i := 0; i < frameSize; i++ { - // Average left and right channels - monoBuf[i] = int16((int32(int16Buffer[i*2]) + int32(int16Buffer[i*2+1])) / 2) - } - outBuf = monoBuf - } - outgoing <- gumble.AudioBuffer(outBuf) + // Preserve the capture device's channel layout for encoding. + outgoing <- gumble.AudioBuffer(int16Buffer) if recorder := s.getRecorder(); recorder != nil { - recorder.RecordAudioFrame(recorderOutgoingSource, outBuf, false) + recorder.RecordAudioFrame(recorderOutgoingSource, int16Buffer, s.sourceChannels == 2) } } } diff --git a/gumble/opus/opus.go b/gumble/opus/opus.go index 0eab4df..8a1fe27 100644 --- a/gumble/opus/opus.go +++ b/gumble/opus/opus.go @@ -10,7 +10,7 @@ var Codec gumble.AudioCodec const ( ID = 4 - VoiceChannels = 1 // Force mono for voice transmission + VoiceChannels = 1 // Channel count for the mono encoder ) func init() { @@ -27,7 +27,7 @@ func (*generator) ID() int { } func (*generator) NewEncoder() gumble.AudioEncoder { - // Force mono for voice transmission + // Create the mono encoder for single-channel capture. e, _ := opus.NewEncoder(gumble.AudioSampleRate, VoiceChannels, opus.AppVoIP) return &Encoder{ Encoder: e, @@ -35,9 +35,8 @@ func (*generator) NewEncoder() gumble.AudioEncoder { } } -// NewStereoEncoder creates a stereo encoder for file playback +// NewStereoEncoder creates a stereo encoder for capture and file playback. func NewStereoEncoder() gumble.AudioEncoder { - // Create stereo encoder for file playback e, _ := opus.NewEncoder(gumble.AudioSampleRate, gumble.AudioChannels, opus.AppAudio) return &Encoder{ Encoder: e,