Compare commits

...

7 Commits

Author SHA1 Message Date
lawl 1aa277446b Add PipeWire support to README 3 years ago
lawl a2a45bc88b UI: Add confirm screen for unload/reload when device in use
Reloading/Unloading destroys the virtual device that an application
is currently using. Add a warning that this may cause strange behaviour.
3 years ago
lawl cb2ee053b6 Handle non-semver versions in audio client gracefully-ish 3 years ago
lawl d1aa44c3c7 pulseaudio: update module with fixed cookie lookup 3 years ago
lawl 077b4763fe CLI: Cleanup temp files on exit
Fixes #135
3 years ago
Andreas Schneider 809f184e87 c:ladspa: Fix linking with libm
Fixes #140
3 years ago
Andreas Schneider a32f73509f Revert "Temporarily force clang if available"
This reverts commit c2bfe0202f.
3 years ago

@ -2,9 +2,6 @@ UPDATE_URL=https://noisetorch.epicgamer.org
UPDATE_PUBKEY=3mL+rBi4yBZ1wGimQ/oSQCjxELzgTh+673H4JdzQBOk=
VERSION := $(shell git describe --tags)
CLANG := $(shell which clang)
dev: rnnoise
mkdir -p bin/
go generate
@ -30,14 +27,6 @@ release: rnnoise
go run scripts/signer.go -s
git describe --tags > bin/version.txt
rnnoise:
# For some reason gcc10 refuses to link libm
# gcc11 seems to work. Temporarily force clang
# if available until i can fix this properly
ifdef CLANG
cd c/ladspa; \
CC=clang make
else
cd c/ladspa; \
make
endif

@ -5,7 +5,7 @@
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
[![Last Release](https://img.shields.io/github/v/release/lawl/NoiseTorch?label=latest&style=flat-square)](https://github.com/lawl/NoiseTorch/releases)
NoiseTorch is an easy to use open source application for Linux with PulseAudio. It creates a virtual microphone that suppresses noise, in any application. Use whichever conferencing or VOIP application you like and simply select the NoiseTorch Virtual Microphone as input to torch the sound of your mechanical keyboard, computer fans, trains and the likes.
NoiseTorch is an easy to use open source application for Linux with PulseAudio or PipeWire. It creates a virtual microphone that suppresses noise, in any application. Use whichever conferencing or VOIP application you like and simply select the NoiseTorch Virtual Microphone as input to torch the sound of your mechanical keyboard, computer fans, trains and the likes.
Don't forget to ~~like, comment and subscribe~~ leave a star ⭐ if this sounds useful to you!

@ -1,3 +1,3 @@
default:
$(CC) -Wall -O2 -c -fPIC ../ringbuf.c ../rnnoise/*.c module.c
$(CC) -shared -lm -Wl,--version-script=export.txt -o rnnoise_ladspa.so *.o
$(CC) -Wall -Werror -O2 -c -fPIC ../ringbuf.c ../rnnoise/*.c module.c
$(CC) -o rnnoise_ladspa.so *.o -shared -Wl,--version-script=export.txt -lm

@ -39,15 +39,15 @@ func doCLI(opt CLIOpts, config *config, librnnoise string) {
if opt.setcap {
err := makeBinarySetcapped()
if err != nil {
os.Exit(1)
cleanupExit(librnnoise, 1)
}
os.Exit(0)
cleanupExit(librnnoise, 0)
}
paClient, err := pulseaudio.NewClient()
if err != nil {
fmt.Fprintf(os.Stderr, "Couldn't create pulseaudio client: %v\n", err)
os.Exit(1)
cleanupExit(librnnoise, 1)
}
defer paClient.Close()
@ -77,7 +77,7 @@ func doCLI(opt CLIOpts, config *config, librnnoise string) {
fmt.Printf("\tDevice Name: %s\n\tDevice ID: %s\n\n", sinks[i].Name, sinks[i].ID)
}
os.Exit(0)
cleanupExit(librnnoise, 0)
}
if opt.threshold > 0 {
@ -93,9 +93,9 @@ func doCLI(opt CLIOpts, config *config, librnnoise string) {
err := unloadSupressor(&ctx)
if err != nil {
fmt.Fprintf(os.Stderr, "Error unloading PulseAudio Module: %+v\n", err)
os.Exit(1)
cleanupExit(librnnoise, 1)
}
os.Exit(0)
cleanupExit(librnnoise, 0)
}
if opt.loadInput {
@ -105,7 +105,7 @@ func doCLI(opt CLIOpts, config *config, librnnoise string) {
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)
cleanupExit(librnnoise, 1)
}
opt.sinkName = defaultSource
}
@ -115,13 +115,13 @@ func doCLI(opt CLIOpts, config *config, librnnoise string) {
err := loadSupressor(&ctx, &sources[i], &device{})
if err != nil {
fmt.Fprintf(os.Stderr, "Error loading PulseAudio Module: %+v\n", err)
os.Exit(1)
cleanupExit(librnnoise, 1)
}
os.Exit(0)
cleanupExit(librnnoise, 0)
}
}
fmt.Fprintf(os.Stderr, "PulseAudio source not found: %s\n", opt.sinkName)
os.Exit(1)
cleanupExit(librnnoise, 1)
}
if opt.loadOutput {
@ -131,7 +131,7 @@ func doCLI(opt CLIOpts, config *config, librnnoise string) {
defaultSink, err := getDefaultSinkID(paClient)
if err != nil {
fmt.Fprintf(os.Stderr, "No sink specified to load and failed to load default sink: %+v\n", err)
os.Exit(1)
cleanupExit(librnnoise, 1)
}
opt.sinkName = defaultSink
}
@ -141,13 +141,18 @@ func doCLI(opt CLIOpts, config *config, librnnoise string) {
err := loadSupressor(&ctx, &device{}, &sinks[i])
if err != nil {
fmt.Fprintf(os.Stderr, "Error loading PulseAudio Module: %+v\n", err)
os.Exit(1)
cleanupExit(librnnoise, 1)
}
os.Exit(0)
cleanupExit(librnnoise, 0)
}
}
fmt.Fprintf(os.Stderr, "PulseAudio sink not found: %s\n", opt.sinkName)
os.Exit(1)
cleanupExit(librnnoise, 1)
}
}
func cleanupExit(librnnoise string, exitCode int) {
removeLib(librnnoise)
os.Exit(exitCode)
}

@ -7,7 +7,7 @@ require (
github.com/BurntSushi/toml v0.3.1
github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046
github.com/aarzilli/nucular v0.0.0-20200615134801-81910c722bba
github.com/lawl/pulseaudio v0.0.0-20200802093727-ab0735955fd0
github.com/lawl/pulseaudio v0.0.0-20210604102109-cb2596d6a8ef
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899
)

@ -21,6 +21,8 @@ github.com/hashicorp/golang-lru v0.0.0-20160813221303-0a025b7e63ad h1:eMxs9EL0Pv
github.com/hashicorp/golang-lru v0.0.0-20160813221303-0a025b7e63ad/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/lawl/pulseaudio v0.0.0-20200802093727-ab0735955fd0 h1:JrvOwrr1teFiqsp0EQxgEPJsm0pet+YLTL+HdYmnMx0=
github.com/lawl/pulseaudio v0.0.0-20200802093727-ab0735955fd0/go.mod h1:9h36x4KH7r2V8DOCKoPMt87IXZ++X90y8D5nnuwq290=
github.com/lawl/pulseaudio v0.0.0-20210604102109-cb2596d6a8ef h1:mpCJg3O6C+B8mh5xoO147NG3Z70GBcFNgqgz2DH/rLQ=
github.com/lawl/pulseaudio v0.0.0-20210604102109-cb2596d6a8ef/go.mod h1:9h36x4KH7r2V8DOCKoPMt87IXZ++X90y8D5nnuwq290=
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI=
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=

@ -247,19 +247,20 @@ func serverInfo(paClient *pulseaudio.Client) (audioserverinfo, error) {
res := versionRegex.FindStringSubmatch(versionString)
if len(res) != 4 {
return audioserverinfo{}, fmt.Errorf("couldn't parse server version, regexp didn't match.")
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{}, err
return audioserverinfo{servertype: servertype}, err
}
minor, err = strconv.Atoi(res[2])
if err != nil {
return audioserverinfo{}, err
return audioserverinfo{servertype: servertype}, err
}
patch, err = strconv.Atoi(res[3])
if err != nil {
return audioserverinfo{}, err
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)

@ -24,8 +24,7 @@ func updateNoiseSupressorLoaded(ctx *ntcontext) {
}
for {
ctx.noiseSupressorState = supressorState(ctx)
ctx.noiseSupressorState, ctx.virtualDeviceInUse = supressorState(ctx)
if !c.Connected() {
break
}
@ -34,16 +33,18 @@ func updateNoiseSupressorLoaded(ctx *ntcontext) {
}
}
func supressorState(ctx *ntcontext) int {
func supressorState(ctx *ntcontext) (int, bool) {
//perform some checks to see if it looks like the noise supressor is loaded
c := ctx.paClient
var inpLoaded, outLoaded, inputInc, outputInc bool
var virtualDeviceInUse bool = false
if ctx.config.FilterInput {
if ctx.serverInfo.servertype == servertype_pipewire {
_, ladspasource, err := findModule(c, "module-ladspa-source", "source_name='NoiseTorch Microphone'")
module, ladspasource, err := findModule(c, "module-ladspa-source", "source_name='NoiseTorch Microphone'")
if err != nil {
log.Printf("Couldn't fetch module list to check for module-ladspa-source: %v\n", err)
}
virtualDeviceInUse = virtualDeviceInUse || (module.NUsed != 0)
inpLoaded = ladspasource
inputInc = false
} else {
@ -59,11 +60,13 @@ func supressorState(ctx *ntcontext) int {
if err != nil {
log.Printf("Couldn't fetch module list to check for module-loopback: %v\n", err)
}
_, remap, err := findModule(c, "module-remap-source", "master=nui_mic_denoised_out.monitor source_name=nui_mic_remap")
module, 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-remap-source: %v\n", err)
}
virtualDeviceInUse = virtualDeviceInUse || (module.NUsed != 0)
if nullsink && ladspasink && loopback && remap {
inpLoaded = true
} else if nullsink || ladspasink || loopback || remap {
@ -76,10 +79,11 @@ func supressorState(ctx *ntcontext) int {
if ctx.config.FilterOutput {
if ctx.serverInfo.servertype == servertype_pipewire {
_, ladspasink, err := findModule(c, "module-ladspa-sink", "sink_name='NoiseTorch Headphones'")
module, ladspasink, err := findModule(c, "module-ladspa-sink", "sink_name='NoiseTorch Headphones'")
if err != nil {
log.Printf("Couldn't fetch module list to check for module-ladspa-sink: %v\n", err)
}
virtualDeviceInUse = virtualDeviceInUse || (module.NUsed != 0)
outLoaded = ladspasink
outputInc = false
} else {
@ -95,10 +99,11 @@ func supressorState(ctx *ntcontext) int {
if err != nil {
log.Printf("Couldn't fetch module list to check for output module-ladspa-sink: %v\n", err)
}
_, outin, err := findModule(c, "module-null-sink", "sink_name=nui_out_in_sink")
module, outin, err := findModule(c, "module-null-sink", "sink_name=nui_out_in_sink")
if err != nil {
log.Printf("Couldn't fetch module list to check for output module-ladspa-sink: %v\n", err)
}
virtualDeviceInUse = virtualDeviceInUse || (module.NUsed != 0)
_, loop2, err := findModule(c, "module-loopback", "source=nui_out_in_sink.monitor")
if err != nil {
log.Printf("Couldn't fetch module list to check for output module-ladspa-sink: %v\n", err)
@ -112,14 +117,14 @@ func supressorState(ctx *ntcontext) int {
}
if (inpLoaded || !ctx.config.FilterInput) && (outLoaded || !ctx.config.FilterOutput) && !inputInc {
return loaded
return loaded, virtualDeviceInUse
}
if (inpLoaded && ctx.config.FilterInput) || (outLoaded && ctx.config.FilterOutput) || inputInc || outputInc {
return inconsistent
return inconsistent, virtualDeviceInUse
}
return unloaded
return unloaded, virtualDeviceInUse
}
func loadSupressor(ctx *ntcontext, inp *device, out *device) error {

125
ui.go

@ -34,6 +34,7 @@ type ntcontext struct {
capsMismatch bool
views *ViewStack
serverInfo audioserverinfo
virtualDeviceInUse bool
}
//TODO pull some of these strucs out of UI, they don't belong here
@ -149,13 +150,13 @@ func mainView(ctx *ntcontext, w *nucular.Window) {
if w.CheckboxText("Filter Microphone", &ctx.config.FilterInput) {
ctx.sourceListColdWidthIndex++ //recompute the with because of new elements
go writeConfig(ctx.config)
go (func() { ctx.noiseSupressorState = supressorState(ctx) })()
go (func() { ctx.noiseSupressorState, _ = supressorState(ctx) })()
}
if w.CheckboxText("Filter Headphones", &ctx.config.FilterOutput) {
ctx.sourceListColdWidthIndex++ //recompute the with because of new elements
go writeConfig(ctx.config)
go (func() { ctx.noiseSupressorState = supressorState(ctx) })()
go (func() { ctx.noiseSupressorState, _ = supressorState(ctx) })()
}
w.TreePop()
@ -239,21 +240,19 @@ func mainView(ctx *ntcontext, w *nucular.Window) {
w.Row(25).Dynamic(2)
if ctx.noiseSupressorState != unloaded {
if w.ButtonText("Unload NoiseTorch") {
ctx.views.Push(loadingView)
ctx.reloadRequired = false
go func() { // don't block the UI thread, just display a working screen so user can't run multiple loads/unloads
if err := unloadSupressor(ctx); err != nil {
log.Println(err)
}
//wait until PA reports it has actually loaded it, timeout at 10s
for i := 0; i < 20; i++ {
if supressorState(ctx) != unloaded {
time.Sleep(time.Millisecond * 500)
}
}
ctx.views.Pop()
(*ctx.masterWindow).Changed()
}()
if ctx.virtualDeviceInUse {
confirm := makeConfirmView(ctx,
"Virtual Device in Use",
"Some applications may behave weirdly when you remove a device they're currently using",
"Unload",
"Go back",
func() { uiUnloadNoisetorch(ctx) },
func() {})
ctx.views.Push(confirm)
} else {
go uiUnloadNoisetorch(ctx)
}
}
} else {
w.Spacing(1)
@ -271,30 +270,20 @@ func mainView(ctx *ntcontext, w *nucular.Window) {
((ctx.config.FilterOutput && ctx.config.GuiltTripped) || !ctx.config.FilterOutput) &&
ctx.noiseSupressorState != inconsistent {
if w.ButtonText(txt) {
ctx.views.Push(loadingView)
ctx.reloadRequired = false
go func() { // don't block the UI thread, just display a working screen so user can't run multiple loads/unloads
if ctx.noiseSupressorState == loaded {
if err := unloadSupressor(ctx); err != nil {
log.Println(err)
}
}
if err := loadSupressor(ctx, &inp, &out); err != nil {
log.Println(err)
}
//wait until PA reports it has actually loaded it, timeout at 10s
for i := 0; i < 20; i++ {
if supressorState(ctx) != loaded {
time.Sleep(time.Millisecond * 500)
}
}
ctx.config.LastUsedInput = inp.ID
ctx.config.LastUsedOutput = out.ID
go writeConfig(ctx.config)
ctx.views.Pop()
(*ctx.masterWindow).Changed()
}()
if ctx.virtualDeviceInUse {
confirm := makeConfirmView(ctx,
"Virtual Device in Use",
"Some applications may behave weirdly when you reload a device they're currently using",
"Reload",
"Go back",
func() { uiReloadNoisetorch(ctx, inp, out) },
func() {})
ctx.views.Push(confirm)
} else {
go uiReloadNoisetorch(ctx, inp, out)
}
}
} else {
w.Spacing(1)
@ -302,6 +291,45 @@ func mainView(ctx *ntcontext, w *nucular.Window) {
}
func uiUnloadNoisetorch(ctx *ntcontext) {
ctx.views.Push(loadingView)
if err := unloadSupressor(ctx); err != nil {
log.Println(err)
}
//wait until PA reports it has actually loaded it, timeout at 10s
for i := 0; i < 20; i++ {
if state, _ := supressorState(ctx); state != unloaded {
time.Sleep(time.Millisecond * 500)
}
}
ctx.views.Pop()
(*ctx.masterWindow).Changed()
}
func uiReloadNoisetorch(ctx *ntcontext, inp, out device) {
ctx.views.Push(loadingView)
if ctx.noiseSupressorState == loaded {
if err := unloadSupressor(ctx); err != nil {
log.Println(err)
}
}
if err := loadSupressor(ctx, &inp, &out); err != nil {
log.Println(err)
}
//wait until PA reports it has actually loaded it, timeout at 10s
for i := 0; i < 20; i++ {
if state, _ := supressorState(ctx); state != loaded {
time.Sleep(time.Millisecond * 500)
}
}
ctx.config.LastUsedInput = inp.ID
ctx.config.LastUsedOutput = out.ID
go writeConfig(ctx.config)
ctx.views.Pop()
(*ctx.masterWindow).Changed()
}
func ensureOnlyOneInputSelected(inps *[]device, current *device) {
if current.checked != true {
return
@ -443,6 +471,27 @@ func makeFatalErrorView(ctx *ntcontext, errorMsg string) ViewFunc {
}
}
func makeConfirmView(ctx *ntcontext, title, text, confirmText, denyText string, confirmfunc, denyfunc func()) ViewFunc {
return func(ctx *ntcontext, w *nucular.Window) {
w.Row(15).Dynamic(1)
w.Label(title, "CB")
w.Row(15).Dynamic(1)
w.Label(text, "CB")
w.Row(40).Dynamic(1)
w.Row(25).Dynamic(2)
if w.ButtonText(denyText) {
ctx.views.Pop()
go denyfunc()
return
}
if w.ButtonText(confirmText) {
ctx.views.Pop()
go confirmfunc()
return
}
}
}
func resetUI(ctx *ntcontext) {
ctx.views = NewViewStack()
ctx.views.Push(mainView)

@ -383,7 +383,7 @@ func cookiePath() (string, error) {
return p, nil
}
p = filepath.Join(os.Getenv("HOME"), "/.pulse_cookie")
p = filepath.Join(os.Getenv("HOME"), "/.pulse-cookie")
if exists(p) {
return p, nil
}

@ -71,7 +71,7 @@ github.com/golang/freetype/truetype
# github.com/hashicorp/golang-lru v0.0.0-20160813221303-0a025b7e63ad
github.com/hashicorp/golang-lru
github.com/hashicorp/golang-lru/simplelru
# github.com/lawl/pulseaudio v0.0.0-20200802093727-ab0735955fd0
# github.com/lawl/pulseaudio v0.0.0-20210604102109-cb2596d6a8ef
## explicit
github.com/lawl/pulseaudio
# github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635

Loading…
Cancel
Save