Keyboard Volume Control with Triggerhappy

Joseph FerrisHow-To0 Comments

One feature that is common in cabinets is the inclusion of some sort of simple volume control. Some of the examples that I have seen involved simply exposing an amplifiers control panel through the back of the cabinet, custom wiring a mounted control knob, or providing buttons which are mapped to a keypress. The latter is the most alluring to me, and the one that I will focus on getting working. What I originally expected to be just a minor project, though, this turned out to be more than I expected. Despite all of the improvement in Linux, sound can sometimes still be a bit of a pain point.

Volume from an End User’s Perspective

We are all end users to someone.  It is very easy to get drawn down the path of developing a solution to a problem that does not make any sense to another person.  Controlling the sound on an arcade cabinet is no different.  Neither RetroPie nor EmulationStation will show you the current volume that you have set.  Instead, most people will simply go to the RetroPie configuration for sound, and drop into alsamixer.  It works, but it is not really an elegant solution.  When my cabinets are done, I do not want anyone who is using to associate it with being a computer, requiring administration, or having any sort of a “learning curve” to use.  Volume control is a convenience feature, and often the need to adjust volume requires a means to change volume quickly.

Where that leaves me is determining what the ideal volume controls are for a cabinet.  At the very least, I would like to be able to:

  • Press a button to either mute or unmute the playback on the cabinet.
  • Press a button to turn the volume down by an incremental value.
  • Press a button to turn the volume up by an incremental value.

Button presses will ultimately be mapped through a keyboard encoder to my Raspberry Pi.  Since I have not built my control panel yet, I should be able to map these to the keyboard and then only require slight modifications, if any at all, once I start mapping my encodings.  RetroPie includes a package that facilitates this already, as well.

Enter Triggerhappy

Triggerhappy is a simple daemon that monitors input devices and then performs an action for any mapped input entering into a specific state.  It comes pre-installed in the RetroPie image, although it does require a small modification to get it working properly.  Many people target Triggerhappy as a package to uninstall when removing unneeded packages to free up resources.  So, if it is not installed, the following command line will take care of that:

sudo apt-get triggerhappy

After Triggerhappy is installed, the configuration needs to be modified so that it has proper privileges within the RetroPie ecosystem.  By default, Triggerhappy will spawn its daemon under the nobody account.  To correct this, open the /etc/init.d/triggerhappy configuration file:

sudo nano /etc/init.d/triggerhappy

Specifically, the DAEMON_ARGS need to be modified to reflect the correct –user parameter.  Once modified, the line should look similar to this:

DAEMON_ARGS="--daemon --triggers /etc/triggerhappy/triggers.d/ --socket /var/run/thd.socket --pidfile $PIDFILE --user pi /dev/input/event*"

The two most important things to note, in terms of how the daemon is spawned, is that the –user is now set to pi, which is the main user account that most work is done on a Raspberry Pi under, and that the final argument indicates which input devices to listen on.  The default of /dev/input/event* will listen to input coming in for all events on any device.  For my purposes, this is fine, for now.  However, in the future, if I wanted to only listen to a keyboard encoder and ignore the physical keyboard, I could do that by being more specific to which event identifier I am listening for.  A keyboard, for example, could be excluded so that it would not register any actual keypresses, while still performing an action for the same mapped keypresses through the encoder.

After modifying and saving /etc/init.d/triggerhappy, we need to actually figure out what we are trying to capture.  Most modern keyboards have a generic volume up, volume down, and mute button.  To see these keys as Triggerhappy sees them, we can invoke a foreground instance of the daemon, specifically to dump those values out to stdout.

thd --dump /dev/input/event*

With the daemon running, simply press the keys that we want to capture, and then press CTRL+C to break out of the daemon.  The lines of the output that we are interested in will look like this:

EV_KEY  KEY_VOLUMEDOWN  1       /dev/input/event3
EV_KEY KEY_VOLUMEDOWN 0 /dev/input/event3
EV_KEY  KEY_VOLUMEDOWN  1       /dev/input/event3
EV_KEY KEY_VOLUMEDOWN 0 /dev/input/event3
EV_KEY  KEY_MIN_INTERESTING  1       /dev/input/event3
EV_KEY KEY_MIN_INTERESTING 0 /dev/input/event3

These are keys that are captured as events, as shown by the EV_KEY designation. Immediately following that is the key code, which identifies the name of the key, as known to Triggerhappy. Next, is the key state, with 1 being that the key was pressed and 0 being that it was released. Additionally, a value of 2 would show that the key was being held down. Finally, the last value in each row shows which physical device captured the event.

The most interesting bit of data is the key code. KEY_VOLUMEUP, KEY_VOLUMEDOWN, and KEY_MIN_INTERESTING are the three keys that we want to map. To map them, we will need to create a configuration file that Triggerhappy will read when the daemon is spawned. Triggerhappy has a directory that it probes on startup of the daemon, and will include all configuration under that folder which it finds. This allows for configurations to be targeted for their specific purposes, if desired.

sudo nano /etc/triggerhappy/triggers.d/audio.conf

This will create a brand new configuration file called audio.conf. We will add three lines to this configuration, specifying that amixer should turn the volume up, turn the volume down, and toggle the mute state.

KEY_VOLUMEUP 1 amixer sset PCM,0 5dB-
KEY_VOLUMEDOWN 1 amixer sset PCM, 0 5dB+
KEY_MIN_INTERESTING 1 amixer sset PCM,0 1+ toggle

Note that PCM and the device identifier of 0 are specific to my machine, and may vary from system to system. The correct values for a given system can easily be found through just running amixer from the command line with no arguments. It should show every device name and identifier. After making sure that the correct values are in the file, save it and restart the daemon to pick up the changes.  Easiest way to do so is:

sudo service triggerhappy restart

If everything worked, pressing the same keys on the keyboard will now turn volume up, down, and toggle mute. In my case it only worked sporadically on my first attempt. After spending a long time researching the issue, I determined that having PulseAudio installed had been interfering with not just this, but my overall system stability. Considering that the package is not pre-installed on RetroPie should have been a clear indicator that there were possibly some issues in using it. After removing it, though, my stability issues subsided, and I could control my volume through my keyboard.

Taking it a Step Further

While it works, I still found this to be a sub-optimal solution.  As an end user, I did not know what the current volume was at.  If I am sitting in EmulationStation, and mash the volume up key on my keyboard and then start a game, for example, would it be too loud?  Also, if the system is muted and I press volume up or volume down, it does not actually do anything unless I explicitly umute.  Those are the sorts of things that an end user would expect and that is actually quite similar to how a television remote would respond.

Given that Triggerhappy has permission to execute something, it does not matter what is mapped to a key.  It can be mapped to an application, a script, or a simple command.  If you wanted to irritate someone and make it perform a sudo shutdown everytime a capital T is pressed, not a problem.  So, to that end, I wanted to make it respond a bit more like the ever-familiar television remote control.  This means that I am adding a few requirements to what I expect of a keypress:

  • When incrementing or decrementing the volume, play a signal tone at the current audio level to illustrate what the current volume level is.
  • When incrementing or decrementing the volume from a muted state, umute and adjust the audio level as if it were not muted.

I was able to accomplish this with a fairly simple script, which I call pi-mix.  First, I created a folder called /home/pi/pi-mix, which puts it on a sibling level with RetroPie and RetroPie-Setup.  I provisioned a child folder under there, called /home/pi/pi-mix/sounds, where I can From there, I assured that the pi user had ownership of the folder.

mkdir /home/pi/pi-mix
mkdir /home/pi/pi-mix/sounds
sudo chown pi:pi /home/pi/pi-mix

With those folders now visible to Triggerhappy, I created a bash script called /home/pi/pi-mix/pi-mix.sh.

#!/bin/bash

# The name of the sound device to control, such as "Master", "PCM", etc.
DEVICE="PCM"

# The amounts to decrease and increase volume by for each volume change 
# command.  These are relative values, either in range or dB, such as "500+"
# or "20dB-".
VOLUME_DECREMENT="534-"
VOLUME_INCREMENT="534+"

# File lookup locations.  $MUTE_FILE and $RESUME_FILE are used to preserve state
# between invocations.  $TONE_FILE, when the file exists, will be used to play a
# signal tone when the volume goes either up or down.
MUTE_FILE="/home/pi/pi-mix/.mute_status.pa"
RESUME_FILE="/home/pi/pi-mix/.resume_volume.pa"
TONE_FILE="/home/pi/pi-mix/sounds/ping.mp3"

# Retrieves the current volume, as specified by amixer.
function get_current_volume() {
  echo $(awk -F"[][]" '/dB/ { print $2 }' <(amixer sget $DEVICE))
} 

# Retrieves the current mute status, as indicated with $MUTE_FILE. 
function get_mute_status() { 
  default_value="OFF"
  last_status=""

  if [ ! -f $MUTE_FILE ]; then 
    set_mute_status $default_value
    last_status=$default_value
  else
    last_status=$(cat $MUTE_FILE)
  fi
  
  echo $last_status
}

# Retrieves the absolute percentage value which an umute will resume at, as # indicated with $RESUME_FILE.
function get_resume_volume() {
  default_value=$(get_current_volume)
  last_value=""

  if [ ! -f $RESUME_FILE ]; then
    set_resume_volume $default_value
    last_value=$default_value
  else
    last_value=$(cat $RESUME_FILE)
  fi

  echo $last_value;
}

# Displays the current volume and mute status.
function get_status() {
  mute_status=$(get_mute_status)
  echo "Mute: $mute_status"

  if [ "$mute_status" == "ON" ]; then
    resume_level=$(get_resume_volume)
    echo "Resume Volume: $resume_level"
  else
    line_level=$(get_current_volume)
    echo "Volume: $line_level" fi }

# When $TONE_FILE exists, plays the file on a background thread.
function play_tone() {
  if [ -f $TONE_FILE ]; then 
    mpg123 -q $TONE_FILE&
  fi
}

# Sets the current volume, in either terms of an absolute volume, or an incremental down/up.
function set_current_volume() {
  case $1 in 
    "absolute")
      amixer -q sset $DEVICE $2 ;;
    "down")
      if [ "$(get_mute_status)" == "ON" ]; then 
        set_mute_status OFF
      fi
      amixer -q sset $DEVICE $VOLUME_DECREMENT
      play_tone ;;
    "up")
      if [ "$(get_mute_status)" == "ON" ]; then
        set_mute_status OFF
      fi
      amixer -q sset $DEVICE $VOLUME_INCREMENT
      play_tone ;;
  esac
}

# Given the requested mute status, save the new status to $MUTE_FILE and set
# absolute volume to either 0% to mute, or to the value from the last
# $RESUME_FILE to unmute.
function set_mute_status() {
  target_status=$1
  if [ "$target_status" == "TOGGLE" ]; then
    current_status=$(get_mute_status);
    if [ "$current_status" == "ON" ]; then
      target_status="OFF"
    else
      target_status="ON"
    fi
  fi
  echo $target_status > $MUTE_FILE
  chmod 0666 $MUTE_FILE
    
  if [ "$target_status" == "ON" ]; then
    set_resume_volume $(get_current_volume)
    set_current_volume absolute 0%
  else
    set_current_volume absolute $(get_resume_volume)
  fi
}

# Set the $RESUME_FILE volume to the provided value.
function set_resume_volume() {
  echo $1 > $RESUME_FILE
  chmod 0666 $RESUME_FILE
}

case $1 in
  # Usage: mute on / mute off / mute toggle
  "mute")
    set_mute_status $(echo $2 | tr 'a-z' 'A-Z')
    get_status
  ;;
  # Usage: status
  "status")
    get_status       
  ;;
  # Usage: tone
  "tone")
    play_tone
  ;;
  # Usage: volume up, volume down, volume absolute 100%
  "volume")
    set_current_volume $2 $3
    get_status
  ;;
esac

Overall, very little should need to be changed in this script to getting it working with other audio devices.  There are four variable declarations inside the script that are worth mentioning, explicitly.

The device name is extracted out to a variable so that it can be easily changed, without having to hunt down all instances in the script.

# The name of the sound device to control, such as "Master", "PCM", etc.
DEVICE="PCM"

The volume decrement and increment values specify the actual amount of change caused by volume down or volume up key presses.  For the default values, I took the total range of my playback device, as seen in amixer, and divided it by 20.  This gives my twenty graduations of sound, or the equivalent of 5% overall change for each step.

# The amounts to decrease and increase volume by for each volume change
# command. These are relative values, either in range or dB, such as "500+"
# or "20dB-".
VOLUME_DECREMENT="534-"
VOLUME_INCREMENT="534+"

Finally, the tone file is optional, but recommended.  Any short indicator sound would be adequate here.  The same tone is played for volume down and volume up, although it would be a fairly simple change to introduce another sound file to give each operation its own sound file.

TONE_FILE="/home/pi/pi-mix/sounds/ping.mp3"

Overall, the script is very straightforward, and in need of a bit of argument checking.  However, it fully accomplishes all of the goals that I currently have.  Make sure that the script is executable, with a chmod +x /home/pi/pi-mix/pi-mix.sh, and then it can perform a few simple tasks, given the correct arguments.  The main features of this script are addressable through the four main command switches, being “mute”, “status”, “tone”, and “volume”.

pi-mix.sh mute

There are three additional arguments that can be specified to pi-mix.sh mute, which are “on”, “off”, or “toggle”.  The latter is most appropriate for binding to Triggerhappy, with the first two being helpful for manually testing from the command line.

To mute playback:
# Force the mute state to on, whether it is currently on or not.
pi-mix.sh mute on
To unmute playback:
# Force the mute state to off, whether it is currently on or not.
pi-mix.sh mute off
To toggle playback without knowing state:
# Switch the mute state to be the opposite of what it currently is.
pi-mix.sh mute toggle

pi-mix.sh status

No additional arguments are required for pi-mix.sh status.  It will simply return output to stdout to indicate if the audio is muted or not, as well as the volume level.  When muted, it shows the volume level that will be resumed when audio is unmuted.  Otherwise, it shows the active audio level, as a percentage.  This is primarily useful for testing from the command line.

pi-mix.sh tone

No additional arguments are required for pi-mix.sh tone.  When a tone file is specified, it will play the given tone at the current volume, and respective of the mute state.  This is primarily useful for testing from the command line.

pi-mix.sh volume

There are three additional arguments that can be specified for pi-mix.sh volume.  These are “absolute”, “down”, and “up”.  The former is mostly useful for command line testing, while the latter two are candidates for binding to Triggerhappy.

To set an absolute volume:
# Set the audio level to 50%, regardless of the current value.
pi-mix.sh absolute 50%
To turn the volume down by the predefined decrement value:
# Adjust the audio level downwards by the predefined decrement value, not to fall below 0% of total range.
pi-mix.sh volume down
To turn the volume up by the predefined increment value:
# Adjust the audio level upwards by the predefined increment value, not to exceed 100% of total range.
pi-mix.sh volume up

For all of these commands, the easiest way to test is to issue the command as it would be expected to be issued from Triggerhappy, and then immediately following with a pi-mix.sh status to verify the expected result.

Modifying /etc/triggerhappy/triggers.d/audio.conf

Since Triggerhappy does not make it clear, when it is unable to run a command, it is best to run the full command lines that Triggerhappy will run, exactly as they will be entered.  To ensure that no relative path issues exist, I switch to the root folder, and then test each command.

cd /
bash /home/pi/pi-mix/pi-mix.sh volume up
bash /home/pi/pi-mix/pi-mix.sh volume down
bash /home/pi/pi-mix/pi-mix.sh mute toggle

Once I am satisfied that running those commands, and checking them with a pi-mix.sh status, are yielding the correct results, I modify /etc/triggerhappy/triggers.d/audio.conf to reflect those changes.

KEY_VOLUMEUP 1 bash /home/pi/pi-mix/pi-mix.sh volume up
KEY_VOLUMEDOWN 1 /home/pi/pi-mix/pi-mix.sh volume down
KEY_MIN_INTERESTING 1 /home/pi/pi-mix/pi-mix.sh mute toggle

Then, one last time, I restart Triggerhappy.

sudo service triggerhappy restart

Now, I have a completely working solution.  Mute toggling works, changing volume forces the mute state to be off/unmuted, and I have aural confirmation of the volume change through a simple tone.  Additionally, even though I do not have my control panel done yet, this approach will be completely reusable with whatever keyboard encoder that I decide to wire up.

If there is one thing that is missing that I wish that I did have, it would be some sort of indicator that the volume is currently muted.  At least for an actual keyboard, there is no clear way to show this, though.  However, when it comes to the actual control panel, it would be trivial to hook some sort of indicator light up to the GPIO and whack it into this script.

Related Posts

Sample Output from Focus Attack SCAN.DL VGA Scanli... When I had put together the Adding a Scanline Generator article, a little over a week ago, I had hoped on quickly capturing some sample output.  While...
Adding a Scanline Generator One of the most immediate feelings that hit me when I was emulating on a computer was not that I missed the era-correct controls for game, but rather ...
Keyboard Volume Control with Triggerhappy was last modified: March 7th, 2017 by Joseph Ferris

Comments on Keyboard Volume Control with Triggerhappy