From bad4e8172a9c35607e8c2e63ddf71f580b28c5b4 Mon Sep 17 00:00:00 2001 From: "Brandon McGinty (chatgpt)" Date: Sun, 9 Aug 2026 23:12:56 -0400 Subject: [PATCH] Detach tone-test savers on reconnect --- barnard.go | 20 ++++++++++++++++---- client.go | 7 ++----- tonetest.go | 30 ++++++++++++++++++++++++------ tonetest_test.go | 24 ++++++++++++++++++++++++ 4 files changed, 66 insertions(+), 15 deletions(-) diff --git a/barnard.go b/barnard.go index d2df2f8..382b0aa 100644 --- a/barnard.go +++ b/barnard.go @@ -64,10 +64,11 @@ type Barnard struct { FileStreamMutex sync.Mutex // Added for tone test mode (bypasses all soundcard/OpenAL) - ToneTest bool - ToneTestOutput string - toneTestStop chan struct{} - toneTestSaver *AudioFileSaver + ToneTest bool + ToneTestOutput string + toneTestStop chan struct{} + toneTestSaver *AudioFileSaver + toneTestSaverDetach gumble.Detacher // Added for recording RecordingMutex sync.Mutex @@ -98,6 +99,17 @@ func (b *Barnard) cleanupConnectionAudio() { b.FileStreamMutex.Unlock() } +func (b *Barnard) cleanupToneTestAudio() { + if b.toneTestSaverDetach != nil { + b.toneTestSaverDetach.Detach() + b.toneTestSaverDetach = nil + } + if b.toneTestSaver != nil { + b.toneTestSaver.Stop() + b.toneTestSaver = nil + } +} + func (b *Barnard) StopTransmission() { if b.Tx { b.Notify("micdown", "me", "") diff --git a/client.go b/client.go index c0f73bb..52e1cf9 100644 --- a/client.go +++ b/client.go @@ -68,7 +68,7 @@ func (b *Barnard) connect(reconnect bool) bool { return false } b.toneTestSaver = saver - b.Client.Config.AttachAudio(saver) + b.toneTestSaverDetach = b.Client.Config.AttachAudio(saver) b.Connected = true if b.toneTestAutoTransmit() { @@ -191,10 +191,7 @@ func (b *Barnard) OnDisconnect(e *gumble.DisconnectEvent) { close(b.toneTestStop) b.toneTestStop = nil } - if b.toneTestSaver != nil { - b.toneTestSaver.Stop() - b.toneTestSaver = nil - } + b.cleanupToneTestAudio() } b.Notify("disconnect", "me", reason) diff --git a/tonetest.go b/tonetest.go index 6892c3a..53c8f5b 100644 --- a/tonetest.go +++ b/tonetest.go @@ -65,10 +65,11 @@ func StartToneGenerator(client *gumble.Client, stop <-chan struct{}) { // AudioFileSaver implements gumble.AudioListener and writes all incoming PCM // audio to a single raw 16-bit little-endian stereo 48kHz file. type AudioFileSaver struct { - file *os.File - stop chan struct{} - mu sync.Mutex - wg sync.WaitGroup + file *os.File + stop chan struct{} + mu sync.Mutex + stopped bool + wg sync.WaitGroup } // NewAudioFileSaver creates the output file and returns a configured saver. @@ -95,15 +96,32 @@ func NewAudioFileSaver(path string) (*AudioFileSaver, error) { // Stop closes the stop channel, waits for all stream goroutines to finish, // and closes the output file. func (s *AudioFileSaver) Stop() { + s.mu.Lock() + if s.stopped { + s.mu.Unlock() + return + } + s.stopped = true close(s.stop) + s.mu.Unlock() + s.wg.Wait() - s.file.Close() + s.mu.Lock() + _ = s.file.Close() + s.mu.Unlock() } // OnAudioStream implements gumble.AudioListener. func (s *AudioFileSaver) OnAudioStream(e *gumble.AudioStreamEvent) { - fmt.Fprintf(os.Stderr, "tonetest: incoming audio stream from %s\n", e.User.Name) + s.mu.Lock() + if s.stopped { + s.mu.Unlock() + return + } s.wg.Add(1) + s.mu.Unlock() + + fmt.Fprintf(os.Stderr, "tonetest: incoming audio stream from %s\n", e.User.Name) go func() { defer s.wg.Done() diff --git a/tonetest_test.go b/tonetest_test.go index 7e5fcd2..d791b13 100644 --- a/tonetest_test.go +++ b/tonetest_test.go @@ -24,3 +24,27 @@ func TestNewAudioFileSaverReportsUnavailableOutputPath(t *testing.T) { t.Fatalf("got saver=%v err=%v", saver, err) } } + +type testDetacher struct{ detached bool } + +func (d *testDetacher) Detach() { d.detached = true } + +// Regression: reconnecting tone-test mode kept prior savers attached to the +// shared audio listener list, causing callbacks to write to closed files. +func TestCleanupToneTestAudioDetachesSaver(t *testing.T) { + saver, err := NewAudioFileSaver(filepath.Join(t.TempDir(), "tone.pcm")) + if err != nil { + t.Fatal(err) + } + detacher := &testDetacher{} + b := &Barnard{toneTestSaver: saver, toneTestSaverDetach: detacher} + + b.cleanupToneTestAudio() + if !detacher.detached { + t.Fatal("tone saver listener was not detached") + } + if b.toneTestSaver != nil || b.toneTestSaverDetach != nil { + t.Fatal("tone saver cleanup retained connection state") + } + b.cleanupToneTestAudio() +}