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.
281 lines
6.5 KiB
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
|
|
}
|