-
Notifications
You must be signed in to change notification settings - Fork 0
MIDI Dispatchers
MIDI input and (optional) output devices can be selected in the dropdown.
Buttons and faders respond to events, which will be sent based on the return value of user-entered JavaScript snippets. There are two snippets, one for input handling (controls originating from a MIDI device) and one for output handling (commands sent to a MIDI device). The snippets will be saved in Local Storage for use between application restarts, but there is otherwise no open or save behavior other than via copy-paste.
Even for the simple fader recipe, you will need to identify how the controls on your MIDI device are identified. Every MIDI input control will be on a channel and be either a Control Change (CC) or a Note On event.
Faders are almost always CC inputs (which carry a value), whereas buttons may either be CC (with a value of 0 or 127 to indicate on/off) or NoteOn (with a velocity that can usually be ignored).
To see what messages your device control sends, there is logging built in to indicate this which can be accessed via:
- Ensure that you are disconnected from the websocket
- Select your MIDI Input Device and Attach MIDI
- Open the Developer Tools console via menu or via Control/Command-Shift-i
- Press/move the control. The Console log should indicate the Channel and Control Channel number or Note Number.
Here are some examples that were/are used by the author.
Set FADER_CHANNEL to the MIDI channel your faders are on, and set 4 values into FADER_CCS for the 4 control numbers matching the faders to be matched to the on-screen faders.
// Input: midiData
// Output: [ eventName, eventData ]
const FADER_CHANNEL = 1
const FADER_CCS = [11, 12, 13, 14]
var i = FADER_CCS.indexOf(midiData.control)
if (i > -1) {
return ['midiFader' + i, {
index: i,
channel: midiData.channel,
control: midiData.control,
value: midiData.value
}]
}
return [null, null]
Buttons are mapped to the DDJ-XP1, which has each side fully mapped to buttons 0 to 127 on different channels, thus we use simple index logic to dispatch a button to the corresponding scene or source. A controller with a non-contiguous button mapping (like the Launchpad) would need more row-wise logic to account for the discontinuity in notes.
// Input: midiData {channel, control, value} or {channel, note}
// Output: [ eventName, eventData ]
const FADER_CHANNEL = 1;
const FADER_CONTROLS = [11, 12, 13, 14];
const SCENE_BTN_CHANNEL = 8;
const SCENE_ZERO_NOTE = 0;
const SCENE_MAX_NOTE = 100;
const SOURCE_BTN_CHANNEL = 10;
const SOURCE_ZERO_NOTE = 0;
const SOURCE_MAX_NOTE = 100;
const TIMETABLE_FWD_CHANNEL = 1;
const TIMETABLE_FWD_NOTE = 121;
const TIMETABLE_REV_CHANNEL = 1;
const TIMETABLE_REV_NOTE = 10;
const TRANSITION_CHANNEL = 7;
const TRANSITION_NOTE = 70;
var eventName = null;
var eventData = null;
if (midiData.channel === FADER_CHANNEL) {
var i = FADER_CONTROLS.indexOf(midiData.control);
if (i > -1) {
eventName = 'midiFader'+i;
eventData = midiData;
eventData.index = i;
}
}
if (midiData.channel === SCENE_BTN_CHANNEL) {
if (midiData.note >= SCENE_ZERO_NOTE && midiData.note <= SCENE_MAX_NOTE) {
eventName = 'scene' + (midiData.note - SCENE_ZERO_NOTE);
eventData = midiData;
console.log("Midi Emit: "+eventName);
}
}
if (midiData.channel === SOURCE_BTN_CHANNEL) {
if (midiData.note >= SOURCE_ZERO_NOTE && midiData.note <= SOURCE_MAX_NOTE) {
eventName = 'source' + (midiData.note - SOURCE_ZERO_NOTE);
eventData = midiData;
console.log("Midi Emit: "+eventName);
}
}
if (midiData.channel === TIMETABLE_FWD_CHANNEL) {
if (midiData.note === TIMETABLE_FWD_NOTE) {
eventName = 'timetable';
eventData = 'advance';
console.log("Midi Emit: "+eventName);
}
}
if (midiData.channel === TIMETABLE_REV_CHANNEL) {
if (midiData.note === TIMETABLE_REV_NOTE) {
eventName = 'timetable';
eventData = 'retract';
console.log("Midi Emit: "+eventName);
}
}
if (midiData.channel === TRANSITION_CHANNEL) {
if (midiData.note === TRANSITION_NOTE) {
eventName = 'transition';
console.log("Midi Emit: "+eventName);
}
}
return [eventName, eventData];
Lights are mapped to the DDJ-XP1 pads, which will set colors (not exactly the same colors for clarity) on pads corresponding to the source and scene buttons. The lights are set on the exact same notes and channel as the corresponding inputs, and are contiguous from 0 to 127 for each group of pads, so there is simple index logic. A controller with discontinuous lights (like the Launchpad) would require more complex logic.
// Input 1: eventData { name, index, color }
// Input 2: midiOutput
// Output: none
const SCENE_LIGHT_CHANNEL = 8;
const SCENE_LIGHT_ZERO_NOTE = 0;
const SOURCE_LIGHT_CHANNEL = 10;
const SOURCE_LIGHT_ZERO_NOTE = 0;
console.log(eventData);
if (eventData.name === 'scene') {
const index = eventData.index
const color = eventData.color
// for (index = 0; index < 127; index ++)
// midiOutput.playNote(index+SCENE_LIGHT_ZERO_NOTE, SCENE_LIGHT_CHANNEL, {velocity: index, rawVelocity: true})
switch (color) {
case 'white':
midiOutput.playNote(index+SCENE_LIGHT_ZERO_NOTE, SCENE_LIGHT_CHANNEL, {velocity: 81, rawVelocity: true})
break;
case 'yellow':
midiOutput.playNote(index+SCENE_LIGHT_ZERO_NOTE, SCENE_LIGHT_CHANNEL, {velocity: 24, rawVelocity: true})
break;
case 'red':
midiOutput.playNote(index+SCENE_LIGHT_ZERO_NOTE, SCENE_LIGHT_CHANNEL, {velocity: 48, rawVelocity: true})
break;
}
} else if (eventData.name === 'source') {
const index = eventData.index;
const color = eventData.color;
switch (color) {
case 'white':
midiOutput.playNote(index+SOURCE_LIGHT_ZERO_NOTE, SOURCE_LIGHT_CHANNEL, {velocity: 0, rawVelocity: true})
break;
case 'yellow':
midiOutput.playNote(index+SOURCE_LIGHT_ZERO_NOTE, SOURCE_LIGHT_CHANNEL, {velocity: 24, rawVelocity: true})
break;
case 'red':
midiOutput.playNote(index+SOURCE_LIGHT_ZERO_NOTE, SOURCE_LIGHT_CHANNEL, {velocity: 48, rawVelocity: true})
break;
}
} else if (eventData.name === 'init') {
for (index = 0; index < 128; index ++) {
midiOutput.playNote(index+SCENE_LIGHT_ZERO_NOTE, SCENE_LIGHT_CHANNEL, {velocity: 63, rawVelocity: true})
midiOutput.playNote(index+SOURCE_LIGHT_ZERO_NOTE, SOURCE_LIGHT_CHANNEL, {velocity: 63, rawVelocity: true})
// midiOutput.playNote(index+SOURCE_LIGHT_ZERO_NOTE, SOURCE_LIGHT_CHANNEL, {velocity: index, rawVelocity: true})
}
}
The MIDI Fighter 3D has buttons mapped from bottom to top. This mapping positions the bank controls at the bottom via a remapping array. Either sources or scenes can be controlled, but not both with this script. Set either SCENE_BTN_CHANNEL or SOURCE_BTN_CHANNEL to 3 to match the MIDI Fighter's arcade buttons.
/* eslint-disable no-undef */
/* eslint-disable semi */
// Input: midiData {channel, control, value} or {channel, note}
// Output: [ eventName, eventData ]
const FADER_CHANNEL = 1;
const FADER_CONTROLS = [11, 12, 13, 14];
const SCENE_BTN_CHANNEL = 8;
const SCENE_ZERO_NOTE = 0;
const SCENE_MAX_NOTE = 63;
const SOURCE_BTN_CHANNEL = 10;
const SOURCE_ZERO_NOTE = 0;
const SOURCE_MAX_NOTE = 63;
const TIMETABLE_FWD_CHANNEL = 1;
const TIMETABLE_FWD_NOTE = 121;
const TIMETABLE_REV_CHANNEL = 1;
const TIMETABLE_REV_NOTE = 10;
const TRANSITION_CHANNEL = 7;
const TRANSITION_NOTE = 70;
const MF_CHANNEL = 3
const ZERO_TO_MF = [
39, 38, 37, 36,
43, 42, 41, 40,
47, 46, 45, 44,
51, 50, 49, 48,
55, 54, 53, 52,
59, 58, 57, 56,
63, 62, 61, 60,
67, 66, 65, 64,
71, 70, 69, 68,
75, 74, 73, 72,
79, 78, 77, 76,
83, 82, 81, 80,
87, 86, 85, 84,
91, 90, 89, 88,
95, 94, 93, 92,
99, 98, 97, 96
]
function mfRemap (data) {
if (data.channel === MF_CHANNEL && data.note && data.note > 35 && data.note < 100) {
mapNote = ZERO_TO_MF.indexOf(data.note)
console.log(`mfRemap() note ${data.note} to ${mapNote}`)
data.note = mapNote
}
return data
}
var eventName = null;
var eventData = null;
midiData = mfRemap(midiData)
if (midiData.channel === FADER_CHANNEL) {
var i = FADER_CONTROLS.indexOf(midiData.control);
if (i > -1) {
eventName = 'midiFader'+i;
eventData = midiData;
eventData.index = i;
}
}
if (midiData.channel === SCENE_BTN_CHANNEL) {
if (midiData.note >= SCENE_ZERO_NOTE && midiData.note <= SCENE_MAX_NOTE) {
eventName = 'scene' + (midiData.note - SCENE_ZERO_NOTE);
eventData = midiData;
console.log("Midi Emit: "+eventName);
}
}
if (midiData.channel === SOURCE_BTN_CHANNEL) {
if (midiData.note >= SOURCE_ZERO_NOTE && midiData.note <= SOURCE_MAX_NOTE) {
eventName = 'source' + (midiData.note - SOURCE_ZERO_NOTE);
eventData = midiData;
console.log("Midi Emit: "+eventName);
}
}
if (midiData.channel === TIMETABLE_FWD_CHANNEL) {
if (midiData.note === TIMETABLE_FWD_NOTE) {
eventName = 'timetable';
eventData = 'advance';
console.log("Midi Emit: "+eventName);
}
}
if (midiData.channel === TIMETABLE_REV_CHANNEL) {
if (midiData.note === TIMETABLE_REV_NOTE) {
eventName = 'timetable';
eventData = 'retract';
console.log("Midi Emit: "+eventName);
}
}
if (midiData.channel === TRANSITION_CHANNEL) {
if (midiData.note === TRANSITION_NOTE) {
eventName = 'transition';
console.log("Midi Emit: "+eventName);
}
}
return [eventName, eventData];
Similar to the input recipe, the mappings here are for the MIDI Fighter oriented with the bank select buttons on the bottom. Only one of source or scene select can have their status mapped to lights. Set either SCENE_LIGHT_CHANNEL or SOURCE_LIGHT_CHANNEL to 3 to use that channel. The other one should be set to an unused channel.
/* eslint-disable semi */
/* eslint-disable no-undef */
// Input 1: eventData { name, index, color }
// Input 2: midiOutput
// Output: none
const SCENE_LIGHT_CHANNEL = 3;
const SOURCE_LIGHT_CHANNEL = 10;
const ZERO_TO_MF = [
39, 38, 37, 36,
43, 42, 41, 40,
47, 46, 45, 44,
51, 50, 49, 48,
55, 54, 53, 52,
59, 58, 57, 56,
63, 62, 61, 60,
67, 66, 65, 64,
71, 70, 69, 68,
75, 74, 73, 72,
79, 78, 77, 76,
83, 82, 81, 80,
87, 86, 85, 84,
91, 90, 89, 88,
95, 94, 93, 92,
99, 98, 97, 96
]
console.log(eventData);
function remapMF (note) {
var mapNote = note
if (note >= 0 && note < 64) {
mapNote = ZERO_TO_MF[note]
}
console.log(`remapMF() output note ${note} to ${mapNote}`)
return mapNote
}
if (eventData.name === 'scene') {
const index = eventData.index
const color = eventData.color
// for (index = 0; index < 127; index ++)
// midiOutput.playNote(index+SCENE_LIGHT_ZERO_NOTE, SCENE_LIGHT_CHANNEL, {velocity: index, rawVelocity: true})
switch (color) {
case 'white':
midiOutput.playNote(remapMF(index), SCENE_LIGHT_CHANNEL, {velocity: 79, rawVelocity: true})
break;
case 'yellow':
midiOutput.playNote(remapMF(index), SCENE_LIGHT_CHANNEL, {velocity: 37, rawVelocity: true})
break;
case 'red':
midiOutput.playNote(remapMF(index), SCENE_LIGHT_CHANNEL, {velocity: 13, rawVelocity: true})
break;
}
} else if (eventData.name === 'source') {
const index = eventData.index;
const color = eventData.color;
switch (color) {
case 'white':
midiOutput.playNote(remapMF(index), SOURCE_LIGHT_CHANNEL, {velocity: 79, rawVelocity: true})
break;
case 'yellow':
midiOutput.playNote(remapMF(index), SOURCE_LIGHT_CHANNEL, {velocity: 37, rawVelocity: true})
break;
case 'red':
midiOutput.playNote(remapMF(index), SOURCE_LIGHT_CHANNEL, {velocity: 13, rawVelocity: true})
break;
}
} else if (eventData.name === 'init') {
for (index = 0; index < 64; index++) {
midiOutput.playNote(remapMF(index), SCENE_LIGHT_CHANNEL, {velocity: 1, rawVelocity: true})
midiOutput.playNote(remapMF(index), SOURCE_LIGHT_CHANNEL, {velocity: 1, rawVelocity: true})
// midiOutput.playNote(index+SOURCE_LIGHT_ZERO_NOTE, SOURCE_LIGHT_CHANNEL, {velocity: index, rawVelocity: true})
}
}