Saturday, February 14, 2015

Sound volume change notifications using pynotify

As I started to use the i3 window manager I wanted to have a visual feedback on sound volume change events. Usually it's done by a daemon that binds to specific keyboard combinations to adjust volume and to send notifications to a notify daemon. In xfce this is done by the xfce4-volumed. I should also point here to other lightweight volume daemons like volnoti or pa-applet. Without a dedicated daemon it is also pretty much simple to handle sound volume keys by a simple bash script which is bind to specific keyboard combinations.

I've seen several examples of such a script the only problem with them was that notify-send command used to send notifications still does not return an id of a notification shown which results in several notification popups floating at the same time. As it's described here one could patch notify-send command so that it will return an id if asked but I came up with an easier solution using python-notify lib. Until patched notify-send is available in an official repository one could use this pynotify workaround. Here is an example of pynotify usage in a script to adjust sound volume:
$ cat bin/volume_notify.sh
#!/bin/bash

# $0 - icon_name, $2 - volume, $3 - id
print_cmd()
{
cat <<HEREDOC
import pynotify
pynotify.init("volume_notif.py")
notif=pynotify.Notification("Volume", "", "$1")
notif.set_hint_int32("value",$2)
notif.set_property("id", $3)
notif.show()
print notif.get_property("id")
HEREDOC
}

step=5
id_file="/tmp/notify_volume.id"

icon_high="/usr/share/icons/elementary-xfce/notifications/48/audio-volume-high.png"
icon_low="/usr/share/icons/elementary-xfce/notifications/48/audio-volume-low.png"
icon_medium="/usr/share/icons/elementary-xfce/notifications/48/audio-volume-medium.png"
icon_muted="/usr/share/icons/elementary-xfce/notifications/48/audio-volume-muted.png"
icon_off="/usr/share/icons/elementary-xfce/notifications/48/audio-volume-off.png"

case $1 in 
  up)
      amixer -q set Master $step+ ;;
  down)
      amixer -q set Master $step- ;;
  toggle)
      #pactl list sinks|grep -q Mute:.yes;pactl set-sink-mute 0 ${PIPESTATUS[1]} ;;
      # amixer needs -D pulse switch as a workaround for the pulseaudio
      # check http://goo.gl/4z4U6R
      amixer -q -D pulse set Master toggle ;;
  *)
      echo "Usage: $0 up/down/toggle"
      exit 1 ;;
esac

muted=$(amixer get Master | tail -n1 | sed -nr 's/.*\[([a-z]+)\].*/\1/p')
volume=$(amixer get Master | tail -n1 | sed -nr 's/[^\[]*.([^%]*).*/\1/p')
if ((volume==0)); then
  icon=$icon_off
elif ((volume<34)); then
  icon=$icon_low
elif ((volume<67)); then
  icon=$icon_medium
else
  icon=$icon_high
fi
if [[ $muted == "off" ]]; then
  icon=$icon_muted
fi

id=$(cat $id_file 2>/dev/null);id=${id:-0}
idn=$(python -c "$(print_cmd $icon $volume $id)")
(($idn != $id)) && echo "$idn" > "$id_file"
exit 0

Here pynotify module is used to send a notification and return its id. This id is stored in a temp file defined by id_file variable so that it can be accessed by another script process.

ps: lines 42-43 could be optimized to this:
val=$(amixer get Master | tail -n1 | sed -nr 'h;s/[^\[]*.([^%]*).*/\1/p;g;s/.*\[([a-z]+)\].*/\1/p')
muted=${val#*$'\n'}
volume=${val%$'\n'*}
This reduces number of created subprocesses two times


Edit 2020.11:

The script above uses older python lib which isn't available any longer. Additionally it controls amixer but pulseaudio should be used instead. Here is an updated version:
 
$ cat bin/volume_notify_pa.sh
#!/bin/bash

# this script requires python3-notify2 lib to be installed first

STEP='5%'

# some sound cards could have different MIN/MAX values 
# this isn't implemented yet as I can't test it
MIN_VOLUME=0
MAX_VOLUME=65536

# file where notification ids are stored
ID_FILE="/tmp/notify_volume.id"

# $1 - icon_name, $2 - volume, $3 - id
print_cmd()
{
cat <<HEREDOC
import notify2
notify2.init("volume_notify.py")
notif=notify2.Notification("Volume", "", "$1")
notif.set_hint_int32("value",$2)
notif.id = $3
notif.show()
print(notif.id)
HEREDOC
}

# input parameters are: $1 - volume, $3 - muted status
send_notification() {
  local icon id idn
  local volume_raw=$1
  local muted=$2

  local volume=$(( (100*(volume_raw-MIN_VOLUME))/(MAX_VOLUME-MIN_VOLUME) ))

  if ((volume==0)); then
    icon=$icon_off
  elif ((volume<34)); then
    icon=$icon_low
  elif ((volume<67)); then
    icon=$icon_medium
  else
    icon=$icon_high
  fi
  if [[ "$muted" == "yes" ]]; then
    icon=$icon_muted
  fi
  id=$(cat $ID_FILE 2>/dev/null);id=${id:-0}
  idn=$(python3 -c "$(print_cmd "$icon" "$volume" "$id")")
  (("$idn" != "$id")) && echo "$idn" > "$ID_FILE"
}

get_volume() {
  local VOLHEX=$(pacmd dump | awk '/^set-sink-volume/{printf("%s", $3)}')
  [ -n "$VOLHEX" ] || {
    echo "$MIN_VOLUME"
    return
  }
  printf '%d' $VOLHEX
}

# returns 'yes' or 'no'
get_muted() {
    MUTED=$(pacmd dump | awk '/set-sink-mute/{print $3}')
    printf "%s" "$MUTED" 
}


icon_high="audio-volume-high"
icon_low="audio-volume-low"
icon_medium="audio-volume-medium"
icon_muted="audio-volume-muted"
icon_off="audio-volume-off"

SINK=$(pactl info | awk '/Default Sink/{print $3}')
[ -n "$SINK" ] || exit 1

case $1 in
  up)
      pactl set-sink-volume  "$SINK" "+$STEP"
      [ "$(get_volume)" -gt $MAX_VOLUME ] && pactl set-sink-volume  "$SINK" "$MAX_VOLUME"        
      ;;
  down)
      pactl set-sink-volume  "$SINK" "-$STEP"
      [ "$(get_volume)" -le $MIN_VOLUME ] && pactl set-sink-volume  "$SINK" "$MIN_VOLUME"        
      ;;
  toggle)
      MUTED=$(pacmd dump | awk '/set-sink-mute/{print $3}')
      if [ "yes" = "$MUTED" ]; then
        pactl set-sink-mute "$SINK" 0
      else
        pactl set-sink-mute "$SINK" 1
      fi
      ;;
  *)
      echo "Usage: $0 up/down/toggle"
      exit 1 ;;
esac

send_notification "$(get_volume)" "$(get_muted)"

exit 0

1 comment:

Felipe Marinho said...

Thank you!!
Here is a modded version that worked for me..

$ cat volume.sh
#!/bin/bash

# $0 - icon_name, $2 - volume, $3 - id
print_cmd()
{
cat </dev/null);id=${id:-0}
idn=$(python2 -c "$(print_cmd $icon $volume $id)")
(($idn != $id)) && echo "$idn" > "$id_file"
exit 0