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

277 lines
6.1 KiB
Go

package main
import (
"fmt"
"image"
"io/ioutil"
"log"
"os"
"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_") {
continue
}
log.Printf("Input %s, %+v\n", sources[i].Name, sources[i])
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_") {
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)
}
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 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)
}
}