Files

297 lines
8.8 KiB
Go

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)
}