package main import _ "net/http/pprof" import ( "al.essio.dev/pkg/shellescape" "crypto/tls" "flag" "fmt" "log" "net" "net/http" "os" "strings" "time" barnlog "git.stormux.org/storm/barnard/log" "git.stormux.org/storm/barnard/config" "git.stormux.org/storm/barnard/gumble/go-openal/openal" "git.stormux.org/storm/barnard/gumble/gumble" _ "git.stormux.org/storm/barnard/gumble/opus" "git.stormux.org/storm/barnard/noise" "git.stormux.org/storm/barnard/uiterm" ) func show_devs(name string, args []string) { if args == nil { fmt.Printf("no items for %s\n", name) } fmt.Printf("%s\n", name) for i := 0; i < len(args); i++ { fmt.Printf("%s\n", args[i]) } } func do_list_devices() { odevs := openal.GetStrings(openal.AllDevicesSpecifier) if odevs != nil && len(odevs) > 0 { show_devs("All outputs:", odevs) } else { odevs = openal.GetStrings(openal.DeviceSpecifier) show_devs("All outputs:", odevs) } idevs := openal.GetStrings(openal.CaptureDeviceSpecifier) show_devs("Inputs:", idevs) } const notificationQueueSize = 32 func setup_notify_runner(notifyCommand string) chan []string { events := make(chan []string, notificationQueueSize) go func() { for event := range events { if notifyCommand != "" { runNotification(expandNotification(notifyCommand, event)) } } }() return events } // expandNotification replaces placeholders in one pass so text supplied for // one field cannot cause another placeholder to be expanded recursively. func expandNotification(template string, event []string) string { return strings.NewReplacer( "%event", shellescape.Quote(event[0]), "%who", shellescape.Quote(event[1]), "%what", shellescape.Quote(event[2]), ).Replace(template) } func main() { // Command line flags server := flag.String("server", "localhost:64738", "the server to connect to") username := flag.String("username", "", "the username of the client") password := flag.String("password", "", "the password of the server") insecure := flag.Bool("insecure", false, "skip server certificate verification") certificate := flag.String("certificate", "", "PEM encoded certificate and private key") cfgfn := flag.String("config", "~/.barnard.toml", "Path to TOML formatted configuration file") audioDriver := flag.String("audio-driver", "", "preferred OpenAL backend (pipewire, pulse, alsa, jack)") list_devices := flag.Bool("list_devices", false, "do not connect; instead, list available audio devices and exit") fifo := flag.String("fifo", "", "path of a FIFO from which to read commands") serverSet := false usernameSet := false configSet := false certificateSet := false buffers := flag.Int("buffers", 16, "number of audio buffers to use") audioInterval := flag.Int("audio-interval", 10, "outgoing audio packet duration in ms (10, 20, 40, or 60)") jitterBuffer := flag.Int("jitter-buffer", 40, "incoming per-user audio buffer in ms (0, 20, 40, or 60)") 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") tcpOnly := flag.Bool("tcp", false, "disable UDP, force audio through TCP tunnel") logLevel := flag.String("log", "warn", "log level: debug, info, warn, error") logFile := flag.String("logfile", "", "write logs to this file (logging is disabled when omitted)") flag.Parse() selectedAudioInterval, err := audioIntervalDuration(*audioInterval) if err != nil { handle_raw_error(err) } selectedJitterBuffer, err := jitterBufferDuration(*jitterBuffer) if err != nil { handle_raw_error(err) } // Set up logging var level barnlog.Level switch strings.ToLower(*logLevel) { case "debug": level = barnlog.LevelDebug case "info": level = barnlog.LevelInfo case "warn": level = barnlog.LevelWarn case "error": level = barnlog.LevelError default: level = barnlog.LevelWarn } // Logging is opt-in. Select /dev/stderr explicitly when terminal logging is // desired; otherwise library diagnostics must not corrupt terminal output. barnlog.SetLogger(nil) if *logFile != "" { f, err := os.OpenFile(*logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) if err != nil { fmt.Fprintf(os.Stderr, "cannot open log file %s: %v\n", *logFile, err) } else { barnlog.SetLogger(barnlog.NewWriterLogger(f, level)) } } if *profile == true { go func() { log.Println(http.ListenAndServe("localhost:6060", nil)) }() } flag.CommandLine.Visit(func(theFlag *flag.Flag) { switch theFlag.Name { case "server": serverSet = true case "username": usernameSet = true case "config": configSet = true case "certificate": certificateSet = true } }) if configSet { if err := config.RequireConfigFile(*cfgfn); err != nil { handle_raw_error(err) } } userConfig := config.NewConfig(cfgfn) if !serverSet { server = userConfig.GetDefaultServer() } if !usernameSet { username = userConfig.GetUsername() } if !certificateSet { certificate = userConfig.GetCertificate() } driver := strings.TrimSpace(*audioDriver) if driver == "" { // Environment variable takes precedence over config if envDriver := os.Getenv("ALSOFT_DRIVERS"); envDriver != "" { driver = envDriver } else { driver = strings.TrimSpace(userConfig.GetAudioDriver()) } } if driver != "" { os.Setenv("ALSOFT_DRIVERS", driver) } if os.Getenv("ALSOFT_LOGLEVEL") == "" { os.Setenv("ALSOFT_LOGLEVEL", "0") } if *list_devices { do_list_devices() os.Exit(0) } *server = serverAddress(*server) // Initialize b := Barnard{ Config: gumble.NewConfig(), UserConfig: userConfig, Address: *server, AutoTransmit: *autoTransmit, ToneTest: *toneTest, ToneTestOutput: *toneTestOutput, MutedChannels: make(map[uint32]bool), NoiseSuppressor: noise.NewSuppressor(), } b.Config.Buffers = *buffers b.Config.AudioInterval = selectedAudioInterval b.Config.IncomingAudioBuffer = selectedJitterBuffer b.Config.DisableUDP = *tcpOnly b.Hotkeys = b.UserConfig.GetHotkeys() if err := b.UserConfig.SaveConfig(); err != nil { fmt.Fprintf(os.Stderr, "could not save configuration: %s\n", err) os.Exit(1) } // Configure noise suppression enabled := b.UserConfig.GetNoiseSuppressionEnabled() if *noiseSuppressionEnabled { enabled = true if err := b.UserConfig.SetNoiseSuppressionEnabled(true); err != nil { fmt.Fprintf(os.Stderr, "could not save configuration: %s\n", err) os.Exit(1) } } b.NoiseSuppressor.SetEnabled(enabled) b.Config.Username = *username b.Config.Password = *password if *insecure { b.TLSConfig.InsecureSkipVerify = true } if *certificate != "" { cert, err := tls.LoadX509KeyPair(*certificate, *certificate) if err != nil { fmt.Fprintf(os.Stderr, "%s\n", err) os.Exit(1) } b.TLSConfig.Certificates = append(b.TLSConfig.Certificates, cert) } reader, err := setup_fifo(*fifo) if err != nil { b.exitMessage = err.Error() b.exitStatus = 1 handle_error(&b) } b.notifyChannel = setup_notify_runner(*b.UserConfig.GetNotifyCommand()) b.Ui = uiterm.New(&b) b.Ui.Run(reader) handle_error(&b) } // audioIntervalDuration converts the packet duration requested at startup to // one of the Opus durations supported by Mumble. func audioIntervalDuration(milliseconds int) (time.Duration, error) { interval := time.Duration(milliseconds) * time.Millisecond switch interval { case 10 * time.Millisecond, 20 * time.Millisecond, 40 * time.Millisecond, 60 * time.Millisecond: return interval, nil default: return 0, fmt.Errorf("audio interval must be 10, 20, 40, or 60 ms, got %d", milliseconds) } } // jitterBufferDuration converts the requested incoming playout delay to a // supported duration. Zero starts playback without an initial safety buffer. func jitterBufferDuration(milliseconds int) (time.Duration, error) { interval := time.Duration(milliseconds) * time.Millisecond switch interval { case 0, 20 * time.Millisecond, 40 * time.Millisecond, 60 * time.Millisecond: return interval, nil default: return 0, fmt.Errorf("jitter buffer must be 0, 20, 40, or 60 ms, got %d", milliseconds) } } // serverAddress adds Mumble's default port without corrupting an IPv6 literal. func serverAddress(address string) string { if _, port, err := net.SplitHostPort(address); err == nil && port != "" { return address } return net.JoinHostPort(strings.Trim(address, "[]"), "64738") } func handle_raw_error(e error) { fmt.Fprintf(os.Stderr, "%s\n", e.Error()) os.Exit(1) } func handle_error(b *Barnard) { if b.exitMessage != "" { fmt.Fprintf(os.Stderr, "%s\n", b.exitMessage) } os.Exit(b.exitStatus) }