From e41d2fd8cb899d37f792540092b2b25630be413e Mon Sep 17 00:00:00 2001 From: Brandon McGinty Date: Thu, 20 Aug 2026 14:25:35 -0400 Subject: [PATCH] validate audio configuration before connecting Reject unsupported audio intervals and non-positive buffer sizes. An interval like 15ms was truncated to 10ms frames while the send ticker kept the original duration, so packets were produced at a rate the frame size did not match. Checking at dial time gives a clear error instead of malformed audio. Require all three coordinates for positional audio. Only X was checked, so supplying X without Y or Z wrote a header that claimed positional data and then read past the values that were actually provided. Co-Authored-By: Claude Opus 5 --- gumble/gumble/client.go | 3 +++ gumble/gumble/config.go | 17 +++++++++++++++++ gumble/gumble/config_regression_test.go | 20 ++++++++++++++++++++ gumble/gumble/conn.go | 5 ++++- 4 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 gumble/gumble/config_regression_test.go diff --git a/gumble/gumble/client.go b/gumble/gumble/client.go index 85b17c0..a80507f 100644 --- a/gumble/gumble/client.go +++ b/gumble/gumble/client.go @@ -125,6 +125,9 @@ func tlsServerName(address string) (string, error) { // min(time.Now() + dialer.Timeout, dialer.Deadline), or if the server rejects // the client. func DialWithDialer(dialer *net.Dialer, config *Config, tlsConfig *tls.Config) (*Client, error) { + if err := config.Validate(); err != nil { + return nil, err + } start := time.Now() rawConn, err := dialer.Dial("tcp", config.Address) diff --git a/gumble/gumble/config.go b/gumble/gumble/config.go index f0add06..0400bca 100644 --- a/gumble/gumble/config.go +++ b/gumble/gumble/config.go @@ -1,6 +1,7 @@ package gumble import ( + "fmt" "time" ) @@ -43,6 +44,22 @@ func NewConfig() *Config { } } +// Validate checks values that are used by the audio ticker and encoder. +func (c *Config) Validate() error { + switch c.AudioInterval { + case 10 * time.Millisecond, 20 * time.Millisecond, 40 * time.Millisecond, 60 * time.Millisecond: + default: + return fmt.Errorf("gumble: AudioInterval must be 10ms, 20ms, 40ms, or 60ms") + } + if c.AudioDataBytes <= 0 { + return fmt.Errorf("gumble: AudioDataBytes must be positive") + } + if c.Buffers <= 0 { + return fmt.Errorf("gumble: Buffers must be positive") + } + return nil +} + // Attach is an alias of c.Listeners.Attach. func (c *Config) Attach(l EventListener) Detacher { return c.Listeners.Attach(l) diff --git a/gumble/gumble/config_regression_test.go b/gumble/gumble/config_regression_test.go new file mode 100644 index 0000000..70d78ca --- /dev/null +++ b/gumble/gumble/config_regression_test.go @@ -0,0 +1,20 @@ +package gumble + +import ( + "testing" + "time" +) + +// Regression: arbitrary intervals were truncated to 10 ms frames while the +// ticker kept the original duration, producing malformed audio timing. +func TestConfigValidateRejectsUnsupportedAudioInterval(t *testing.T) { + config := NewConfig() + config.AudioInterval = 15 * time.Millisecond + if err := config.Validate(); err == nil { + t.Fatal("invalid audio interval was accepted") + } + config.AudioInterval = 60 * time.Millisecond + if err := config.Validate(); err != nil { + t.Fatalf("valid audio interval rejected: %v", err) + } +} diff --git a/gumble/gumble/conn.go b/gumble/gumble/conn.go index 61572f4..ed58cfa 100644 --- a/gumble/gumble/conn.go +++ b/gumble/gumble/conn.go @@ -80,7 +80,10 @@ func (c *Conn) WriteAudio(format, target byte, sequence int64, final bool, data header := buff[:1+n+m] var positionalLength int - if X != nil { + if X != nil || Y != nil || Z != nil { + if X == nil || Y == nil || Z == nil { + return errors.New("gumble: positional audio requires X, Y, and Z") + } positionalLength = 3 * 4 }