pull/3/head
lawl 4 years ago
parent d29abc7a0d
commit e758ce0ac7

2
.gitignore vendored

@ -0,0 +1,2 @@
noiseui
librnnoise.go

@ -0,0 +1,3 @@
release:
go generate
CGO_ENABLED=0 GOOS=linux go build -a -ldflags '-s -w -extldflags "-static"' .

@ -0,0 +1,74 @@
package main
import (
"bytes"
"io/ioutil"
"log"
"os"
"path/filepath"
"github.com/BurntSushi/toml"
)
type config struct {
Threshold int
DisplayMonitorSources bool
}
const configDir = ".config/noiseui/"
const configFile = "config.toml"
func initializeConfigIfNot() {
log.Println("Checking if config needs to be initialized")
conf := config{Threshold: 95, DisplayMonitorSources: false}
configdir := filepath.Join(os.Getenv("HOME"), configDir)
ok, err := exists(configdir)
if err != nil {
log.Fatalf("Couldn't check if config directory exists: %v\n", err)
}
if !ok {
err = os.MkdirAll(configdir, 0700)
if err != nil {
log.Fatalf("Couldn't create config directory: %v\n", err)
}
}
tomlfile := filepath.Join(configdir, configFile)
ok, err = exists(tomlfile)
if err != nil {
log.Fatalf("Couldn't check if config file exists: %v\n", err)
}
if !ok {
log.Println("Initializing config")
writeConfig(&conf)
}
}
func readConfig() *config {
f := filepath.Join(os.Getenv("HOME"), configDir, configFile)
config := config{}
if _, err := toml.DecodeFile(f, &config); err != nil {
log.Fatalf("Couldn't read config file: %v\n", err)
}
return &config
}
func writeConfig(conf *config) {
f := filepath.Join(os.Getenv("HOME"), configDir, configFile)
var buffer bytes.Buffer
if err := toml.NewEncoder(&buffer).Encode(&conf); err != nil {
log.Fatalf("Couldn't write config file: %v\n", err)
}
ioutil.WriteFile(f, []byte(buffer.String()), 0644)
}
func exists(path string) (bool, error) {
_, err := os.Stat(path)
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}

@ -0,0 +1,10 @@
module noiseui
go 1.14
require (
gioui.org v0.0.0-20200630184602-223f8fd40ae4 // indirect
github.com/BurntSushi/toml v0.3.1
github.com/aarzilli/nucular v0.0.0-20200615134801-81910c722bba
github.com/lawl/pulseaudio v0.0.0-20200704145757-7d4b4b92e7b7
)

@ -0,0 +1,48 @@
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
gioui.org v0.0.0-20200417085050-0cfc914d8b7d/go.mod h1:AHI9rFr6AEEHCb8EPVtb/p5M+NMJRKH58IOp8O3Je04=
gioui.org v0.0.0-20200630184602-223f8fd40ae4 h1:Pgz3ROw4pcpdxs62BRaqt4PbBeRHsW+mf5ee92CKZ0M=
gioui.org v0.0.0-20200630184602-223f8fd40ae4/go.mod h1:jiUwifN9cRl/zmco43aAqh0aV+s9GbhG13KcD+gEpkU=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/aarzilli/nucular v0.0.0-20200615134801-81910c722bba h1:7OBB0+T/f0gGMdqTwoXF872nDKosq4dIK/H2cRlrnWI=
github.com/aarzilli/nucular v0.0.0-20200615134801-81910c722bba/go.mod h1:TsFEH0qn2Uu3C3guJjfIaoCqgpoCvU+laq0SSK2TOyY=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72 h1:b+9H1GAsx5RsjvDFLoS5zkNBzIQMuVKUYQDmxU3N5XE=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/golang/freetype v0.0.0-20161208064710-d9be45aaf745 h1:0d9whnMsm0iklqvoBXNEgHPt8pkXdfDplBAswA/F8YA=
github.com/golang/freetype v0.0.0-20161208064710-d9be45aaf745/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/hashicorp/golang-lru v0.0.0-20160813221303-0a025b7e63ad h1:eMxs9EL0PvIGS9TTtxg4R+JxuPGav82J8rA+GFnY7po=
github.com/hashicorp/golang-lru v0.0.0-20160813221303-0a025b7e63ad/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/lawl/pulseaudio v0.0.0-20200704145757-7d4b4b92e7b7 h1:LjLOowMTfYESP3XW5/KV6TaQVuhdvkBHJoEaReGds6M=
github.com/lawl/pulseaudio v0.0.0-20200704145757-7d4b4b92e7b7/go.mod h1:9h36x4KH7r2V8DOCKoPMt87IXZ++X90y8D5nnuwq290=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3 h1:n9HxLrNxWWtEb1cA950nuEEj3QnKbtsCJ6KjcgisNUs=
golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE=
golang.org/x/exp v0.0.0-20191224044220-1fea468a75e9 h1:HLuLY2KniBsHW28uXd1i2UZKjifeJUy//P/wTK6AJwI=
golang.org/x/exp v0.0.0-20191224044220-1fea468a75e9/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200618115811-c13761719519 h1:1e2ufUJNM3lCHEY5jIgac/7UTjd6cgJNdatjPdFWf34=
golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9 h1:1/DFK4b7JH8DmkqhUk48onnSfrPzImPoVxuomtbT2nk=
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

@ -0,0 +1 @@
Put a compiled librnnoise_ladspa.so in this folder, it will be required for compilation

@ -0,0 +1,102 @@
package main
import (
"image"
"io/ioutil"
"log"
"os"
"github.com/aarzilli/nucular/font"
"github.com/lawl/pulseaudio"
"github.com/aarzilli/nucular"
"github.com/aarzilli/nucular/style"
)
//go:generate go run scripts/embedlibrnnoise.go
type input struct {
ID string
Name string
isMonitor bool
checked bool
}
func main() {
f, err := os.OpenFile("/tmp/noiseui.log", 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.Println("Application starting.")
initializeConfigIfNot()
rnnoisefile := dumpLib()
defer removeLib(rnnoisefile)
ui := uistate{}
ui.config = readConfig()
ui.librnnoise = rnnoisefile
paClient, err := pulseaudio.NewClient()
defer paClient.Close()
ui.paClient = paClient
if err != nil {
log.Fatalf("Couldn't create pulseaudio client\n")
}
go updateNoiseSupressorLoaded(paClient, &ui.noiseSupressorState)
sources, err := paClient.Sources()
if err != nil {
log.Fatalf("Couldn't fetch sources from pulseaudio\n")
}
inputs := make([]input, 0)
for i := range sources {
if sources[i].Name == "nui_mic_remap" {
continue
}
var inp input
inp.ID = sources[i].Name
inp.Name = sources[i].PropList["device.description"]
inp.isMonitor = (sources[i].MonitorSourceIndex != 0xffffffff)
inputs = append(inputs, inp)
}
ui.inputList = inputs
wnd := nucular.NewMasterWindowSize(0, "NoiseUI", image.Point{550, 300}, func(w *nucular.Window) {
updatefn(w, &ui)
})
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)
}

@ -0,0 +1,155 @@
package main
import (
"fmt"
"log"
"strings"
"time"
"github.com/lawl/pulseaudio"
)
const (
loaded = iota
unloaded
inconsistent
)
// the ugly and (partially) repeated strings are unforunately difficult to avoid, as it's what pulse audio expects
func updateNoiseSupressorLoaded(c *pulseaudio.Client, b *int) {
upd, err := c.Updates()
if err != nil {
fmt.Printf("Error listening for updates: %v\n", err)
}
for {
*b = supressorState(c)
<-upd
}
}
func supressorState(c *pulseaudio.Client) int {
//perform some checks to see if it looks like the noise supressor is loaded
_, nullsink, err := findModule(c, "module-null-sink", "sink_name=nui_mic_denoised_out")
if err != nil {
log.Printf("Couldn't fetch module list to check for module-null-sink: %v\n", err)
}
_, ladspasink, err := findModule(c, "module-ladspa-sink", "sink_name=nui_mic_raw_in sink_master=nui_mic_denoised_out")
if err != nil {
log.Printf("Couldn't fetch module list to check for module-ladspa-sink: %v\n", err)
}
_, loopback, err := findModule(c, "module-loopback", "sink=nui_mic_raw_in")
if err != nil {
log.Printf("Couldn't fetch module list to check for module-nulll-sink: %v\n", err)
}
_, remap, err := findModule(c, "module-remap-source", "master=nui_mic_denoised_out.monitor source_name=nui_mic_remap")
if err != nil {
log.Printf("Couldn't fetch module list to check for module-nulll-sink: %v\n", err)
}
if nullsink && ladspasink && loopback && remap {
return loaded
}
if nullsink || ladspasink || loopback || remap {
return inconsistent
}
return unloaded
}
func loadSupressor(c *pulseaudio.Client, inp input, ui *uistate) error {
log.Printf("Loading supressor\n")
idx, err := c.LoadModule("module-null-sink", "sink_name=nui_mic_denoised_out")
if err != nil {
return err
}
log.Printf("Loaded null sink as idx: %d\n", idx)
time.Sleep(time.Millisecond * 500) // pulseaudio actually crashes if we send these too fast
idx, err = c.LoadModule("module-ladspa-sink",
fmt.Sprintf("sink_name=nui_mic_raw_in sink_master=nui_mic_denoised_out "+
"label=noise_suppressor_mono plugin=%s control=%d", ui.librnnoise, ui.config.Threshold))
if err != nil {
return err
}
log.Printf("Loaded ladspa sink as idx: %d\n", idx)
time.Sleep(time.Millisecond * 500) // pulseaudio actually crashes if we send these too fast
idx, err = c.LoadModule("module-loopback",
fmt.Sprintf("source=%s sink=nui_mic_raw_in channels=1 latency_msec=1", inp.ID))
if err != nil {
return err
}
log.Printf("Loaded loopback as idx: %d\n", idx)
time.Sleep(time.Millisecond * 500) // pulseaudio actually crashes if we send these too fast
idx, err = c.LoadModule("module-remap-source", `master=nui_mic_denoised_out.monitor `+
`source_name=nui_mic_remap source_properties="device.description='Denoised Microphone'"`)
if err != nil {
return err
}
log.Printf("Loaded ladspa sink as idx: %d\n", idx)
return nil
}
func unloadSupressor(c *pulseaudio.Client) error {
log.Printf("Unloading pulseaudio modules\n")
log.Printf("Searching for null-sink\n")
m, found, err := findModule(c, "module-null-sink", "sink_name=nui_mic_denoised_out")
if err != nil {
return err
}
if found {
log.Printf("Found null-sink at id [%d], sending unload command\n", m.Index)
c.UnloadModule(m.Index)
}
log.Printf("Searching for ladspa-sink\n")
m, found, err = findModule(c, "module-ladspa-sink", "sink_name=nui_mic_raw_in sink_master=nui_mic_denoised_out")
if err != nil {
return err
}
if found {
log.Printf("Found ladspa-sink at id [%d], sending unload command\n", m.Index)
c.UnloadModule(m.Index)
}
log.Printf("Searching for loopback\n")
m, found, err = findModule(c, "module-loopback", "sink=nui_mic_raw_in")
if err != nil {
return err
}
if found {
log.Printf("Found loopback at id [%d], sending unload command\n", m.Index)
c.UnloadModule(m.Index)
}
log.Printf("Searching for remap-source\n")
m, found, err = findModule(c, "module-remap-source", "master=nui_mic_denoised_out.monitor source_name=nui_mic_remap")
if err != nil {
return err
}
if found {
log.Printf("Found remap source at id [%d], sending unload command\n", m.Index)
c.UnloadModule(m.Index)
}
return nil
}
// Finds a module by exactly matching the module name, and checking if the second string is a substring of the argument
func findModule(c *pulseaudio.Client, name string, argMatch string) (module pulseaudio.Module, found bool, err error) {
lst, err := c.ModuleList()
if err != nil {
return pulseaudio.Module{}, false, err
}
for _, m := range lst {
if m.Name == name && strings.Contains(m.Argument, argMatch) {
return m, true, nil
}
}
return pulseaudio.Module{}, false, nil
}

@ -0,0 +1,30 @@
package main
import (
"fmt"
"io/ioutil"
"os"
"strconv"
)
func main() {
b, err := ioutil.ReadFile("librnnoise_ladspa/librnnoise_ladspa.so")
if err != nil {
fmt.Printf("Couldn't read librnnoise_ladspa.so: %v\n", err)
fmt.Println("Drop a compiled librnnoise_ladspa.so in at librnnoise_ladspa/librnnoise_ladspa.so,\n" +
"it will is required for compilation so we can embed it.")
os.Exit(1)
}
out, _ := os.Create("librnnoise.go")
defer out.Close()
out.Write([]byte("package main \n\nvar libRNNoise = []byte{\n"))
for i, c := range b {
out.Write([]byte(strconv.Itoa(int(c))))
out.Write([]byte(","))
if i%32 == 0 && i != 0 {
out.Write([]byte("\n"))
}
}
out.Write([]byte("}\n"))
}

128
ui.go

@ -0,0 +1,128 @@
package main
import (
"fmt"
"image/color"
"log"
"github.com/aarzilli/nucular"
"github.com/lawl/pulseaudio"
)
type uistate struct {
inputList []input
noiseSupressorState int
paClient *pulseaudio.Client
librnnoise string
sourceListColdWidthIndex int
useBuiltinRNNoise bool
config *config
}
func updatefn(w *nucular.Window, ui *uistate) {
w.Row(15).Dynamic(2)
w.Label("NoiseUI", "LC")
if ui.noiseSupressorState == loaded {
w.LabelColored("Denoised virtual microphone active", "RC", color.RGBA{0, 255, 0, 255} /*green*/)
} else if ui.noiseSupressorState == unloaded {
w.LabelColored("Denoised virtual microphone inactive", "RC", color.RGBA{255, 0, 0, 255} /*red*/)
} else if ui.noiseSupressorState == inconsistent {
w.LabelColored("Inconsistent state, please unload first.", "RC", color.RGBA{255, 140, 0, 255} /*orange*/)
}
if w.TreePush(nucular.TreeTab, "Settings", true) {
w.Row(15).Dynamic(2)
if w.CheckboxText("Display Monitor Sources", &ui.config.DisplayMonitorSources) {
ui.sourceListColdWidthIndex++ //recompute the with because of new elements
go writeConfig(ui.config)
}
w.Spacing(1)
w.Row(25).Ratio(0.5, 0.45, 0.05)
w.Label("Filter strictness", "LC")
if w.Input().Mouse.HoveringRect(w.LastWidgetBounds) {
w.Tooltip("If you have a decent microphone, you can usually turn this all the way up.")
}
if w.SliderInt(0, &ui.config.Threshold, 95, 1) {
go writeConfig(ui.config)
}
w.Label(fmt.Sprintf("%d%%", ui.config.Threshold), "RC")
w.TreePop()
}
if w.TreePush(nucular.TreeTab, "Select Device", true) {
w.Row(15).Dynamic(1)
w.Label("Select an input device below:", "LC")
for i := range ui.inputList {
el := &ui.inputList[i]
if el.isMonitor && !ui.config.DisplayMonitorSources {
continue
}
w.Row(15).Static()
w.LayoutFitWidth(0, 0)
if w.CheckboxText("", &el.checked) {
ensureOnlyOneInputSelected(&ui.inputList, el)
}
w.LayoutFitWidth(ui.sourceListColdWidthIndex, 0)
w.Label(el.Name, "LC")
}
w.Row(30).Dynamic(1)
w.Spacing(1)
w.Row(25).Dynamic(2)
if ui.noiseSupressorState != unloaded {
if w.ButtonText("Unload Denoised Virtual Microphone") {
if err := unloadSupressor(ui.paClient); err != nil {
log.Println(err)
}
}
} else {
w.Spacing(1)
}
txt := "Load Denoised Virtual Microphone"
if ui.noiseSupressorState == loaded {
txt = "Reload Denoised Virtual Microphone"
}
if inp, ok := inputSelection(ui); ok && ui.noiseSupressorState != inconsistent {
if w.ButtonText(txt) {
if ui.noiseSupressorState == loaded {
if err := unloadSupressor(ui.paClient); err != nil {
log.Println(err)
}
}
if err := loadSupressor(ui.paClient, inp, ui); err != nil {
log.Println(err)
}
}
} else {
w.Spacing(1)
}
w.TreePop()
}
}
func ensureOnlyOneInputSelected(inps *[]input, current *input) {
if current.checked != true {
return
}
for i := range *inps {
el := &(*inps)[i]
el.checked = false
}
current.checked = true
}
func inputSelection(ui *uistate) (input, bool) {
for _, in := range ui.inputList {
if in.checked {
return in, true
}
}
return input{}, false
}
Loading…
Cancel
Save