Building a Command Line Alarm Clock for Linux with Bash
I've often wanted my computer to act as my alarm. It would need to automatically resume from suspend at a specified time and begin playing music with gradually increasing volume.
The complete script can be found on GitHub, at this Gist
The Script, Explained Bit by Bit
Instead of dumping the entire script in one obnoxious block, I'm inclined to walk you through it, section by section, function by function. This, I should think, will provide a more interesting and educational read.
Default Values
Usually, we humans are fairly regular and scheduled (or should be), thus we begin our script with default values, as follows:
#!/bin/bash
__script_version="1.2"
#-----------------------------------------------------------------------
# Default values
#-----------------------------------------------------------------------
human_time="6:20 tomorrow"
media_url="http://live.toker.fm:6060/"
In case Bash is new to you (maybe read some introductory materials first) variables are defined by a bare word follow immediately by an equal sign then immediately again by the value.
http://live.toker.fm:6060/
is the stream URL for the Internet radio station toker.fm
Gradual Volume
Here we have a function, which we'll call later, to gradually increase the volume. We'll call it using gradual &
to ensure it isn't blocking and the script can continue while it runs.
#=== FUNCTION ================================================================
# NAME: gradual
# DESCRIPTION: Increase the volume gradually to 100.
#===============================================================================
function gradual ()
{
ponymix set-volume 10 > /dev/null
current_volume=$(ponymix get-volume)
while [[ current_volume -lt 100 ]]
do
sleep 0.5
current_volume=$(ponymix increase 1)
done
} # ---------- end of function gradual ----------
The gradual
function is simple. We set the volume to 10%, then loop while the volume is less than 100%, increasing the volume 1% every half a second.
For those new to Bash, we use > /dev/null
to do IO redirection and send the output to oblivion, otherwise known as the null device, which is conveniently accessible via the file located at /dev/null
.
ponymix
In order to control the volume from the shell, we need a command line mixer program. I chose ponymix because I use PulseAudio and it was the first option I saw. You could also use pulseaudio-ctl.
If you use something other than PulseAudio, like Alsa, you'll need to figure that out on your own. Perhaps try this advice
Fallback
In the case where the media can't be played, for whatever reason, we use the fallback function to produce a series of jarring beeps and boops, sure to awaken you from even the deepest of slumbers.
#=== FUNCTION ================================================================
# NAME: fallback
# DESCRIPTION: Play obnoxious speaker-test tones.
#===============================================================================
function fallback ()
{
got_input=142
until [ $got_input -eq 0 ]
do
frequency=$(shuf -i 200-600 -n 1)
speaker-test -f $frequency -t sine -l 1 > /dev/null &
disown $! # avoid useless kill messages
duration=$(echo "$(shuf -i 5-25 -n 1) * 0.1" | bc)
read -t $duration # play beep for .5 to 2.5 seconds
got_input=$?
pkill -9 --ns $$ speaker-test
done
} # ---------- end of function fallback ----------
Fallback is the most complex code in this script, so bear with me, and if you grasp this, the rest will be calm waters.
We begin by acquiring a frequency with shuf
. In this context, I am using shuf
to shuffle a range of integers with the -i
option, and pull out only the first one with the -n 1
option. This acts like a random number generator.
At this point we use the speaker-test
command with the -t sine
option to produce a tone at the given frequency. Boom we've got sound!
Now we disown
the speaker-test
process. Otherwise, when I kill -9
it, my shell agressively informs me the process has been axed, and I'm trying to avoid extraneous output.
By default, speaker-test
tones play for far too long, so we need to generate a duration. We once again leverage shuf
, but this time we want a floating point value. For that we use the bc
command, piping in a string containing some multiplication.
Finally, we use that duration to pause waiting for user input. When we receive it, or the timer runs out, we agressively kill the running instances of "speaker-test" that belong to the current scope, by using the --ns $$
option.
If we didn't receive input, we try again with another irritating tone of a different frequency and duration.
Arguments
In case we don't want to use the default values, defined above, we include the option to pass the script explicit media and time values.
#=== FUNCTION ================================================================
# NAME: usage
# DESCRIPTION: Display usage information.
#===============================================================================
function usage ()
{
echo "Usage : $0 [options] [--]
Options:
-h|help Display this message
-v|version Display script version
-m|media The audio url, to be played by mpv
-t|time The human readable time and date for the alarm"
} # ---------- end of function usage ----------
#-----------------------------------------------------------------------
# Handle command line arguments
#-----------------------------------------------------------------------
while getopts ":hvm:t:" opt
do
case $opt in
h|help ) usage; exit 0 ;;
v|version ) echo "$0 -- version $__script_version"; exit 0 ;;
m|media ) media_url=$OPTARG ;;
t|time ) human_time=$OPTARG ;;
* ) echo -e "\n Option does not exist : $OPTARG\n"
usage; exit 1 ;;
esac # --- end of case ---
done
shift $(($OPTIND-1))
I honestly don't entirely understand this. It was mostly auto-completed for me by YouCompleteMe. If you want to know more about how getopts works, try this tutorial, or search on the Googles.
Suspend and Resume
For my purposes, I want my computer to sleep through the night, just like me. However, because time repeats, I need to make sure I know in exactly how long the alarm will sound.
#-----------------------------------------------------------------------
# Suspend the machine, and wake at the given time
#-----------------------------------------------------------------------
unix_time=$(date +%s -d "$human_time")
seconds=$(($unix_time - $(date +%s)))
if [ $? -ne 0 ]
then
exit 1
fi
hours_minutes="$(($seconds / 3600)) hours and $((($seconds / 60) % 60)) minutes"
read -p "Set alarm for $hours_minutes from now? [y/n] " go
if [ "$go" == n ]
then
exit 0
fi
sudo rtcwake -m mem -t $unix_time > /dev/null
sleep 30 # give time to restore network connectivity
The line defining our unix_time
variable, is one of the few places you'll notice > /dev/null
is missing. This is intentional. I actually want any errors to print to the console before we exit 1
. After all, for wrong media we have a fallback, but there's no fallback for wrong time.
The date command, for our purposes, takes a human readable date and time, like "7 tomorrow" or "8 hours", and transform it into Unix time, by using the +%s
(seconds since 1970-01-01 00:00:00 UTC) format option.
We also need to make sure we are setting the alarm for the correct time. We do so by calculating the difference between the given time and now. We then display it in a readable hour minute format and request confirmation.
At this point we use rtcwake to suspend, and automatically resume, by leveraging the power of ACPI Wakeup. While this is a complex subject, using rtcwake is very simple.
It requires three things: to run as root, which we can accomplish using sudo; the type of suspend to activate, which for us is suspend to RAM (mem); and to time to awaken the computer, given in Unix time.
Playing Our Media
After executing the alarm, we'll want to restore the volume, so first we save it. Only afterward do we run our gradual
function in the background.
#-----------------------------------------------------------------------
# Set the volume and play our media
#-----------------------------------------------------------------------
saved_volume=$(ponymix get-volume)
gradual &
mpv --no-terminal $media_url &
We use mpv, a fork of mplayer with simplified command line ergonomics among other improvements, to play our media. Because we don't want any output we use --no-terminal
which ensures no terminal output whatsoever.
Activating Fallback Mode
At this point we want to wait for one of two things to occur. Either for mpv to fail, or for user input. If mpv fails, we'll move to our fallback. The question becomes, how can we wait for both occurrences.
This evolved into an involved discussion on stackoverflow, wait for process to finish, or user input.
Basically we loop, and if we receive user input, we kill mpv, and we're done. Otherwise, if we don't receive user input, yet discover mpv is dead, we active our fallback.
#-----------------------------------------------------------------------
# If mpv fails, use our fallback
#-----------------------------------------------------------------------
got_input=142
while true; do
if read -t 1; then
got_input=$?
pkill --ns $$ mpv > /dev/null
break
elif ! pgrep --ns $$ mpv > /dev/null; then
break
fi
done
if [ $got_input -ne 0 ]
then
gradual &
fallback
fi
Wrapping Up
To finish, we reset our volume and greet the user with an overly chipper message, sure to annoy them into alertness.
ponymix set-volume $saved_volume > /dev/null
echo "Good Morning! ;)"
Enjoy!