diff --git a/bin/omarchy-audio-output-sink b/bin/omarchy-audio-output-sink index f47bf984..6eab9374 100755 --- a/bin/omarchy-audio-output-sink +++ b/bin/omarchy-audio-output-sink @@ -50,6 +50,38 @@ if [[ -n $downstream ]]; then fi fi +# EasyEffects 8 does not play into the physical sink through a stream of its +# own: its output node links its ports straight to the sink's playback ports, +# so the sink-input search above finds nothing and the keys would move the +# EasyEffects sink -- whose volume its monitor, the input of the processing, +# does not follow. Follow the port links instead: the first ALSA or Bluetooth +# sink any EasyEffects output port is linked to. +if [[ $sink == "easyeffects_sink" ]]; then + linked="$(pw-link -l 2>/dev/null | + awk ' + /^[^ ]/ {src = $0} + /^[[:space:]]*\|->/ { + if (src ~ /^ee_/ && $2 ~ /^(alsa|bluez)_output\..*:playback_/) { + sub(/:playback_.*$/, "", $2); print $2; exit + } + }')" + + # Idle, EasyEffects drops those links: fall back to the output device it is + # configured to feed, when that sink exists. + if [[ -z $linked ]]; then + configured="$(sed -n 's/^outputDevice=//p' \ + "${XDG_CONFIG_HOME:-$HOME/.config}/easyeffects/db/easyeffectsrc" 2>/dev/null)" + if [[ -n $configured ]] && pactl list sinks short 2>/dev/null | awk '{print $2}' | grep -qxF -- "$configured"; then + linked="$configured" + fi + fi + + if [[ -n $linked ]]; then + printf '%s\n' "$linked" + exit 0 + fi +fi + # Nothing resolvable downstream -- the DSP sink may simply be idle and unlinked. # Fall back to the sink itself so callers still have something to act on. printf '%s\n' "$sink" diff --git a/test/shell.d/audio-output-sink-test.sh b/test/shell.d/audio-output-sink-test.sh new file mode 100755 index 00000000..1ff5f7c1 --- /dev/null +++ b/test/shell.d/audio-output-sink-test.sh @@ -0,0 +1,106 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +test_home=$(mktemp -d) +test_bin=$(mktemp -d) + +cleanup() { + rm -rf "$test_home" "$test_bin" +} +trap cleanup EXIT + +# Stub PipeWire's clients: the default sink, the sink list, the sink inputs and +# the port links are all data files the test writes per scenario. +cat >"$test_bin/pactl" <<'STUB' +#!/bin/bash +case "$1 $2" in + "get-default-sink ") cat "$TEST_DATA/default-sink" ;; + "list sinks") cat "$TEST_DATA/sinks" ;; + "list sink-inputs") cat "$TEST_DATA/sink-inputs" 2>/dev/null ;; + *) exit 1 ;; +esac +STUB +cat >"$test_bin/pw-link" <<'STUB' +#!/bin/bash +cat "$TEST_DATA/links" 2>/dev/null +STUB +chmod +x "$test_bin/pactl" "$test_bin/pw-link" + +physical=alsa_output.pci-0000_00_1f.3.analog-surround-40 + +resolve() { + HOME="$test_home" XDG_CONFIG_HOME="$test_home/.config" TEST_DATA="$test_home/data" \ + PATH="$test_bin:$PATH" bash "$ROOT/bin/omarchy-audio-output-sink" "$@" +} + +reset_scenario() { + rm -rf "$test_home/data" "$test_home/.config" + mkdir -p "$test_home/data" + printf '%s\n' easyeffects_sink >"$test_home/data/default-sink" + printf '267\t%s\tPipeWire\ts32le 4ch 44100Hz\tRUNNING\n870\teasyeffects_sink\tPipeWire\tfloat32le 2ch 48000Hz\tRUNNING\n' \ + "$physical" >"$test_home/data/sinks" +} + +# A physical default is returned as is. +reset_scenario +printf '%s\n' "$physical" >"$test_home/data/default-sink" +[[ $(resolve) == "$physical" ]] || fail "a physical default sink resolves to itself" +pass "physical default sink resolves to itself" + +# EasyEffects 8 playing: its output ports are linked straight to the card. +reset_scenario +cat >"$test_home/data/links" < $physical:playback_FL +ee_soe_output_level:output_FR + |-> $physical:playback_FR +ee_soe_output_level:input_FL + |<- ee_soe_limiter:output_FL +LINKS +[[ $(resolve) == "$physical" ]] || fail "easyeffects_sink resolves through the port links to the card" +pass "easyeffects_sink resolves through its port links" + +# EasyEffects 8 playing into Bluetooth headphones: the live links win over a +# settings file that still names the speakers. +headphones=bluez_output.00_11_22_33_44_55.1 +reset_scenario +printf '301\t%s\tPipeWire\ts16le 2ch 48000Hz\tRUNNING\n' "$headphones" >>"$test_home/data/sinks" +cat >"$test_home/data/links" < $headphones:playback_FL +ee_soe_output_level:output_FR + |-> $headphones:playback_FR +LINKS +mkdir -p "$test_home/.config/easyeffects/db" +printf '[StreamOutputs]\noutputDevice=%s\n' "$physical" >"$test_home/.config/easyeffects/db/easyeffectsrc" +[[ $(resolve) == "$headphones" ]] || fail "easyeffects_sink resolves through its port links to Bluetooth headphones" +pass "easyeffects_sink resolves through its port links to Bluetooth headphones" + +# EasyEffects 8 idle: no links, but its settings name the output device. +reset_scenario +mkdir -p "$test_home/.config/easyeffects/db" +printf '[StreamOutputs]\noutputDevice=%s\n' "$physical" >"$test_home/.config/easyeffects/db/easyeffectsrc" +[[ $(resolve) == "$physical" ]] || fail "idle easyeffects_sink resolves through the configured output device" +pass "idle easyeffects_sink resolves through the configured output device" + +# A configured device that is gone (headphones unplugged) is not returned. +reset_scenario +mkdir -p "$test_home/.config/easyeffects/db" +printf '[StreamOutputs]\noutputDevice=alsa_output.usb-gone.analog-stereo\n' >"$test_home/.config/easyeffects/db/easyeffectsrc" +[[ $(resolve) == "easyeffects_sink" ]] || fail "a configured device that no longer exists falls back to the DSP sink" +pass "missing configured device falls back to the DSP sink" + +# The stream-based resolution (filter-chain tunings) still comes first. +reset_scenario +printf '%s\n' omarchy_speaker_tuning >"$test_home/data/default-sink" +cat >"$test_home/data/sink-inputs" <