Initial commit

This commit is contained in:
Marcelo
2025-11-20 15:27:34 -06:00
commit cc72c9fc5d
3221 changed files with 737477 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,176 @@
<!--
Copyright 2013, 2016, 2018, 2019 IBM Corp.
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.
-->
<script type="text/x-red" data-template-name="microphone">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Microphone">
</div>
</script>
<script type="text/x-red" data-help-name="microphone">
<p>A simple microphone node to record audio directly from the web browser</p>
<p>Usage:</p>
<ol>
<li>Note - you must be using <code>https</code> to record</li>
<li>Click the <code>button</code> to start recording</li>
<li>Click the <code>button</code> again to stop recording</li>
</ol>
<p>The recorded audio is stored in the nodes <code>msg.payload</code> object</p>
<p>To see if the microphone node is supported in your browser check <a href="http://caniuse.com/#feat=stream" target="_blank">this</a> page</p>
<p>Supported output formats:</p>
<ul>
<li>WAV</li>
</ul>
</script>
<script type="text/javascript">
(function() {
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;
window.AudioContext = window.AudioContext || window.webkitAudioContext || window.mozAudioContext || window.oAudioContext || window.msAudioContext;;
window.URL = window.URL || window.webkitURL || window.mozURL || window.msURL;
const MICSTATUS = {
OFF : '1',
ON : '2',
CONTEXTERROR : '4'
}
$.getScript( 'microphone/js/recorder.js' )
.done(function( script, textStatus ) {
// script loaded, do nothing
})
.fail(function( jqxhr, settings, exception ) {
console.log('FAILED to load recorder script: '+exception)
});
var instances = {}
var audio_context = new AudioContext
function checkAudioContextState() {
var p = new Promise(function resolver(resolve, reject) {
if (audio_context.state && 'running' !== audio_context.state && audio_context.resume) {
audio_context.resume().then(() => {
resolve()
})
} else {
resolve()
}
});
return p
}
function startRecording(id, stream) {
checkAudioContextState()
.then(() => {
var input = audio_context.createMediaStreamSource(stream)
instances[id] = new Recorder(input)
instances[id] && instances[id].record()
})
.catch(() => {
console.log('Audio Context Error')
setMicStatus(MICSTATUS.CONTEXTERROR, id)
})
}
function stopRecording(id, callback) {
if(!instances[id]) {
return
}
instances[id].stop()
instances[id].exportWAV(function(blob) {
instances[id].clear()
instances[id] = null
callback & callback(id, blob)
})
setMicStatus(MICSTATUS.OFF, id);
}
function getRecorder(id) {
const constraints = {video : false, audio : true};
navigator.mediaDevices.getUserMedia(constraints)
.then(function(stream) {
setMicStatus(MICSTATUS.ON, id)
startRecording(id, stream)
})
.catch(function(err) {
window.alert('Your browser does not support audio recording')
console.log(err)
})
}
function getRecorderXX(id) {
if(!navigator.getUserMedia) {
window.alert('Your browser does not support audio recording')
return
}
navigator.getUserMedia({audio: true},
function(stream){
setMicStatus(MICSTATUS.ON, id)
startRecording(id, stream)
},
function(e) {
window.alert(e.message)
})
}
function uploadRecord(id, blob) {
var xhr = new XMLHttpRequest()
xhr.open('POST', 'node-red-microphone/' + id, true)
xhr.send(blob)
}
function setMicStatus(record, id) {
$.getJSON('node-red-microphone/status', {status: record, id: id})
.done(function () {})
.fail(function (err) {
console.log(err);
})
.always(function () {});
}
RED.nodes.registerType('microphone', {
category: 'input',
defaults: {
name: {value: ''}
},
color: 'rgb(204, 230, 171)',
inputs: 0,
outputs: 1,
icon: 'microphone.png',
paletteLabel: 'microphone',
label: function() {
return this.name || 'microphone';
},
labelStyle: function() {
return this.name ? 'node_label_italic' : '';
},
button: {
onclick: function(){
if(instances[this.id]) {
stopRecording(this.id, uploadRecord)
} else {
getRecorder(this.id)
}
}
}
});
})();
</script>

View File

@@ -0,0 +1,84 @@
/**
* Copyright 2013, 2016 IBM Corp.
*
* 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.
**/
var bodyParser = require('body-parser')
module.exports = function (RED) {
var requestSize = '50mb'
var requestType = 'audio/wav'
const MICSTATUS = {
OFF : '1',
ON : '2',
CONTEXTERROR : '4'
}
function Node (config) {
RED.nodes.createNode(this, config);
var node = this;
RED.httpAdmin.get('/node-red-microphone/status', function (req, res) {
var n = RED.nodes.getNode(req.query.id)
var status = {};
switch(req.query.status) {
case MICSTATUS.ON :
status = {fill:'red', shape:'dot', text:'recording...'}
break;
case MICSTATUS.CONTEXTERROR :
status = {fill:'red', shape:'dot', text:'error resuming audio context'}
break;
}
if (n) {
n.status(status);
}
res.json({});
});
RED.httpAdmin.post('/node-red-microphone/:id', bodyParser.raw({ type: requestType, limit: requestSize }), function(req,res) {
var node = RED.nodes.getNode(req.params.id)
if (node != null) {
try {
node.receive({payload: req.body})
res.sendStatus(200)
} catch(err) {
node.status({fill:'red', shape:'dot', text:'upload failed'});
res.sendStatus(500)
node.error(RED._("upload-microphone.failed", { error: err.toString() }))
}
} else {
res.status(404).send("no node found")
}
})
this.on('input', function (msg) {
if(msg.payload !== '') {
node.send(msg)
}
})
}
RED.nodes.registerType('microphone', Node)
RED.httpAdmin.get('/microphone/js/*', function(req, res){
var options = {
root: __dirname + '/static/',
dotfiles: 'deny'
};
res.sendFile(req.params[0], options);
});
};

View File

@@ -0,0 +1,357 @@
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Recorder = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
"use strict";
module.exports = require("./recorder").Recorder;
},{"./recorder":2}],2:[function(require,module,exports){
'use strict';
var _createClass = (function () {
function defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];descriptor.enumerable = descriptor.enumerable || false;descriptor.configurable = true;if ("value" in descriptor) descriptor.writable = true;Object.defineProperty(target, descriptor.key, descriptor);
}
}return function (Constructor, protoProps, staticProps) {
if (protoProps) defineProperties(Constructor.prototype, protoProps);if (staticProps) defineProperties(Constructor, staticProps);return Constructor;
};
})();
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.Recorder = undefined;
var _inlineWorker = require('inline-worker');
var _inlineWorker2 = _interopRequireDefault(_inlineWorker);
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
var Recorder = exports.Recorder = (function () {
function Recorder(source, cfg) {
var _this = this;
_classCallCheck(this, Recorder);
this.config = {
bufferLen: 4096,
numChannels: 2,
mimeType: 'audio/wav'
};
this.recording = false;
this.callbacks = {
getBuffer: [],
exportWAV: []
};
Object.assign(this.config, cfg);
this.context = source.context;
this.node = (this.context.createScriptProcessor || this.context.createJavaScriptNode).call(this.context, this.config.bufferLen, this.config.numChannels, this.config.numChannels);
this.node.onaudioprocess = function (e) {
if (!_this.recording) return;
var buffer = [];
for (var channel = 0; channel < _this.config.numChannels; channel++) {
buffer.push(e.inputBuffer.getChannelData(channel));
}
_this.worker.postMessage({
command: 'record',
buffer: buffer
});
};
source.connect(this.node);
this.node.connect(this.context.destination); //this should not be necessary
var self = {};
this.worker = new _inlineWorker2.default(function () {
var recLength = 0,
recBuffers = [],
sampleRate = undefined,
numChannels = undefined;
self.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 'getBuffer':
getBuffer();
break;
case 'clear':
clear();
break;
}
};
function init(config) {
sampleRate = config.sampleRate;
numChannels = config.numChannels;
initBuffers();
}
function record(inputBuffer) {
for (var channel = 0; channel < numChannels; channel++) {
recBuffers[channel].push(inputBuffer[channel]);
}
recLength += inputBuffer[0].length;
}
function exportWAV(type) {
var buffers = [];
for (var channel = 0; channel < numChannels; channel++) {
buffers.push(mergeBuffers(recBuffers[channel], recLength));
}
var interleaved = undefined;
if (numChannels === 2) {
interleaved = interleave(buffers[0], buffers[1]);
} else {
interleaved = buffers[0];
}
var dataview = encodeWAV(interleaved);
var audioBlob = new Blob([dataview], { type: type });
self.postMessage({ command: 'exportWAV', data: audioBlob });
}
function getBuffer() {
var buffers = [];
for (var channel = 0; channel < numChannels; channel++) {
buffers.push(mergeBuffers(recBuffers[channel], recLength));
}
self.postMessage({ command: 'getBuffer', data: buffers });
}
function clear() {
recLength = 0;
recBuffers = [];
initBuffers();
}
function initBuffers() {
for (var channel = 0; channel < numChannels; channel++) {
recBuffers[channel] = [];
}
}
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) {
var buffer = new ArrayBuffer(44 + samples.length * 2);
var view = new DataView(buffer);
/* RIFF identifier */
writeString(view, 0, 'RIFF');
/* RIFF chunk length */
view.setUint32(4, 36 + 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, numChannels, 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, numChannels * 2, 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;
}
}, self);
this.worker.postMessage({
command: 'init',
config: {
sampleRate: this.context.sampleRate,
numChannels: this.config.numChannels
}
});
this.worker.onmessage = function (e) {
var cb = _this.callbacks[e.data.command].pop();
if (typeof cb == 'function') {
cb(e.data.data);
}
};
}
_createClass(Recorder, [{
key: 'record',
value: function record() {
this.recording = true;
}
}, {
key: 'stop',
value: function stop() {
this.recording = false;
}
}, {
key: 'clear',
value: function clear() {
this.worker.postMessage({ command: 'clear' });
}
}, {
key: 'getBuffer',
value: function getBuffer(cb) {
cb = cb || this.config.callback;
if (!cb) throw new Error('Callback not set');
this.callbacks.getBuffer.push(cb);
this.worker.postMessage({ command: 'getBuffer' });
}
}, {
key: 'exportWAV',
value: function exportWAV(cb, mimeType) {
mimeType = mimeType || this.config.mimeType;
cb = cb || this.config.callback;
if (!cb) throw new Error('Callback not set');
this.callbacks.exportWAV.push(cb);
this.worker.postMessage({
command: 'exportWAV',
type: mimeType
});
}
}], [{
key: 'forceDownload',
value: function forceDownload(blob, filename) {
var url = (window.URL || window.webkitURL).createObjectURL(blob);
var link = window.document.createElement('a');
link.href = url;
link.download = filename || 'output.wav';
var click = document.createEvent("Event");
click.initEvent("click", true, true);
link.dispatchEvent(click);
}
}]);
return Recorder;
})();
exports.default = Recorder;
},{"inline-worker":3}],3:[function(require,module,exports){
"use strict";
module.exports = require("./inline-worker");
},{"./inline-worker":4}],4:[function(require,module,exports){
(function (global){
"use strict";
var _createClass = (function () { function defineProperties(target, props) { for (var key in props) { var prop = props[key]; prop.configurable = true; if (prop.value) prop.writable = true; } Object.defineProperties(target, props); } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();
var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } };
var WORKER_ENABLED = !!(global === global.window && global.URL && global.Blob && global.Worker);
var InlineWorker = (function () {
function InlineWorker(func, self) {
var _this = this;
_classCallCheck(this, InlineWorker);
if (WORKER_ENABLED) {
var functionBody = func.toString().trim().match(/^function\s*\w*\s*\([\w\s,]*\)\s*{([\w\W]*?)}$/)[1];
var url = global.URL.createObjectURL(new global.Blob([functionBody], { type: "text/javascript" }));
return new global.Worker(url);
}
this.self = self;
this.self.postMessage = function (data) {
setTimeout(function () {
_this.onmessage({ data: data });
}, 0);
};
setTimeout(function () {
func.call(self);
}, 0);
}
_createClass(InlineWorker, {
postMessage: {
value: function postMessage(data) {
var _this = this;
setTimeout(function () {
_this.self.onmessage({ data: data });
}, 0);
}
}
});
return InlineWorker;
})();
module.exports = InlineWorker;
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{}]},{},[1])(1)
});