add a tone test mode that bypasses the soundcard

Add -tone-test to transmit a generated 440 Hz Opus tone and save
incoming audio to the file named by -tone-out.
Diagnosing an audio problem previously required a working capture and
playback device, which is exactly what is in question. This mode opens
no OpenAL device at all, so the network and codec path can be tested
on a machine with no sound hardware.

Open the capture file before starting the generator.
Starting transmission first left a tone goroutine running with nowhere
to write when the path was unusable.

Refuse file playback while in tone test mode, and ignore the
microphone volume keys.
Both operate on a stream that does not exist here.

Stop the generator and detach the file saver on disconnect.
A reconnect otherwise attached a second saver to the same file.

Add -auto-transmit to key the microphone as soon as the connection is
up.
Useful for a bot or a monitoring client that should never need a
keypress. It runs from both the connect event and the point where the
audio stream is created, because the server's welcome arrives before
the stream exists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Brandon McGinty
2026-08-20 14:42:52 -04:00
co-authored by Claude Opus 5
parent 17a4662c6c
commit ecca0a63ed
6 changed files with 354 additions and 14 deletions
+69 -5
View File
@@ -17,16 +17,27 @@ func (b *Barnard) start() {
b.Config.Attach(gumbleutil.AutoBitrate)
b.Config.Attach(b)
b.Config.Address = b.Address
// test Audio
_, err := gumbleopenal.New(b.Client, b.UserConfig.GetInputDevice(), b.UserConfig.GetOutputDevice(), true)
if err != nil {
b.exitWithError(err)
return
if b.ToneTest {
// Tone test mode: skip all OpenAL/soundcard initialization.
// We just need a network connection — the tone generator and
// file saver are set up in connect().
} else {
// test Audio
_, err := gumbleopenal.New(b.Client, b.UserConfig.GetInputDevice(), b.UserConfig.GetOutputDevice(), true)
if err != nil {
b.exitWithError(err)
return
}
}
//connect, not reconnect
b.connect(false)
}
func (b *Barnard) toneTestAutoTransmit() bool {
return b.ToneTest && b.AutoTransmit
}
func (b *Barnard) exitWithError(err error) {
b.Ui.Close()
b.exitStatus = 1
@@ -45,6 +56,31 @@ func (b *Barnard) connect(reconnect bool) bool {
return false
}
if b.ToneTest {
// --- Tone test mode: skip all OpenAL; generate 440 Hz tone
// --- and save incoming audio to a file.
// Open the output first. Starting transmission before this succeeds
// leaves an orphaned tone goroutine when the path is unusable.
saver, err := NewAudioFileSaver(b.ToneTestOutput)
if err != nil {
b.exitWithError(err)
return false
}
b.toneTestSaver = saver
b.toneTestSaverDetach = b.Client.Config.AttachAudio(saver)
b.Connected = true
if b.toneTestAutoTransmit() {
b.toneTestStop = make(chan struct{})
go StartToneGenerator(b.Client, b.toneTestStop)
b.Tx = true
b.UpdateGeneralStatus(" Tx ", true)
b.AddOutputLine("Tone test transmission started")
}
return true
}
stream, err := gumbleopenal.New(b.Client, b.UserConfig.GetInputDevice(), b.UserConfig.GetOutputDevice(), false)
if err != nil {
b.exitWithError(err)
@@ -71,6 +107,9 @@ func (b *Barnard) connect(reconnect bool) bool {
b.FileStreamMutex.Unlock()
b.Connected = true
// Dial delivers OnConnect before connect creates the OpenAL stream, so
// start auto-transmit here as well for initial connections and reconnects.
b.startAutoTransmit()
return true
}
@@ -105,6 +144,21 @@ func (b *Barnard) OnConnect(e *gumble.ConnectEvent) {
b.AddOutputLine(fmt.Sprintf("Welcome message: %s", wmsg))
}
b.Ui.Refresh()
b.startAutoTransmit()
}
func (b *Barnard) startAutoTransmit() {
if !b.AutoTransmit || b.Tx || b.Stream == nil {
return
}
if err := b.Stream.StartSource(b.UserConfig.GetInputDevice()); err != nil {
b.AddOutputLine(fmt.Sprintf("auto-transmit failed: %s", err.Error()))
return
}
b.Tx = true
b.UpdateGeneralStatus(" AutoTx ", true)
b.AddOutputLine("Auto-transmit started")
}
func (b *Barnard) OnDisconnect(e *gumble.DisconnectEvent) {
@@ -121,6 +175,16 @@ func (b *Barnard) OnDisconnect(e *gumble.DisconnectEvent) {
reason = e.String
}
b.stopRecordingForDisconnect()
// Tone test cleanup
if b.ToneTest {
if b.toneTestStop != nil {
close(b.toneTestStop)
b.toneTestStop = nil
}
b.cleanupToneTestAudio()
}
b.Notify("disconnect", "me", reason)
if reason == "" {
b.AddOutputLine("Disconnected")