From a2a45bc88bd549eb35a652941f0a26c5c4da09f0 Mon Sep 17 00:00:00 2001 From: lawl Date: Fri, 4 Jun 2021 13:12:35 +0200 Subject: [PATCH] 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. --- module.go | 25 ++++++----- ui.go | 125 +++++++++++++++++++++++++++++++++++++----------------- 2 files changed, 102 insertions(+), 48 deletions(-) diff --git a/module.go b/module.go index 12783e8..8e6b87a 100644 --- a/module.go +++ b/module.go @@ -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 { diff --git a/ui.go b/ui.go index 56aefa5..327f35f 100644 --- a/ui.go +++ b/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)