Walkie Talkie
Credit
This flow is an adaption of the great flow developed by Sammachin
[{"id":"ac4eab3a772da4ec","type":"tab","label":"Walkie Talkie","disabled":false,"info":""},{"id":"24650ddaa7403284","type":"group","z":"ac4eab3a772da4ec","name":"","style":{"fill":"#0070c0","label":true},"nodes":["212b52d73426cffb","3c8b69d0cc55d8a3","39a1f967e2ebbce9"],"x":28,"y":33,"w":1050,"h":614},{"id":"212b52d73426cffb","type":"group","z":"ac4eab3a772da4ec","g":"24650ddaa7403284","name":"www","style":{"label":true},"nodes":["67566ad976e34966","08d1db58fc98eb3a","b66d6e07fc89c848"],"x":504,"y":59,"w":542,"h":82},{"id":"3c8b69d0cc55d8a3","type":"group","z":"ac4eab3a772da4ec","g":"24650ddaa7403284","name":"Session","style":{"label":true},"nodes":["34db912780da5765","76ba4a5862e0834c","0b0fde7e1f8f9eea","ea328269a74eae56"],"x":54,"y":539,"w":992,"h":82},{"id":"39a1f967e2ebbce9","type":"group","z":"ac4eab3a772da4ec","g":"24650ddaa7403284","name":"Websocket","style":{"label":true},"nodes":["a63b76f70839dc96","02d4401363eb9ffe","da1729ded5dc2d8e","7a4ca05c29d57993","c02a930755b45eba","067811a23b6ab558","bba60d053a1ad990","5b0b4c5fad57b058"],"x":54,"y":179,"w":998,"h":308},{"id":"5b0b4c5fad57b058","type":"group","z":"ac4eab3a772da4ec","g":"39a1f967e2ebbce9","style":{"stroke":"#93a1a1","stroke-opacity":"1","fill":"#eee8d5","fill-opacity":"0.5","label":true,"label-position":"nw","color":"#657b83"},"nodes":["c2e8fea0b7c66e9f","c42812838122d473","4a14f41bfb7a5ba4"],"x":214,"y":379,"w":812,"h":82},{"id":"a63b76f70839dc96","type":"websocket in","z":"ac4eab3a772da4ec","g":"39a1f967e2ebbce9","name":"","server":"305b5c990ac144fb","client":"","x":150,"y":280,"wires":[["da1729ded5dc2d8e"]]},{"id":"02d4401363eb9ffe","type":"websocket out","z":"ac4eab3a772da4ec","g":"39a1f967e2ebbce9","name":"","server":"305b5c990ac144fb","client":"","x":950,"y":340,"wires":[]},{"id":"da1729ded5dc2d8e","type":"switch","z":"ac4eab3a772da4ec","g":"39a1f967e2ebbce9","name":"Binary/Text","property":"payload","propertyType":"msg","rules":[{"t":"istype","v":"string","vt":"string"},{"t":"istype","v":"buffer","vt":"buffer"}],"checkall":"true","repair":false,"outputs":2,"x":350,"y":280,"wires":[["7a4ca05c29d57993"],["067811a23b6ab558"]]},{"id":"7a4ca05c29d57993","type":"json","z":"ac4eab3a772da4ec","g":"39a1f967e2ebbce9","name":"","property":"payload","action":"","pretty":false,"x":550,"y":220,"wires":[["bba60d053a1ad990"]]},{"id":"c02a930755b45eba","type":"debug","z":"ac4eab3a772da4ec","g":"39a1f967e2ebbce9","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":950,"y":220,"wires":[]},{"id":"067811a23b6ab558","type":"function","z":"ac4eab3a772da4ec","g":"39a1f967e2ebbce9","name":"Copy message to all other sessions","func":"var src = msg._session.id\n\nvar sessions = flow.get('sessions');\n\nsessions.forEach(function(s){\n if (s !=src){\n msg._session.id = s\n node.send(msg)\n } \n});\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":640,"y":340,"wires":[["02d4401363eb9ffe"]]},{"id":"bba60d053a1ad990","type":"function","z":"ac4eab3a772da4ec","g":"39a1f967e2ebbce9","name":"Add Session to flows list","func":"var src = msg._session.id\nvar sessions = flow.get('sessions') || [];\nsessions.push(src)\nflow.set('sessions', sessions)","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":750,"y":220,"wires":[["c02a930755b45eba"]]},{"id":"34db912780da5765","type":"inject","z":"ac4eab3a772da4ec","g":"3c8b69d0cc55d8a3","name":"View Session List","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"sessions","payloadType":"flow","x":180,"y":580,"wires":[["76ba4a5862e0834c"]]},{"id":"76ba4a5862e0834c","type":"debug","z":"ac4eab3a772da4ec","g":"3c8b69d0cc55d8a3","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":390,"y":580,"wires":[]},{"id":"0b0fde7e1f8f9eea","type":"change","z":"ac4eab3a772da4ec","g":"3c8b69d0cc55d8a3","name":"","rules":[{"t":"delete","p":"sessions","pt":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"x":920,"y":580,"wires":[[]]},{"id":"ea328269a74eae56","type":"inject","z":"ac4eab3a772da4ec","g":"3c8b69d0cc55d8a3","name":"Clear Sessions on Startup","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"true","payloadType":"bool","x":660,"y":580,"wires":[["0b0fde7e1f8f9eea"]]},{"id":"4a14f41bfb7a5ba4","type":"status","z":"ac4eab3a772da4ec","g":"5b0b4c5fad57b058","name":"Websocket Status Events","scope":["a63b76f70839dc96"],"x":350,"y":420,"wires":[["c42812838122d473"]]},{"id":"c42812838122d473","type":"switch","z":"ac4eab3a772da4ec","g":"5b0b4c5fad57b058","name":"Look for disconnect","property":"status.event","propertyType":"msg","rules":[{"t":"eq","v":"disconnect","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":640,"y":420,"wires":[["c2e8fea0b7c66e9f"]]},{"id":"c2e8fea0b7c66e9f","type":"function","z":"ac4eab3a772da4ec","g":"5b0b4c5fad57b058","name":"Remove Session","func":"var src = msg.status._session.id\nvar sessions = flow.get('sessions') || [];\n\nconst index = sessions.indexOf(src);\nif (index > -1) {\n sessions.splice(index, 1);\n}\n\nflow.set('sessions', sessions)","outputs":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":910,"y":420,"wires":[],"icon":"font-awesome/fa-remove"},{"id":"08d1db58fc98eb3a","type":"http in","z":"ac4eab3a772da4ec","g":"212b52d73426cffb","name":"","url":"/intercom","method":"get","upload":false,"swaggerDoc":"","x":600,"y":100,"wires":[["67566ad976e34966"]]},{"id":"b66d6e07fc89c848","type":"http response","z":"ac4eab3a772da4ec","g":"212b52d73426cffb","name":"","statusCode":"","headers":{},"x":970,"y":100,"wires":[]},{"id":"67566ad976e34966","type":"template","z":"ac4eab3a772da4ec","g":"212b52d73426cffb","name":"Web Interface","field":"payload","fieldType":"msg","format":"html","syntax":"plain","template":"<!DOCTYPE html>\n<html>\n <head>\n <meta charset=\"utf-8\">\n <title>WS Intercom</title>\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <style>\n body{\n background: #646970;\n text-align: center;\n }\n\n input {\n width: 400px;\n font-size:16px;\n font-family:monospace;\n }\n\n .button {\n width: 400px;\n\t\t height: 100px;\n background-color: #d0d6dd; \n border: 0px;\n color: #646970;\n text-align: center;\n text-decoration: none;\n margin-top: 10px;\n font-size: 20px;\n font-family:monospace;\n }\n h1{\n\t\t color: #f4f4f4;\n font-size:30px;\n font-family:monospace;\n }\n h3{\n\t\t color: #f4f4f4;\n font-size:24px;\n font-family:monospace;\n }\n </style>\n </head>\n <body>\n <h1>WS Intercom</h1>\n\t<h3>Connection:<span id='conn'>⚪️</span> Tx:<span id='tx'>⚪️</span></h3>\n\t<button class=\"button\" id=\"con_btn\", onclick='connect()' style=\"height:50px\">Connect</button>\n\t<br>\n\t<button class=\"button\" id=\"ptt_btn\" style=\"height:200px\">Talk</button> \n <script>\n var wsurl;\n var headerdata;\n\t var ptt_btn=document.getElementById(\"ptt_btn\");\n\t var con_btn=document.getElementById(\"con_btn\");\n var ptt=false;\n\t var status_off = '⚪️';\n\t var status_on = '🔵';\n\t var status_tx = '🔴';\n var AudioContext = window.AudioContext || window.webkitAudioContext\n var context = new AudioContext()\n\t var time;\n\t var ws;\n\t \n\t ptt_btn.disabled = true; \n\t ptt_btn.onmousedown = function(e){\n\t ptt=true;\n\t\tdocument.getElementById(\"tx\").innerHTML=status_tx;\n\t } \n\t ptt_btn.onmouseup = function(e){\n\t \tptt=false;\n\t\tdocument.getElementById(\"tx\").innerHTML=status_off;\n\t }\n\t\t\t\t\t\n \n function extend(obj, src) {\n \t for (var key in src) {\n \t if (src.hasOwnProperty(key)) obj[key] = src[key];\n \t }\n \t return obj;\n \t}\n \n\t function connect(){\n\t if (window.location.protocol == 'https:'){\n\t wsurl = 'wss://'+window.location.host+'/socket';\n\t } else{\n\t wsurl = 'ws://'+window.location.host+'/socket';\n\t }\n\n console.log(wsurl);\n\t\t ws = new WebSocket(wsurl, \"WSBRIDGE\")\n\t ws.binaryType = 'arraybuffer';\n\t\t time = 0\n\t\t con_btn.innerText = 'Disconnect';\n\t\t con_btn.onclick = function () { ws.close()};\n\t\t ptt_btn.disabled = false; \n\t\t\n\t\t ws.onopen = function(){\n\t\t document.getElementById(\"conn\").innerHTML=status_on;\n\t\t var evt = {\"event\": \"websocket:connected\", \"content-type\" : \"audio/l16;rate=16000\"};\n \n \t\t ws.send(JSON.stringify(evt));\n\t\t\t console.log(\"Sent: \"+JSON.stringify(evt));\n\t\t\t console.log('connected');\n\t\t\t\n\t\t }\n\t\t ws.onclose = function(){\n\t\t\t document.getElementById(\"conn\").innerHTML=status_off;\n\t\t\t\tconsole.log('disconnected');\n\t\t\t\tcon_btn.innerText = 'Connect';\n\t\t\t\tcon_btn.onclick = function () { connect()};\n\t\t\t\tptt_btn.disabled = true; \n\t\t }\n\t\t \n\t ws.onmessage = function(event){\n if(event.data instanceof ArrayBuffer) {\n\t time = Math.max(context.currentTime, time)\n\t var input = new Int16Array(event.data)\n\t if(input.length) {\n\t var buffer = context.createBuffer(1, input.length, 16000)\n\t var data = buffer.getChannelData(0)\n\t for (var i = 0; i < data.length; i++) {\n\t data[i] = input[i] / 32767\n\t }\n\t var source = context.createBufferSource()\n\t source.buffer = buffer\n\t source.connect(context.destination)\n\t source.start(time += buffer.duration)\n\t }\n\t } else {\n\t console.log(\"Recieved: \"+ event.data);\n\t }\n } \n\t }\n\t \n\n navigator.mediaDevices.getUserMedia({\n video: false,\n audio: true\n })\n\t .then( stream => {\n var source = context.createMediaStreamSource(stream)\n var processor = context.createScriptProcessor(1024, 1, 1)\n var downsampled = new Int16Array(2048)\n var downsample_offset = 0\n\n function process_samples(){\n while(downsample_offset > 320) {\n var output = downsampled.slice(0, 320)\n downsampled.copyWithin(0, 320)\n downsample_offset -= 320\n\t\t\tif(ptt == true) {\n ws.send(output.buffer)\n }\n }\n }\n var sampleRatio = context.sampleRate / 16000\n processor.onaudioprocess = (audioProcessingEvent) => {\n var inputBuffer = audioProcessingEvent.inputBuffer\n var outputBuffer = audioProcessingEvent.outputBuffer\n var inputData = inputBuffer.getChannelData(0)\n var outputData = outputBuffer.getChannelData(0)\n for (var i = 0; i < inputData.length; i += sampleRatio) {\n var sidx = Math.floor(i)\n var tidx = Math.floor(i/sampleRatio)\n downsampled[downsample_offset + tidx] = inputData[sidx] * 32767\n }\n downsample_offset += ~~(inputData.length/sampleRatio)\n if(downsample_offset > 320) {\n process_samples()\n }\n for (var sample = 0; sample < inputBuffer.length; sample++) {\n // Silence the output\n outputData[sample] = 0\n }\n }\n source.connect(processor)\n processor.connect(context.destination)\n })\n </script>\n </body>\n</html>\n","output":"str","x":800,"y":100,"wires":[["b66d6e07fc89c848"]]},{"id":"305b5c990ac144fb","type":"websocket-listener","path":"/socket","wholemsg":"false"}]