Skip to content

MIDI Dispatchers

James Kao edited this page Dec 20, 2020 · 6 revisions

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.

Identifying MIDI Controls

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:

  1. Ensure that you are disconnected from the websocket
  2. Select your MIDI Input Device and Attach MIDI
  3. Open the Developer Tools console via menu or via Control/Command-Shift-i
  4. Press/move the control. The Console log should indicate the Channel and Control Channel number or Note Number.

Example Dispatcher Recipes

Here are some examples that were/are used by the author.

Simple Fader MIDI Input Recipe

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]

DDJ-XP1 Faders and Buttons MIDI Input Recipe

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];

DDJ-XP1 Button Lights MIDI Output Recipe

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})
    }
}

Midi Fighter 3D Buttons and Faders Input Recipe

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];

MIDI Fighter 3D Button Lights MIDI Output Recipe

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})
  }
}