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

281 lines
6.5 KiB
Go

package main
import (
"flag"
"fmt"
"image"
"io/ioutil"
"log"
"os"
"path/filepath"
"syscall"
"time"
"github.com/aarzilli/nucular/font"
"github.com/lawl/pulseaudio"
"github.com/aarzilli/nucular"
"github.com/aarzilli/nucular/style"
)
//go:generate go run scripts/embedbinary.go librnnoise_ladspa/bin/ladspa/librnnoise_ladspa.so librnnoise.go libRNNoise
//go:generate go run scripts/embedbinary.go assets/patreon.png patreon.go patreonPNG
//go:generate go run scripts/embedversion.go
//go:generate go run scripts/embedlicenses.go
type input struct {
ID string
Name string
isMonitor bool
checked bool
dynamicLatency bool
}
func main() {
var pulsepid int
var setcap bool
var sourceName string
var unload bool
var load bool
var threshold int
var list bool
flag.IntVar(&pulsepid, "removerlimit", -1, "for internal use only")
flag.BoolVar(&setcap, "setcap", false, "for internal use only")
flag.StringVar(&sourceName, "s", "", "Use the specified source device ID")
flag.BoolVar(&load, "i", false, "Load supressor for input. If no source device ID is specified the default pulse audio source is used.")
flag.BoolVar(&unload, "u", false, "Unload supressor")
flag.IntVar(&threshold, "t", -1, "Voice activation threshold")
flag.BoolVar(&list, "l", false, "List available PulseAudio sources")
flag.Parse()
// we also execute this opportunistically on pulsepid since that's also called as root, but need to do so silently, so no os.Exit()'s
if setcap || pulsepid > 0 {
err := makeBinarySetcapped()
if err != nil && !(pulsepid > 0) {
os.Exit(1)
}
if !(pulsepid > 0) {
os.Exit(0)
}
}
if pulsepid > 0 {
const MaxUint = ^uint64(0)
new := syscall.Rlimit{Cur: MaxUint, Max: MaxUint}
err := setRlimit(pulsepid, &new)
if err != nil {
os.Exit(1)
}
os.Exit(0)
}
date := time.Now().Format("2006_01_02_03_04_05")
tmpdir := os.TempDir()
f, err := os.OpenFile(filepath.Join(tmpdir, fmt.Sprintf("noisetorch-%s.log", date)), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
log.Fatalf("error opening file: %v\n", err)
}
defer f.Close()
log.SetOutput(f)
log.Printf("Application starting. Version: %s\n", version)
log.Printf("CAP_SYS_RESOURCE: %t\n", hasCapSysResource(getCurrentCaps()))
initializeConfigIfNot()
rnnoisefile := dumpLib()
defer removeLib(rnnoisefile)
ctx := ntcontext{}
ctx.config = readConfig()
ctx.librnnoise = rnnoisefile
paClient, err := pulseaudio.NewClient()
if err != nil {
log.Printf("Couldn't create pulseaudio client: %v\n", err)
os.Exit(1)
}
if list {
sources := getSources(paClient)
for i := range sources {
fmt.Printf("Device Name: %s\nDevice ID: %s\n\n", sources[i].Name, sources[i].ID)
}
os.Exit(0)
}
if threshold > 0 {
if threshold > 95 {
fmt.Fprintf(os.Stderr, "Threshold of '%d' too high, setting to maximum of 95.\n", threshold)
ctx.config.Threshold = 95
} else {
ctx.config.Threshold = threshold
}
}
if unload {
unloadSupressor(paClient)
os.Exit(0)
}
if load {
ctx.paClient = paClient
sources := getSources(paClient)
if supressorState(paClient) != unloaded {
fmt.Fprintf(os.Stderr, "Supressor is already loaded.\n")
os.Exit(1)
}
if sourceName == "" {
defaultSource, err := getDefaultSourceID(paClient)
if err != nil {
fmt.Fprintf(os.Stderr, "No source specified to load and failed to load default source: %+v\n", err)
os.Exit(1)
}
sourceName = defaultSource
}
for i := range sources {
if sources[i].ID == sourceName {
err := loadSupressor(&ctx, sources[i])
if err != nil {
fmt.Fprintf(os.Stderr, "Error loading PulseAudio Module: %+v\n", err)
os.Exit(1)
}
os.Exit(0)
}
}
fmt.Fprintf(os.Stderr, "PulseAudio source not found: %s\n", sourceName)
os.Exit(1)
}
if ctx.config.EnableUpdates {
go updateCheck(&ctx)
}
go paConnectionWatchdog(&ctx)
wnd := nucular.NewMasterWindowSize(0, "NoiseTorch", image.Point{550, 300}, func(w *nucular.Window) {
updatefn(&ctx, w)
})
ctx.masterWindow = &wnd
style := style.FromTheme(style.DarkTheme, 2.0)
style.Font = font.DefaultFont(16, 1)
wnd.SetStyle(style)
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) []input {
sources, err := client.Sources()
if err != nil {
log.Printf("Couldn't fetch sources from pulseaudio\n")
}
inputs := make([]input, 0)
for i := range sources {
if sources[i].Name == "nui_mic_remap" {
continue
}
log.Printf("Input %s, %+v\n", sources[i].Name, sources[i])
var inp input
inp.ID = sources[i].Name
inp.Name = sources[i].PropList["device.description"]
inp.isMonitor = (sources[i].MonitorSourceIndex != 0xffffffff)
//PA_SOURCE_DYNAMIC_LATENCY = 0x0040U
inp.dynamicLatency = sources[i].Flags&uint32(0x0040) != 0
inputs = append(inputs, inp)
}
return inputs
}
func paConnectionWatchdog(ctx *ntcontext) {
for {
if ctx.paClient.Connected() {
time.Sleep(500 * time.Millisecond)
continue
}
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(paClient, &ctx.noiseSupressorState)
ctx.inputList = getSourcesWithPreSelectedInput(ctx)
resetUI(ctx)
time.Sleep(500 * time.Millisecond)
}
}
func getSourcesWithPreSelectedInput(ctx *ntcontext) []input {
inputs := getSources(ctx.paClient)
preselectedInputID := &ctx.config.LastUsedInput
inputExists := false
if preselectedInputID != nil {
for _, input := range inputs {
inputExists = inputExists || input.ID == *preselectedInputID
}
}
if !inputExists {
defaultSource, err := getDefaultSourceID(ctx.paClient)
if err != nil {
log.Printf("Failed to load default source: %+v\n", err)
} else {
preselectedInputID = &defaultSource
}
}
if preselectedInputID != nil {
for i := range inputs {
if inputs[i].ID == *preselectedInputID {
inputs[i].checked = true
}
}
}
return inputs
}
func getDefaultSourceID(client *pulseaudio.Client) (string, error) {
server, err := client.ServerInfo()
if err != nil {
return "", err
}
return server.DefaultSource, nil
}