node-red-contrib-music
Version:
Synthesise music with node-red. The beat node creates regular beats at a rate you can control, and which can be sunchronised with other machines. The divider node adds information to beat events, dividing beats into bars, bars into phrases, phrases into s
175 lines (141 loc) • 5.03 kB
JavaScript
const sc = require('./synth_common');
module.exports = function (RED) {
'use strict';
function LooperNode (config) {
RED.nodes.createNode(this, config);
const node = this;
reset();
this.on('input', function (msg) {
switch (msg.payload) {
case 'tick':
tickHandler(msg);
break;
case 'play': // the action
case 'record': // the action
node.count = -1;
setState(msg.payload);
break;
case 'stop':
switch (node.state) {
case 'play':
case 'record':
node.count = -1;
setState('waiting');
}
break;
case 'reset':
reset();
// do not send on message
break;
default:
// do nothing
break;
}
});
function tickHandler (msg) {
switch (node.state) {
case 'wait':
// do nothing
break;
case 'play': // the state
case 'record': // the state
let start = msg.start || [];
if (node.count <= 0) {
if (start.includes(node.start)) {
node.count = node.length;
setState(node.state); // to display status
createSynth(node, msg, node.state);
} else {
// ignore all ticks until the start event
}
} else {
if (start.includes(node.input)) {
node.count--;
}
if (node.count <= 0) {
if (node.loop) {
// whether we are recording or looping we carry on to play
setState('play');
// call the handler immediately in case the looper starts playing immediately it finishes recording
tickHandler(msg);
} else {
setState('wait'); // TODO: unless we are looping
}
}
}
break;
default:
// do nothing
}
}
function reset () {
node.volume = config.volume || 100;
node.input = config.input || 'beat';
node.start = config.start || 'bar'; // sampler won't start playing/recording until this
node.loop = config.loop || false;
node.length = Number(config.length) || 4;
node.tags = [];
node.count = -1; // the number of relevant ticks left to the end of the play/record. -1 indicates it hasn't started yet
setState('wait');
// wait a little while to allow wires to be created
setTimeout(function () {
node.send({ topic: 'looper', nodeID: node.id });
}, 200);
}
function setState (state) {
// state can be:
// 'wait' for a command ('play' or 'record');
// 'play'; although if beatCount is -1 it is not actually play yet
// 'record'; ditto
let shape, colour;
node.state = state;
switch (state) {
case 'record':
colour = 'red';
break;
case 'play':
colour = 'green';
break;
case 'wait':
colour = 'grey';
break;
}
if (node.count === -1) {
shape = 'ring';
} else {
shape = 'dot';
}
node.status({ fill: colour, shape: shape, text: state });
}
}
function createSynth (node, msg, action) {
if (!['play', 'record'].includes(action)) {
node.warn('no synth for action ' + action);
return;
}
const amp = sc.volume2amp(node);
let details = { amp };
const bpm = msg.bpm || node.context().flow.get('bpm') || node.context().global.get('bpm');
let sustain = node.length * 60 / bpm;
const multiple = msg['beats_per_' + node.input];
if (multiple) {
sustain *= multiple;
}
Object.assign(details, { bpm, sustain });
let looperMsg = {
payload: 'tick',
details,
looper: action,
nodeID: node.id
};
if (msg.timeTag) {
let timeTag = msg.timeTag;
if (msg.latency) {
timeTag -= msg.latency / 2;
}
looperMsg.timeTag = timeTag;
}
node.send(looperMsg);
}
RED.nodes.registerType('looper', LooperNode);
};