Good way of calculating tempo?

hi all... ive got a flow where im calculating the tempo of some music by tapping a button in time with the beat.. problem is i dont think its doing a great job and im wondering if someone might have a better method.. possibly a way of doing it all in a function node where timing could be pretty accurate ?

here is what i have..


[
    {
        "id": "6c913a509314f175",
        "type": "tab",
        "label": "Flow 1",
        "disabled": false,
        "info": "",
        "env": []
    },
    {
        "id": "6d10a55b844e105c",
        "type": "function",
        "z": "6c913a509314f175",
        "name": "Tempo",
        "func": "let interval = msg.interval\nlet count\nlet tempo\n\ncount = 1-(interval / 1000);\ntempo = count*4*60;\n\nmsg.payload = Math.floor(tempo/1)*1 \n\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 690,
        "y": 200,
        "wires": [
            [
                "ad1050e6144ac0ae"
            ]
        ]
    },
    {
        "id": "e6319cdc2abfa468",
        "type": "interval-length",
        "z": "6c913a509314f175",
        "format": "mills",
        "bytopic": false,
        "minimum": "",
        "maximum": "",
        "window": "",
        "timeout": false,
        "msgTimeout": "2",
        "minimumunit": "msecs",
        "maximumunit": "msecs",
        "windowunit": "msecs",
        "msgTimeoutUnit": "secs",
        "reset": true,
        "startup": false,
        "msgField": "interval",
        "timestampField": "timestamp",
        "repeatTimeout": false,
        "name": "Interval",
        "x": 380,
        "y": 200,
        "wires": [
            [
                "5660bc8ade2a3098"
            ],
            [
                "9f839aae647eb9eb"
            ]
        ]
    },
    {
        "id": "9f839aae647eb9eb",
        "type": "change",
        "z": "6c913a509314f175",
        "name": "reset",
        "rules": [
            {
                "t": "set",
                "p": "reset",
                "pt": "msg",
                "to": "true",
                "tot": "bool"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 390,
        "y": 260,
        "wires": [
            [
                "e6319cdc2abfa468"
            ]
        ]
    },
    {
        "id": "78444743675c6619",
        "type": "inject",
        "z": "6c913a509314f175",
        "name": "tap tempo",
        "props": [
            {
                "p": "payload"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "",
        "payloadType": "str",
        "x": 220,
        "y": 200,
        "wires": [
            [
                "e6319cdc2abfa468"
            ]
        ]
    },
    {
        "id": "ad1050e6144ac0ae",
        "type": "debug",
        "z": "6c913a509314f175",
        "name": "debug 183",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "false",
        "statusVal": "",
        "statusType": "auto",
        "x": 830,
        "y": 200,
        "wires": []
    },
    {
        "id": "5660bc8ade2a3098",
        "type": "smooth",
        "z": "6c913a509314f175",
        "name": "",
        "property": "payload",
        "action": "mean",
        "count": "3",
        "round": "",
        "mult": "single",
        "reduce": false,
        "x": 560,
        "y": 200,
        "wires": [
            [
                "6d10a55b844e105c"
            ]
        ]
    }
]

This comes up regularly in the forum.

  1. Desktop OS's such as Windows, Linux and MacOS are not real-time, lots of stuff going on that can impact the performance of an individual application. Music and similar apps on these systems typically use special techniques and libraries to reduce this potential latency issue.

  2. Node-RED is built on Node.JS which is primarily single-threaded and uses a single, core, loop approach. Loads of things have access to the loop and can impact what gets run when. There is no realistic way to get exact timings on Node.JS.

  3. Node-RED is a complex, general-purpose, low-code programming environment. It does lots of heavy lifting for you but certainly does not attempt to provide exact timing (even if it could - see above).

The simplest way to get exactly what you want is to do some simple electronics with an ESP32 development board, a very simple switch wired in and some simple, near real-time code to record the button presses, calculates the timing and outputs (over Wi-Fi) the timing values (not the raw data) to MQTT. Then Node-RED can easily access that timing data and display it. Of course, you could get an ESP32 with a screen and output the timing to that instead/as well as.

Somewhat less accurate but still better than Node-RED directly would be to create a simple web page and use a keyboard instead of a button to record the taps. Feeding the calculated timing back to Node-RED. Doing the calculations again in the front-end for accuracy.

1 Like

When you say tempo do you mean beats per minute?
As if so your maths seems off
Have a look at this example

[{"id":"78444743675c6619","type":"inject","z":"6c913a509314f175","name":"reset","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"0","payloadType":"num","x":190,"y":200,"wires":[["93d95e3a05136670","9c29a40e12ec4c55","029238ba63773e03"]]},{"id":"93d95e3a05136670","type":"trigger","z":"6c913a509314f175","name":"","op1":"1","op2":"0","op1type":"str","op2type":"str","duration":"-1000","extend":false,"overrideDelay":false,"units":"ms","reset":"0","bytopic":"all","topic":"topic","outputs":1,"x":460,"y":240,"wires":[["6d10a55b844e105c"]]},{"id":"9c29a40e12ec4c55","type":"trigger","z":"6c913a509314f175","name":"","op1":"1","op2":"0","op1type":"str","op2type":"str","duration":"-500","extend":false,"overrideDelay":false,"units":"ms","reset":"0","bytopic":"all","topic":"topic","outputs":1,"x":460,"y":280,"wires":[["6d10a55b844e105c"]]},{"id":"029238ba63773e03","type":"trigger","z":"6c913a509314f175","name":"","op1":"1","op2":"0","op1type":"str","op2type":"str","duration":"-259","extend":false,"overrideDelay":false,"units":"ms","reset":"0","bytopic":"all","topic":"topic","outputs":1,"x":460,"y":320,"wires":[["6d10a55b844e105c"]]},{"id":"1c405b382224e477","type":"inject","z":"6c913a509314f175","name":"1 second","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":200,"y":240,"wires":[["93d95e3a05136670"]]},{"id":"6d10a55b844e105c","type":"function","z":"6c913a509314f175","name":"Tempo","func":"let last = context.get(\"last\") || 0;\n\nlet now = new Date().getTime();\nlet interval = now-last\n\nmsg.payload = Math.round(60000/(interval));\ncontext.set(\"last\", now)\nif (interval > 2000) msg = null;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":650,"y":300,"wires":[["ad1050e6144ac0ae"]]},{"id":"0ee78b762368d6b3","type":"inject","z":"6c913a509314f175","name":"0.5 seconds","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"500","payloadType":"num","x":210,"y":280,"wires":[["9c29a40e12ec4c55"]]},{"id":"5caf56c9f76a346e","type":"inject","z":"6c913a509314f175","name":"0.25 seconds","props":[{"p":"delay","v":"250","vt":"num"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":210,"y":320,"wires":[["029238ba63773e03"]]},{"id":"ad1050e6144ac0ae","type":"debug","z":"6c913a509314f175","name":"debug 183","active":false,"tosidebar":true,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"payload","statusType":"auto","x":750,"y":360,"wires":[]}]

no probs.. thanks for the info.. do you think there is any better method than what im using with the interval node though or is that about as good as it would get ? that method is ok but off by a bit more than id like... for example a music program that's at 200bpm, the interval node suggests its around 170-180 bpm.. if i could get within 5% say around 190-195 range for that same thing that would be much better..

yeah beats per minute.. im a novice with this stuff so yes i wouldn't be surprised if my math is off..

i looked at your flow its different to what im trying to do.. i want to press a button repeatedly and convert the interval between each press into beats per minute.. if you are listening to a song you tap the button in time with the music you create the BPM value by the presses... the problem i have at the moment is the interval node i think is just a bit too far out.. i just wondered if there is some method using a function node or something else which might be a bit better ? if not i'll stick with what i have as its not too bad..

Thats what the function does, the rest is just for testing. Try adding your button direct to the function.

The triggers simulate presses at different time rates.

Here is same thing with some smoothing in the function

[{"id":"78444743675c6619","type":"inject","z":"6c913a509314f175","name":"reset","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"0","payloadType":"num","x":190,"y":200,"wires":[["93d95e3a05136670","9c29a40e12ec4c55","029238ba63773e03"]]},{"id":"93d95e3a05136670","type":"trigger","z":"6c913a509314f175","name":"","op1":"1","op2":"0","op1type":"str","op2type":"str","duration":"-1000","extend":false,"overrideDelay":false,"units":"ms","reset":"0","bytopic":"all","topic":"topic","outputs":1,"x":460,"y":240,"wires":[["6d10a55b844e105c"]]},{"id":"9c29a40e12ec4c55","type":"trigger","z":"6c913a509314f175","name":"","op1":"1","op2":"0","op1type":"str","op2type":"str","duration":"-500","extend":false,"overrideDelay":false,"units":"ms","reset":"0","bytopic":"all","topic":"topic","outputs":1,"x":460,"y":280,"wires":[["6d10a55b844e105c"]]},{"id":"029238ba63773e03","type":"trigger","z":"6c913a509314f175","name":"","op1":"1","op2":"0","op1type":"str","op2type":"str","duration":"-259","extend":false,"overrideDelay":false,"units":"ms","reset":"0","bytopic":"all","topic":"topic","outputs":1,"x":460,"y":320,"wires":[["6d10a55b844e105c"]]},{"id":"1c405b382224e477","type":"inject","z":"6c913a509314f175","name":"1 second","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":200,"y":240,"wires":[["93d95e3a05136670"]]},{"id":"6d10a55b844e105c","type":"function","z":"6c913a509314f175","name":"Tempo","func":"let last = context.get(\"last\") || [0,[]];\nlet count = 5;\nvar now = new Date().getTime();\nlet interval = now-last[0];\nlet bpm = 60000/interval;\nlast[1].push(bpm)\nlast[1] = last[1].slice(-count);\nlast = [now,last[1]]\nmsg.payload = Math.round((last[1].reduce((acc, val) => acc + val,0))/last[1].length);\nif (interval > 2000) {\n    msg = null;\n    last = [now,[]];\n}\ncontext.set(\"last\", last)\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":650,"y":300,"wires":[["ad1050e6144ac0ae"]]},{"id":"0ee78b762368d6b3","type":"inject","z":"6c913a509314f175","name":"0.5 seconds","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"500","payloadType":"num","x":210,"y":280,"wires":[["9c29a40e12ec4c55"]]},{"id":"5caf56c9f76a346e","type":"inject","z":"6c913a509314f175","name":"0.25 seconds","props":[{"p":"delay","v":"250","vt":"num"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":210,"y":320,"wires":[["029238ba63773e03"]]},{"id":"ad1050e6144ac0ae","type":"debug","z":"6c913a509314f175","name":"debug 183","active":false,"tosidebar":true,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"payload","statusType":"auto","x":750,"y":360,"wires":[]}]

In testing at 240bpm it outputs 231bpm that's with in 5% accuracy.

my apologies.. i didnt realise what the repeated sends meant at first.. yes this is working better and its reduced four nodes down to one which is great.. you helped me with my countdown question a few days ago also.. the help is much appreciated..

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.