Add --tone-test mode: 440 Hz Opus tone + incoming audio capture
Adds a --tone-test flag that bypasses all OpenAL/soundcard initialization and instead: - Generates a 440 Hz sine wave at 48kHz mono, encodes it via Opus, and sends it to the Mumble server. - Saves all incoming decoded audio to a raw PCM file (s16le, stereo, 48kHz) specified by --tone-out (default: incoming.pcm). Useful for end-to-end testing of the Opus encode/decode pipeline and Mumble transport without requiring physical audio hardware. Usage: barnard --server HOST --tone-test --tone-out /tmp/in.pcm ffplay -f s16le -ar 48000 -ac 2 incoming.pcm
This commit is contained in:
committed by
Brandon McGinty
parent
b2a1d2f846
commit
29be821155
+14
@@ -63,6 +63,12 @@ type Barnard struct {
|
||||
FileStream *fileplayback.Player
|
||||
FileStreamMutex sync.Mutex
|
||||
|
||||
// Added for tone test mode (bypasses all soundcard/OpenAL)
|
||||
ToneTest bool
|
||||
ToneTestOutput string
|
||||
toneTestStop chan struct{}
|
||||
toneTestSaver *AudioFileSaver
|
||||
|
||||
// Added for recording
|
||||
RecordingMutex sync.Mutex
|
||||
Recorder *recording.Recorder
|
||||
@@ -80,8 +86,16 @@ func (b *Barnard) StopTransmission() {
|
||||
b.Notify("micdown", "me", "")
|
||||
b.Tx = false
|
||||
b.UpdateGeneralStatus(" Idle ", false)
|
||||
if b.ToneTest {
|
||||
// Stop the tone generator.
|
||||
if b.toneTestStop != nil {
|
||||
close(b.toneTestStop)
|
||||
b.toneTestStop = nil
|
||||
}
|
||||
} else if b.Stream != nil {
|
||||
b.Stream.StopSource()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Barnard) TreeItemCharacter(ui *uiterm.Ui, tree *uiterm.Tree, item uiterm.TreeItem, ch rune) {
|
||||
|
||||
@@ -17,12 +17,19 @@ func (b *Barnard) start() {
|
||||
b.Config.Attach(gumbleutil.AutoBitrate)
|
||||
b.Config.Attach(b)
|
||||
b.Config.Address = b.Address
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -45,6 +52,27 @@ 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.
|
||||
|
||||
// Start the tone generator goroutine.
|
||||
b.toneTestStop = make(chan struct{})
|
||||
go StartToneGenerator(b.Client, b.toneTestStop)
|
||||
|
||||
// Attach the audio file saver.
|
||||
saver, err := NewAudioFileSaver(b.ToneTestOutput)
|
||||
if err != nil {
|
||||
b.exitWithError(err)
|
||||
return false
|
||||
}
|
||||
b.toneTestSaver = saver
|
||||
b.Client.Config.AttachAudio(saver)
|
||||
|
||||
b.Connected = true
|
||||
return true
|
||||
}
|
||||
|
||||
stream, err := gumbleopenal.New(b.Client, b.UserConfig.GetInputDevice(), b.UserConfig.GetOutputDevice(), false)
|
||||
if err != nil {
|
||||
b.exitWithError(err)
|
||||
@@ -138,6 +166,19 @@ 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
|
||||
}
|
||||
if b.toneTestSaver != nil {
|
||||
b.toneTestSaver.Stop()
|
||||
b.toneTestSaver = nil
|
||||
}
|
||||
}
|
||||
|
||||
b.Notify("disconnect", "me", reason)
|
||||
if reason == "" {
|
||||
b.AddOutputLine("Disconnected")
|
||||
|
||||
@@ -117,6 +117,8 @@ func main() {
|
||||
profile := flag.Bool("profile", false, "add http server to serve profiles")
|
||||
noiseSuppressionEnabled := flag.Bool("noise-suppression", false, "enable noise suppression for microphone input")
|
||||
autoTransmit := flag.Bool("auto-transmit", false, "start transmitting immediately on connect")
|
||||
toneTest := flag.Bool("tone-test", false, "send a 440 Hz test tone instead of microphone (bypasses soundcard)")
|
||||
toneTestOutput := flag.String("tone-out", "incoming.pcm", "file to save incoming audio to in tone-test mode")
|
||||
logLevel := flag.String("log", "warn", "log level: debug, info, warn, error")
|
||||
logFile := flag.String("logfile", "", "write logs to this file (in addition to stderr)")
|
||||
|
||||
@@ -210,6 +212,8 @@ func main() {
|
||||
UserConfig: userConfig,
|
||||
Address: *server,
|
||||
AutoTransmit: *autoTransmit,
|
||||
ToneTest: *toneTest,
|
||||
ToneTestOutput: *toneTestOutput,
|
||||
MutedChannels: make(map[uint32]bool),
|
||||
NoiseSuppressor: noise.NewSuppressor(),
|
||||
}
|
||||
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.stormux.org/storm/barnard/gumble/gumble"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 440 Hz tone generator
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// StartToneGenerator begins generating a 440 Hz sine wave and writing it to
|
||||
// the client's outgoing audio channel at the configured interval. It blocks
|
||||
// until the stop channel is closed.
|
||||
func StartToneGenerator(client *gumble.Client, stop <-chan struct{}) {
|
||||
interval := client.Config.AudioInterval
|
||||
frameSize := client.Config.AudioFrameSize() // mono samples per frame
|
||||
sampleRate := float64(gumble.AudioSampleRate)
|
||||
frequency := 440.0
|
||||
|
||||
// Pre-compute one full sine wave cycle so we can just index into it.
|
||||
// This avoids calling math.Sin in the hot loop.
|
||||
phase := 0.0
|
||||
phaseIncrement := 2.0 * math.Pi * frequency / sampleRate
|
||||
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
outgoing := client.AudioOutgoing()
|
||||
defer close(outgoing)
|
||||
|
||||
fmt.Fprintf(os.Stderr, "tonetest: starting 440 Hz tone generator (frameSize=%d, interval=%v)\n",
|
||||
frameSize, interval)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
fmt.Fprintf(os.Stderr, "tonetest: tone generator stopped\n")
|
||||
return
|
||||
case <-ticker.C:
|
||||
buf := make([]int16, frameSize)
|
||||
for i := 0; i < frameSize; i++ {
|
||||
// Generate sine wave with amplitude 0.5 to avoid clipping
|
||||
buf[i] = int16(math.Sin(phase) * 16000) // ~ -6dBFS
|
||||
phase += phaseIncrement
|
||||
if phase > 2.0*math.Pi {
|
||||
phase -= 2.0 * math.Pi
|
||||
}
|
||||
}
|
||||
outgoing <- gumble.AudioBuffer(buf)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Incoming audio file saver
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// NewAudioFileSaver creates the output file and returns a configured saver.
|
||||
// The file is raw PCM: s16le, stereo, 48000 Hz.
|
||||
// Play it back with:
|
||||
//
|
||||
// ffplay -f s16le -ar 48000 -ac 2 <file>
|
||||
//
|
||||
// or convert with:
|
||||
//
|
||||
// ffmpeg -f s16le -ar 48000 -ac 2 -i <file> output.wav
|
||||
func NewAudioFileSaver(path string) (*AudioFileSaver, error) {
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "tonetest: saving incoming audio to %s\n", path)
|
||||
return &AudioFileSaver{
|
||||
file: f,
|
||||
stop: make(chan struct{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Stop closes the stop channel, waits for all stream goroutines to finish,
|
||||
// and closes the output file.
|
||||
func (s *AudioFileSaver) Stop() {
|
||||
close(s.stop)
|
||||
s.wg.Wait()
|
||||
s.file.Close()
|
||||
}
|
||||
|
||||
// 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.wg.Add(1)
|
||||
go func() {
|
||||
defer s.wg.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-s.stop:
|
||||
return
|
||||
case packet, ok := <-e.C:
|
||||
if !ok {
|
||||
fmt.Fprintf(os.Stderr, "tonetest: audio stream from %s ended\n", e.User.Name)
|
||||
return
|
||||
}
|
||||
samples := packet.AudioBuffer
|
||||
if len(samples) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Write as raw PCM s16le. The decoder outputs stereo
|
||||
// interleaved, so the sample count already accounts for
|
||||
// both channels.
|
||||
buf := make([]byte, len(samples)*2)
|
||||
for i, s := range samples {
|
||||
binary.LittleEndian.PutUint16(buf[i*2:], uint16(s))
|
||||
}
|
||||
s.mu.Lock()
|
||||
if _, err := s.file.Write(buf); err != nil {
|
||||
s.mu.Unlock()
|
||||
fmt.Fprintf(os.Stderr, "tonetest: write error: %v\n", err)
|
||||
return
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
Reference in New Issue
Block a user