pulseaudio-control (14937B)
1 #!/usr/bin/env bash 2 3 ################################################################## 4 # Polybar Pulseaudio Control # 5 # https://github.com/marioortizmanero/polybar-pulseaudio-control # 6 ################################################################## 7 8 # Defaults for configurable values, expected to be set by command-line arguments 9 AUTOSYNC="no" 10 COLOR_MUTED="%{F#6b6b6b}" 11 ICON_MUTED= 12 ICON_SINK= 13 NOTIFICATIONS="no" 14 OSD="no" 15 SINK_NICKNAMES_PROP= 16 VOLUME_STEP=${INC:-2} 17 VOLUME_MAX=130 18 # shellcheck disable=SC2016 19 FORMAT='$VOL_ICON ${VOL_LEVEL}% $ICON_SINK $SINK_NICKNAME' 20 declare -A SINK_NICKNAMES 21 declare -a ICONS_VOLUME 22 declare -a SINK_BLACKLIST 23 24 # Environment & global constants for the script 25 export LANG=en_US # Some calls depend on English outputs of pactl 26 END_COLOR="%{F-}" # For Polybar colors 27 28 29 # Saves the currently default sink into a variable named `curSink`. It will 30 # return an error code when pulseaudio isn't running. 31 function getCurSink() { 32 if ! pactl info &>/dev/null; then return 1; fi 33 local curSinkName 34 35 curSinkName=$(pactl info | awk '/Default Sink: / {print $3}') 36 curSink=$(pactl list sinks | grep -B 4 -E "Name: $curSinkName\$" | sed -nE 's/^Sink #([0-9]+)$/\1/p') 37 } 38 39 40 # Saves the sink passed by parameter's volume into a variable named `VOL_LEVEL`. 41 function getCurVol() { 42 VOL_LEVEL=$(pactl list sinks | grep -A 15 -E "^Sink #$1\$" | grep 'Volume:' | grep -E -v 'Base Volume:' | awk -F : '{print $3; exit}' | grep -o -P '.{0,3}%' | sed 's/.$//' | tr -d ' ') 43 } 44 45 46 # Saves the name of the sink passed by parameter into a variable named 47 # `sinkName`. 48 function getSinkName() { 49 sinkName=$(pactl list sinks short | awk -v sink="$1" '{ if ($1 == sink) {print $2} }') 50 portName=$(pactl list sinks | grep -e 'Sink #' -e 'Active Port: ' | sed -n "/^Sink #$1\$/,+1p" | awk '/Active Port: / {print $3}') 51 } 52 53 54 # Saves the name to be displayed for the sink passed by parameter into a 55 # variable called `SINK_NICKNAME`. 56 # If a mapping for the sink name exists, that is used. Otherwise, the string 57 # "Sink #<index>" is used. 58 function getNickname() { 59 getSinkName "$1" 60 unset SINK_NICKNAME 61 62 if [ -n "$sinkName" ] && [ -n "$portName" ] && [ -n "${SINK_NICKNAMES[$sinkName/$portName]}" ]; then 63 SINK_NICKNAME="${SINK_NICKNAMES[$sinkName/$portName]}" 64 elif [ -n "$sinkName" ] && [ -n "${SINK_NICKNAMES[$sinkName]}" ]; then 65 SINK_NICKNAME="${SINK_NICKNAMES[$sinkName]}" 66 elif [ -n "$sinkName" ] && [ -n "$SINK_NICKNAMES_PROP" ]; then 67 getNicknameFromProp "$SINK_NICKNAMES_PROP" "$sinkName" 68 # Cache that result for next time 69 SINK_NICKNAMES["$sinkName"]="$SINK_NICKNAME" 70 fi 71 72 if [ -z "$SINK_NICKNAME" ]; then 73 SINK_NICKNAME="Sink #$1" 74 fi 75 } 76 77 # Gets sink nickname based on a given property. 78 function getNicknameFromProp() { 79 local nickname_prop="$1" 80 local for_name="$2" 81 82 SINK_NICKNAME= 83 while read -r property value; do 84 case "$property" in 85 Name:) 86 sink_name="$value" 87 unset sink_desc 88 ;; 89 "$nickname_prop") 90 if [ "$sink_name" != "$for_name" ]; then 91 continue 92 fi 93 SINK_NICKNAME="${value:3:-1}" 94 break 95 ;; 96 esac 97 done < <(pactl list sinks) 98 } 99 100 # Saves the status of the sink passed by parameter into a variable named 101 # `IS_MUTED`. 102 function getIsMuted() { 103 IS_MUTED=$(pactl list sinks | grep -E "^Sink #$1\$" -A 15 | awk '/Mute: / {print $2}') 104 } 105 106 107 # Saves all the sink inputs of the sink passed by parameter into a string 108 # named `sinkInputs`. 109 function getSinkInputs() { 110 sinkInputs=$(pactl list sink-inputs | grep -B 4 "Sink: $1" | sed -nE 's/^Sink Input #([0-9]+)$/\1/p') 111 } 112 113 114 function volUp() { 115 # Obtaining the current volume from pulseaudio into $VOL_LEVEL. 116 if ! getCurSink; then 117 echo "PulseAudio not running" 118 return 1 119 fi 120 getCurVol "$curSink" 121 local maxLimit=$((VOLUME_MAX - VOLUME_STEP)) 122 123 # Checking the volume upper bounds so that if VOLUME_MAX was 100% and the 124 # increase percentage was 3%, a 99% volume would top at 100% instead 125 # of 102%. If the volume is above the maximum limit, nothing is done. 126 if [ "$VOL_LEVEL" -le "$VOLUME_MAX" ] && [ "$VOL_LEVEL" -ge "$maxLimit" ]; then 127 pactl set-sink-volume "$curSink" "$VOLUME_MAX%" 128 elif [ "$VOL_LEVEL" -lt "$maxLimit" ]; then 129 pactl set-sink-volume "$curSink" "+$VOLUME_STEP%" 130 fi 131 132 if [ $OSD = "yes" ]; then showOSD "$curSink"; fi 133 if [ $AUTOSYNC = "yes" ]; then volSync; fi 134 } 135 136 137 function volDown() { 138 # Pactl already handles the volume lower bounds so that negative values 139 # are ignored. 140 if ! getCurSink; then 141 echo "PulseAudio not running" 142 return 1 143 fi 144 pactl set-sink-volume "$curSink" "-$VOLUME_STEP%" 145 146 if [ $OSD = "yes" ]; then showOSD "$curSink"; fi 147 if [ $AUTOSYNC = "yes" ]; then volSync; fi 148 } 149 150 151 function volSync() { 152 if ! getCurSink; then 153 echo "PulseAudio not running" 154 return 1 155 fi 156 getSinkInputs "$curSink" 157 getCurVol "$curSink" 158 159 # Every output found in the active sink has their volume set to the 160 # current one. This will only be called if $AUTOSYNC is `yes`. 161 for each in $sinkInputs; do 162 pactl set-sink-input-volume "$each" "$VOL_LEVEL%" 163 done 164 } 165 166 167 function volMute() { 168 # Switch to mute/unmute the volume with pactl. 169 if ! getCurSink; then 170 echo "PulseAudio not running" 171 return 1 172 fi 173 if [ "$1" = "toggle" ]; then 174 getIsMuted "$curSink" 175 if [ "$IS_MUTED" = "yes" ]; then 176 pactl set-sink-mute "$curSink" "no" 177 else 178 pactl set-sink-mute "$curSink" "yes" 179 fi 180 elif [ "$1" = "mute" ]; then 181 pactl set-sink-mute "$curSink" "yes" 182 elif [ "$1" = "unmute" ]; then 183 pactl set-sink-mute "$curSink" "no" 184 fi 185 186 if [ $OSD = "yes" ]; then showOSD "$curSink"; fi 187 } 188 189 190 function nextSink() { 191 # The final sinks list, removing the blacklisted ones from the list of 192 # currently available sinks. 193 if ! getCurSink; then 194 echo "PulseAudio not running" 195 return 1 196 fi 197 198 # Obtaining a tuple of sink indexes after removing the blacklisted devices 199 # with their name. 200 sinks=() 201 local i=0 202 while read -r line; do 203 index=$(echo "$line" | cut -f1) 204 name=$(echo "$line" | cut -f2) 205 206 # If it's in the blacklist, continue the main loop. Otherwise, add 207 # it to the list. 208 for sink in "${SINK_BLACKLIST[@]}"; do 209 if [ "$sink" = "$name" ]; then 210 continue 2 211 fi 212 done 213 214 sinks[$i]="$index" 215 i=$((i + 1)) 216 done < <(pactl list short sinks | sort -n) 217 218 # If the resulting list is empty, nothing is done 219 if [ ${#sinks[@]} -eq 0 ]; then return; fi 220 221 # If the current sink is greater or equal than last one, pick the first 222 # sink in the list. Otherwise just pick the next sink avaliable. 223 local newSink 224 if [ "$curSink" -ge "${sinks[-1]}" ]; then 225 newSink=${sinks[0]} 226 else 227 for sink in "${sinks[@]}"; do 228 if [ "$curSink" -lt "$sink" ]; then 229 newSink=$sink 230 break 231 fi 232 done 233 fi 234 235 # The new sink is set 236 pactl set-default-sink "$newSink" 237 238 # Move all audio threads to new sink 239 local inputs 240 inputs="$(pactl list short sink-inputs | cut -f 1)" 241 for i in $inputs; do 242 pactl move-sink-input "$i" "$newSink" 243 done 244 245 if [ $NOTIFICATIONS = "yes" ]; then 246 getNickname "$newSink" 247 248 if command -v dunstify &>/dev/null; then 249 notify="dunstify --replace 201839192" 250 else 251 notify="notify-send" 252 fi 253 $notify "PulseAudio" "Changed output to $SINK_NICKNAME" --icon=audio-headphones-symbolic & 254 fi 255 } 256 257 258 # This function assumes that PulseAudio is already running. It only supports 259 # KDE OSDs for now. It will show a system message with the status of the 260 # sink passed by parameter, or the currently active one by default. 261 function showOSD() { 262 if [ -z "$1" ]; then 263 curSink="$1" 264 else 265 getCurSink 266 fi 267 getCurVol "$curSink" 268 getIsMuted "$curSink" 269 qdbus org.kde.kded /modules/kosd showVolume "$VOL_LEVEL" "$IS_MUTED" 270 } 271 272 273 function listen() { 274 local firstRun=0 275 276 # Listen for changes and immediately create new output for the bar. 277 # This is faster than having the script on an interval. 278 pactl subscribe 2>/dev/null | { 279 while true; do 280 { 281 # If this is the first time just continue and print the current 282 # state. Otherwise wait for events. This is to prevent the 283 # module being empty until an event occurs. 284 if [ $firstRun -eq 0 ]; then 285 firstRun=1 286 else 287 read -r event || break 288 # Avoid double events 289 if ! echo "$event" | grep -e "on card" -e "on sink" -e "on server"; then 290 continue 291 fi 292 fi 293 } &>/dev/null 294 output 295 done 296 } 297 } 298 299 300 function output() { 301 if ! getCurSink; then 302 echo "PulseAudio not running" 303 return 1 304 fi 305 getCurVol "$curSink" 306 getIsMuted "$curSink" 307 308 # Fixed volume icons over max volume 309 local iconsLen=${#ICONS_VOLUME[@]} 310 if [ "$iconsLen" -ne 0 ]; then 311 local volSplit=$((VOLUME_MAX / iconsLen)) 312 for i in $(seq 1 "$iconsLen"); do 313 if [ $((i * volSplit)) -ge "$VOL_LEVEL" ]; then 314 VOL_ICON="${ICONS_VOLUME[$((i-1))]}" 315 break 316 fi 317 done 318 else 319 VOL_ICON="" 320 fi 321 322 getNickname "$curSink" 323 324 # Showing the formatted message 325 if [ "$IS_MUTED" = "yes" ]; then 326 # shellcheck disable=SC2034 327 VOL_ICON=$ICON_MUTED 328 echo "${COLOR_MUTED}$(eval echo "$FORMAT")${END_COLOR}" 329 else 330 eval echo "$FORMAT" 331 fi 332 } 333 334 335 function usage() { 336 echo "\ 337 Usage: $0 [OPTION...] ACTION 338 339 Options: 340 --autosync | --no-autosync 341 Whether to maintain same volume for all programs. 342 Default: $AUTOSYNC 343 --color-muted <rrggbb> 344 Color in which to format when muted. 345 Default: ${COLOR_MUTED:4:-1} 346 --notifications | --no-notifications 347 Whether to show notifications when changing sinks. 348 Default: $NOTIFICATIONS 349 --osd | --no-osd 350 Whether to display KDE's OSD message. 351 Default: $OSD 352 --icon-muted <icon> 353 Icon to use when muted. 354 Default: none 355 --icon-sink <icon> 356 Icon to use for sink. 357 Default: none 358 --format <string> 359 Use a format string to control the output. 360 Available variables: 361 * \$VOL_ICON 362 * \$VOL_LEVEL 363 * \$ICON_SINK 364 * \$SINK_NICKNAME 365 * \$IS_MUTED (yes/no) 366 Default: $FORMAT 367 --icons-volume <icon>[,<icon>...] 368 Icons for volume, from lower to higher. 369 Default: none 370 --volume-max <int> 371 Maximum volume to which to allow increasing. 372 Default: $VOLUME_MAX 373 --volume-step <int> 374 Step size when inc/decrementing volume. 375 Default: $VOLUME_STEP 376 --sink-blacklist <name>[,<name>...] 377 Sinks to ignore when switching. 378 Default: none 379 --sink-nicknames-from <prop> 380 pactl property to use for sink names, unless overriden by 381 --sink-nickname. Its possible values are listed under the 'Properties' 382 key in the output of \`pactl list sinks\` 383 Default: none 384 --sink-nickname <name>:<nick> 385 Nickname to assign to given sink name, taking priority over 386 --sink-nicknames-from. May be given multiple times, and 'name' is 387 exactly as listed in the output of \`pactl list sinks short | cut -f2\`. 388 Note that you can also specify a port name for the sink with 389 \`<name>/<port>\`. 390 Default: none 391 392 Actions: 393 help display this message and exit 394 output print the PulseAudio status once 395 listen listen for changes in PulseAudio to automatically update 396 this script's output 397 up, down increase or decrease the default sink's volume 398 mute, unmute mute or unmute the default sink's audio 399 togmute switch between muted and unmuted 400 next-sink switch to the next available sink 401 sync synchronize all the output streams volume to be the same as 402 the current sink's volume 403 404 Author: 405 Mario Ortiz Manero 406 More info on GitHub: 407 https://github.com/marioortizmanero/polybar-pulseaudio-control" 408 } 409 410 while [[ "$1" = --* ]]; do 411 unset arg 412 unset val 413 if [[ "$1" = *=* ]]; then 414 arg="${1//=*/}" 415 val="${1//*=/}" 416 shift 417 else 418 arg="$1" 419 # Support space-separated values, but also value-less flags 420 if [[ "$2" != --* ]]; then 421 val="$2" 422 shift 423 fi 424 shift 425 fi 426 427 case "$arg" in 428 --autosync) 429 AUTOSYNC=yes 430 ;; 431 --no-autosync) 432 AUTOSYNC=no 433 ;; 434 --color-muted|--colour-muted) 435 COLOR_MUTED="%{F#$val}" 436 ;; 437 --notifications) 438 NOTIFICATIONS=yes 439 ;; 440 --no-notifications) 441 NOTIFICATIONS=no 442 ;; 443 --osd) 444 OSD=yes 445 ;; 446 --no-osd) 447 OSD=no 448 ;; 449 --icon-muted) 450 ICON_MUTED="$val" 451 ;; 452 --icon-sink) 453 # shellcheck disable=SC2034 454 ICON_SINK="$val" 455 ;; 456 --icons-volume) 457 IFS=, read -r -a ICONS_VOLUME <<< "${val//[[:space:]]/}" 458 ;; 459 --volume-max) 460 VOLUME_MAX="$val" 461 ;; 462 --volume-step) 463 VOLUME_STEP="$val" 464 ;; 465 --sink-blacklist) 466 IFS=, read -r -a SINK_BLACKLIST <<< "${val//[[:space:]]/}" 467 ;; 468 --sink-nicknames-from) 469 SINK_NICKNAMES_PROP="$val" 470 ;; 471 --sink-nickname) 472 SINK_NICKNAMES["${val//:*/}"]="${val//*:}" 473 ;; 474 --format) 475 FORMAT="$val" 476 ;; 477 *) 478 echo "Unrecognised option: $arg" >&2 479 exit 1 480 ;; 481 esac 482 done 483 484 case "$1" in 485 up) 486 volUp 487 ;; 488 down) 489 volDown 490 ;; 491 togmute) 492 volMute toggle 493 ;; 494 mute) 495 volMute mute 496 ;; 497 unmute) 498 volMute unmute 499 ;; 500 sync) 501 volSync 502 ;; 503 listen) 504 listen 505 ;; 506 next-sink) 507 nextSink 508 ;; 509 output) 510 output 511 ;; 512 help) 513 usage 514 ;; 515 *) 516 echo "Unrecognised action: $1" >&2 517 exit 1 518 ;; 519 esac