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