From 2efafec9004564438a3fce21b1b1e856a1a6caeb Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 30 Mar 2020 21:49:57 +0100 Subject: [PATCH] Add ui_microphone (#33) * Add ui_microphone * [microphon] remove stray console.log * [microphone] Fix up copyright --- node-red-node-ui-microphone/LICENSE | 201 ++++++++++++++++ node-red-node-ui-microphone/README.md | 33 +++ .../lib/recorderWorker.js | 161 +++++++++++++ node-red-node-ui-microphone/package.json | 25 ++ .../ui_microphone.html | 127 ++++++++++ node-red-node-ui-microphone/ui_microphone.js | 220 ++++++++++++++++++ 6 files changed, 767 insertions(+) create mode 100644 node-red-node-ui-microphone/LICENSE create mode 100644 node-red-node-ui-microphone/README.md create mode 100644 node-red-node-ui-microphone/lib/recorderWorker.js create mode 100644 node-red-node-ui-microphone/package.json create mode 100644 node-red-node-ui-microphone/ui_microphone.html create mode 100644 node-red-node-ui-microphone/ui_microphone.js diff --git a/node-red-node-ui-microphone/LICENSE b/node-red-node-ui-microphone/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/node-red-node-ui-microphone/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/node-red-node-ui-microphone/README.md b/node-red-node-ui-microphone/README.md new file mode 100644 index 0000000..ec9bfa6 --- /dev/null +++ b/node-red-node-ui-microphone/README.md @@ -0,0 +1,33 @@ +node-red-node-ui-microphone +============================ + +A Node-RED UI widget node that allows audio to be recorded from the dashboard. + +## Install + +Either use the Editor - Menu - Manage Palette - Install option, or run the following command in your Node-RED user directory (typically `~/.node-red`) after installing node-red-dashboard: + + npm i node-red-node-ui-microphone + +## Usage + +The node provides a single button that, when clicked, will begin to capture audio. + +This node provides a single button widget in the dashboard that, when pressed, +will begin to capture audio. It will continue to capture audio until the button +is pressed again, or it reaches its configured maximum duration. + +The audio is captured in WAV format and published by the node as a Buffer object. +This can be written straight to a file or passed to any other node that expects +audio data. + +## Privacy + +When the button is first pressed, the browser will ask the user's permission for +the page to access the microphone. The node cannot record audio until the user +has given their permission. + + +## Notices + +This node uses `recorderWorker.js` Copyright © 2013 Matt Diamond, under the MIT License diff --git a/node-red-node-ui-microphone/lib/recorderWorker.js b/node-red-node-ui-microphone/lib/recorderWorker.js new file mode 100644 index 0000000..3246838 --- /dev/null +++ b/node-red-node-ui-microphone/lib/recorderWorker.js @@ -0,0 +1,161 @@ +/*License (MIT) + +Copyright © 2013 Matt Diamond + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of +the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +*/ + +var recLength = 0, + recBuffersL = [], + recBuffersR = [], + sampleRate; + +this.onmessage = function(e){ + switch(e.data.command){ + case 'init': + init(e.data.config); + break; + case 'record': + record(e.data.buffer); + break; + case 'exportWAV': + exportWAV(e.data.type); + break; + case 'exportMonoWAV': + exportMonoWAV(e.data.type); + break; + case 'getBuffers': + getBuffers(); + break; + case 'clear': + clear(); + break; + } +}; + +function init(config){ + sampleRate = config.sampleRate; +} + +function record(inputBuffer){ + recBuffersL.push(inputBuffer[0]); + recBuffersR.push(inputBuffer[1]); + recLength += inputBuffer[0].length; +} + +function exportWAV(type){ + var bufferL = mergeBuffers(recBuffersL, recLength); + var bufferR = mergeBuffers(recBuffersR, recLength); + var interleaved = interleave(bufferL, bufferR); + var dataview = encodeWAV(interleaved); + var audioBlob = new Blob([dataview], { type: type }); + + this.postMessage(audioBlob); +} + +function exportMonoWAV(type){ + var bufferL = mergeBuffers(recBuffersL, recLength); + var dataview = encodeWAV(bufferL, true); + var audioBlob = new Blob([dataview], { type: type }); + + this.postMessage(audioBlob); +} + +function getBuffers() { + var buffers = []; + buffers.push( mergeBuffers(recBuffersL, recLength) ); + buffers.push( mergeBuffers(recBuffersR, recLength) ); + this.postMessage(buffers); +} + +function clear(){ + recLength = 0; + recBuffersL = []; + recBuffersR = []; +} + +function mergeBuffers(recBuffers, recLength){ + var result = new Float32Array(recLength); + var offset = 0; + for (var i = 0; i < recBuffers.length; i++){ + result.set(recBuffers[i], offset); + offset += recBuffers[i].length; + } + return result; +} + +function interleave(inputL, inputR){ + var length = inputL.length + inputR.length; + var result = new Float32Array(length); + + var index = 0, + inputIndex = 0; + + while (index < length){ + result[index++] = inputL[inputIndex]; + result[index++] = inputR[inputIndex]; + inputIndex++; + } + return result; +} + +function floatTo16BitPCM(output, offset, input){ + for (var i = 0; i < input.length; i++, offset+=2){ + var s = Math.max(-1, Math.min(1, input[i])); + output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true); + } +} + +function writeString(view, offset, string){ + for (var i = 0; i < string.length; i++){ + view.setUint8(offset + i, string.charCodeAt(i)); + } +} + +function encodeWAV(samples, mono){ + var buffer = new ArrayBuffer(44 + samples.length * 2); + var view = new DataView(buffer); + + /* RIFF identifier */ + writeString(view, 0, 'RIFF'); + /* file length */ + view.setUint32(4, 32 + samples.length * 2, true); + /* RIFF type */ + writeString(view, 8, 'WAVE'); + /* format chunk identifier */ + writeString(view, 12, 'fmt '); + /* format chunk length */ + view.setUint32(16, 16, true); + /* sample format (raw) */ + view.setUint16(20, 1, true); + /* channel count */ + view.setUint16(22, mono?1:2, true); + /* sample rate */ + view.setUint32(24, sampleRate, true); + /* byte rate (sample rate * block align) */ + view.setUint32(28, sampleRate * 4, true); + /* block align (channel count * bytes per sample) */ + view.setUint16(32, 4, true); + /* bits per sample */ + view.setUint16(34, 16, true); + /* data chunk identifier */ + writeString(view, 36, 'data'); + /* data chunk length */ + view.setUint32(40, samples.length * 2, true); + + floatTo16BitPCM(view, 44, samples); + + return view; +} diff --git a/node-red-node-ui-microphone/package.json b/node-red-node-ui-microphone/package.json new file mode 100644 index 0000000..4c51b82 --- /dev/null +++ b/node-red-node-ui-microphone/package.json @@ -0,0 +1,25 @@ +{ + "name": "node-red-node-ui-microphone", + "version": "0.1.0", + "description": "A Node-RED ui node to record audio on a dashboard.", + "author": "Nick O'Leary", + "license": "Apache-2.0", + "keywords": [ + "node-red", + "node-red-dashboard", + "microphone" + ], + "bugs": { + "url": "https://github.com/node-red/node-red-ui-nodes/issues" + }, + "homepage": "https://github.com/node-red/node-red-ui-nodes/node-red-node-ui-microphone", + "repository": { + "type": "git", + "url": "git+https://github.com/node-red/node-red-ui-nodes.git" + }, + "node-red": { + "nodes": { + "ui_microphone": "ui_microphone.js" + } + } +} diff --git a/node-red-node-ui-microphone/ui_microphone.html b/node-red-node-ui-microphone/ui_microphone.html new file mode 100644 index 0000000..0ca8816 --- /dev/null +++ b/node-red-node-ui-microphone/ui_microphone.html @@ -0,0 +1,127 @@ + + + + + + + diff --git a/node-red-node-ui-microphone/ui_microphone.js b/node-red-node-ui-microphone/ui_microphone.js new file mode 100644 index 0000000..c50b653 --- /dev/null +++ b/node-red-node-ui-microphone/ui_microphone.js @@ -0,0 +1,220 @@ +/** + * Copyright 2020 OpenJS Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + + +const path = require('path'); + +module.exports = function(RED) { + + function HTML(config) { + var configAsJson = JSON.stringify(config); + var html = String.raw` + + + + `; + return html; + } + + function checkConfig(node, conf) { + if (!conf || !conf.hasOwnProperty("group")) { + node.error(RED._("ui_microphone.error.no-group")); + return false; + } + return true; + } + + var ui = undefined; // instantiate a ui variable to link to the dashboard + + function MicrophoneNode(config) { + try { + var node = this; + if(ui === undefined) { + ui = RED.require("node-red-dashboard")(RED); + } + RED.nodes.createNode(this, config); + + // placing a "debugger;" in the code will cause the code to pause its execution in the web browser + // this allows the user to inspect the variable values and see how the code is executing + //debugger; + + var done = null; + + if (checkConfig(node, config)) { + var html = HTML(config); // *REQUIRED* get the HTML for this node using the function from above + done = ui.addWidget({ // *REQUIRED* add our widget to the ui dashboard using the following configuration + node: node, // *REQUIRED* + order: config.order, // *REQUIRED* placeholder for position in page + group: config.group, // *REQUIRED* + width: config.width, // *REQUIRED* + height: config.height, // *REQUIRED* + format: html, // *REQUIRED* + templateScope: "local", // *REQUIRED* + emitOnlyNewValues: false, // *REQUIRED* + forwardInputMessages: false, // *REQUIRED* + storeFrontEndInputAsState: false, // *REQUIRED* + convertBack: function (value) { + return value; + }, + beforeEmit: function(msg) { + return { msg: msg }; + }, + beforeSend: function (msg, orig) { + if (orig) { return orig.msg; } + }, + /** + * The initController is where most of the magic happens. + * This is the section where you will write the Javascript needed for your node to function. + * The 'msg' object will be available here. + */ + initController: function($scope) { + + $scope.init = function (config) { + $scope.config = config; + } + + var worker; + var mediaRecorder; + var audioContext; + var stopTimeout; + var active = false; + + var button = $("#microphone_control_"+$scope.$id); + $scope.toggleMicrophone = function() { + if (!active) { + active = true; + $("#microphone_control_"+$scope.$id+" i").removeClass("fa-microphone fa-2x").addClass("fa-circle-o-notch fa-spin"); + navigator.mediaDevices.getUserMedia({ audio: true, video: false }).then(handleSuccess).catch(handleError); + } else { + if (mediaRecorder) { + mediaRecorder.stop(); + } + } + } + $scope.stop = function() { + if (active) { + mediaRecorder.stop(); + } + } + var handleError = function(err) { + console.warn("Failed to access microphone:",err); + active = false; + $("#microphone_control_"+$scope.$id+" i").addClass("fa-microphone fa-2x").removeClass("fa-circle-o-notch fa-spin"); + } + var handleSuccess = function(stream) { + mediaRecorder = new MediaRecorder(stream, {mimeType: 'audio/webm'}); + mediaRecorder.ondataavailable = function(evt) { + if (evt.data.size > 0) { + sendBlob(new Blob([evt.data])); + } + }; + + mediaRecorder.onstop = function() { + if (active) { + active = false; + $("#microphone_control_"+$scope.$id+" i").addClass("fa-microphone fa-2x").removeClass("fa-circle-o-notch fa-spin"); + if (stopTimeout) { + clearTimeout(stopTimeout); + stopTimeout = null; + } + } + }; + // Timeslice is not current exposed. + var timeslice = 0; + if ($scope.config.timeslice) { + timeslice = parseInt($scope.config.timeslice)*1000; + } + if (timeslice) { + mediaRecorder.start(timeslice); + } else { + mediaRecorder.start(); + } + + if ($scope.config.maxLength) { + stopTimeout = setTimeout(function() { + if (active) { + mediaRecorder.stop(); + } + },$scope.config.maxLength*1000) + } + }; + + var sendBlob = function(blob) { + if (!audioContext) { + audioContext= new AudioContext() + } + var fileReader = new FileReader() + // Set up file reader on loaded end event + fileReader.onloadend = function() { + audioContext.decodeAudioData(fileReader.result, function(audioBuffer) { + convertToWav(audioBuffer) + }) + } + fileReader.readAsArrayBuffer(blob) + } + + var convertToWav = function(buffer) { + if (!worker) { + worker = new Worker('ui_microphone/recorderWorker.js'); + } + worker.postMessage({ command: 'init', config: {sampleRate: 44100} }); + worker.onmessage = function( e ) { + $scope.send({payload:e.data}); + worker.postMessage({ command: 'clear' }); + }; + worker.postMessage({ + command: 'record', + buffer: [ + buffer.getChannelData(0), + buffer.getChannelData(0) + ] + }); + worker.postMessage({ command: 'exportMonoWAV', type: 'audio/wav' }); + } + } + }); + } + } + catch (e) { + // eslint-disable-next-line no-console + console.warn(e); // catch any errors that may occur and display them in the web browsers console + } + + node.on("close", function() { + if (done) { done(); } + }); + } + + /** + * REQUIRED + * Registers the node with a name, and a configuration. + * Type MUST start with ui_ + */ + RED.nodes.registerType("ui_microphone", MicrophoneNode); + + var uipath = 'ui'; + if (RED.settings.ui) { uipath = RED.settings.ui.path; } + var fullPath = path.join('/', uipath, '/ui_microphone/*').replace(/\\/g, '/'); + RED.httpNode.get(fullPath, function (req, res) { + var options = { + root: __dirname + '/lib/', + dotfiles: 'deny' + }; + res.sendFile(req.params[0], options) + }); + +};