From fad3e1d4f4e76a74c6e32cf11f892b6ba3022c69 Mon Sep 17 00:00:00 2001 From: lawl Date: Wed, 19 May 2021 18:59:47 +0200 Subject: [PATCH] Add support for PipeWire Warning: Alpha quality. Please manage your expectations. --- c/ladspa/Makefile | 2 +- c/ladspa/module.c | 10 +- cli.go | 8 +- main.go | 80 +++++++++++- module.go | 325 +++++++++++++++++++++++++++++++--------------- ui.go | 50 ++++++- 6 files changed, 351 insertions(+), 124 deletions(-) diff --git a/c/ladspa/Makefile b/c/ladspa/Makefile index f61aded..ee7b64d 100644 --- a/c/ladspa/Makefile +++ b/c/ladspa/Makefile @@ -1,3 +1,3 @@ default: $(CC) -Wall -Werror -O2 -c -fPIC ../ringbuf.c ../rnnoise/*.c module.c - $(CC) -shared -Wl,--version-script=export.txt -o rnnoise_ladspa.so *.o + $(CC) -shared -lm -Wl,--version-script=export.txt -o rnnoise_ladspa.so *.o diff --git a/c/ladspa/module.c b/c/ladspa/module.c index dc4a37a..bc3886d 100644 --- a/c/ladspa/module.c +++ b/c/ladspa/module.c @@ -14,9 +14,9 @@ #include "../ringbuf.h" #include "../rnnoise/rnnoise.h" -#define SF_VAD 0 -#define SF_INPUT 1 -#define SF_OUTPUT 2 +#define SF_INPUT 0 +#define SF_OUTPUT 1 +#define SF_VAD 2 #define FRAMESIZE_NSAMPLES 480 #define FRAMESIZE_BYTES (480 * sizeof(float)) @@ -187,9 +187,7 @@ ON_LOAD_ROUTINE { g_psDescriptor->PortRangeHints = (const LADSPA_PortRangeHint *)psPortRangeHints; psPortRangeHints[SF_VAD].HintDescriptor = - (LADSPA_HINT_BOUNDED_BELOW | LADSPA_HINT_BOUNDED_ABOVE | - LADSPA_HINT_SAMPLE_RATE | LADSPA_HINT_LOGARITHMIC | - LADSPA_HINT_DEFAULT_440); + (LADSPA_HINT_BOUNDED_BELOW | LADSPA_HINT_BOUNDED_ABOVE); psPortRangeHints[SF_VAD].LowerBound = 0; psPortRangeHints[SF_VAD].UpperBound = 95; psPortRangeHints[SF_INPUT].HintDescriptor = 0; diff --git a/cli.go b/cli.go index 887927e..d8b09ee 100644 --- a/cli.go +++ b/cli.go @@ -45,7 +45,6 @@ func doCLI(opt CLIOpts, config *config, librnnoise string) { } paClient, err := pulseaudio.NewClient() - if err != nil { fmt.Fprintf(os.Stderr, "Couldn't create pulseaudio client: %v\n", err) os.Exit(1) @@ -53,6 +52,13 @@ func doCLI(opt CLIOpts, config *config, librnnoise string) { defer paClient.Close() ctx := ntcontext{} + + info, err := serverInfo(paClient) + if err != nil { + fmt.Fprintf(os.Stderr, "Couldn't fetch audio server info: %s\n", err) + } + ctx.serverInfo = info + ctx.config = config ctx.librnnoise = librnnoise diff --git a/main.go b/main.go index c072bea..0ed7d2f 100644 --- a/main.go +++ b/main.go @@ -6,6 +6,8 @@ import ( "io/ioutil" "log" "os" + "regexp" + "strconv" "strings" "time" @@ -123,12 +125,10 @@ func getSources(client *pulseaudio.Client) []device { outputs := make([]device, 0) for i := range sources { - if strings.Contains(sources[i].Name, "nui_") { + if strings.Contains(sources[i].Name, "nui_") || strings.Contains(sources[i].Name, "NoiseTorch") { continue } - log.Printf("Input %s, %+v\n", sources[i].Name, sources[i]) - var inp device inp.ID = sources[i].Name @@ -153,7 +153,7 @@ func getSinks(client *pulseaudio.Client) []device { inputs := make([]device, 0) for i := range sources { - if strings.Contains(sources[i].Name, "nui_") { + if strings.Contains(sources[i].Name, "nui_") || strings.Contains(sources[i].Name, "NoiseTorch") { continue } @@ -190,6 +190,14 @@ func paConnectionWatchdog(ctx *ntcontext) { fmt.Fprintf(os.Stderr, "Couldn't create pulseaudio client: %v\n", err) } + info, err := serverInfo(paClient) + if err != nil { + log.Printf("Couldn't fetch audio server info: %s\n", err) + } + ctx.serverInfo = info + + log.Printf("Connected to audio server. Server name '%s'\n", info.name) + ctx.paClient = paClient go updateNoiseSupressorLoaded(ctx) @@ -203,6 +211,70 @@ func paConnectionWatchdog(ctx *ntcontext) { } } +func serverInfo(paClient *pulseaudio.Client) (audioserverinfo, error) { + info, err := paClient.ServerInfo() + if err != nil { + log.Printf("Couldn't fetch pulse server info: %v\n", err) + fmt.Fprintf(os.Stderr, "Couldn't fetch pulse server info: %v\n", err) + } + + pkgname := info.PackageName + log.Printf("Audioserver package name: %s\n", pkgname) + log.Printf("Audioserver package version: %s\n", info.PackageVersion) + isPipewire := strings.Contains(pkgname, "PipeWire") + + var servername string + var servertype uint + var major, minor, patch int + var versionRegex *regexp.Regexp + var versionString string + + var outdatedPipeWire bool + + if isPipewire { + servername = "PipeWire" + servertype = servertype_pipewire + versionRegex = regexp.MustCompile(`.*?on PipeWire (\d+)\.(\d+)\.(\d+).*?`) + versionString = pkgname + log.Printf("Detected PipeWire\n") + } else { + servername = "PulseAudio" + servertype = servertype_pulse + versionRegex = regexp.MustCompile(`.*?(\d+)\.(\d+)\.(\d+).*?`) + versionString = info.PackageVersion + log.Printf("Detected PulseAudio\n") + } + + res := versionRegex.FindStringSubmatch(versionString) + if len(res) != 4 { + return audioserverinfo{}, fmt.Errorf("couldn't parse server version, regexp didn't match.") + } + major, err = strconv.Atoi(res[1]) + if err != nil { + return audioserverinfo{}, err + } + minor, err = strconv.Atoi(res[2]) + if err != nil { + return audioserverinfo{}, err + } + patch, err = strconv.Atoi(res[3]) + if err != nil { + return audioserverinfo{}, err + } + if isPipewire && major <= 0 && minor <= 3 && patch < 28 { + log.Printf("pipewire version %d.%d.%d too old.\n", major, minor, patch) + outdatedPipeWire = true + } + + return audioserverinfo{ + servertype: servertype, + name: servername, + major: major, + minor: minor, + patch: patch, + outdatedPipeWire: outdatedPipeWire}, nil +} + func preselectDevice(ctx *ntcontext, devices []device, preselectID string, fallbackFunc func(client *pulseaudio.Client) (string, error)) []device { diff --git a/module.go b/module.go index 25bd45a..6947537 100644 --- a/module.go +++ b/module.go @@ -39,56 +39,74 @@ func supressorState(ctx *ntcontext) int { c := ctx.paClient var inpLoaded, outLoaded, inputInc, outputInc bool if ctx.config.FilterInput { - _, 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-loopback: %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-remap-source: %v\n", err) - } + if ctx.serverInfo.servertype == servertype_pipewire { + _, 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) + } + inpLoaded = ladspasource + inputInc = false + } else { + _, 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-loopback: %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-remap-source: %v\n", err) + } - if nullsink && ladspasink && loopback && remap { - inpLoaded = true - } else if nullsink || ladspasink || loopback || remap { - inputInc = true + if nullsink && ladspasink && loopback && remap { + inpLoaded = true + } else if nullsink || ladspasink || loopback || remap { + inputInc = true + } } } else { inpLoaded = true } if ctx.config.FilterOutput { - _, out, err := findModule(c, "module-null-sink", "sink_name=nui_out_out_sink") - if err != nil { - log.Printf("Couldn't fetch module list to check for output module-ladspa-sink: %v\n", err) - } - _, lad, err := findModule(c, "module-ladspa-sink", "sink_name=nui_out_ladspa") - if err != nil { - log.Printf("Couldn't fetch module list to check for output module-ladspa-sink: %v\n", err) - } - _, loop, err := findModule(c, "module-loopback", "source=nui_out_out_sink.monitor") - 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") - if err != nil { - log.Printf("Couldn't fetch module list to check for output module-ladspa-sink: %v\n", err) - } - _, 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) - } + if ctx.serverInfo.servertype == servertype_pipewire { + _, 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) + } + outLoaded = ladspasink + outputInc = false + } else { + _, out, err := findModule(c, "module-null-sink", "sink_name=nui_out_out_sink") + if err != nil { + log.Printf("Couldn't fetch module list to check for output module-ladspa-sink: %v\n", err) + } + _, lad, err := findModule(c, "module-ladspa-sink", "sink_name=nui_out_ladspa") + if err != nil { + log.Printf("Couldn't fetch module list to check for output module-ladspa-sink: %v\n", err) + } + _, loop, err := findModule(c, "module-loopback", "source=nui_out_out_sink.monitor") + 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") + if err != nil { + log.Printf("Couldn't fetch module list to check for output module-ladspa-sink: %v\n", err) + } + _, 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) + } - outLoaded = out && lad && loop && outin && loop2 - outputInc = out || lad || loop || outin || loop2 + outLoaded = out && lad && loop && outin && loop2 + outputInc = out || lad || loop || outin || loop2 + } } else { outLoaded = true } @@ -105,110 +123,200 @@ func supressorState(ctx *ntcontext) int { } func loadSupressor(ctx *ntcontext, inp *device, out *device) error { + if ctx.serverInfo.servertype == servertype_pulse { + log.Printf("Querying pulse rlimit\n") + pid, err := getPulsePid() + if err != nil { + return err + } + + lim, err := getRlimit(pid) + if err != nil { + return err + } + log.Printf("Rlimit: %+v. Trying to remove.\n", lim) + + removeRlimit(pid) - log.Printf("Querying pulse rlimit\n") + defer setRlimit(pid, &lim) // lowering RLIMIT doesn't require root + + newLim, err := getRlimit(pid) + if err != nil { + return err + } + log.Printf("Rlimit: %+v\n", newLim) + } + if inp.checked { + var err error + if ctx.serverInfo.servertype == servertype_pipewire { + err = loadPipeWireInput(ctx, inp) + } else { + err = loadPulseInput(ctx, inp) + } + if err != nil { + log.Printf("Error loading input: %v\n", err) + return err + } + } + + if out.checked { + var err error + if ctx.serverInfo.servertype == servertype_pipewire { + err = loadPipeWireOutput(ctx, out) + } else { + err = loadPulseOutput(ctx, out) + } + if err != nil { + log.Printf("Error loading output: %v\n", err) + return err + } + } + + return nil +} + +func loadPipeWireInput(ctx *ntcontext, inp *device) error { c := ctx.paClient + log.Printf("Loading supressor for pipewire\n") + idx, err := c.LoadModule("module-ladspa-source", + fmt.Sprintf("source_name='NoiseTorch Microphone' master=%s "+ + "rate=48000 channels=1 "+ + "label=noisetorch plugin=%s control=%d", inp.ID, ctx.librnnoise, ctx.config.Threshold)) - pid, err := getPulsePid() if err != nil { return err } + log.Printf("Loaded ladspa source as idx: %d\n", idx) + return nil +} + +func loadPipeWireOutput(ctx *ntcontext, out *device) error { + c := ctx.paClient + log.Printf("Loading supressor for pipewire\n") + idx, err := c.LoadModule("module-ladspa-sink", + fmt.Sprintf("sink_name='NoiseTorch Headphones' master=%s "+ + "rate=48000 channels=1 "+ + "label=noisetorch plugin=%s control=%d", out.ID, ctx.librnnoise, ctx.config.Threshold)) - lim, err := getRlimit(pid) if err != nil { return err } - log.Printf("Rlimit: %+v. Trying to remove.\n", lim) - - removeRlimit(pid) + log.Printf("Loaded ladspa source as idx: %d\n", idx) + return nil +} - defer setRlimit(pid, &lim) // lowering RLIMIT doesn't require root +func loadPulseInput(ctx *ntcontext, inp *device) error { + c := ctx.paClient + log.Printf("Loading supressor for pulse\n") + idx, err := c.LoadModule("module-null-sink", "sink_name=nui_mic_denoised_out rate=48000") + if err != nil { + return err + } + log.Printf("Loaded null sink as idx: %d\n", idx) - newLim, err := getRlimit(pid) + idx, err = c.LoadModule("module-ladspa-sink", + fmt.Sprintf("sink_name=nui_mic_raw_in sink_master=nui_mic_denoised_out "+ + "label=noisetorch plugin=%s control=%d", ctx.librnnoise, ctx.config.Threshold)) if err != nil { return err } - log.Printf("Rlimit: %+v\n", newLim) + log.Printf("Loaded ladspa sink as idx: %d\n", idx) - if inp.checked { - log.Printf("Loading supressor\n") - idx, err := c.LoadModule("module-null-sink", "sink_name=nui_mic_denoised_out rate=48000") + if inp.dynamicLatency { + idx, err = c.LoadModule("module-loopback", + fmt.Sprintf("source=%s sink=nui_mic_raw_in channels=1 latency_msec=1 source_dont_move=true sink_dont_move=true", inp.ID)) if err != nil { return err } - log.Printf("Loaded null sink as idx: %d\n", idx) - - idx, err = c.LoadModule("module-ladspa-sink", - fmt.Sprintf("sink_name=nui_mic_raw_in sink_master=nui_mic_denoised_out "+ - "label=noisetorch plugin=%s control=%d", ctx.librnnoise, ctx.config.Threshold)) + log.Printf("Loaded loopback as idx: %d\n", idx) + } else { + idx, err = c.LoadModule("module-loopback", + fmt.Sprintf("source=%s sink=nui_mic_raw_in channels=1 latency_msec=50 source_dont_move=true sink_dont_move=true adjust_time=1", inp.ID)) if err != nil { return err } - log.Printf("Loaded ladspa sink as idx: %d\n", idx) + log.Printf("Loaded fixed latency loopback as idx: %d\n", idx) + } - if inp.dynamicLatency { - idx, err = c.LoadModule("module-loopback", - fmt.Sprintf("source=%s sink=nui_mic_raw_in channels=1 latency_msec=1 source_dont_move=true sink_dont_move=true", inp.ID)) - if err != nil { - return err - } - log.Printf("Loaded loopback as idx: %d\n", idx) - } else { - idx, err = c.LoadModule("module-loopback", - fmt.Sprintf("source=%s sink=nui_mic_raw_in channels=1 latency_msec=50 source_dont_move=true sink_dont_move=true adjust_time=1", inp.ID)) - if err != nil { - return err - } - log.Printf("Loaded fixed latency loopback as idx: %d\n", idx) - } + idx, err = c.LoadModule("module-remap-source", `master=nui_mic_denoised_out.monitor `+ + `source_name=nui_mic_remap source_properties="device.description='NoiseTorch Microphone'"`) + if err != nil { + return err + } + log.Printf("Loaded remap source as idx: %d\n", idx) + return nil +} - idx, err = c.LoadModule("module-remap-source", `master=nui_mic_denoised_out.monitor `+ - `source_name=nui_mic_remap source_properties="device.description='NoiseTorch Microphone'"`) - if err != nil { - return err - } - log.Printf("Loaded remap source as idx: %d\n", idx) +func loadPulseOutput(ctx *ntcontext, out *device) error { + c := ctx.paClient + _, err := c.LoadModule("module-null-sink", `sink_name=nui_out_out_sink`) + if err != nil { + return err } - if out.checked { + _, err = c.LoadModule("module-null-sink", `sink_name=nui_out_in_sink sink_properties="device.description='NoiseTorch Headphones'"`) + if err != nil { + return err + } - _, err := c.LoadModule("module-null-sink", `sink_name=nui_out_out_sink`) - if err != nil { - return err - } + _, err = c.LoadModule("module-ladspa-sink", fmt.Sprintf(`sink_name=nui_out_ladspa sink_master=nui_out_out_sink `+ + `label=noisetorch channels=1 plugin=%s control=%d rate=%d`, + ctx.librnnoise, ctx.config.Threshold, 48000)) + if err != nil { + return err + } - _, err = c.LoadModule("module-null-sink", `sink_name=nui_out_in_sink sink_properties="device.description='NoiseTorch Headphones'"`) - if err != nil { - return err - } + _, err = c.LoadModule("module-loopback", + fmt.Sprintf("source=nui_out_out_sink.monitor sink=%s channels=2 latency_msec=50 source_dont_move=true sink_dont_move=true", out.ID)) + if err != nil { + return err + } - _, err = c.LoadModule("module-ladspa-sink", fmt.Sprintf(`sink_name=nui_out_ladspa sink_master=nui_out_out_sink `+ - `label=noisetorch channels=1 plugin=%s control=%d rate=%d`, - ctx.librnnoise, ctx.config.Threshold, 48000)) - if err != nil { - return err - } + _, err = c.LoadModule("module-loopback", + fmt.Sprintf("source=nui_out_in_sink.monitor sink=nui_out_ladspa channels=1 latency_msec=50 source_dont_move=true sink_dont_move=true")) + if err != nil { + return err + } + return nil +} - _, err = c.LoadModule("module-loopback", - fmt.Sprintf("source=nui_out_out_sink.monitor sink=%s channels=2 latency_msec=50 source_dont_move=true sink_dont_move=true", out.ID)) - if err != nil { - return err - } +func unloadSupressor(ctx *ntcontext) error { + if ctx.serverInfo.servertype == servertype_pipewire { + return unloadSupressorPipeWire(ctx) + } else { + return unloadSupressorPulse(ctx) + } +} - _, err = c.LoadModule("module-loopback", - fmt.Sprintf("source=nui_out_in_sink.monitor sink=nui_out_ladspa channels=1 latency_msec=50 source_dont_move=true sink_dont_move=true")) - if err != nil { - return err - } +func unloadSupressorPipeWire(ctx *ntcontext) error { + log.Printf("Unloading modules for pipewire\n") + log.Printf("Searching for module-ladspa-source\n") + c := ctx.paClient + m, found, err := findModule(c, "module-ladspa-source", "source_name='NoiseTorch Microphone'") + if err != nil { + return err + } + if found { + log.Printf("Found module-ladspa-source at id [%d], sending unload command\n", m.Index) + c.UnloadModule(m.Index) } + log.Printf("Searching for module-ladspa-sink\n") + m, found, err = findModule(c, "module-ladspa-sink", "sink_name='NoiseTorch Headphones'") + if err != nil { + return err + } + if found { + log.Printf("Found module-ladspa-sink at id [%d], sending unload command\n", m.Index) + c.UnloadModule(m.Index) + } return nil } -func unloadSupressor(ctx *ntcontext) error { - log.Printf("Unloading pulseaudio modules\n") +func unloadSupressorPulse(ctx *ntcontext) error { + log.Printf("Unloading modules for pulseaudio\n") if pid, err := getPulsePid(); err == nil { if lim, err := getRlimit(pid); err == nil { @@ -318,6 +426,7 @@ func unloadSupressor(ctx *ntcontext) error { // 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 } diff --git a/ui.go b/ui.go index bbab17f..56aefa5 100644 --- a/ui.go +++ b/ui.go @@ -33,8 +33,24 @@ type ntcontext struct { haveCapabilities bool capsMismatch bool views *ViewStack + serverInfo audioserverinfo } +//TODO pull some of these strucs out of UI, they don't belong here +type audioserverinfo struct { + servertype uint + name string + major int + minor int + patch int + outdatedPipeWire bool +} + +const ( + servertype_pulse = iota + servertype_pipewire +) + var green = color.RGBA{34, 187, 69, 255} var red = color.RGBA{255, 70, 70, 255} var orange = color.RGBA{255, 140, 0, 255} @@ -84,6 +100,11 @@ func mainView(ctx *ntcontext, w *nucular.Window) { w.LabelColored("Inconsistent state, please unload first.", "RC", orange) } + if ctx.serverInfo.servertype == servertype_pipewire { + w.Row(20).Dynamic(1) + w.Label("Running in PipeWire mode. PipeWire support is currently alpha quality. Please report bugs.", "LC") + } + if ctx.update.available && !ctx.update.triggered { w.Row(20).Ratio(0.9, 0.1) w.LabelColored("Update available! Click to install version: "+ctx.update.serverVersion, "LC", green) @@ -376,23 +397,23 @@ func capabilitiesView(ctx *ntcontext, w *nucular.Window) { if w.ButtonText("Grant capability (requires root)") { err := pkexecSetcapSelf() if err != nil { - ctx.views.Push(makeErrorView(ctx, w, err.Error())) + ctx.views.Push(makeErrorView(ctx, err.Error())) return } self, err := os.Executable() if err != nil { - ctx.views.Push(makeErrorView(ctx, w, err.Error())) + ctx.views.Push(makeErrorView(ctx, err.Error())) return } err = syscall.Exec(self, []string{""}, os.Environ()) if err != nil { - ctx.views.Push(makeErrorView(ctx, w, err.Error())) + ctx.views.Push(makeErrorView(ctx, err.Error())) return } } } -func makeErrorView(ctx *ntcontext, w *nucular.Window, errorMsg string) ViewFunc { +func makeErrorView(ctx *ntcontext, errorMsg string) ViewFunc { return func(ctx *ntcontext, w *nucular.Window) { w.Row(15).Dynamic(1) w.Label("Error", "CB") @@ -407,6 +428,21 @@ func makeErrorView(ctx *ntcontext, w *nucular.Window, errorMsg string) ViewFunc } } +func makeFatalErrorView(ctx *ntcontext, errorMsg string) ViewFunc { + return func(ctx *ntcontext, w *nucular.Window) { + w.Row(15).Dynamic(1) + w.Label("Fatal Error", "CB") + w.Row(15).Dynamic(1) + w.Label(errorMsg, "CB") + w.Row(40).Dynamic(1) + w.Row(25).Dynamic(1) + if w.ButtonText("Quit") { + os.Exit(1) + return + } + } +} + func resetUI(ctx *ntcontext) { ctx.views = NewViewStack() ctx.views.Push(mainView) @@ -414,6 +450,12 @@ func resetUI(ctx *ntcontext) { if !ctx.haveCapabilities { ctx.views.Push(capabilitiesView) } + + if ctx.serverInfo.outdatedPipeWire { + ctx.views.Push(makeFatalErrorView(ctx, + fmt.Sprintf("Your PipeWire version is too old. Detected %d.%d.%d. Require at least 0.3.28.", + ctx.serverInfo.major, ctx.serverInfo.minor, ctx.serverInfo.patch))) + } } func loadPatreonImg() *image.RGBA {