HackValue »

Personal Trainer

Some people believes in the benefits of physical exercise. To get a good workout you need to keep a certain intensity for a period of time. "Time" is easy to keep track of, but how about the intensity? The heart rate is a measure as good as anything else. Get a heart rate monitor and go out running for an hour each other day.

But exercise can be boring, especially stationary training like spinning bikes or elliptical cross trainers. Music does things better. Still I found a very frustrating catch biking to music. When the music tempo was close to my own pace (cadence), I tend to fall into the music tempo instead of the cadence I was supposed to keep for a suitable intensity. Too slow music and I'll slow down, too fast and my veins start pumping lactic acid. Ouch.

So why not turn this drawback to my advantage?

Things needed;

  • One heart rate monitor (I used Garmin)
  • A GNU/Linux machine
  • Some MIDI files
  • A sound card

The idea was to play the MIDI music at a perfect tempo to keep my heart rate at optimal. The MIDI music have the feature of being able to adjust the tempo without distorting the pitch of the instruments. If my heart rate is too high, lower the tempo of the music, and vice versa. Feed the heart rate into a PID controller adjusting the music.

figure 1

Green is the desired heart rate (set point), red actual heart rate, black the music tempo in beats per minute (i.e. my cadence, "step per minute") and blue the controller drive output. The time line is in seconds. The green set point illustrates first a 16 minutes warm up, followed by 48 minutes workout at heart rate 170 bpm followed by 6 minutes cool down. Yes, I like powers of two, but the fact that 70 minutes equals 4200 seconds is a pure coincident ...

I wear the heart rate monitor, start the program and pedal away in the pace of the music .. 1 .. 2 .. continuing so until the music ends. All done!

Compared to only keeping an eye at the heart rate clock, this will keep the heart rate more stable. More stable will make you able to keep a higher tempo during a tempo run. If too fast, the lactic acid build up will be too high, and if too low you are not using your full capacity. As you can see in figure 1 the heart rate is almost always within +/-2 of the target.

figure 2

Note that target heart rate is 170 for the electronic personal trainer (pink) and 160 for the clock (blue) above. Also the clock (Garmin Forerunner 405) have a quite stochastic sample interval.

A nice feature is that this "personal trainer" is impossible to cheat. If you don't keep the pace but starts to slow down "just a little", your heart rate will not reflect the intended pace, and the system will up the tempo, making you even more out of pace. Either you keep the pace exactly, or not at all. If you slow down for a few seconds, you will need to catch up at an even higher pace. This is what I call Good Motivation. On the other hand it works reverse also, you know your "trainer" will never let you work too hard, but always lower the pace when you get too exhausted (i.e. too high heart rate).

An interesting reflection I did was when I one day noticed the workout was much less intense than usual. I didn't feel as exhausted as usual and didn't cover as many kilometer as usual. I didn't realized why until a day or two later when the cold broke out.

Heart rate monitor

The Garmin Heart Rate Monitor (HRM) uses a 0.25s loop, but only transmits the data if a heart beat occurred during that 0.25s window. This causes an irregular sample interval of 0.25s, 0.50s or 0.75s depending on your heart beat. The monitor transmits both an averaged heat rate and the exact time since the last heat beat (R-R time).

Music tempo adjustment

I implemented a PID controller adjusting the music tempo from the heat rate. It turned out that the I and D gain factors didn't actually do anything better, so I suggest you keep it simple and use a pure proportional controller. Without the integrating part you must make sure you have an integrator elsewhere in the system;

bpm+= (targetHeartRate - actualHeartRate) * pGain;

This will do the trick. Optimal pGain is so small, the music tempo will change so gradually you will never notice the individual adjustments (see fig1). Only a fraction of a percent of the music speed (bpm) will change each sample (0.25s). Ignoring I and D gain and you can ignore the irregularities in the sample interval as well since the proportional gain isn't time sensitive. Keep is simple and stupid.

Music player

There are two ways of changing the tempo of MIDI music. Either you change the timings signatures in the MIDI stream, or you do not care about them and play the notes by your own means of synchronization. I chose the later and play the MIDI events direct with the fluidsynth libraries. This forced me to implement a MIDI file (SMF) sequencer, and you really don't want to do this. MIDI is a horrific standard - consider yourself warned. In retrospect I think a better approach is to output the MIDI commands to a synth (e.g. soundcard) via an ALSA MIDI port and then send MIDI timing commands direct (snd_seq_ev_set_direct()). This way the GNU/Linux ALSA libraries takes care of all the hard work sequencing it.

The MIDI file contain tempo signatures so it is trivial for the program to know the music tempo. This signature is accurate, I have only found one MIDI SMF file this was incorrect. Even if you know the music tempo, you don't know what (beats? bars? it differs!) in the music keeping this pace, but it is a minor problem. You need to divide this by the factor of two getting it as close as possible to the current pace. This result you then have to adjust with a scale factor to get correct tempo.

E.g. you are at present biking at cadence 60 rpm. The MIDI tempo signature says "140" (whatever unit). 140/2 = 70, scaled down by 6/7 gives 60 rpm. The music will go 6/7 (86%) of the original tempo. With this method the music will get as close as possible to the correct rhythm, but you still have to select suitable music.


The PID controller works well and always keep the heart rate at desired rate. Still a work out pass do not feel particular good anyway. The speed after the initial warm up phase is too high and not comfortable, and the speed at the second half too low. And what about this negative cardiac drift (figure 4)? The speed at fixed heart rate is actually increasing with time? This is odd. How come?

The heart rate do not reflect the current workload due to sluggishness in the biological responses. Initially the pace is too high causing acidosis and onset of blood lactate accumulation (OBLA), more often referred to as "milk acid". This debt the body needs to catch up, hence the significantly lower pace later in the workout. This can be seen as an interval training with the high interval followed by an lower interval not low enough for quick recovery. This explains the negative cardiac drift, because the recovery takes place over a long period at target heart rate.

A better solution

A better solution would be to control the workload, and adjust it to a fix intensity resulting in the desired heart rate over time. The problem is we have no idea how high intensity this is. Therefor we will treat this as an ordinary search problem.

The optimal workload (e.g. speed) search algorithm as follows;

Begin at some pace, any pace, preferable lower (by guess) than the target pace we are looking for, i.e. the optimal pace keeping our target heart rate.

  1. Keep this pace until the heart rate stabilizes (a few minutes).
  2. When stable, calculate an approximation what target pace might be using
    speed = target_heart_rate * current_pace / current_heart_rate.
  3. If approximated pace higher than current, set new pace to halfway from current to the approximated.
  4. If lower, set the new pace to the approximated directly.
  5. Repeat over again from step 1

The approximation speed = target_heart_rate * current_pace / current_heart_rate is based on the fact that it takes approximately the same amount of effort running the same distance at different paces (more power, less time). This is far from true, but serves well as an approximation for the binary half interval search used in step 3 based on this calculation. I only use binary search if approaching the target speed from below. If the speed is too high, I drop the speed faster with the risk of overshooting (undershooting) the target. There are two reasons for this. First a too high pace start to accumulate lactic acid taking a long time to recover from, and this is very bad for the convergence rate of this search. Second the pace/heartrate ration drops quite rapidly when exceeding MLSS (maximum lactate steady state) making the approximation more inaccurate. Also I don't like lactic acid. No pain, no pain.

After every speed change, I'll wait for 150 seconds before I start to estimate the heart rate stability. I do this by using the average derivate of the heart rate over the last 150s by using linear regression and the least squares method. When the derivate (or k in the line kt+m) is low enough (close to zero), I consider the heart rate as stable over time, and readjust the speed. Note that "speed" here actually is "power", i.e. watt. It would be perfectly possible instead of adjusting the speed, adjusting the resistance of the ergometer keeping the speed constant, but I'll call it "speed" (or pace) here to make it more intuitive compared to running (no hills). There is nothing saying a bike or ergometer have a linear relation speed:power, but within the narrow intervals used here it is most likely close enough for the binary search. Other factors probably introduce larger errors, like variances in the speed/heartrate quotient itself. This is why we need to use the binary search, and can not step directly to what we approximate the optimal speed would be.

figure 3

In this figure the red is the heart rate and the blue the speed. Here we see a slower increasing heart rate than above, but a more even speed and an over all longer distance covered than in the example above, despite of a lower target heart rate (160 vs 170). Also the workout felt much more comfortable with an increasing exhaustion over time and also a normal cardiac drift as could be expected (i.e. decrease of speed at constant heart rate). The change in speed can look quite steep here, but is only about 1 rpm cadence per 4 seconds, so it is hardly noticeable. The first 8 minutes is a warm up not part of the search algorithm.

figure 4

In figure 4 above you see the speed of the PID control (blue) at heart rate 170 bmp compared to the binary half search (pink) at 160 bpm. Please note this is during an elliptical crosstrainer workout, and the speed is not comparable with running or bicycling.

figure 5 - Heart rate comparison of the same two workouts.

If covered distance is of an issue (like in a competition), the initial speed (here cadence 40 rpm or 15 km/h) can be determined from previous workouts final pace to faster reach target heart rate. If a practice run, the slow increase of pace is a nice way warming up.



  • Easy to get a good workout if pushed by this "personal trainer" (be your own galley slave!).
  • Keeping the heart rate more stable than watching the heart rate clock.
  • Not possible to cheat.


  • Much (General) MIDI music sounds quite terrible.
  • Not possible to cheat.
  • Heart rate do not necessary reflect workload.


And remember - always mount a scratch monkey.

/By Mikael Q Kuisma


Ping site

Page last modified 2013-10-14 20:17Z