Restore ability to transmit in stereo.

This commit is contained in:
Storm Dragon
2026-09-29 01:10:35 -04:00
parent c1722c2491
commit abf6729ce9
6 changed files with 55 additions and 27 deletions
+1 -1
View File
@@ -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. // the previous connection rather than allocating another.
if b.stereoEncoder == nil { if b.stereoEncoder == nil {
b.stereoEncoder = opus.NewStereoEncoder() b.stereoEncoder = opus.NewStereoEncoder()
+11 -4
View File
@@ -52,15 +52,22 @@ type AudioStreamEvent struct {
// AudioBuffer is a slice of PCM audio samples. // AudioBuffer is a slice of PCM audio samples.
type AudioBuffer []int16 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 { func (a AudioBuffer) writeAudio(client *Client, seq int64, final bool) error {
// Encoding shares mutable codec state with server-configuration and file // Encoding shares mutable codec state with server-configuration and file
// playback changes. Keep the client read lock through Encode and Reset so a // 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. // stereo encoder cannot be replaced or reset while it is in use.
client.volatile.RLock() client.volatile.RLock()
encoder := client.AudioEncoder encoder := client.encoderForAudioBuffer(a)
if client.useStereoEncoder && client.AudioEncoderStereo != nil {
encoder = client.AudioEncoderStereo
}
if encoder == nil { if encoder == nil {
client.volatile.RUnlock() client.volatile.RUnlock()
return nil return nil
+34
View File
@@ -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)
}
}
}
+3 -4
View File
@@ -399,7 +399,7 @@ func (c *Client) Send(message Message) {
message.writeMessage(c) 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) { func (c *Client) SetStereoEncoder(encoder AudioEncoder) {
c.volatile.Lock() c.volatile.Lock()
defer c.volatile.Unlock() defer c.volatile.Unlock()
@@ -458,9 +458,8 @@ func (c *Client) UDPActive() bool {
return c.udpActive return c.udpActive
} }
// DisableStereoEncoder switches back to mono encoding for voice and // DisableStereoEncoder clears file playback mode and resets the stereo
// resets the stereo encoder so stale state does not bleed into the // encoder. Stereo microphone frames still select it by channel count.
// next file playback.
func (c *Client) DisableStereoEncoder() { func (c *Client) DisableStereoEncoder() {
c.volatile.Lock() c.volatile.Lock()
defer c.volatile.Unlock() defer c.volatile.Unlock()
+3 -14
View File
@@ -1064,21 +1064,10 @@ func (s *Stream) sourceRoutine(inputDevice *string, stop chan bool, done chan st
recorder.RecordAudioFrame(recorderOutgoingSource, outputBuffer, true) recorder.RecordAudioFrame(recorderOutgoingSource, outputBuffer, true)
} }
} else if hasMicInput { } else if hasMicInput {
// Send mic when no file is playing. If the microphone is // Preserve the capture device's channel layout for encoding.
// stereo, downmix to mono since Mumble voice transmission outgoing <- gumble.AudioBuffer(int16Buffer)
// 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)
if recorder := s.getRecorder(); recorder != nil { if recorder := s.getRecorder(); recorder != nil {
recorder.RecordAudioFrame(recorderOutgoingSource, outBuf, false) recorder.RecordAudioFrame(recorderOutgoingSource, int16Buffer, s.sourceChannels == 2)
} }
} }
} }
+3 -4
View File
@@ -10,7 +10,7 @@ var Codec gumble.AudioCodec
const ( const (
ID = 4 ID = 4
VoiceChannels = 1 // Force mono for voice transmission VoiceChannels = 1 // Channel count for the mono encoder
) )
func init() { func init() {
@@ -27,7 +27,7 @@ func (*generator) ID() int {
} }
func (*generator) NewEncoder() gumble.AudioEncoder { 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) e, _ := opus.NewEncoder(gumble.AudioSampleRate, VoiceChannels, opus.AppVoIP)
return &Encoder{ return &Encoder{
Encoder: e, 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 { func NewStereoEncoder() gumble.AudioEncoder {
// Create stereo encoder for file playback
e, _ := opus.NewEncoder(gumble.AudioSampleRate, gumble.AudioChannels, opus.AppAudio) e, _ := opus.NewEncoder(gumble.AudioSampleRate, gumble.AudioChannels, opus.AppAudio)
return &Encoder{ return &Encoder{
Encoder: e, Encoder: e,