Command-line hacking: paced breathing
This is another in my series of articles on doing unusual and, perhaps, interesting things with Linux command-line tools and scripts. This month's example is, perhaps, even more unusual that most -- a shell script to guide paced breathing exercises. Paced breathing is simply breathing with a timer -- a fixed time for breathing in, and a fixed time for breathing out. A common protocol is to breath in for four seconds, and then out for six seconds, to give a respiration rate of six breaths per minutes.
Exercises of this kind are often recommended for people who suffer from stress-related disorders, or just for general relaxation. Paced breathing apps for smartphones are widely available at low cost -- even at no cost, if you're prepared to tolerate the advertising. However, I thought it would be interesting to implement something for the Linux terminal, using just a shell script.
This script generates a kind of moving bargraph display, that provides
visual cues about how to time your breaths. If you have sox
and
aplay
available, it will also generate rising and falling
audible tones, so it can be used without looking at the terminal.
Although simple, the utility demonstrates some interesting scripting features, particularly around arithmetic.
I haven't included the full source code in this article, because it's
too long. If you're interested, the full source
for paced_breathing
is available
from my GitHub repository.
Basic principle
The principle is simple: decide how wide the terminal bar-graph display will be, in terms of character cells. Divide the total length of the breath by that number of cells, to give a time between terminal updates. On each update, draw a new character in the bargraph, and sleep for the calculated time. Continue until the end of the breathing cycle, then start a new line. Repeat indefinitely. For audio playback (more on this below), start the playback of a pre-computed audio file at the start of each breathing cycle.
Arithmetic
The Bash shell does not support floating-point, or even fixed-point arithmetic. The simple solution to this problem ought to be to do all the timing in milliseconds, in the hope that all the math can be done as integers. Unfortunately, Bash does not provide any millisecond-level timing functions, and we need to be able to update the display at intervals of significantly less than one second, in order for the bargraph to get drawn smoothly.
There is not even a generally-available Linux command-line utility that
delays for a number of milliseconds or microseconds. Fedora does
have usleep
, but
it is part of a deprecated software package. The standard
sleep
utility will delay for a number of milliseconds -- but
it requires an argument as a fractional number of seconds. So there is
a need for at least a little fractional arithmetic in this utility.
We could have the script delegate arithmetic
to a utility like bc
. However, I thought it would be fun
to implement fractional division in the shell script itself. It's
practicable to take such an approach because the only place the script
uses non-integer match is when providing a number as an argument to
other utilities (sleep
and sox
in this
case).
A limitation of this approach, though, is that the script can't actually work with non-integer numbers in its own calculations. So, for example, the breathing times can only be specified as whole numbers of seconds. Still, this is a demonstration of shell scripting, not a fully-featured product, so I can live with this limitation.
To implement non-integer division in a script, the basic approach is
to scale the numerator by a large power of ten, and then divide by the
denominator. For example, to divide 20/3, we might scale by a factor of
100,000, and then divide the resulting 2,000,000 by 3.
The result is 666,666 (since Bash
does this division using integers). Because this result is 100,000
times too large, we must shift the decimal point five places to the
left, to give 6.6666. This kind of string manipulation is well within
the scope of a Bash script (see the div
function
in the source code). The choice of scaling factors needs a bit of
care -- a larger scaling factor will give more precision but, if it's
too large, we run the risk of an arithmetic overflow.
Note that we're doing fixed point arithmetic here. I suspect that any kind of floating point arithmetic would be magnificently impractical in a shell script, and there's no need for it in this application.
Once we have the div
function, implementing a function to
wait for a number of milliseconds is trivial:
function sleep_ms { ms=$(div $1 1000) sleep $ms; }
However, as convenient and expressive as this function is, it would be
a mistake to use it. Most obviously, as this sleep_ms
function will be called
dozens of times per second, doing the scripted division that often would
be a waste of CPU resources, and introduce timing errors. Worse, the
call to div
is implemented as a sub-shell. It has
to be, because Bash does not provide a way to return a string value
from a function (we're using a string because that's the only way to
pass a non-integer number in Bash).
div
writes its result string to stdout in a sub-shell, and
the main script captures the data sent to stdout by the sub-shell.
The use of a sub-shell
here means that we are subject to timing inaccuracies resulting from
the Linux scheduler, quite apart from the CPU load occasioned by
spawning thousands of sub-shells.
So, rather than using the sleep_ms
function, we will just
pre-calculate the (fractional) argument needed for sleep
, and
call it explicitly. It's not as elegant, but it's a lot more efficient.
There are some other subtleties here that would be extremely important if we were timing air-traffic control operations. Most obviously, it's hard to get millisecond timing when the operations we are doing the timing with take a significant -- and unpredictable -- number of milliseconds themselves. Bash is not all that fast, when we get down to millisecond timing.
Audio
Bash provides no built-in audio support, and nobody would expect it to. This application really does require audio -- I use a rising pitch to indicate breathing in, and a falling pitch for breathing out. That way, the program can be used whilst doing other things, without needing to look at the terminal display.
If I were implementing this application in C, I would run an audio tone generator in a separate thread of execution, and change its pitch whenever the user interface display was updated. That way, the visual and audible cues would always be perfectly in sync.
What we can do instead in a Bash script is to use a utility like
sox
to generate the rising and falling pitches in advance,
as audio files (in WAV format, perhaps). For example:
$ sox -n rising.wav synth 2.0 sine 200:400 fade 0.1 0
This invocation of sox
generates a frequency sweep from
200Hz to 400Hz that lasts 2.0 seconds. The fade
operation
avoids clicking noises at the start and the end of the playback.
We can then use
something like aplay
to play the audio files
as the script runs.
The problem here is synchronization -- how do we keep the visual display
and audio in sync? It's not a huge problem, because we know
exactly how long the breathing-in and breathing-out times are. If the
breathing-in time is exactly two seconds, then we need to generate a
rising pitch that last two seconds.
Or do we? In practice, it's not all that clear. Invoking aplay
in a sub-shell will take a certain amount of time, and aplay
itself will take some time to set things up before it starts to play.
So the audio playback will be a little longer than the length of the tone
generated.
If the playback is longer that the breathing time generated in the
visual display, then one audio file will start to play before the
previous one has finished. Depending on the hosts system's audio configuration
that may, or may not, be allowed. Even if it's allowed, it will probably
sound odd.
On the other hand, the audio playback could be too short for the visual display. If the selected breathing time is, again, two seconds, and we want to update the display 40 times in that period, that's an update every 50 msec. But it probably takes, say, 5 msec to make the update, so the total breathing time in the visual display will actually be about 10% longer than expected.
I don't really know any foolproof solution here. On the basis that it's
less catastrophic to have the sound finish a little before the visual
display, rather than after, I've arranged for the audio output to
be shorter than the visual display in each breathing cycle by an
adjustable amount -- defaulting to 200 msec. In practice, if you
want the display and the audio to line up precisely, you'll need
to adjust this quantity (it's called tone_gen_latency
in the script) by a process of trial and error.
Further work
A useful, and simple, addition would be to allow for breath-holding periods
at the end of expiration and inspiration. Simple sleep
invocations
should take care of this.
A more complicated extension would be to allow some or all of the various
configuration parameters to be set on the command line, so they can
be changed without editing the script. The Bash built-in command
getopts
can simplify this kind of operation, although
it's an interesting exercise to parse a command line using only
standard scripting.