You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
NoiseTorch/main.go

350 lines
8.3 KiB
Go

4 years ago
package main
import (
"fmt"
4 years ago
"image"
"io/ioutil"
"log"
"os"
"regexp"
"strconv"
"strings"
"time"
4 years ago
"github.com/BurntSushi/xgbutil"
"github.com/BurntSushi/xgbutil/ewmh"
"github.com/BurntSushi/xgbutil/icccm"
4 years ago
"github.com/aarzilli/nucular/font"
"github.com/lawl/pulseaudio"
_ "embed"
4 years ago
"github.com/aarzilli/nucular"
"github.com/aarzilli/nucular/style"
)
//go:generate go run scripts/embedlicenses.go
4 years ago
//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
4 years ago
}
const appName = "NoiseTorch"
var version = "unknown" // will be changed by build
var distribution = "custom" // ditto
var updateURL = "" // ditto
var publicKeyString = "" // ditto
4 years ago
func main() {
opt := parseCLIOpts()
4 years ago
if opt.doLog {
log.SetOutput(os.Stdout)
} else {
log.SetOutput(ioutil.Discard)
4 years ago
}
log.Printf("Application starting. Version: %s (%s)\n", version, distribution)
log.Printf("CAP_SYS_RESOURCE: %t\n", hasCapSysResource(getCurrentCaps()))
4 years ago
initializeConfigIfNot()
rnnoisefile := dumpLib()
defer removeLib(rnnoisefile)
ctx := ntcontext{}
ctx.config = readConfig()
ctx.librnnoise = rnnoisefile
4 years ago
doCLI(opt, ctx.config, ctx.librnnoise)
if ctx.config.EnableUpdates {
go updateCheck(&ctx)
4 years ago
}
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)
4 years ago
})
ctx.masterWindow = &wnd
(*ctx.masterWindow).Changed()
go paConnectionWatchdog(&ctx)
4 years ago
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()
4 years ago
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)
}
}