Get duration of audio

Hi,

I have realized a connection of the Amazon Polly Service via Node-Red.
From polly I get an audio buffer back, I enrich it with wav headers to output it as audio.
I would like to be able to determine the duration from the buffer.

For example:
Polly transmits the buffer for the sentence "This is a test"
I want to pause the music while the sentence is spoken.
But for this I need to know how long the sentence is spoken.

you don't say how you are playing it - but if you call out to something like aplay then the call will return once it has finished so that would let you trigger an unpause.

I do not play the audio directly with node-red.
I have a Raspberry Pi with rhasspy. Via an API I transfer the WAV file from Node-Red to rhasspy and rhasspy does the rest.

Maybe a search on StackOverflow may be helpful as this isn't specific to Node-RED - https://stackoverflow.com/questions/6474765/how-can-i-determine-the-length-of-a-local-wav-file-in-javascript

Thank you but how can I use "loadModule" in Node-Red?

I have found an NPM module that does exactly what I want.

In my nodejs test environment it also works with this code

const { getAudioDurationInSeconds } = require('get-audio-duration');

// From a local path...
getAudioDurationInSeconds('D:/Users/TA/_data/wakeword.wav').then((duration) => {
    //console.log(duration);
    console.log (duration)
});

But I don't know how to pass on the promise to the next node

The node red docs page on writing functions has a section Loading Additional Modules which tells you how to do it.

Hi @DerT94,
Not exactly an answer to your last question, but 'perhaps' there is an easier way to do this:

  1. Get the WAV header with my node-red-contrib-wav node.

  2. Calculate the duration in a function node, based on the information in that WAV header (similar like the wav-audio-length npm module does it). Something like this:

    // Get the required info from the WAV headers
    const numChannels = msg.payload.numChannels;
    const byteRate = msg.payload.byteRate;
    const byteLength = msg.payload.chunkSize;
    
    // Calculate the WAV chunk duration
    msg.duration = byteLength / numChannels / byteRate;
    

P.S. I have not been able to test whether this code is syntax free, or whether the calculated duration is correct!

EDIT: if you get this working, I will add it as an extra output to my node...

Bart

Hello @DerT94,

You have not given any feedback to our suggestions, which makes it hard for us and other users to learn from this discussion ...

Anyway I have published a new 1.0.1 version of the node-red-contrib-wav node on NPM:

image

This version calculates the duration of the WAV chunk based on the WAV headers.
Like in this example flow:

image

[{"id":"e63858e1.7a7698","type":"wav-headers","z":"4142483e.06fca8","name":"","action":"get","channels":"1","samplerate":"22050","bitwidth":"16","x":670,"y":580,"wires":[[],["9420840.1c8fb8"]]},{"id":"3c4e2aed.d61106","type":"http request","z":"4142483e.06fca8","name":"Download WAV file","method":"GET","ret":"bin","paytoqs":false,"url":"https://file-examples-com.github.io/uploads/2017/11/file_example_WAV_10MG.wav","tls":"","persist":false,"proxy":"","authType":"basic","x":450,"y":580,"wires":[["e63858e1.7a7698"]]},{"id":"94486c9c.e89ed","type":"inject","z":"4142483e.06fca8","name":"Start test","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":260,"y":580,"wires":[["3c4e2aed.d61106"]]},{"id":"9420840.1c8fb8","type":"debug","z":"4142483e.06fca8","name":"Headers","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":860,"y":580,"wires":[]}]

Which downloads a WAV file from the web, gets the WAV headers and calculates the WAV file duration (in seconds, rounded at 2 decimals):

image

My first calculation - which I had proposed in the above discussion - did not work!
With this new calculation, I seem to get similar durations as when I open the same WAV file e.g. with the VLC media player:

image

Would be nice to know if this calculation is correct, and whether it can solve your issue.
Bart

Sorry
I have not answered yet as I have not been able to test.
But today I got around to it.

Thanks for the quick implementation.

I can confirm that the calculated time is identical to the display in VLC Meida Player.
But there is a problem with my implementation.
In the initial message that starts the flow there is an ID that I need together with the duration.
There is certainly a way to solve this with global variables.
But I'm not sure how to do that

Here is an example flow for my implementation

[
    {
        "id": "48ce9130.1e2f8",
        "type": "function",
        "z": "923f92f6.5d9f2",
        "name": "SetHeadersAndText",
        "func": "const val = msg.payload.val\nlet newMsg = {};\nnewMsg.filename = `/usr/local/bin/iobroker/rhasspy/wav/${val.filename}`\nnewMsg.headers = {};\nnewMsg.headers['User-Agent'] = 'ioBroker';\nnewMsg.headers['Content-Type'] = 'audio/wav';\nnewMsg.url = `http://${val.host}:${val.port}/api/play-wav`\nreturn newMsg",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "x": 400,
        "y": 320,
        "wires": [
            [
                "55db20e5.403e1"
            ]
        ]
    },
    {
        "id": "55db20e5.403e1",
        "type": "file in",
        "z": "923f92f6.5d9f2",
        "name": "GetFile",
        "filename": "",
        "format": "",
        "chunk": false,
        "sendError": false,
        "encoding": "none",
        "x": 640,
        "y": 320,
        "wires": [
            [
                "f80f3dca.3cf5f"
            ]
        ]
    },
    {
        "id": "f80f3dca.3cf5f",
        "type": "wav-headers",
        "z": "923f92f6.5d9f2",
        "name": "",
        "action": "get",
        "channels": 1,
        "samplerate": 22050,
        "bitwidth": 16,
        "x": 890,
        "y": 320,
        "wires": [
            [
                "b0763f2d.85a44"
            ],
            [
                "dee21ce6.8dcc1"
            ]
        ]
    },
    {
        "id": "dee21ce6.8dcc1",
        "type": "debug",
        "z": "923f92f6.5d9f2",
        "name": "",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 1190,
        "y": 420,
        "wires": []
    },
    {
        "id": "b0763f2d.85a44",
        "type": "http request",
        "z": "923f92f6.5d9f2",
        "name": "-> rhasspy",
        "method": "POST",
        "ret": "bin",
        "paytoqs": "ignore",
        "url": "",
        "tls": "",
        "persist": false,
        "proxy": "",
        "authType": "",
        "x": 1170,
        "y": 300,
        "wires": [
            []
        ]
    },
    {
        "id": "4d1fa247.ca4e2c",
        "type": "inject",
        "z": "923f92f6.5d9f2",
        "name": "",
        "props": [
            {
                "p": "payload.val",
                "v": "{\"filename\":\"beep_error.wav\",\"host\":\"testpi.example.loc\",\"port\":\"12101\",\"siteid\":\"testpi\"}",
                "vt": "json"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "x": 200,
        "y": 320,
        "wires": [
            [
                "48ce9130.1e2f8"
            ]
        ]
    },
    {
        "id": "4ce99781.a51998",
        "type": "comment",
        "z": "923f92f6.5d9f2",
        "name": "comment",
        "info": "Here I need the rhasspySiteId from the initial message",
        "x": 1200,
        "y": 460,
        "wires": []
    }
]

Got it :slight_smile:

[{"id":"48ce9130.1e2f8","type":"function","z":"923f92f6.5d9f2","name":"SetHeadersAndText","func":"const val = msg.payload.val\n\nflow.set(\"rhasspySiteId\", val.rhasspySiteId)\n\nlet newMsg = {};\nnewMsg.filename = `/usr/local/bin/iobroker/rhasspy/wav/${val.filename}`\nnewMsg.headers = {};\nnewMsg.headers['User-Agent'] = 'ioBroker';\nnewMsg.headers['Content-Type'] = 'audio/wav';\nnewMsg.url = `http://${val.host}:${val.port}/api/play-wav`\nreturn newMsg","outputs":1,"noerr":0,"initialize":"","finalize":"","x":400,"y":320,"wires":[["55db20e5.403e1"]]},{"id":"55db20e5.403e1","type":"file in","z":"923f92f6.5d9f2","name":"GetFile","filename":"","format":"","chunk":false,"sendError":false,"encoding":"none","x":640,"y":320,"wires":[["f80f3dca.3cf5f"]]},{"id":"f80f3dca.3cf5f","type":"wav-headers","z":"923f92f6.5d9f2","name":"","action":"get","channels":1,"samplerate":22050,"bitwidth":16,"x":890,"y":320,"wires":[["92701f9.d1139e"],["b0ecf8e0.5cb4b8"]]},{"id":"b0763f2d.85a44","type":"http request","z":"923f92f6.5d9f2","name":"-> rhasspy","method":"POST","ret":"bin","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","authType":"","x":1350,"y":320,"wires":[[]]},{"id":"92701f9.d1139e","type":"delay","z":"923f92f6.5d9f2","name":"","pauseType":"delay","timeout":"1","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":1120,"y":320,"wires":[["b0763f2d.85a44"]]},{"id":"ae3e18ad.93ee98","type":"function","z":"923f92f6.5d9f2","name":"","func":"const rhasspySiteId = flow.get(\"rhasspySiteId\")\n\nlet newMsg = {};\nnewMsg.payload = { \n    \"id\": msg.uuid,\n    \"timeout\": msg.payload.duration+2,\n    \"rhasspySiteId\": rhasspySiteId\n}\nnewMsg.topic = `0_userdata.0.Rhasspy.ScriptStates.${rhasspySiteId}.MopidyPaused2`\nreturn newMsg","outputs":1,"noerr":0,"initialize":"","finalize":"","x":1260,"y":400,"wires":[["5195dc22.52ab64"]]},{"id":"b0ecf8e0.5cb4b8","type":"uuid","z":"923f92f6.5d9f2","uuidVersion":"v4","namespaceType":"","namespace":"","namespaceCustom":"","name":"","field":"uuid","fieldType":"msg","x":1090,"y":400,"wires":[["ae3e18ad.93ee98"]]},{"id":"4d1fa247.ca4e2c","type":"inject","z":"923f92f6.5d9f2","name":"","props":[{"p":"payload.val","v":"{\"filename\":\"beep_error.wav\",\"host\":\"testpi.example.loc\",\"port\":\"12101\",\"rhasspySiteId\":\"testpi\"}","vt":"json"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":200,"y":320,"wires":[["48ce9130.1e2f8"]]},{"id":"715a700d.9b08b","type":"comment","z":"923f92f6.5d9f2","name":"","info":"play wav with 1sec delay\n\nThe delay is intended so that the music can be paused before","x":1350,"y":280,"wires":[]},{"id":"6a30d5b7.a0167c","type":"comment","z":"923f92f6.5d9f2","name":"","info":"use ioBroker to pause mopidy and play after the duration +2 seconds","x":1450,"y":440,"wires":[]},{"id":"5195dc22.52ab64","type":"debug","z":"923f92f6.5d9f2","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1430,"y":400,"wires":[]}]

Required modules

  • node-red-contrib-wav
  • node-red-contrib-uuid

Also, my node-red runs inside an ioBroker.
The rest of the logic for pausing and the actual trigger comes from ioBroker.
But this logic will be of no interest to most people in this forum.
In case someone needs it. Just report it here :slight_smile:

Thanks for your help

1 Like