297 lines
8.8 KiB
Go
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)
|
|
}
|