From 245630786c71913bab29a48c163cb4fcda934569 Mon Sep 17 00:00:00 2001 From: Omarchybot Date: Sun, 4 Oct 2026 16:07:28 +0200 Subject: [PATCH] Resolve effect_input/effect_output audio filter pairs (#14241) * Resolve effect_input/effect_output audio filter pairs User filter chains can name their input and output nodes differently, so the existing input-name prefix check cannot find their downstream sink. Match effect_output. exactly for effect_input., avoiding similarly named filters while retaining prefix-compatible custom tunings and the existing EasyEffects fallbacks. Generated by GPT-6 in Codex. Reviewed by Claude Opus 5.5 Medium. Co-Authored-By: Claude Opus 5.5 Medium * Prefer an exact filter output over legacy prefix matches An earlier stream for a similarly named filter can satisfy the legacy prefix check and select the wrong device before the exact paired output is read. Keep the first prefix candidate as a fallback for effect_input sinks and finish searching for the exact effect_output pair. Preserve first-match behavior for all other sink names. Generated by GPT-6 in Codex. Reviewed by Claude Opus 5.5 Medium. Co-Authored-By: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --------- Co-authored-by: Omarchy Bot <317366263+omarchybot@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Medium Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- bin/omarchy-audio-output-sink | 13 +++++- test/shell.d/audio-output-sink-test.sh | 56 ++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/bin/omarchy-audio-output-sink b/bin/omarchy-audio-output-sink index 6eab9374..6efbb93d 100755 --- a/bin/omarchy-audio-output-sink +++ b/bin/omarchy-audio-output-sink @@ -29,16 +29,25 @@ fi # stream down to the sink underneath. downstream="$(pactl list sink-inputs 2>/dev/null | awk -v virt="$sink" ' + BEGIN { + output = virt + sub(/^effect_input\./, "effect_output.", output) + } /^Sink Input #/ {target = ""} /^[[:space:]]*Sink:/ {target = $2} /node\.name = / { name = $0 sub(/.*node\.name = "/, "", name) sub(/"$/, "", name) - if (index(name, virt) == 1 && target != "") {print target; exit} + if (target != "" && (name == output || (output == virt && index(name, virt) == 1))) {selected = target; exit} + if (target != "" && fallback == "" && index(name, virt) == 1) fallback = target } /application\.name = "EasyEffects"/ { - if (virt == "easyeffects_sink" && target != "") {print target; exit} + if (virt == "easyeffects_sink" && target != "") {selected = target; exit} + } + END { + if (selected != "") print selected + else if (fallback != "") print fallback }')" if [[ -n $downstream ]]; then diff --git a/test/shell.d/audio-output-sink-test.sh b/test/shell.d/audio-output-sink-test.sh index 1ff5f7c1..745699ae 100755 --- a/test/shell.d/audio-output-sink-test.sh +++ b/test/shell.d/audio-output-sink-test.sh @@ -104,3 +104,59 @@ Sink Input #42 INPUTS [[ $(resolve) == "$physical" ]] || fail "a filter-chain tuning still resolves through its stream" pass "filter-chain tuning resolves through its stream" + +# The shipped tuning uses an underscore before its output stream suffix. +reset_scenario +printf '%s\n' omarchy_speaker_tuning >"$test_home/data/default-sink" +printf 'Sink Input #42\n Sink: 267\n node.name = "omarchy_speaker_tuning_output"\n' >"$test_home/data/sink-inputs" +[[ $(resolve) == "$physical" ]] || fail "the shipped tuning output still resolves through its stream" +pass "shipped tuning output resolves through its stream" + +# Keep custom stream suffixes that the existing prefix rule already handles. +printf 'Sink Input #42\n Sink: 267\n node.name = "omarchy_speaker_tuning-playback"\n' >"$test_home/data/sink-inputs" +[[ $(resolve) == "$physical" ]] || fail "custom legacy tuning suffixes still resolve" +pass "custom legacy tuning suffixes still resolve" + +# Reproduce the reported input/output naming pair and downstream sink ID. +reset_scenario +filter=effect_input.Dolby_Balanced +printf '%s\n' "$filter" >"$test_home/data/default-sink" +printf '68\t%s\tPipeWire\ts32le 4ch 44100Hz\tRUNNING\n' "$physical" >"$test_home/data/sinks" +printf 'Sink Input #42\n Sink: 68\n node.name = "effect_output.Dolby_Balanced"\n' >"$test_home/data/sink-inputs" +[[ $(resolve) == "$physical" ]] || fail "the reported effect_input/effect_output pair resolves downstream" +pass "reported effect_input/effect_output pair resolves downstream" + +# An explicitly requested filter resolves even when another output is default. +printf '%s\n' "$headphones" >"$test_home/data/default-sink" +[[ $(resolve "$filter") == "$physical" ]] || fail "an explicit filter resolves independently of the default" +pass "explicit filter resolves independently of the default" +printf '%s\n' "$filter" >"$test_home/data/default-sink" + +# An effect_input sink can also use a legacy prefix-compatible output name. +printf 'Sink Input #42\n Sink: 68\n node.name = "effect_input.Dolby_Balanced.playback"\n' >"$test_home/data/sink-inputs" +[[ $(resolve) == "$physical" ]] || fail "effect_input sinks preserve prefix-compatible outputs" +pass "effect_input sinks preserve prefix-compatible outputs" + +# A similarly named output must not win just because it appears first. +printf '301\t%s\tPipeWire\ts16le 2ch 48000Hz\tRUNNING\n' "$headphones" >>"$test_home/data/sinks" +printf 'Sink Input #41\n Sink: 301\n node.name = "effect_output.Dolby_Balanced2"\nSink Input #42\n Sink: 68\n node.name = "effect_output.Dolby_Balanced"\n' >"$test_home/data/sink-inputs" +[[ $(resolve) == "$physical" ]] || fail "a filter output matches the complete suffix" +pass "filter output matches the complete suffix" + +# The exact paired output wins over an earlier competing legacy prefix. +printf 'Sink Input #41\n Sink: 301\n node.name = "effect_input.Dolby_Balanced2.playback"\nSink Input #42\n Sink: 68\n node.name = "effect_output.Dolby_Balanced"\n' >"$test_home/data/sink-inputs" +[[ $(resolve) == "$physical" ]] || fail "the exact output wins over a competing legacy prefix" +pass "exact output wins over a competing legacy prefix" + +printf 'Sink Input #41\n Sink: 301\n node.name = "effect_input.Dolby_Balanced.playback"\nSink Input #42\n Sink: 68\n node.name = "effect_output.Dolby_Balanced"\n' >"$test_home/data/sink-inputs" +[[ $(resolve) == "$physical" ]] || fail "the exact output wins over a valid legacy fallback" +pass "exact output wins over a valid legacy fallback" + +printf 'Sink Input #41\n Sink: 301\n node.name = "effect_output.Dolby_Balanced2"\n' >"$test_home/data/sink-inputs" +[[ $(resolve) == "$filter" ]] || fail "a filter with no exact output falls back to itself" +pass "filter with no exact output falls back to itself" + +# A stale sink ID must not turn into a different output. +printf 'Sink Input #42\n Sink: 999\n node.name = "effect_output.Dolby_Balanced"\n' >"$test_home/data/sink-inputs" +[[ $(resolve) == "$filter" ]] || fail "a missing downstream sink falls back to the filter" +pass "missing downstream sink falls back to the filter"