Search code examples
c#datetimetimelogiclerp

How can I best calculate beats per min, given the time in between two beats?


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;
    }
}

Solution

  • 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