-1

I'm creating an app that tests how fast in beats a user can achieve with taps, mouse clicks / button pushes etc.

Each time the user hits the button, I call this function, beat(); to calculate their current BPM and fill a graphic with that BPM. I also want to calculate an average at the end. See the code I am currently using below.

The issue I am currently having is that as the user clicks, the BPM is all over the place, sometimes its consistent, sometimes it is inaccurate. Reviewing my code has me confused, as I don't think I'm actually calculating BPM here, or at least, not accurately.

I also tried adding in a lerp between the old and new BPM values, to smooth the gauge, but it doesnt seem to fix the problem.

The closest thing I have found on stack overflow is this question : Tap BPM code in Processing
But I still am not sure if that is actually calculating the users BPM accurately. I would greatly appreciate some assistance here, specifically an explanation on the math behind how to calculate BPM with only 2 timestamps, an old and a new beat.

void beat()
{

    if (beat0 == null)
    {
        beat0 = DateTime.Now;
        return;
    }
    else if (beat1 == null)
    {
        beat1 = DateTime.Now;
    }
    else
    {
        beat0 = beat1;
        beat1 = DateTime.Now;
    }

    if (beat0 != null && beat1 != null)
    {
        double delta = (beat1 - beat0).TotalSeconds;
        double bpm = 60 / delta;
        curBpm = (int)bpm;

        if (oldBpm == -1)
        {
            oldBpm = curBpm;
            return;
        }

        lerpBpm = (int)Mathf.Lerp(oldBpm, curBpm, (float)delta);

        bpmText.text = lerpBpm.ToString();
        FillGauge(lerpBpm);
        oldBpm = lerpBpm;
    }
}
NCarlson
  • 330
  • 2
  • 13
  • 2
    DateTime is terrible for timing anything; the resolution of the system clock is too coarse. Use a StopWatch. Have a small buffer of eg 5 readings in an array accessed using modulo of an incrementing counter (circular array) that 1ups on very click and stores the stopwatch elapsed in the array. Do the average of the readings (max minus Min divide by array length) and calc the bpm from that average otherwise it will be all over the place – Caius Jard Aug 27 '21 at 14:17
  • Can you please elaborate on this comment, perhaps in an answer with an example / some psuedo code? Thanks! – NCarlson Aug 27 '21 at 14:26
  • I ask because I'm a little bit confused; average = max - min / array length? isnt average the sum of all the numbers in the array / array length ? – NCarlson Aug 27 '21 at 14:35
  • 1
    The stopwatch counts up constantly, you store the elapsed time in the array every time the user clicks so the max time in the array minus the min time in the array is the time it took the user to make the recent 5 clicks (if its a 5 long array), didcide by 5 is the average time between clicks. If you added them all up you'd get the average stopwatch reading rather than the change in time from then to now (which you need to calculate the clicks per minute. 5 clicks per 1 second, say, is 300 clicks per minute) – Caius Jard Aug 27 '21 at 15:01
  • 1
    You asked how to calculate BPM. If there are 100 milliseconds per beat, and there are 60000 milliseconds per minute, then you can fit 600 lot of 100ms intervals in a minute.. once you have the ms/b you can get the b/m by dividing the ms/m by the ms/b.. in essential math terms `(ms/b) / (ms/m)` is the same as flipping one of the x/y over and changing the middle `/` to `*`. If you flip `(ms/b) / (ms/m)` so it's `(b/ms) * (ms/m)` which is `(b * ms) / (ms * m)` then the two `ms` too and bottom cancel out leaving just `(b/m)` i.e. beats per minute – Caius Jard Aug 27 '21 at 15:57

1 Answers1

0

I would have something like this..

An array for eg 5 samples, and a counter to know which array element is "current"

TimeSpan[] samples = new TimeSpan[5];
int index = 0;

I'd have a stopwatch, running. It's a high resolution timing class for .net

When the user clicks a button I would take the stopwatch reading and store it in the array:

var current = stopwatch.Elapsed;     //you'll see why I store in a temp variable in a moment 
samples[index] = current;

And bump the index on, using a modulo to keep it within the array bounds (circular array)

index = (index +1)%samples.Length;

Then I'd need to look at the min and the max in the array. You could use LINQ min/max, or could just use the fact that the index is now pointing to the oldest item in the array..

int oldest = samples[index];

Now, if you do current - oldest you'll get the time it's taken to acquire 5 samples, divided by 5 gives the average time per sample over the last 5 samples

var ms = (current - oldest).TotalMilliseconds;

And if that's the time from one beat to the next you want to know how many beats you can fit into a time period 60000 ms long

var bpm = 60000 / ms;

Now all that remains I suppose is to put a bit of a check on if the earliest reading is TimeSpan.Zero (indicating it's never been set) so that you don't start calculating crazy bpm in the first five taps

Caius Jard
  • 72,509
  • 5
  • 49
  • 80