scripts

Utilitity scripts
git clone git://gtms.dev:scripts
Log | Files | Refs

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