I took a look at the other thread and got curious about the browser's audio api... I thought I'd try to replace that pcm-player with code that directly calls the audio API. The result is...
- a widget that can take "streaming" audio
- streaming means you can feed it a buffer of PCM samples and it enqueues it
- it plays the buffers back-to-back seamlessly
- each buffer specifies the channels, format, and sample rate, so you can mix sources, if desired...
- at the end of each buffer it outputs a message with the number of buffers still enqueued as well as the number of milliseconds of data it has left
The resulting widget has fewer lines of code than the pcm-player thing on its own
I also tweaked the sample flow. It now reads the sample audio file, splits it up into 1 second chunks, then outputs a pile of messages with one chunk per message to simulate streaming where you might get a buffer at a time from some socket.
Then the first buffer is fed directly into the pcm-player, the second chunk is fed 500ms later, and the subsequent chunks come at 1s spacing. A debug node prints the output produced by the pcm-player widget but that info is not used in this simple demo.
(Also, I'm cutting the audio off after 8 seconds to make it easier to experiment, no need for 74 seconds...)
The widget currently prints debug info to the web browser console. It looks something like this:
PcmPlayer: converting 8000 samples from
Int16Array(16000) [ -1014, 5831, -1169, 10705, 5769, 13117, -7071, 363, -22978, -10826, … ]
PcmPlayer: queued 1.000s of audio, start @0s
PcmPlayer: converting 8000 samples from
Int16Array(16000) [ -2669, -12442, -6595, -11176, -3900, -13132, -4149, -19305, -3381, -9180, … ]
PcmPlayer: queued 1.000s of audio, start @1s
PcmPlayer: buffer finished, time left: 0.990
PcmPlayer: converting 8000 samples from
Int16Array(16000) [ 4863, 619, 1977, -927, 350, -920, -2811, -4016, -7320, -8231, … ]
PcmPlayer: queued 1.000s of audio, start @2s
PcmPlayer: buffer finished, time left: 0.989
...
PcmPlayer: converting 8000 samples from
Int16Array(16000) [ -2846, -1768, -4667, -2141, -5440, -2113, -6251, 15, -4954, 1757, … ]
PcmPlayer: queued 1.000s of audio, start @7s
PcmPlayer: buffer finished, time left: 0.990
PcmPlayer: all buffers finished
Sample flow:
[{"id":"9c482194a0f045d9","type":"fd-pcm-player","z":"777070ae3efc56ba","fd_container":"f10726be88dc0ea5","fd_cols":2,"fd_rows":1,"fd_array":false,"fd_array_max":10,"name":"","title":"Pcm Player","popup_info":"","x":910,"y":140,"wires":[["ea61f6d52a51ce68"]]},{"id":"1c0a6d92da477476","type":"file in","z":"777070ae3efc56ba","name":"read sample","filename":"/usr/src/node-red/node-red-fd-pcm-player/resources/16bit-8000.raw","filenameType":"str","format":"","chunk":false,"sendError":false,"encoding":"none","allProps":false,"x":350,"y":140,"wires":[["ceda8a1156c28b15"]]},{"id":"adcf16b317d63b30","type":"inject","z":"777070ae3efc56ba","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":150,"y":140,"wires":[["1c0a6d92da477476"]]},{"id":"ceda8a1156c28b15","type":"function","z":"777070ae3efc56ba","name":"buf2arrays","func":"// except msg.payload to be a buffer of PCM audio samples at 8kHz\n// split the buffer up into 1 second chunks\n// convert each chunk into a typed array (UInt8Array)\n// send the first chunk on the first output as an audio descriptor\n// send the remaining chunks as audio descriptors in individual messages\nif (!Buffer.isBuffer(msg.payload)) {\n node.warn(\"msg.payload must be a buffer\")\n return\n}\n\nconst chunk_length = 2 * 2 * 8000 // 2 channels by 2 bytes/sample by 8000Hz rate\nnode.warn(`cutting ${Math.ceil(msg.payload.length/chunk_length)} chunks`)\nconst chunks = Array(Math.ceil(msg.payload.length/chunk_length)).fill(0)\n .map((_,ix) => Uint8Array.from(msg.payload.subarray(ix*chunk_length, (ix+1)*chunk_length)) )\nconst msgs = chunks.map(c => ({\n source: \"16bit-8000\",\n stream: [{\n channels: 2,\n rate: 8000,\n format: 'Int16',\n data: c,\n }],\n}))\nnode.warn(`Got ${msgs.length} messages`)\nmsgs.splice(8, 10000) // TWEAK-ME: this throws away all but the first 8 seconds\nconst msg0 = msgs.shift()\nreturn [msg0, msgs]","outputs":2,"noerr":0,"initialize":"","finalize":"","libs":[],"x":550,"y":140,"wires":[["28eaca34691bad86"],["c5d12331b4e247a2"]]},{"id":"c5d12331b4e247a2","type":"delay","z":"777070ae3efc56ba","name":"","pauseType":"delay","timeout":"500","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":590,"y":180,"wires":[["6ddd9ca705faea92"]]},{"id":"6ddd9ca705faea92","type":"delay","z":"777070ae3efc56ba","name":"","pauseType":"rate","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":640,"y":220,"wires":[["28eaca34691bad86"]]},{"id":"8605977df7cfa6e0","type":"debug","z":"777070ae3efc56ba","name":"audio","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":890,"y":180,"wires":[]},{"id":"ea61f6d52a51ce68","type":"debug","z":"777070ae3efc56ba","name":"buffer ended","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1100,"y":140,"wires":[]},{"id":"28eaca34691bad86","type":"junction","z":"777070ae3efc56ba","x":800,"y":140,"wires":[["9c482194a0f045d9","8605977df7cfa6e0"]]},{"id":"f10726be88dc0ea5","type":"flexdash container","name":"pcm-player","kind":"StdGrid","fd_children":",9c482194a0f045d9","title":"","tab":"7f846d7b8252c652","min_cols":"1","max_cols":"20","parent":"bdf2b4b185a1a993","solid":false,"cols":"1","rows":"1"},{"id":"7f846d7b8252c652","type":"flexdash tab","name":"pcm-player","icon":"mdi-music","title":"","fd_children":",f10726be88dc0ea5","fd":"e8f5aea52ab49500"},{"id":"bdf2b4b185a1a993","type":"flexdash container","name":"Container1","kind":"StdGrid","fd_children":"","title":"Cont1","tab":"99faa9f04adc062c","min_cols":"1","max_cols":"20","parent":"","solid":false,"cols":"1","rows":"1"},{"id":"99faa9f04adc062c","type":"flexdash tab","name":"Imported tab","icon":"mdi-view-dashboard","title":"Navnet","fd_children":",bdf2b4b185a1a993","fd":"e8f5aea52ab49500"}]
I did push the built file to github, so you can npm install the package and it should work in prod mode as well...
Note that this doesn't address the issues I flagged. The streaming works if you don't push it too hard. I like the way it looks in the widget but what happens on the NR side needs some enhancement.
Hope this works for you!