#!/bin/bash # # DESCRIPTION: # dmenu-based audio helper # # TODO # - auto-select the currently active profile set -e [ -n "$DEBUG" ] && set -x # check requirements for tool in jq swq pactl perl pw-dump; do if ! command -v "$tool" >/dev/null; then printf "%s: %s not installed but required\n" "$(basename "$0")" "$tool" >&2 exit 1 fi done DEFAULT_PROFILE="output:analog-stereo+input:analog-stereo" # DEFAULT_PROFILE="pro-audio" HDMI_PROFILE="output:hdmi-stereo" _menu() { [ -n "${mode}" ] && prompt="soundswitch (${mode})" || prompt="soundswitch" export MENU_PROMPT=${MENU_PROMPT:-${prompt}} menu -l10 --ifne --accept-single } mode= output= input= mute= volume_down= volume_up= usage() { printf "usage: %s [-hIOgfpHRmMvV]\n" "$(basename "$0")" printf "\n" printf " -O \t(manipulate outputs)\n" printf " -I \t(manipulate inputs)\n" printf " -g \t(manipulate globally)\n" # printf " -f \t(manipulate focused container)\n" printf " -p \t(switch card profile)\n" # printf " -H \t(switch card to HDMI profile)\n" # printf " -R \t(switch card to default profile)\n" printf "\n" # printf " -m \t(mute default sink)\n" # printf " -M \t(mute default source)\n" printf " -v \t(reduce sink volume by 5%%)\n" printf " -V \t(increase sink volume by 5%%)\n" printf "\n" printf "Needs swq(1) to work.\n" exit 2 } _args="$*" while getopts hIOgfpHRmMvVBj name; do case ${name} in j) mode=json ;; g) mode=global ;; f) mode=focused ;; p) mode=profile output=1 input=1 ;; H) mode=hdmi output=1 input=1 ;; R) mode=reset ;; B) mode=mute ;; O) output=1 ;; I) input=1 ;; # quick actions, to make move to pipewire easier m) mode=none output=1 mute=1 ;; M) mode=none input=1 mute=1 ;; v) mode=none output=1 volume_down=1 ;; V) mode=none output=1 volume_up=1 ;; h | ?) echo "$*" usage ;; esac done shift $((OPTIND - 1)) # ask mode _modes=( "󰴸 output" "󰍬 input" "󱂇 profile" # "󰻿 focused" # "󰡁 hdmi profile" # "󰕍 reset profile" # "󰂛 mute" ) if [ -z "$mode" ]; then printf "%s\n" "${_modes[@]}" | _menu | { read -r line case ${line} in "${_modes[0]}") exec "$0" -gO "$*" ;; "${_modes[1]}") exec "$0" -gI "$*" ;; "${_modes[2]}") exec "$0" -p "$*" ;; # "${_modes[1]}") exec "$0" -f "$*" ;; # "${_modes[3]}") exec "$0" -H "$*" ;; # "${_modes[4]}") exec "$0" -R "$*" ;; # "${_modes[5]}") exec "$0" -B "$*" ;; *) usage ;; esac } exit $? fi # process quick actions first [ -n "$output" ] && [ -n "$mute" ] && exec pactl set-sink-mute '@DEFAULT_SINK@' toggle [ -n "$input" ] && [ -n "$mute" ] && exec pactl set-source-mute '@DEFAULT_SINK@' toggle [ -n "$volume_down" ] && exec pactl set-sink-volume '@DEFAULT_SINK@' '-2%' [ -n "$volume_up" ] && exec pactl set-sink-volume '@DEFAULT_SINK@' '+2%' [ -n "$output" ] && keyword="sink" && what="sink-input" [ -n "$input" ] && keyword="source" && what="source-output" # returns the sink_id default() { key=$1 case "$1" in sink) key="audio.default.sink" ;; source) key="audio.default.source" ;; esac pw-dump | jq -r --arg key "$key" '.[]|select(.type=="PipeWire:Interface:Metadata")|.metadata[]|select(.key==$key)|.value.name' } # __identifier: compacted json containing index and description # e.g. {"i":60,"d":"Built-in Audio Analog Stereo"} __identifier_list() { if [ "$1" = "card" ]; then pactl --format=json list "${1}s" | jq -c '.[]|{i:.index,d:(.properties | .["device.alias"] // .["alsa.card_name"])}' else pactl --format=json list "${1}s" | jq -c '.[]|{i:.index,d:.description}' fi } __identifier_index() { if [ -z "$1" ]; then jq -r '.i' else printf "%s" "$1" | __identifier_index fi } __identifier_desc() { if [ -z "$1" ]; then jq -r '.d' else printf "%s" "$1" | __identifier_desc fi } __identifier_fmt() { jq -r '[.i,.d]|@csv' | tr -d '"' | column -s ',' -t } __identifier_fmt_parse() { fmt=$(cat) if [ -z "${fmt}" ]; then exit 1 fi i=$(printf "%s" "$fmt" | tr -s ' ' | cut -d' ' -f1) d=$(printf "%s" "$fmt" | tr -s ' ' | cut -d' ' -f2-) jq -cn --arg i "$i" --arg d "$d" '{i:$i,d:$d}' } selects() { __identifier_list "$1" | __identifier_fmt | grep -v "Monitor of" | _menu | __identifier_fmt_parse } notif() { topic=$1 mesg=$2 urgency=normal [ -n "$3" ] && urgency=critical notify-send -i /usr/share/icons/mdi/scalable/cast-audio.svg -u ${urgency} -a soundswitch "soundswitch: ${topic}" "${mesg}" } case "${mode}" in json) selects "${keyword}" ;; global) target=$(selects "${keyword}") notif "global" "switch to $(__identifier_desc "${target}")" # set new default pactl "set-default-${keyword}" "$(__identifier_index "${target}")" # move all attached to the new target pactl list "${what}s" short | cut -f1 | xargs -i pactl "move-${what}" "{}" "$(__identifier_index "${target}")" exit $? ;; # focused) # # parse pid of currently focused container # # then get the sink-inputs for it and parse the sink-input-id # notif "not implemented, do we need it?" error # exit 1 # # focused_binary=$(swq current con | jq -r '.pid' | xargs ps -o comm=) # what_id=$(printf "%s\n" "${focused_binary}" | # xargs pactl list "${what}s" | # tr '\n' '\r' | # perl -pe 's/[^\r]*index: ([0-9]+).+?application\.process\.binary = "([^\r]+)"\r.+?(?=index:|$)/\2=\1\r/g' | # tr '\r' '\n' | # grep "^{}" | # cut -d'=' -f2) # if [ -z "${what_id}" ]; then # notif "focused" "no ${what}" error # exit 1 # fi # # # select where to switch to # target=$(selects "${keyword}") # if [ -z "${target}" ]; then # exit 1 # fi # # notif "${focused_binary}" "switch to ${target}" # pactl move-sink-input "${what_id}" "${target}" # exit $? # ;; # profile) target_card=$(selects "card" | jq -r '.i') if [ -z "${target_card}" ]; then exit 1 fi target_profile=$( pactl --format=json list cards | jq -cr --argjson idx "$target_card" '.[] | select(.index == $idx) | .profiles | keys[]' | _menu ) if [ -z "${target_profile}" ]; then exit 1 fi notif "profile" "switch ${target_card} to ${target_profile} profile" pactl set-card-profile "${target_card}" "${target_profile}" ;; # hdmi) # notif "not implemented, do we need it?" "" error # exit 1 # # target_card=$(selects "card") # if [ -z "${target_card}" ]; then # exit 1 # fi # # target_profile=$(pactl list cards | # tr '\n' '\r' | # perl -pe 's/[^\r]#([0-9]+).*?Name: ([^\r]+).*?Profiles:\r(.*?)Active.*?(?=Card #|$)/\2~\3`/g' | # tr -d '\t' | # tr '`' '\n' | # grep "${target_card}" | # cut -d'~' -f2- | # tr '\r' '\n' | # grep -i "hdmi" | # grep -i -v "input" | # xargs -L1 | # _menu | # perl -pe 's/^\s*([^\s]+):\s.*$/\1/') # if [ -z "${target_profile}" ]; then # target_profile=${HDMI_PROFILE} # fi # # notif "hdmi" "switch ${target_card} to HDMI profile\n${target_profile}" # pactl set-card-profile "${target_card}" "${target_profile}" # exit $? # ;; # # reset) # notif "not implemented, do we need it?" "" error # exit 1 # # target_card=$(selects "card") # if [ -z "${target_card}" ]; then # exit 1 # fi # # notif "reset" "switch ${target_card} to default profile" # pactl set-card-profile "${target_card}" ${DEFAULT_PROFILE} # exit $? # ;; # # mute) # notif "not implemented, do we need it?" "" error # exit 1 # # _data=$(pw-dump -N | jq -r '.. | select(.type?=="PipeWire:Interface:Node" and .info.props."media.class"?=="Stream/Output/Audio" and .info.props."stream.is-live"?==true) | @text') # _id=$(printf "${_data}" | jq -r '. | [ .info.props."application.name", .info.props."application.process.id", .id ] | @tsv' | # awk '{ system("swaymsg -t get_tree | jq -r \".. | select(.pid?==" $2 ") | .name\" | grep -v Picture-in-Picture | tr -d \"\\n\""); printf " [%s]", $3 }' | # _menu | # perl -pe 's/^.*\[([0-9]+)\]$/\1/') # if [ -z "${_id}" ]; then # exit 1 # fi # # _oid=$(printf "${_data}" | jq -r --argjson id "${_id}" '.. | select(.id?==$id) | .info.props."object.id"') # _target_state=$(printf "${_data}" | jq -r --argjson id "${_id}" '.. | select(.id?==$id) | .info.params.Props[0] | .mute = (.mute | not) | @text') # pw-cli set-param "${_oid}" Props "${_target_state}" # exit $? # ;; # esac