jw-xtouch
Version:
Wrapper for communicating with the Behringer X-Touch via midi
1,593 lines (1,375 loc) • 55.2 kB
JavaScript
/* TODO
-startup options (ports)
*/
/*
ver 1.0.0 20-11-19
*/
/* Information about the X-Touch keys:
key: [144, 0-101, 0/127]
faders: [224-232, ???, 0-127]
knobs (rotation): [176, 16-23, 1/65]
knobs (button): [144, 32-39, 0/127]
knobs (lights): [176, 48-55,0-127]
jog/shuttle: [176,60, 1/65]
Display-lights: 176
small lights:
solo: 115
beats: 114
smpte: 113
Assignment: 75, 74
Hours/Bars: 73, 72, 71
Minutes/Beats: 70, 69
Seconds/Sub Division: 68, 67
Frames/Ticks: 66, 65, 64
Values:
48=0
49=1
50=2
51=3
52=4
53=5
54=6
55=7
56=8
57=9
58=OFF
Level Lights: [208, 0-127?, ???]
*/
var midi = require('midi');
var midiIn = new midi.Input();
var midiOut = new midi.Output();
var ON = 127;
var OFF = 0;
var BLINK = 1;
var BUTTON = 144;
var KNOB = 176;
var JOG = 60;
var DISPLAY = 176;
var SIGNAL = 208;
var faderModes = {
'CH1': { mode: 'decibel', resolution: 1 },
'CH2': { mode: 'decibel', resolution: 1 },
'CH3': { mode: 'decibel', resolution: 1 },
'CH4': { mode: 'decibel', resolution: 1 },
'CH5': { mode: 'decibel', resolution: 1 },
'CH6': { mode: 'decibel', resolution: 1 },
'CH7': { mode: 'decibel', resolution: 1 },
'CH8': { mode: 'decibel', resolution: 1 },
'MAIN': { mode: 'decibel', resolution: 1 },
};
//raw is the midi response
//percent is the location of the fader at max resolution
//value is the converted value for the user
var faders = {
CH1: { raw: [0,0], position: null, value: null},
CH2: { raw: [0,0], position: null, value: null},
CH3: { raw: [0,0], position: null, value: null},
CH4: { raw: [0,0], position: null, value: null},
CH5: { raw: [0,0], position: null, value: null},
CH6: { raw: [0,0], position: null, value: null},
CH7: { raw: [0,0], position: null, value: null},
CH8: { raw: [0,0], position: null, value: null},
MAIN: { raw: [0,0], position: null, value: null},
};
var faderMap = {
//as buttons
104: 'CH1',
105: 'CH2',
106: 'CH3',
107: 'CH4',
108: 'CH5',
109: 'CH6',
110: 'CH7',
111: 'CH8',
112: 'MAIN',
//as fader movement
224: 'CH1',
225: 'CH2',
226: 'CH3',
227: 'CH4',
228: 'CH5',
229: 'CH6',
230: 'CH7',
231: 'CH8',
232: 'MAIN',
//reverse lookup movement
'CH1': 224,
'CH2': 225,
'CH3': 226,
'CH4': 227,
'CH5': 228,
'CH6': 229,
'CH7': 230,
'CH8': 231,
'MAIN': 232
};
function debug(msg) {
emit('debug', msg);
}
function error(msg) {
emit('error', msg);
}
/******************* Custom Emitter Code ************************************/
//this is for future browser compatibility
var _events = {};
function on(event, callback) {
//attaches a callback function to an event
_events[event] = callback;
}
function emit(event, msg) {
if (typeof _events[event] === 'function') { //the client has registered the event
_events[event](msg); //run the event function provided
}
if (event === 'action') {
runControlMap(msg);
}
}
/******************************************************************************/
/*****************************************************************************/
/***** sendMidi functions ****************************************************/
var commandStack = [];
var commandInterval = null;
var commandDelay = 10;
var keySequence = null;
function sendMidi(midiMsg) {
function inRange(num) {
if (num <0 || num>127) { return false; }
else { return true; }
}
function fail() {
emit('sendMidi: ' + JSON.stringify(midiMsg) + ' is not array, is not 3 elements or has invalid elements');
}
if (!Array.isArray(midiMsg)) {
fail();
} else if (midiMsg.length !== 3) {
fail();
} else if (!inRange(midiMsg[1])) {
fail();
}
commandStack.push(midiMsg);
//if the midi interval is not already running
if (!commandInterval) {
//immediately send the fist midi msg
debug('MIDI Out: ' + commandStack[0]);
try {
midiOut.sendMessage(commandStack[0]);
} catch (e) {
emit('error', 'MIDI: ' + e);
}
commandStack.shift();
//start the interval
startMidiInterval();
}
}
function startMidiInterval() {
commandInterval = setInterval(function() {
if (commandStack.length > 0) {
try {
midiOut.sendMessage(commandStack[0]);
} catch (e) {
emit('error', 'MIDI: ' + e);
}
debug('MIDI Out: ' + commandStack[0]);
commandStack.shift();
} else {
stopMidiInterval();
}
}, commandDelay);
}
function stopMidiInterval() {
clearInterval(commandInterval);
commandInterval = null;
if (keySequence) {
sendKeys(keySequence);
keySequence = null;
}
}
/*****************************************************************************/
/*****************************************************************************/
var mode = '';
var modeMap = {};
var controlMap = { foo: 'bar'};
function setMode(newMode) {
if (typeof newMode !== 'undefined') { mode = newMode; }
return mode;
}
function updateControlMap(map) {
controlMap = map;
return controlMap;
}
function updateModeMap(map) {
modeMap = map;
return modeMap;
}
function runControlMap(action) {
var control = action.control;
var name = action.name;
var state = action.state;
//First the specialized mode configurations
try {
if(typeof modeMap[mode][control] === 'function') {
modeMap[mode][control](name, state);
return true;
}
} catch (e) { }
try {
if (typeof modeMap[mode][control][name] === 'function') {
modeMap[mode][control][name](state);
return true;
}
} catch(e) { }
try {
if (typeof modeMap[mode][control][state][name] === 'function') {
modeMap[mode][control][state][name]();
return true;
}
} catch (e) { }
try {
if (typeof modeMap[mode][control][name][state] === 'function') {
modeMap[mode][control][name][state]();
return true;
}
} catch(e) { }
//now the standard mappings
//this checks for a mapping just based on the type
//useful if you want to handle the entire type in your own function
try {
if (typeof controlMap[control] === 'function') {
controlMap[control](name, state);
}
} catch (e) { }
//this checks for a mapping based on the type, then the name and passes the state
//this is most useful for faders, knobs or groups which have a variety of states
try {
if (typeof controlMap[control][name] === 'function') {
controlMap[control][name](state);
return true;
}
} catch(e) { }
//this checks for a mapping based on type/state/name
//so for example all the button presses that should fire on the 'up' state
try {
if (typeof controlMap[control][state][name] === 'function') {
controlMap[control][state][name]();
return true;
}
} catch(e) { }
//this checks for a mapping based on type/name/state
//this is better for when a different event happens on each state
//best for the jog-shuttle wheel or when the knobs are in default mode
try {
if (typeof controlMap[control][name][state] === 'function') {
controlMap[control][name][state]();
return true;
}
} catch(e) { }
}
function callbackSafe(callback, option) {
if (typeof callback === 'function') { callback(option); }
}
/**
* Starts the midi device
*
* @param {function} callback
* @param {object} options
*/
function start(callback, options) {
var midiDeviceIndex = -1;
debug('MIDI Devices:');
for (var i=0; i<midiOut.getPortCount(); i++) {
var name = midiOut.getPortName(i);
debug(i + ': ' + name);
if (name.toLowerCase().indexOf('x-touch') > -1 && midiDeviceIndex === -1) {
midiDeviceIndex = i;
}
}
if (midiDeviceIndex) {
debug('Found probable X-Touch on device ' + midiDeviceIndex);
} else {
debug("Didn't find expected midi device, using port " + midiDeviceIndex);
midiDeviceIndex = 1;
}
if (options) {
if (options.port) {
if (!isNaN(options.port)) {
midiDeviceIndex = options.port;
debug('Using options.port = ' + options.port);
}
}
}
midiIn.openPort(0);
try {
midiOut.openPort(midiDeviceIndex);
callbackSafe(callback, 'Midi open on port ' + midiDeviceIndex);
} catch(e) {
emit('error', e);
callbackSafe(callback, 'Midi init failed on port ' + midiDeviceIndex);
}
}
/**
* Stops the midi device listener
*/
function stop() {
midiIn.closePort();
midiOut.closePort();
}
/**
* Returns a listing of available midi ports
*/
function getPorts() {
var ports = [];
for (var i=0; i<midiOut.getPortCount(); i++) {
ports.push(midiOut.getPortName(i));
}
return ports;
}
/*****************************************************************************/
/*****************************************************************************/
var buttons = {
0:'CH1.REC', 1:'CH2.REC',2:'CH3.REC',3:'CH4.REC',4:'CH5.REC',5:'CH6.REC',6:'CH7.REC',7:'CH8.REC',
8:'CH1.SOLO', 9:'CH2.SOLO', 10:'CH3.SOLO', 11:'CH4.SOLO', 12:'CH5.SOLO', 13:'CH6.SOLO',14:'CH7.SOLO',15:'CH8.SOLO',
16:'CH1.MUTE', 17:'CH2.MUTE', 18:'CH3.MUTE', 19:'CH4.MUTE', 20:'CH5.MUTE', 21:'CH6.MUTE', 22:'CH7.MUTE', 23:'CH8.MUTE',
24:'CH1.SELECT', 25:'CH2.SELECT', 26:'CH3.SELECT', 27:'CH4.SELECT', 28:'CH5.SELECT', 29:'CH6.SELECT', 30:'CH7.SELECT', 31:'CH8.SELECT',
40:'TRACK', 41:'SEND', 42:'PAN/SURROUND', 43:'PLUG-IN', 44:'EQ', 45:'INST',
46:'FADER BANK LEFT', 47:'FADER BANK RIGHT', 48:'CHANNEL LEFT', 49:'CHANNEL RIGHT',
50:'FLIP',
51:'GLOBAL VIEW',
52:'NAME/VALUE',
53:'BEATS',
54:'F1', 55:'F2', 56:'F3', 57:'F4', 58:'F5', 59:'F6', 60:'F7', 61:'F8',
62:'MIDI TRACKS', 63:'INPUTS', 64:'AUDIO TRACKS', 65:'AUDIO INST', 66:'AUX', 67:'BUSES', 68:'OUTPUTS', 69:'USER',
70:'SHIFT', 71:'OPTION', 72:'CONTROL', 73:'ALT',
74:'READ/OFF', 75:'WRITE', 76:'TRIM', 77:'TOUCH', 78:'LATCH', 79:'GROUP',
80:'SAVE', 81:'UNDO', 82:'CANCEL', 83:'ENTER',
84:'MARKER', 85:'NUDGE', 86:'CYCLE', 87:'DROP', 88:'REPLACE', 89:'CLICK', 90:'SOLO',
91:'REW', 92:'FWD', 93:'STOP', 94:'PLAY', 95:'REC',
96:'UP', 97:'DOWN', 98:'LEFT', 99:'RIGHT', 100:'ENTER',
101:'SCRUB',
};
var knobs = {
16:'CH1', 17:'CH2', 18:'CH3', 19:'CH4', 20:'CH5', 21:'CH6', 22:'CH7', 23:'CH8',
};
var knobButtons = {
32:'CH1', 33:'CH2', 34:'CH3', 35:'CH4', 36:'CH5', 37:'CH6', 38:'CH7', 39:'CH8'
};
knobLights = {
'CH1':48, 'CH2':49,'CH3':50,'CH4':51,'CH5':52,'CH6':53,'CH7':54,'CH8':55
};
//codes for all the light patterns the knobs can display
var knobLightMap = {
OFF: 0,
noSideLights: {
OFF:0,
sequence: {
0:0, OFF:0,
1:1, 2:2, 3:3, 4:4, 5:5, 6:6,
7:7, 8:8, 9:9, 10:10, 11:11
},
pan: {
OFF: 0,
'-5':17, '-4':18, '-3':19, '-2':20, '-1':21,
'0':22,
'1':23, '2':24, '3':25, '4':26, '5':27
},
level: {
0:32, 1:33, 2:34, 3:35, 4:36, 5:37, 6: 38,
7:39, 8:40, 9:41, 10:42, 11:43, //12:107
//12 kinda works as it adds one light to the end,
//but it also adds an extra one to the beginning, so for now, no.
},
fill: {
0:0, OFF:0,
1:49, 2:50, 3:51, 4:52, 5:53, 6:54, 7:118
//fill does get an extra value when not using side lights
},
normal: {
0:0
},
//simple way to turn all lights on or off
all: {
on: 118,
off: 0
}
},
sideLights: {
OFF: 0,
sequence: {
0:64,
1:65, 2:66, 3:67, 4:68, 5:69, 6:70,
7:71, 8:72, 9:73, 10:74, 11:75
},
pan: {
OFF: 80,
'-5':81, '-4':82, '-3':83, '-2':84, '-1':85,
'0':86,
'1':87, '2':88, '3':89, '4':90, '5':91
},
level: {
0:96, 1:97, 2:98, 3:99, 4:100, 5:101, 6: 102,
7:103, 8:104, 9:105, 10:106, 11:107
},
fill: {
0:112, OFF:112,
1:113, 2:114, 3:115, 4:116, 5:117, 6:118
},
normal: {
0:0
}
}
};
//codes for lighting up the signal level meters
var signalMap = {
'CH1' : {
1:1, 2:3, 3:5, 4:7, 5:9, 6:11, 7:13, 8:14 ,'CLEAR':15
},
'CH2': {
1:17, 2:19, 3:21, 4:23, 5:25, 6:27, 7:29, 8:30 ,'CLEAR':31
},
'CH3': {
1:33, 2:35, 3:37, 4:39, 5:41, 6:43, 7:45, 8:46, 'CLEAR':47
},
'CH4': {
1:49, 2:51, 3:53, 4:55, 5:57, 6:59, 7:61, 8:62, 'CLEAR':63
},
'CH5': {
1:65, 2:67, 3:69, 4:71, 5:73, 6:75, 7:77, 8:78, 'CLEAR':79
},
'CH6': {
1:81, 2:83, 3:85, 4:87, 5:89, 6:91, 7:93, 8:94, 'CLEAR':95
},
'CH7': {
1:97, 2:99, 3:101, 4:103, 5:105, 6:107, 7:109, 8:110, 'CLEAR':111
},
'CH8': {
1:113, 2:115, 3:117, 4:119, 5:121, 6:123, 7:125, 8:126, 'CLEAR':127
},
};
//this are intervals for holding signal levels
var signalTimers = {
'CH1': null, 'CH2': null, 'CH3': null, 'CH4': null,
'CH5': null, 'CH6': null, 'CH7': null, 'CH8': null
};
var displayElements = {
'ASSIGNMENT': [75,74],
'HOURS': [73,72,71],
'BARS': [73,72,71],
'MINUTES': [70,69],
'BEATS': [70,69],
'SECONDS': [68,67],
'SUB DIVISION': [68,67],
'FRAMES': [66,65,64],
'TICKS': [66,65,64]
};
//this tracks what's currently on the display so we don't need to waste time updating
//elements with the value they already have.
var displayElementValues= {
75:null, 74:null, //assignment
73:null, 72:null,71:null, //hours
70:null, 69:null, //minutes
68:null, 67:null, //seconds
66:null, 65:null,64:null //frames
};
//TODO: remove
//declared outside scope for showTempMessage
//var oldDisplayMsg = {};
var displayMap = {
'OFF':0, ' ':0,
'A':1,'B':2,'C':3,'D':4,'E':5,'F':6,'G':7,'H':8,'I':9,'J':10,'K':11,'L':12,'M':13,
'N':14,'O':15,'P':16,'Q':17,'R':18,'S':19,'T':20,'U':21,'V':22,'W':23,'X':24,'Y':25,'Z':26,
'_':31, '"':34, '°':35, "'":39,
'[':40,']':41, '|':44,
'=':61,
'.':64, '-':45, //65-90 letters with .
'0':48,'1':49,'2':50,'3':51,'4':52,'5':53,'6':54,'7':55,'8':56,'9':57,
'0.':112,'1.':113,'2.':114,'3.':115,'4.':116,'5.':117,'6.':118,'7.':119,'8.':120,'9.':121
};
var displayLights = {
SOLO: 115, BEATS: 114, SMPTE: 113
};
//converts db into the closest point on the gross 127 point fader scale
var dbMap = {
'-100':0, '-93':2, '-87':3, '-80':4, '-73':5, '-67':6, '-60':7, '-59':8,
'-58':9, '-56':10, '-55':11, '-54':12, '-53':13, '-51':14, '-50':15,
'-49':16, '-48':17, '-46':18, '-45':19, '-44':20, '-43':21, '-41':22,
'-40':23, '-39':24, '-38':25, '-36':26, '-35':27, '-34':28, '-33':29,
'-31':30, '-30':31, '-29':32, '-28':35, '-27':36, '-26':38, '-25':39,
'-24':41, '-23':42, '-22':44, '-21':45, '-20':47, '-19':49, '-18':50,
'-17':52, '-16':53, '-15':55, '-14':56, '-13':58, '-12':60, '-11':62,
'-10':63, '-9':66, '-8':70, '-7':73, '-6':76, '-5':79, '-4':82, '-3':85,
'-2':89, '-1':92, '0':95, '1':98, '2':101, '3':105, '4':108, '5':111,
'6':114, '7':117, '8':120, '9':124, '10':127,
};
/*****************************************************************************/
/*****************************************************************************/
var toggles = {};
var groups = {};
//these lights should light when pressed
var autoButtonLights = [];
//object to store automatic light/value modes for knobs
var knobModes = {
CH1: { mode:'normal', value:0, sideLights: false, min:0, max:0 },
CH2: { mode:'normal', value:0, sideLights: false, min:0, max:0 },
CH3: { mode:'normal', value:0, sideLights: false, min:0, max:0 },
CH4: { mode:'normal', value:0, sideLights: false, min:0, max:0 },
CH5: { mode:'normal', value:0, sideLights: false, min:0, max:0 },
CH6: { mode:'normal', value:0, sideLights: false, min:0, max:0 },
CH7: { mode:'normal', value:0, sideLights: false, min:0, max:0 },
CH8: { mode:'normal', value:0, sideLights: false, min:0, max:0 }
};
/**
* Set the mode for a knob to automate lights and values
* @param {string} knob - the knob to set
* @param {string} mode - the mode
* @param {boolean} sideLights - sidelights on
* @param {number} value - a starting value
*/
function setKnobMode(name, mode, sideLights, value) {
if (typeof name === 'undefined') {
emit('error', 'setKnobMode: No knob specified');
return false;
}
if (typeof mode === 'undefined') { mode = 'normal'; }
if (typeof sideLights === 'undefined') { sideLights = false; }
if (typeof value === 'undefined') { value = 0; }
name = name.toUpperCase();
mode = mode.toLowerCase();
try {
var knob = knobModes[name];
knob.mode = mode;
knob.sideLights = sideLights;
knob.value = value;
//set the min-max values
switch(mode) {
case 'sequence':
knob.min = 1;
knob.max = 11;
knob.value = clamp(knob.value, knob.min, knob.max);
setKnobLight(name, knob.mode, knob.value, knob.sideLights);
break;
case 'level':
knob.min = 0;
knob.max = 11;
knob.value = clamp(knob.value, knob.min, knob.max);
setKnobLight(name, knob.mode, knob.value, knob.sideLights);
break;
case 'pan':
knob.min = -5;
knob.max = 5;
knob.value = clamp(knob.value, knob.min, knob.max);
setKnobLight(name, knob.mode, knob.value, knob.sideLights);
break;
case 'fill':
knob.min = 1;
knob.max = sideLights? 6: 7; //bonus value if not using sidelights
knob.value = clamp(knob.value, knob.min, knob.max);
setKnobLight(name, knob.mode, knob.value, knob.sideLights);
break;
case 'normal':
knob.min = 0;
knob.max = 0;
knob.value = 0;
knob.sideLights = false;
setKnobLight(name, knob.mode, knob.value, knob.sideLightMode);
break;
default:
emit('error', 'setKnobMode: Invalid mode: ' + mode);
}
} catch (e) {
emit('error', 'setKnobMode: ' + e);
}
}
function processKnobModes(action) {
try {
var name = action.name;
var change = action.state ==='right' ? 1 : -1;
var knob = knobModes[name];
var oldValue = knob.value;
if (knob.mode !== 'normal') {
//set the new value
knob.value += change;
knob.value = clamp(knob.value, knob.min, knob.max);
//set the lights
setKnobLight(name, knob.mode, knob.value, knob.sideLights);
//emit the action only if the value has changed.
if (oldValue !== knob.value) {
emit('action', {
control: 'knob',
name: name,
state: knob.value
});
}
}
} catch (e) {
emit('error', 'Problem processing automatic knob mode: ' + e);
}
}
/**
* Adds a toggle group
* @param {string} buttonName
* @param {object} toggle
*/
function addToggle(buttonName, toggle) {
toggles[buttonName] = toggle;
//console.log('Toggles: ' + JSON.stringify(toggles));
setButtonLight(buttonName, toggle.state);
}
function removeToggle(buttonName) {
setButtonLight(buttonName, 'off');
delete toggles[buttonName];
}
function setToggle(name, state, noCallback) {
if (toggles[name]) {
var toggle = toggles[name];
if (toggle.state !== state) {
setButtonLight(name, state);
if (!noCallback) {
if (state === 'on' || state === 'blink') {
emit('toggle', name, 'activate');
} else {
emit('toggle', name, 'deactivate');
}
}
}
if (state === 'blink') {
toggle.blink = true;
}
toggle.state = state;
}
}
function processToggles(name) {
if (toggles[name]) {
var toggle = toggles[name]; //simplify code
if (toggle.state === 'on' || toggle.state === 'blink') {
toggle.state = 'off';
setButtonLight(name, toggle.state);
emit('action', {
control: 'toggle',
name: name,
state: 'deactivate'
});
return true;
} else {
if (toggle.blink) { toggle.state = 'blink'; }
else { toggle.state = 'on'; }
setButtonLight(name, toggle.state);
emit('action', {
control: 'toggle',
name: name,
state: 'activate'
});
return true;
}
}
return false;
}
/**
* Adds a button group
* @param {string} name
* @param {object} group
*/
function addGroup(name, group) {
if (typeof groups[name] === 'object') {
emit('error', 'addGroup: group "' + name + '" already exists');
return false;
}
//add the group
groups[name] = group;
//make sure all the lights are off
group.members.forEach(function(member) {
setButtonLight(member, 'off');
});
//light the light if specified
if (group.activeButton) {
setButtonLight(group.activeButton, 'on');
}
}
function removeGroup(name) {
if (typeof groups[name] === 'object') {
//first disable any lit lights
setButtonLight(groups[name].activeButton, 'off');
delete groups[name];
}
}
function setGroup(name, button, noCallback) {
if (typeof groups[name] === 'object') {
button = button.toUpperCase();
var group = groups[name];
if (group.activeButton !== button) {
setButtonLight(group.activeButton, 'off'); //the old button
group.activeButton = button;
setButtonLight(group.activeButton, 'on'); //the new button
if (!noCallback) {
emit('action', {
control: 'group',
name: name,
state: group.activeButton
});
}
}
}
}
function isInGroup(name) {
var keys = Object.keys(groups);
for (var i=0; i<keys.length; i++) {
if (groups[keys[i]].members.includes(name)) { return true ;}
}
return false;
}
function isInToggle(name) {
var keys = Object.keys(toggles);
for (var i=0; i<keys.length; i++) {
if (keys[i] == name) { return true ;}
}
return false;
}
function processGroups(btnName) {
var oldBtn = '';
var group = {};
//check each group
var keys = Object.keys(groups);
var name = '';
for (var i=0; i<keys.length; i++) {
name = keys[i];
if (typeof groups[name] === 'object') {
group = groups[name];
var member = '';
for (var m=0; m<group.members.length; m++) {
member = group.members[m];
if (member == btnName) { //soft check
//if an active button was set, turn it off
if (group.activeButton) {
setButtonLight(group.activeButton, 'off');
oldBtn = group.activeButton;
}
//set the active button to the one pressed
group.activeButton = btnName;
//turn it on
setButtonLight(group.activeButton, 'on');
//only run the callback if there's a change
if (oldBtn !== group.activeButton) {
emit('action', {
control: 'group',
name: name,
state: group.activeButton
});
return true;
} else {
emit('action', {
control: 'group',
name: name,
state: 'touch'
});
return true;
}
}
}
}
}
return false;
}
function processFaderRelease(value) {
var response = {
control: 'fader',
name: faderMap[value],
state: 'release'
};
emit('action', response);
var fader = faderMap[value];
//console.log('fader: ' + value + '=' + fader);
//console.log('raw: ' + faders[fader].raw);
sendMidi([faderMap[fader], faders[fader].raw[0], faders[fader].raw[1]]);
}
function processFader(fader, value, state) {
//convert fader number to name
fader = faderMap[fader];
//check to see if the new value is the same as value
//this will be true when the fader is touched and before it moves
//console.log('OLD: ' + faders[fader].raw + ' = ' + [value,state]);
if (faders[fader].raw[0] === value && faders[fader].raw[1] === state) {
var response = {
control: 'fader',
name: fader,
state: 'touch'
};
emit('action', response);
faders[fader].raw = [value, state];
} else {
faders[fader].raw = [value, state];
processFaderValue(fader);
}
}
function processFaderValue(fader) {
var val, iPart, fPart;
iPart = faders[fader].raw[1]; //the integer part
iPart /= 127; //convert to ratio
fPart = faders[fader].raw[0]; //the fractional part
fPart /= 127; //convert to ratio
fPart /= 100; //convert to fraction
val = iPart + fPart; //add them together
faders[fader].position = val;
var oldValue = faders[fader].value; //save the old value for later
if (faderModes[fader].mode === 'position') {
val = faders[fader].position; //just to be safe
val *= faderModes[fader].resolution; //convert to provided resolution
val = parseInt(val); //strip fractional part
faders[fader].value = val; // set the value
} else if (faderModes[fader].mode === 'decibel') {
var lastMajor = 'debug'; //this will hold the last major decibel marking we will count from
var offset = null; //this is the range of the lastMajor decibel
var range = faders[fader].raw[1]; //this is a 0-127 increment
var D = 0; //the gross Distance between the major decibel markings
var C = 0; // the Count between the decibels
//we're using a 127-point scale for the range, so convert the position to that scale
//we're not going to strip the decimal though.
var position = faders[fader].position * 127;
//determine the D value
if (range >=31) {
D = 16;
} else if (range >=7 && range < 31) {
D = 8;
} else if (range < 7) {
D = 8;
}
//determine the C value
if (range >=7 && range <63) {
C = 10; //10 decibels between each major
} else if (range >= 63) {
C = 5; //5 db btw each major
} else if (range <7) {
C = 40;
}
//determine the lastMajor decibel
if (range >= 7 && range < 15) {
lastMajor = -60;
offset = 7;
} else if (range >=15 && range < 23) {
lastMajor = -50;
offset = 15;
} else if (range >=23 && range < 31) {
lastMajor = -40;
offset = 23;
} else if (range >= 31 && range <47) {
lastMajor = -30;
offset = 31;
} else if (range >=47 && range <63) {
lastMajor = -20;
offset = 47;
} else if (range >=63 && range < 79) {
lastMajor = -10;
offset = 63;
} else if (range >=79 && range <95) {
lastMajor = -5;
offset = 79;
} else if (range >=95 && range <111) {
lastMajor = 0;
offset = 95;
} else if (range >=111) {
lastMajor = 5;
offset = 111;
} else if (range <7) {
lastMajor = -100;
offset = 1;
}
//compute the final decimal value
if (D && C && lastMajor != 'debug') { //TODO: Remove this test after debugging
var delta = position - offset; //how far we've moved from the gross range
//console.log('---D:' + D + '|C:' + C + '|LM:' + lastMajor + '|P:' + position + '|DLT:' + delta + '|R:' + range);
val = delta / D; //percent we've moved based on distance between major decibels
val *= C; //convert to the count
var db = lastMajor + val; //the final decibel value
//console.log('DB: ' + db);
db = roundToFraction(db, faderModes[fader].resolution);
faders[fader].value = db;
}
}
if (faders[fader].value !== oldValue) {
//console.log('FADER ' + fader + ': ' + faders[fader].position + '=' + faders[fader].value);
var response = {
control: 'fader',
name: fader,
state: faders[fader].value
};
emit('action', response);
}
}
/***************************************************************************************/
/***************************************************************************************/
function getRotation(state) {
if (state>0 && state <6) {
return 'right';
} else if (state>60 & state<70) {
return 'left';
} else {
return null;
}
}
midiIn.on('message', function(d, message) {
debug('Midi Received: ' + message);
var type = message[0];
var value = message[1];
var state = message[2];
var response = {};
//this is set to true if a toggle or group takes precedence
//end the normal button state action will not trigger
var doNotEmitStandardActions = false;
//find the name associated with the index
//response.name = buttons[value];
//will fail if the name isn't found
var name = null;
if (type === BUTTON ) {
//first check to see if it was a fader
if (value >=104 && value <=112) {
processFaderRelease(value);
} else { //now process the button
name = buttons[value];
if (name) {
response.control = 'button';
response.name = name;
if (state === ON) {
response.state = 'down';
doNotEmitStandardActions = processToggles(response.name);
//if a toggle is found doNotEmitStandardActions will be true and we wont check the groups
if (!doNotEmitStandardActions) {
doNotEmitStandardActions = processGroups(response.name);
}
if (autoButtonLights.includes(name) && !doNotEmitStandardActions) {
setButtonLight(name, 'on');
}
}
else if (state === OFF) {
response.state = 'up';
doNotEmitStandardActions = isInToggle(name);
//if the button is a toggle dont bother testing groups
if (!doNotEmitStandardActions) {
doNotEmitStandardActions = isInGroup(name);
}
if (autoButtonLights.includes(name) && !doNotEmitStandardActions) {
setTimeout(function() {
setButtonLight(name, 'off');
}, 50);
}
}
} else {
name = knobButtons[value];
if (name) {
response.control = 'knob';
response.name = name;
if (state === ON) { response.state = 'down'; }
else if (state === OFF) { response.state = 'up'; }
}
}
}
} else if (type === KNOB) {
name = knobs[value];
if (name) {
response.name = name;
response.state = getRotation(state);
//if the rotation isn't found then the control wont be set so it wont emit an action
if(response.state) { response.control = 'knob'; }
} else if (value === JOG) {
response.name = 'JOG/SHUTTLE';
response.state = getRotation(state);
if(response.state) { response.control = 'jog/shuttle'; }
}
} else if (type >=224 && type <=232) {
processFader(type, value, state);
return null;
}
if (response.control && !doNotEmitStandardActions) {
emit('action', response);
//if a knob was found run the automatic knob mode
//this will double-emit if found, which seems like the right call
//the user can simply listen for the appropriate state and even act on both if wanted
if (response.control === 'knob') {
processKnobModes(response);
}
}
});
/**
* Turns all the lights on/off/blink
* @param {('on'|'off'|'blink')} state
*/
function setAllButtonLights(state) {
if (state !== 'on' && state !== 'off' && state !== 'blink') {
error('setAllButtonLights - invalid state "' + state + '", use on/off/blink');
return null;
} else {
if (state === 'on') { state = ON; }
else if (state === 'off') { state = OFF; }
else if (state === 'blink') { state = BLINK; }
}
Object.keys(buttons).forEach(function(key) {
sendMidi([BUTTON, key, state]);
});
}
/**
* Changes the light for a button
* @param {string} name - the name of the button
* @param {('on'|'off'|'blink')} state
*/
function setButtonLight(name, state) {
if (state !== 'on' && state !== 'off' && state !== 'blink') {
error('setLight - invalid state "' + state + '", use on/off/blink');
return null;
} else {
if (state === 'on') { state = ON; }
else if (state === 'off') { state = OFF; }
else if (state === 'blink') { state = BLINK; }
}
/**********************************************************************/
var btnIndexes = Object.keys(buttons);
for (var i=0; i<btnIndexes.length; i++) {
if (buttons[btnIndexes[i]] == name) {
sendMidi([BUTTON, btnIndexes[i], state]);
return true;
}
}
//this block will only be found if the name isn't found
error('setLight - button name not found "' + name + '"');
return false;
}
/**
* Clears the display area
* @param {boolean} force
*/
function clearDisplay(force) {
Object.keys(displayElements).forEach(function(key) {
displayElements[key].forEach(function(elem) {
if (force) {
sendMidi([DISPLAY, elem, displayMap.OFF]);
} else {
if (displayElementValues[elem] != 'OFF') {
sendMidi([DISPLAY, elem, displayMap.OFF]);
}
}
});
});
Object.keys(displayElementValues).forEach(function(key) {
displayElementValues[key] = 'OFF';
});
}
function addDecimal(value, isNeg) {
var newValue = [];
var start = 0;
if (isNeg) {
newValue[0] = value[0];
start = 1;
}
for (var i=0; i<value.length; i++) {
if (!isNaN(value[i])) {
newValue.push(value[i]); //copy the element
if (i+1<value.length && isNaN(value[i+1])) { //the next element is NaN = decimal point
newValue[i] = value[i].toString() + '.';
}
}
}
return newValue;
}
/**
* Outputs a value to the display
* @param {string} elementName
* @param {string|number} value
* @param {boolean} rightAlign
* @param {number} toFixedValue
*/
function setDisplay(elementName, value, rightAlign, toFixedValue) {
//if no elementName provided, then use all available elements
if (!elementName || elementName === 'all') {
elementArray = ['assignment','HOURS','beats','SECONDS','ticks'];
} else if (elementName === 'full') {
//if you send 'full' then all elements after the dash will be used
elementName = ['HOURS','beats','SECONDS','ticks'];
}
if (toFixedValue) {
value = parseFloat(value); //convert from string
if (toFixedValue === 0) {
value = Math.round(value); //toFixed doesn't round integers for some reason
} else {
value = value.toFixed(toFixedValue); //now it's a string regardless
}
}
var val = null;
var i;
var elementArray = [];
if (typeof elementName === 'string' && isNaN(elementName)) {
elementArray = displayElements[elementName.toUpperCase()];
} else if (Array.isArray(elementName)) {
elementName.forEach(function(element) {
elementArray = elementArray.concat(displayElements[element.toUpperCase()]);
});
} else if (!isNaN(elementName)) {
//if the user give the actual display number code
//this is meant for internal use for showTempMessage
elementArray.push(elementName);
}
//console.log(elementArray);
if (elementArray) {
//clear the element and quit if the value is specifically false
if (!value && value !== 0 && typeof value !== 'string') {
for (i=0; i<elementArray.length; i++) {
if (displayElementValues[elementArray[i]] != 'OFF') {
sendMidi([DISPLAY, elementArray[i], displayMap.OFF]);
}
}
return true;
}
//this will convert a number into an array and add formatting
if (!isNaN(value)) {
var isNeg = false;
if (value < 0) { isNeg = true; }
value = Array.from(String(value), Number);
if (isNeg) { value[0] = '-'; } //first element will be null for negative numbers, replace with -
value = addDecimal(value, isNeg); //decimal will be null, replace with .
} else if (typeof value === 'string') {
value = value.toUpperCase().split('');
//this makes sure spaces render properly
for (var v=1; v<value.length; v++) {
if (value[v] === ' ') { value[v] = 'OFF'; }
}
}
//set the alignment
var sizeDiff = elementArray.length-value.length; //compute the white-space
if (value.length < elementArray.length) {
if (rightAlign) {
for (i=0; i<sizeDiff; i++) {
value.unshift('OFF');
}
}
else {
for (i=0; i<sizeDiff; i++) {
value.push('OFF');
}
}
}
for (i=0; i<elementArray.length; i++) {
val = null;
if (displayMap[value[i]] || displayMap[value[i]] === 0) {
val = displayMap[value[i].toString().toUpperCase()];
}
if (val || val === 0) {
//check to see if the element already has the value
if (displayElementValues[elementArray[i]] != value[i]) { //soft check cause why not
//send the new value
sendMidi([DISPLAY, elementArray[i], val]);
//update the map
displayElementValues[elementArray[i]] = value[i];
//if (value[i] === 'OFF')
}
} else {
if (val !== null) { error('Invalid display value: ' + displayMap[value[i]]); }
}
}
} else {
error('Invalid element name ' + elementName);
}
}
/**
* Shows a temporary message on the display
* @param {string} message
* @param {number} delay
*/
function showTempMessage(message, delay) {
if (typeof delay === 'undefined') { delay = 1000; }
//make a copy not a reference
var oldDisplayMsg = JSON.parse(JSON.stringify(displayElementValues));
//console.log('old msg = ' + JSON.stringify(oldDisplayMsg));
setDisplay('full', message);
setTimeout(function() {
//console.log('Setting display to: ' + JSON.stringify(oldDisplayMsg));
Object.keys(oldDisplayMsg).forEach(function(elem) {
if (oldDisplayMsg[elem] === 'OFF') {
sendMidi([DISPLAY, elem, displayMap[' ']]);
} else {
sendMidi([DISPLAY, elem, displayMap[oldDisplayMsg[elem]]]);
}
});
}, delay);
}
/**
* Set the small indicators on the display panel
* @param {string} name
* @param {string|boolean} state
*/
function setDisplayLight(name, state) {
try {
if (state === 'on' || state === true) {
sendMidi([BUTTON, displayLights[name], ON]);
} else if (state === 'blink') {
sendMidi([BUTTON, displayLights[name], BLINK]);
} else {
sendMidi([BUTTON, displayLights[name], OFF]);
}
} catch(e) {
emit('error', 'setDisplayLights: Invalid params, ' + [name, state]);
}
}
function setAllDisplayLights(state) {
Object.keys(displayLights).forEach(function(name) {
if (state === 'on' || state === true) {
sendMidi([BUTTON, displayLights[name], ON]);
} else if (state === 'blink') {
sendMidi([BUTTON, displayLights[name], BLINK]);
} else {
sendMidi([BUTTON, displayLights[name], OFF]);
}
});
}
/**
* Sets the position of the fader
*
* @param {string} fader
* @param {number} value
*/
function setFader(fader, value) {
if (typeof fader === 'string') { fader = fader.toUpperCase(); }
else { return null; }
if (isNaN(value)) {
emit('error', 'setFader: value is NaN (' + value + ')');
return null;
}
var payload = [];
var pos = 0;
if (faderModes[fader].mode == 'position') {
value = clamp(value, 0, faderModes[fader].resolution);
pos = 0;
var k = value / faderModes[fader].resolution; //the percentage
pos = parseInt(k * 127);// k will be a fraction, there are 127 steps in the gross midi command
} else if (faderModes[fader].mode === 'decibel') {
value = clamp(value, -100, 10);
value = parseInt(value);
pos = dbMap[value];
}
payload = [faderMap[fader], 0, pos];
sendMidi(payload);
}
/**
* Resets the faders to unity or -infinity
* @param {boolean} unity
*/
function resetFaders(unity) {
var state = unity? 98: 0;
for (var i=224; i<=232; i++) {
sendMidi([i, 0, state]);
}
}
/**
* Turns off all the knob lights and resets the mode
*/
function clearKnobModes() {
Object.keys(knobModes).forEach(function(knob) {
setKnobLight(knob, 'sequence', 'OFF');
});
knobModes = {
CH1: { mode:'normal', value:0, sideLights: false, min:0, max:0 },
CH2: { mode:'normal', value:0, sideLights: false, min:0, max:0 },
CH3: { mode:'normal', value:0, sideLights: false, min:0, max:0 },
CH4: { mode:'normal', value:0, sideLights: false, min:0, max:0 },
CH5: { mode:'normal', value:0, sideLights: false, min:0, max:0 },
CH6: { mode:'normal', value:0, sideLights: false, min:0, max:0 },
CH7: { mode:'normal', value:0, sideLights: false, min:0, max:0 },
CH8: { mode:'normal', value:0, sideLights: false, min:0, max:0 }
};
}
/**
* Sets the mode and resolution of 1 or all faders
* @param {string} fader - the name of the fader or "all"
* @param {string} mode - decibel/position
* @param {number} resolution - a number or fraction setting the resolution
*/
function setFaderMode(fader, mode, resolution) {
if (mode != 'decibel' && mode != 'position') {
emit('error', 'setFaderMode: Invalid mode');
}
if (fader == 'all') {
faderModes = {
'CH1': { mode: mode, resolution: resolution },
'CH2': { mode: mode, resolution: resolution },
'CH3': { mode: mode, resolution: resolution },
'CH4': { mode: mode, resolution: resolution },
'CH5': { mode: mode, resolution: resolution },
'CH6': { mode: mode, resolution: resolution },
'CH7': { mode: mode, resolution: resolution },
'CH8': { mode: mode, resolution: resolution },
'MAIN': { mode: mode, resolution: resolution },
};
}
try {
faderModes[fader].mode = mode;
faderModes[fader].resolution = resolution;
} catch (e) {
emit('error', 'setFaderMode: ' + e);
}
}
/**
*
* @param {string} knob - the knob to light
* @param {string} mode - light mode
* @param {number/string} value - which lights to light
* @param {boolean} showSideLights - show the lights on the side
*/
function setKnobLight(knob, mode, value, showSideLights) {
var knobCode = knobLights[knob];
var sideLightMode = 'noSideLights';
if (showSideLights) { sideLightMode = 'sideLights'; }
var lightCode = knobLightMap[sideLightMode][mode][value];
if (lightCode || lightCode === 0) {
sendMidi([KNOB, knobCode, lightCode]);
} else {
emit('error', 'setKnobLight: no light code associated with ' + mode + ':' + value);
}
}
/**
* sets the signal level bars
* @param {string} channel
* @param {number|string} level
*/
function setSignalLevel(channel, level) {
if (typeof level === 'number') { level = clamp(level, 0, 8); }
try {
var code = signalMap[channel][level];
//('sig code: ' + code);
//console.log('Send: ' + [SIGNAL, code, 0]);
sendMidi([SIGNAL, code, 0]);
} catch (e) {
emit('error', 'setSignalLevel: ' + e);
}
}
/**
* clears all the clip lights on the signal bars
*/
function clearSignalBars() {
Object.keys(signalMap).forEach(function(channel) {
sendMidi([SIGNAL, signalMap[channel].CLEAR, 0]);
});
}
/**
* Sends a raw midi msg to the controller
* @param {array} triplet
*/
function sendRAW(triplet) {
sendMidi(triplet);
}
function holdSignalLevel(channel, level) {
if (typeof level === 'number') { level = clamp(level, 1, 8); } //should this be 0-8?
signalTimers[channel] = setInterval(function() {
sendMidi([SIGNAL, signalMap[channel][level], 0]);
}, 125);
}
function clearSignalHold(channel) {
clearInterval(signalTimers[channel]);
sendMidi([SIGNAL, signalMap[channel].CLEAR, 0]);
}
function setAutoButtonLights(shouldAutoLight, ...buttonNames) {
if (buttonNames[0] === 'all') {
buttonNames = [];
Object.keys(buttons).forEach(function(key) {
buttonNames.push(buttons[key]);
});
}
buttonNames.forEach(function(button) {
if (shouldAutoLight) {
if (!autoButtonLights.includes(button)) {
autoButtonLights.push(button);
}
} else {
if (autoButtonLights.includes(button))