UNPKG

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

130 lines (106 loc) 4.62 kB
module.exports = function (RED) { 'use strict'; function DividerNode (config) { const nrp = require('node-red-contrib-properties'); RED.nodes.createNode(this, config); var node = this; let properties = new nrp.NodeRedProperties(node, config, { name: { value: 'bar' }, input: { value: 'beat' }, output: { value: 'bar' }, ratio: { value: 4 } }); /* * node.offsetCount is used as an offest to the number of beats when calculating the number of bars and the beat of bar from the beat number * This is usually 0, but changes when the ratio is changed */ properties.handle(setRatio, 'ratio'); reset(); setRatio(config.ratio || 4); this.on('input', function (msg) { if (properties.input(msg)) { return; } switch (msg.payload) { case 'tick': const start = msg.start || []; const end = msg.end || []; let inputVal, inputCount, outputCount; inputVal = msg[node.input]; inputCount = counts(inputVal).input; outputCount = counts(inputVal).output; if (inputCount >= node.ratio && end.includes(node.input)) { end.push(node.output); } if (inputCount === 1 && start.includes(node.input)) { start.push(node.output); } msg.start = start; msg.end = end; var counter = node.input + '_of_' + node.output; var beatsPerName = 'beats_per_' + node.output; let beatsPerVal; if (node.input === 'beat') { beatsPerVal = node.ratio; } else { beatsPerVal = node.ratio * msg['beats_per_' + node.input]; } msg[beatsPerName] = beatsPerVal; msg[counter] = inputCount; node.lastInput = inputCount; msg[node.output] = outputCount; node.lastOutput = outputCount; node.status({ 'text': node.output + ':' + outputCount + ' ' + node.input + ':' + inputCount + ' ' + ' ratio ' + node.ratio }); node.send(msg); break; case 'reset': reset(); node.send(msg); break; default: node.send(msg); break; } }); // calculate the counts (e.g. beat of bar, bar) function counts (inputVal) { let beatExpr = inputVal - 1 + node.offsetCount; return { input: beatExpr % node.ratio + 1, output: Math.floor(beatExpr / node.ratio) + 1 }; } function setRatio (ratio, msg) { msg = msg || {}; ratio = Number(ratio); if (ratio > 0 && Number.isInteger(ratio)) { let oldRatio = node.ratio; node.ratio = ratio; // recalculate the offset to retain the same e.g. bar and beat of bar (bob) // beat = (bar-1)*ratio + bob + offset if (node.lastOutput) { node.offsetCount = node.offsetCount + (node.lastOutput - 1) * (ratio - oldRatio); // when changing down ratio, make sure the new input is at most the last beat of bar let changeDown = Math.max(node.lastInput - ratio, 0); node.offsetCount -= changeDown; } else { // should only happen when first deploying node.offsetCount = 0; } } else { node.warn('Not a valid ratio: ' + ratio); } } function reset () { node.input = config.input || 'beat'; node.output = config.output || 'bar'; node.ratio = config.ratio || 4; node.name = config.name; node.offsetCount = 0; node.status(''); } } RED.nodes.registerType('divider', DividerNode); };