package main import ( "fmt" "image" "io/ioutil" "log" "os" "regexp" "strconv" "strings" "time" "github.com/BurntSushi/xgbutil" "github.com/BurntSushi/xgbutil/ewmh" "github.com/BurntSushi/xgbutil/icccm" "github.com/aarzilli/nucular/font" "github.com/lawl/pulseaudio" _ "embed" "github.com/aarzilli/nucular" "github.com/aarzilli/nucular/style" ) //go:generate go run scripts/embedlicenses.go //go:embed c/ladspa/rnnoise_ladspa.so var libRNNoise []byte //go:embed assets/patreon.png var patreonPNG []byte type device struct { ID string Name string isMonitor bool checked bool dynamicLatency bool rate uint32 } const appName = "NoiseTorch" var version = "unknown" // will be changed by build var distribution = "custom" // ditto var updateURL = "" // ditto var publicKeyString = "" // ditto func main() { opt := parseCLIOpts() if opt.doLog { log.SetOutput(os.Stdout) } else { log.SetOutput(ioutil.Discard) } log.Printf("Application starting. Version: %s (%s)\n", version, distribution) log.Printf("CAP_SYS_RESOURCE: %t\n", hasCapSysResource(getCurrentCaps())) initializeConfigIfNot() rnnoisefile := dumpLib() defer removeLib(rnnoisefile) ctx := ntcontext{} ctx.config = readConfig() ctx.librnnoise = rnnoisefile doCLI(opt, ctx.config, ctx.librnnoise) if ctx.config.EnableUpdates { go updateCheck(&ctx) } ctx.haveCapabilities = hasCapSysResource(getCurrentCaps()) ctx.capsMismatch = hasCapSysResource(getCurrentCaps()) != hasCapSysResource(getSelfFileCaps()) resetUI(&ctx) wnd := nucular.NewMasterWindowSize(0, appName, image.Point{600, 400}, func(w *nucular.Window) { updatefn(&ctx, w) }) ctx.masterWindow = &wnd (*ctx.masterWindow).Changed() go paConnectionWatchdog(&ctx) style := style.FromTheme(style.DarkTheme, 2.0) style.Font = font.DefaultFont(16, 1) wnd.SetStyle(style) //this is a disgusting hack that searches for the noisetorch window //and then fixes up the WM_CLASS attribute so it displays //properly in the taskbar go fixWindowClass() wnd.Main() } func dumpLib() string { f, err := ioutil.TempFile("", "librnnoise-*.so") if err != nil { log.Fatalf("Couldn't open temp file for librnnoise\n") } f.Write(libRNNoise) log.Printf("Wrote temp librnnoise to: %s\n", f.Name()) return f.Name() } func removeLib(file string) { err := os.Remove(file) if err != nil { log.Printf("Couldn't delete temp librnnoise: %v\n", err) } log.Printf("Deleted temp librnnoise: %s\n", file) } func getSources(client *pulseaudio.Client) []device { sources, err := client.Sources() if err != nil { log.Printf("Couldn't fetch sources from pulseaudio\n") } outputs := make([]device, 0) for i := range sources { if strings.Contains(sources[i].Name, "nui_") || strings.Contains(sources[i].Name, "NoiseTorch") { continue } var inp device inp.ID = sources[i].Name inp.Name = sources[i].PropList["device.description"] inp.isMonitor = (sources[i].MonitorSourceIndex != 0xffffffff) inp.rate = sources[i].SampleSpec.Rate //PA_SOURCE_DYNAMIC_LATENCY = 0x0040U inp.dynamicLatency = sources[i].Flags&uint32(0x0040) != 0 outputs = append(outputs, inp) } return outputs } func getSinks(client *pulseaudio.Client) []device { sources, err := client.Sinks() if err != nil { log.Printf("Couldn't fetch sources from pulseaudio\n") } inputs := make([]device, 0) for i := range sources { if strings.Contains(sources[i].Name, "nui_") || strings.Contains(sources[i].Name, "NoiseTorch") { continue } log.Printf("Output %s, %+v\n", sources[i].Name, sources[i]) var inp device inp.ID = sources[i].Name inp.Name = sources[i].PropList["device.description"] inp.rate = sources[i].SampleSpec.Rate // PA_SINK_DYNAMIC_LATENCY = 0x0080U inp.dynamicLatency = sources[i].Flags&uint32(0x0080) != 0 inputs = append(inputs, inp) } return inputs } func paConnectionWatchdog(ctx *ntcontext) { for { if ctx.paClient.Connected() { time.Sleep(500 * time.Millisecond) continue } ctx.views.Push(connectView) (*ctx.masterWindow).Changed() paClient, err := pulseaudio.NewClient() if err != nil { log.Printf("Couldn't create pulseaudio client: %v\n", err) fmt.Fprintf(os.Stderr, "Couldn't create pulseaudio client: %v\n", err) } info, err := serverInfo(paClient) if err != nil { log.Printf("Couldn't fetch audio server info: %s\n", err) } ctx.serverInfo = info log.Printf("Connected to audio server. Server name '%s'\n", info.name) ctx.paClient = paClient go updateNoiseSupressorLoaded(ctx) ctx.inputList = preselectDevice(ctx, getSources(ctx.paClient), ctx.config.LastUsedInput, getDefaultSourceID) ctx.outputList = preselectDevice(ctx, getSinks(paClient), ctx.config.LastUsedOutput, getDefaultSinkID) resetUI(ctx) (*ctx.masterWindow).Changed() time.Sleep(500 * time.Millisecond) } } func serverInfo(paClient *pulseaudio.Client) (audioserverinfo, error) { info, err := paClient.ServerInfo() if err != nil { log.Printf("Couldn't fetch pulse server info: %v\n", err) fmt.Fprintf(os.Stderr, "Couldn't fetch pulse server info: %v\n", err) } pkgname := info.PackageName log.Printf("Audioserver package name: %s\n", pkgname) log.Printf("Audioserver package version: %s\n", info.PackageVersion) isPipewire := strings.Contains(pkgname, "PipeWire") var servername string var servertype uint var major, minor, patch int var versionRegex *regexp.Regexp var versionString string var outdatedPipeWire bool if isPipewire { servername = "PipeWire" servertype = servertype_pipewire versionRegex = regexp.MustCompile(`.*?on PipeWire (\d+)\.(\d+)\.(\d+).*?`) versionString = pkgname log.Printf("Detected PipeWire\n") } else { servername = "PulseAudio" servertype = servertype_pulse versionRegex = regexp.MustCompile(`.*?(\d+)\.(\d+)\.(\d+).*?`) versionString = info.PackageVersion log.Printf("Detected PulseAudio\n") } res := versionRegex.FindStringSubmatch(versionString) if len(res) != 4 { log.Printf("couldn't parse server version, regexp didn't match version: %s\n", versionString) return audioserverinfo{servertype: servertype}, nil } major, err = strconv.Atoi(res[1]) if err != nil { return audioserverinfo{servertype: servertype}, err } minor, err = strconv.Atoi(res[2]) if err != nil { return audioserverinfo{servertype: servertype}, err } patch, err = strconv.Atoi(res[3]) if err != nil { return audioserverinfo{servertype: servertype}, err } if isPipewire && major <= 0 && minor <= 3 && patch < 28 { log.Printf("pipewire version %d.%d.%d too old.\n", major, minor, patch) outdatedPipeWire = true } return audioserverinfo{ servertype: servertype, name: servername, major: major, minor: minor, patch: patch, outdatedPipeWire: outdatedPipeWire}, nil } func preselectDevice(ctx *ntcontext, devices []device, preselectID string, fallbackFunc func(client *pulseaudio.Client) (string, error)) []device { deviceExists := false for _, input := range devices { deviceExists = deviceExists || input.ID == preselectID } if !deviceExists { defaultDevice, err := fallbackFunc(ctx.paClient) if err != nil { log.Printf("Failed to load default device: %+v\n", err) } else { preselectID = defaultDevice } } for i := range devices { if devices[i].ID == preselectID { devices[i].checked = true } } return devices } func getDefaultSourceID(client *pulseaudio.Client) (string, error) { server, err := client.ServerInfo() if err != nil { return "", err } return server.DefaultSource, nil } func getDefaultSinkID(client *pulseaudio.Client) (string, error) { server, err := client.ServerInfo() if err != nil { return "", err } return server.DefaultSink, nil } //this is disgusting func fixWindowClass() { xu, err := xgbutil.NewConn() defer xu.Conn().Close() if err != nil { log.Printf("Couldn't create XU xdg conn: %+v\n", err) return } for i := 0; i < 100; i++ { wnds, _ := ewmh.ClientListGet(xu) for _, w := range wnds { n, _ := ewmh.WmNameGet(xu, w) if n == appName { _, err := icccm.WmClassGet(xu, w) //if we have *NO* WM_CLASS, then the above call errors. We *want* to make sure this errors if err == nil { continue } class := icccm.WmClass{} class.Class = appName class.Instance = appName icccm.WmClassSet(xu, w, &class) return } } time.Sleep(100 * time.Millisecond) } }