Skip to content

Instantly share code, notes, and snippets.

@b0o
Last active October 22, 2021 16:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save b0o/c4b04f6228fedff475935eed45599078 to your computer and use it in GitHub Desktop.
Save b0o/c4b04f6228fedff475935eed45599078 to your computer and use it in GitHub Desktop.
audioctl
#!/usr/bin/env bash
# Copyright (c) 2018-2021 Maddison Hellstrom (github.com/b0o)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
set -euo pipefail
shopt -s inherit_errexit
declare prog="audioctl"
declare version="v0.1.0"
declare authors=("Maddison Hellstrom <github.com/b0o>")
declare repository="https://github.com/b0o/doots"
declare license="GPL-3.0-or-later"
declare license_url="https://www.gnu.org/licenses/gpl-3.0.txt"
function usage() {
mapfile -t usage << EOF
$prog $version ($repository)
Usage: $prog [global opts] <command> [command opts] [command args]
Global Options:
-h display usage information for $prog
-v output version
-n enable sending notifications via notify-send
Commands:
volume [opts] <up|down> [amount]
Adjust the volume level for the active sink (default amount: 5)
Options:
-b boost (allow volume to exceed 100%)
volume mute
Mute/unmute the active sink
volume <amount>
Set the volume to the given percentage
player <play-pause|toggle>
Toggle player state between playing and paused
player <next|previous>
Skip track forward/backward
player favorite
Favorite the current track
Note: Only supported if current player is MellowPlayer
sink unify
Move all playback streams to the current default sink
sink [opts] <next|previous>
Cycle the default sink forward/backward
Options:
-u unify after cycling
sink show
Output the current sink
(c) 2018-$(date +%Y) ${authors[*]}
$license ($license_url)
EOF
printf '%s\n' "${usage[@]}" >&2
}
declare -gi AUDIOCTL_NOTIFY_DURATION=${AUDIOCTL_NOTIFY_DURATION:-2000}
# TODO: make this configurable
declare -ga PLAYERCTL_PRECEDENCE=(
'MellowPlayer'
'spotify'
'Office_speaker'
'chrom(e|ium)'
'firefox.*'
)
_playerctl() {
local match
local -i min=-1
while read -r player; do
local -i i=0
for e in "${PLAYERCTL_PRECEDENCE[@]}"; do
[[ $min -ge 0 && $i -ge $min ]] && break
[[ "$player" =~ $e ]] && {
match="$player"
min=$i
break
}
((i += 1))
done
done <<< "$(playerctl -l)"
local -a args=()
[[ -n "$match" ]] && args=("-p" "$match")
playerctl "${args[@]}" "$@"
}
declare -i notify=0
notify() {
if [[ $notify == 1 ]]; then
notify-send -a "$prog" -u low -t "$AUDIOCTL_NOTIFY_DURATION" "$@"
fi
if [[ $# -gt 0 ]]; then
echo "$1" >&2
fi
if [[ $# -gt 1 ]]; then
echo "${@:2}"
fi
}
player() {
case ${1:-} in
play-pause | toggle)
notify "Player" "Play/pause"
_playerctl play-pause > /dev/null 2>&1
;;
prev*)
notify "Player" "Prev"
_playerctl previous > /dev/null 2>&1
;;
next)
notify "Player" "Next"
_playerctl next > /dev/null 2>&1
;;
fav*)
notify "Player" "Toggle favorite"
# XXX: Assumes player is MellowPlayer
# We should at least check if MellowPlayer is running
MellowPlayer --toggle-favorite-song > /dev/null 2>&1 &
;;
esac
}
sink_parse_short() {
local -a query_vars=""
local -i OPTIND=0
local OPTARG opt
while getopts "q:" opt "$@"; do
case $opt in
q)
[[ -n "$query_vars" ]] && query_vars+=","
query_vars+="$OPTARG"
;;
\?)
echo "Invalid option: -$OPTARG" >&2
exit 1
;;
esac
done
shift $((OPTIND - 1))
local id=
local device__api=
local device__bus_path=
local device__string=
local device__profile__name=
# shellcheck disable=1004
eval "$(awk -e '
{
print gensub(/^([[:alnum:]-]+)_([[:alnum:]]+)\.([[:alnum:]_-]*)(\.[[:alnum:]]+)?\.([[:alnum:]_-]+)/,
"device__api=\"\\1\"\n" \
"device__type=\"\\2\"\n" \
"id=\"\\3\\4\"\n" \
"device__profile__name=\"\\5\"",
1, $0);
}' <<< "$*")"
if [[ $device__api == "alsa" ]]; then
device__bus_path="$id"
elif [[ $device__api == "bluez" ]]; then
device__string="$id"
# TODO: else ...
fi
# shellcheck disable=1004
pactl list sinks | awk \
-v "query_vars=$query_vars" \
-v "id=$id" \
-v "device__api=$device__api" \
-v "device__string=$device__string" \
-v "device__bus_path=$device__bus_path" \
-v "device__profile__name=$device__profile__name" \
'BEGIN {
a = 0
m = 0
cur=""
id = gensub(/_/, ":", "g", id)
};
/\tProperties:/ {
a = 1
next
};
a == 1 && /^\t\t/ {
var = gensub(/[.-]/, "__", "g", $1)
val = gensub(/^\t\t([[:alnum:]._-]+)\s+=\s+"(.*)"$/, "\\2", 1, $0)
if ( (device__api == "alsa" && var == "device__bus_path" && val == id) \
|| (device__api == "bluez" && var == "device__string" && val == id) ) {
m = 1
}
if (query_vars == "" || match(query_vars, "(^|,)" var "($|,)") > 0) {
if (cur != "") {
cur = cur "\n"
}
cur = cur var "=\"" val "\""
}
};
a == 1 && !/^\t\t/ {
if(m == 1) {
print cur
exit 0
}
a = 0
m = 0
cur = ""
}'
}
sink_cycle_default() {
local -i dir="${1:-1}"
local cur new
local -i idx
local -a sinks
cur="$(pactl info | awk -e '/^Default Sink:/ { print $3 }')"
mapfile -t sinks <<< "$(pactl list short sinks | awk '{ print $2 }')"
idx=$(awk -v "cur=$cur" -v "dir=$dir" -e '$0 == cur { print FNR - 1 + dir }' <<< "$(printf '%s\n' "${sinks[@]}")")
if [[ $idx -gt $((${#sinks[@]} - 1)) ]]; then
idx=0
elif [[ $idx -lt 0 ]]; then
idx=$((${#sinks[@]} - 1))
fi
new="${sinks[$idx]}"
pactl set-default-sink "$new"
local device__description
local device__profile__description
eval "$(sink_parse_short -q "device__profile__description" -q "device__description" "$new")"
echo "${device__profile__description:-${device__description:-unknown}}"
}
sink_unify_streams() {
local default
default="$(pactl info | awk -e '/^Default Sink:/ { print $3 }')"
local -a streams
mapfile -t streams <<< "$(pactl list short sink-inputs)"
for stream in "${streams[@]}"; do
local id
id="$(cut -f 1 <<< "$stream")"
pactl move-sink-input "$id" "$default"
done
local device__description
local device__profile__description
eval "$(sink_parse_short -q "device__profile__description" -q "device__description" "$default")"
echo "${device__profile__description:-${device__description:-unknown}}"
}
sink_show() {
local default
default="$(pactl info | awk -e '/^Default Sink:/ { print $3 }')"
local device__description
local device__profile__description
eval "$(sink_parse_short -q "device__profile__description" -q "device__description" "$default")"
echo "${device__profile__description:-${device__description:-unknown}}"
}
sink() {
local unify_suffix=""
local -i unify=0
local -i OPTIND=0
local OPTARG opt
while getopts "u" opt "$@"; do
case $opt in
u)
unify_suffix+=" (Unify)"
unify=1
;;
\?)
echo "Invalid option: -$OPTARG" >&2
exit 1
;;
esac
done
shift $((OPTIND - 1))
local nt="" # Notification title
local nm="" # Notification message
local cmd="${1:-}"
case "$cmd" in
n*) # next
nt="Next default sink${unify_suffix}"
nm="$(sink_cycle_default 1)" || true
if [[ $unify -eq 1 ]]; then
sink_unify_streams 2>&1 || true
fi
;;
p*) # previous
nt="Previous default sink${unify_suffix}"
nm="$(sink_cycle_default -1)" || true
if [[ $unify -eq 1 ]]; then
sink_unify_streams 2>&1 || true
fi
;;
u*) # unify
nt="Unify playback streams"
nm="$(sink_unify_streams)" || true
;;
s*) # show
nt="Current output sink"
nm="$(sink_show)" || true
;;
*)
echo "unknown sink command: $cmd" >&2
return 1
;;
esac
notify "$nt" "$nm"
}
volume() {
local boost=
local -i OPTIND=0
local OPTARG opt
while getopts "b" opt "$@"; do
case $opt in
b)
boost="--allow-boost"
;;
\?)
echo "Invalid option: -$OPTARG" >&2
exit 1
;;
esac
done
shift $((OPTIND - 1))
local nt="" # Notification title
local nm="" # Notification message
local cmd="${1:-}"
case "$cmd" in
up)
nt="Volume Up"
nm="$(pamixer --unmute $boost --increase "${2:-5}" --get-volume)%" || true
;;
down)
nt="Volume Down"
nm="$(pamixer --unmute $boost --decrease "${2:-5}" --get-volume)%" || true
;;
mute)
pamixer --toggle-mute
if [[ "$(pamixer --get-mute)" == "true" ]]; then
nt="Volume Mute"
nm="0%"
else
nt="Volume Unmute"
nm="$(pamixer --get-volume)%"
fi
;;
*)
local -i target="$cmd"
if [[ $cmd -ne $target ]]; then
echo "unknown volume command: $cmd" >&2
return 1
fi
nt="Volume Set"
nm="$(pamixer --unmute $boost --set-volume "${target:-5}" --get-volume)%" || true
;;
esac
notify "$nt" "$nm"
}
main() {
local -i OPTIND=0
local OPTARG opt
while getopts ":hvn" opt "$@"; do
case $opt in
h)
usage
exit 0
;;
v)
echo "$version"
exit 0
;;
n)
notify=1
;;
\?)
break
;;
esac
done
shift $((OPTIND - 1))
case ${1:-} in
sink)
sink "${@:2}"
;;
vol*)
volume "${@:2}"
;;
play*)
player "${@:2}"
;;
*)
echo "unknown command: ${1:-}" >&2
return 1
;;
esac
}
[[ ${BASH_SOURCE[0]} == "$0" ]] && {
main "$@"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment