Friday, January 18, 2008

mediactrl: controlling laptop media buttons

My laptop media buttons don't do quite what I want. Specifically, I want

  • Volume changes to affect multiple channels.
  • A sound to play when I change volume.
  • Play/pause, forward, rewind, and stop to control whatever application I'm using, not just always the same one (e.g., pause the video if I'm watching a video, but the music if I'm listening to music).

So I first wrote a script for volume changing and media control, then told KDE to call it when I press media buttons.

Modifying multiple channels and playing a sound on volume changes is trivial. Controlling multiple media players is trickier. A heuristic that works well is to have a list of players that are controlled in order. In my case, I first list my video players, kaffeine and kplayer, then my music player, amarok. Thus, since amarok is always running, the media buttons control it by default. However, when I watch a video with kplayer, the media buttons control it instead because I listed it before amarok.

The player list, and other parameters, are in a config file:

/etc/mediactrl.conf
################################################################################
# Volume settings
################################################################################

# What percent to step on volume up/down
VOL_STEP_PERCENT=5

# The sound to play when volume is changed
VOL_NOTIFICATION_SOUND="/usr/share/sounds/KDE_Click.wav"

# The player to use when volume is changed (default is /usr/bin/aplay)
#VOL_NOTIFICATION_PLAYER="/usr/bin/artsplay"

# Comma-separated list of channels to alter (default is "PCM")
VOL_CHANNELS="Master, Master Mono, Headphone, PCM"


################################################################################
# Media player settings
################################################################################

# Comma-separated list of media players in the order in which they should be
# checked. (these need to be the actual command name). For each of these, the
# command to be run when play/pause, stop, prev, and next are pressed should be
# specified. Examples are below.
MEDIA_PLAYERS="kaffeine, kplayer, amarok"

kaffeine_playpause='dcop kaffeine KaffeineIface pause'
kaffeine_stop="dcop kaffeine KaffeineIface stop"
kaffeine_prev="dcop kaffeine KaffeineIface posMinus"
kaffeine_next="dcop kaffeine KaffeineIface posPlus"

kplayer_playpause="dcop kplayer kplayer-mainwindow#1 activateAction player_pause"
kplayer_stop="dcop kplayer kplayer-mainwindow#1 activateAction player_stop"
kplayer_prev="dcop kplayer kplayer-mainwindow#1 activateAction player_backward"
kplayer_next="dcop kplayer kplayer-mainwindow#1 activateAction player_forward"

amarok_playpause="dcop amarok player playPause"
amarok_stop="dcop amarok player stop"
amarok_prev="dcop amarok player prev"
amarok_next="dcop amarok player next"

For each player, the config specifies commands to execute for each media button. Since I happen to use all KDE apps, these commands are calls to dcop. The config should be placed in /etc/mediacntrl.conf.

The script itself is below:

mediactrl
#!/bin/bash

function usage() {
  echo "Usage: mediactrl "
  echo "  playpause    - toggles play/pause on active media player"
  echo "  stop         - stops play on active media player"
  echo "  prev         - goes to previous on active media player"
  echo "  next         - goes to next on active media player"
  echo "  volume  - alters volume.  is up, down, mute, unmute, or toggle"
  exit
}

if [ ! -f /etc/mediactrl.conf -o ! -r /etc/mediactrl.conf ]; then
   echo "Cannot read /etc/mediactrl.conf!" >&2
   exit 1
fi
. /etc/mediactrl.conf

case "$1" in
   playpause|stop|prev|next)
       echo $MEDIA_PLAYERS | perl -ne 's/,\s*/\n/g; print' | while read PLAYER; do
           if pgrep -u "$USER" "$PLAYER" > /dev/null; then
               eval CMD="\${${PLAYER}_${1}}"
               [ -n "$CMD" ] && eval "$CMD"
               break
           fi
       done
       ;;
   volume)
       case "$2" in
           up|down)
               echo ${VOL_CHANNELS:=PCM} | perl -ne 's/,/\n/g; print' | while read CHANNEL; do
                   if [ -z "$VOL" ]; then
                       VOL=$(amixer get "$CHANNEL" | grep % | head -n 1 | sed -e 's/.*\[\(.*\)%\].*/\1/')
                       PLUS_MINUS=$(if [ "$2" = "up" ]; then echo "+"; else echo "-"; fi)
                       VOL=$((VOL $PLUS_MINUS ${VOL_STEP_PERCENT:=5}))
                       [ $VOL -lt 0 ] && VOL=0
                       amixer set "$CHANNEL" "$VOL%" > /dev/null
                       dcop kded kmilod displayProgress "Volume " "$VOL"
                   else
                       amixer set "$CHANNEL" "$VOL%" > /dev/null
                   fi
               done
               ;;
           mute|unmute|toggle)
               echo ${VOL_CHANNELS:=PCM} | perl -ne 's/,/\n/g; print' | while read CHANNEL; do
                   amixer set "$CHANNEL" "$2" > /dev/null
               done

               CHANNEL=$(echo $VOL_CHANNELS | sed -e 's/,.*$//')
               MUTE=$(amixer get "$CHANNEL" | grep % | head -n 1 | sed -e 's/.*\[\(.*\)\]/\1/')
               dcop kded kmilod displayText "Sound $MUTE"
               ;;
           *) usage;;
       esac

       [ -n "$VOL_NOTIFICATION_SOUND" ] && eval "${VOL_NOTIFICATION_PLAYER:=/usr/bin/aplay}" "$VOL_NOTIFICATION_SOUND" 1>/dev/null 2>&1
       ;;
   *) usage ;;
esac

The bit of craziness with setting volumes ensures all channels are truly set to the same level.

Note that this script was written for a laptop running Kubuntu, and may need tweaking for other settings. For example, it uses kmilo to display a volume bar, which will not work unless the kmilo service is running.

Once this script is executable (chmod u+x mediactrl), I map it to the laptop media buttons through the KDE Accessibility settings (K Menu -> System Settings -> Accessibility -> Input Actions). Voila! More intelligent media buttons.

8 comments:

travisn000 said...

..Can you expand on:
"I map it to the laptop media buttons through the KDE Accessibility settings"

What action type and settings/conditions have to be specified?

For example for volume up, what action type and settings/conditions do I specify?

Pedro DeRose said...

travisn000 said...Can you expand on: "I map it to the laptop media buttons through the KDE Accessibility settings"

Sure. I did the following.

1. Go to K->System Settings->Accessibility, then choose "Input Actions" on the left.
2. Click the "New Group" button along the bottom, then rename the group to "Media Keys" (on the right).
3. Click the "New Action" button. Rename the action to "Next", and choose "Action type" as "Keyboard Shortcut -> Command/URL (simple)" from the drop-down menu (all on the right). This changes the available tabs along the top.
4. Click on the "Keyboard Shortcut" tab, then on the "None" button, and press the keyboard button you want to map to mediactrl's "next" action.
5. Click on the "Command/URL Settings" tab, and set the command to "/path/to/mediactrl next" (of course, replace /path/to/mediactrl with the actual path to mediactrl).
6. Repeat steps 3-5 for the other mediactrl commands.
7. Click the "Apply" button at the very bottom, and everything should work.

travisn000 said...
This comment has been removed by the author.
travisn000 said...

Thanks..

I had a bit of trouble with key mapping, but then I found your post on this.

Thanks Again!

travisn000 said...

..still cannot get it to work :(

I mapped my media keys to XF86AudioMute, etc, and added Input actions in KCC as described..

Can I run mediactrl volume up or down from Konsole (how)? I ran /etc/mediactrl playpause in console and got:

ERROR: Couldn't attach to DCOP server!

I'm using PCLinuxOS if it matters.

Pedro DeRose said...

Can I run mediactrl volume up or down from Konsole (how)?

You sure can. And you're right to do so for debugging. mediactrl is a script, so I'd suggest putting it in /usr/local/bin instead of /etc (though mediactrl.conf should still go in /etc). Then, you can type
/usr/local/bin/mediactrl playpause into Konsole,
for example. This will run one of the playpause commands you specify in /etc/mediactrl.conf.

Since mediactrl just runs commands, I'd first check that the command works. For example, with amarok running, type dcop amarok player playPause into Konsole. That should make amarok start playing. Once it does, since that's set as amarok_playpause in mediactrl.conf, then mediactrl should work.

audiodef said...

I just tried this, but when I hit, say, the "next" button, the mediactrl file gets opened in a text editor. And even though I just emerged kmilo, I'm not getting any OSD (someone told me it seems to be broken for Dell laptops). Have you run into anything like that?

Pedro DeRose said...

when I hit, say, the "next" button, the mediactrl file gets opened in a text editor

Try making sure that mediactrl is executable by chmoding it correctly. If you can run it from the command line, it should work fine.

I'm not getting any OSD (someone told me it seems to be broken for Dell laptops)

I haven't had any trouble getting it to work myself. Make sure you've turned on the kmilo service in your System Settings. That may make a difference.