push-wrapper
Version:
A wrapper for the Ableton Push to give easy to use APIs for controlling/being controlled by the Push
240 lines (217 loc) • 10.2 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = global || self, factory(global.pushWrapper = {}));
}(this, (function (exports) { 'use strict';
const buttonToCC = {
'TapTempo': 3,
'Metronome': 9,
'Undo': 119,
'Delete': 118,
'Double': 117,
'Quantize': 116,
'FixedLength': 90,
'Automation': 89,
'Duplicate': 88,
'New': 87,
'Rec': 86,
'Play': 85,
'Master': 28,
'Stop': 29,
'Left': 44,
'Right': 45,
'Up': 46,
'Down': 47,
'Volume': 114,
'Pan&Send': 115,
'Track': 112,
'Clip': 113,
'Device': 110,
'Browse': 111,
'StepIn': 62,
'StepOut': 63,
'Mute': 60,
'Solo': 61,
'Scales': 58,
'User': 59,
'Repeat': 56,
'Accent': 57,
'OctaveDown': 54,
'OctaveUp': 55,
'AddEffect': 52,
'AddTrack': 53,
'Note': 50,
'Session': 51,
'Select': 48,
'Shift': 49
};
const timeDivisionButtonToCC = {
'1/4': 36,
'1/4t': 37,
'1/8': 38,
'1/8t': 39,
'1/16': 40,
'1/16t': 41,
'1/32': 42,
'1/32t': 43
};
var buttonMap = {buttonToCC, timeDivisionButtonToCC};
const { timeDivisionButtonToCC: timeDivisionButtonToCC$1, buttonToCC: buttonToCC$1 } = buttonMap;
const zeroToSeven = [0, 1, 2, 3, 4, 5, 6, 7];
const lcdOffsets = [0, 9, 17, 26, 34, 43, 51, 60];
const compose = (elem, ...parts) => Object.assign({}, ...parts.map(part => (typeof part === 'function') ? part(elem) : part));
const dispatchIncomingMidi = dispatchMap => ([one, two, ...rest]) => {
let messageType = one & 0xf0;
switch (messageType) {
case (128): // NOTE-OFF
messageType = 144;
// fall through
case (176): // CC
case (144): // NOTE-ON
case (160): // POLY PRESSURE (AFTERTOUCH)
dispatchMap[messageType][two](...rest);
break
case (224): // PITCHBEND
const fourteenBitValue = (rest[0] << 7) + two;
if (fourteenBitValue !== 8192) dispatchMap[messageType](fourteenBitValue);
break
}
};
const listenable = () => {
let store = [];
return {
dispatch: value => { store.forEach(listener => listener(value)); },
listen: listener => {
if (store.indexOf(listener) === -1) store.push(listener);
return () => { store = store.filter(cb => cb !== listener); }
}
}
};
const aftertouchable = ({ aftertouch: { listen: onAftertouch } }) => ({ onAftertouch });
const pitchbendable = ({ bend: { listen: onPitchBend } }) => ({ onPitchBend });
const pressable = ({ press: { listen: onPressed } }) => ({ onPressed });
const releaseable = ({ release: { listen: onReleased } }) => ({ onReleased });
const turnable = ({ turn: { listen: onTurned } }) => ({ onTurned });
const rgbButton = (sendOnOff, sendRgbSysex) => ({
ledOn: (value = 100) => sendOnOff(value),
ledOff: () => sendOnOff(0),
ledRGB: (r = 0, g = 0, b = 0) => sendRgbSysex(r, g, b)
});
const dimmableLed = send => ({
ledOn: () => send(4),
ledDim: () => send(1),
ledOff: () => send(0)
});
const dimColours = { orange: 7, red: 1, green: 19, yellow: 13 };
const dimColour = colour => dimColours[colour] || dimColours['orange'];
const roygLed = send => ({
ledOn: (colour = 'orange') => send(dimColour(colour) + 3),
ledDim: (colour = 'orange') => send(dimColour(colour)),
ledOff: () => send(0)
});
function eigthCharLcdData (input) {
let chars = String(input).split('').slice(0, 8).map(x => x.charCodeAt(0));
while (chars.length < 8) chars.push(32);
return chars
}
const lcdSegment = (send, x, y) => ({
clear: () => send(x, y, ''),
display: (text = '') => send(x, y, text)
});
function webMIDIio (inputPortName = 'Ableton Push User Port', outputPortName = 'Ableton Push User Port') {
if (navigator && navigator.requestMIDIAccess) {
return navigator.requestMIDIAccess({ sysex: true }).then(midiAccess => {
const portWithName = name => port => port.name === name;
const inputPort = [...midiAccess.inputs.values()].find(portWithName(inputPortName));
const outputPort = [...midiAccess.outputs.values()].find(portWithName(outputPortName));
return (inputPort && outputPort)
? Promise.resolve({ inputPort, outputPort })
: Promise.reject(new Error(`No MIDI IO ports found with names "${inputPortName}"/"${outputPortName}"`))
})
}
return Promise.reject(new Error('Web MIDI API not supported by this browser!'))
}
var push = {
webMIDIio,
push: () => {
let midiOutCallBacks = [];
const midiOut = bytes => { midiOutCallBacks.forEach(callback => callback(bytes)); };
const sendCC = ({cc}) => value => { midiOut([176, cc, value]); };
const sendMidiNote = ({note}) => value => { midiOut([144, note, value]); };
const sendSysex = data => { midiOut([240, 71, 127, 21, ...data, 247]); };
const lcdSegmentSysex = (x, y, data) => sendSysex([27 - y, 0, 9, lcdOffsets[x], ...eigthCharLcdData(data)]);
const rgbSysex = index => (r, g, b) => {
const msb = [r, g, b].map(x => (x & 240) >> 4);
const lsb = [r, g, b].map(x => x & 15);
sendSysex([4, 0, 8, index, 0, msb[0], lsb[0], msb[1], lsb[1], msb[2], lsb[2]]);
};
const buttons = Object.keys(buttonToCC$1).map(name => ({ cc: buttonToCC$1[name], name, press: listenable(), release: listenable() }));
const channelSelectButtons = zeroToSeven.map(x => ({ cc: 20 + x, press: listenable(), release: listenable() }));
const gridSelectButtons = zeroToSeven.map(x => ({ cc: 102 + x, press: listenable(), release: listenable() }));
const timeDivisionButtons = Object.keys(timeDivisionButtonToCC$1).map(name => ({ cc: timeDivisionButtonToCC$1[name], name, press: listenable(), release: listenable() }));
const dimButtonApi = button => compose(button, dimmableLed(sendCC(button)), pressable, releaseable);
const roygButtonApi = button => compose(button, roygLed(sendCC(button)), pressable, releaseable);
const rgbButtonApi = button => compose(button, rgbButton(sendCC(button), rgbSysex(button.cc - 38)), pressable, releaseable);
const pads = Array(64).fill(36).map((x, i) => x + i).map(note => ({ note, press: listenable(), release: listenable(), aftertouch: listenable() }));
const padApi = pad => compose(pad, rgbButton(sendMidiNote(pad), rgbSysex(pad.note - 36)), aftertouchable, pressable, releaseable);
const channelKnobs = zeroToSeven.map(x => ({ cc: 71 + x, note: x, press: listenable(), release: listenable(), turn: listenable() }));
const masterKnob = { cc: 79, note: 8, press: listenable(), release: listenable(), turn: listenable() };
const swingKnob = { cc: 15, note: 9, press: listenable(), release: listenable(), turn: listenable() };
const tempoKnob = { cc: 14, note: 10, press: listenable(), release: listenable(), turn: listenable() };
const knobApi = knob => compose(knob, pressable, releaseable, turnable);
const touchstrip = { note: 12, press: listenable(), release: listenable(), bend: listenable() };
const dispatchPressOrRelease = elem => value => value > 0 ? elem.press.dispatch(value) : elem.release.dispatch();
const dispatchers = {
// MIDI NOTES
144: Object.assign({},
[masterKnob, swingKnob, tempoKnob, ...channelKnobs, ...pads]
.reduce((acc, elem) => { acc[elem.note] = dispatchPressOrRelease(elem); return acc }, {}),
{[touchstrip.note]: value => {
if (value > 0) {
touchstrip.press.dispatch();
} else {
touchstrip.release.dispatch();
touchstrip.bend.dispatch(8192);
}
}}
),
// POLY PRESSURE
160: pads.reduce((acc, pad) => { acc[pad.note] = pad.aftertouch.dispatch; return acc }, {}),
// MIDI CC
176: Object.assign({},
[...buttons, ...channelSelectButtons, ...gridSelectButtons, ...timeDivisionButtons]
.reduce((acc, button) => { acc[button.cc] = dispatchPressOrRelease(button); return acc }, {}),
[masterKnob, swingKnob, tempoKnob, ...channelKnobs].reduce((acc, knob) => {
acc[knob.cc] = value => knob.turn.dispatch(value < 64 ? value : value - 128);
return acc
}, {})
),
224: touchstrip.bend.dispatch // PITCHBEND
};
return {
button: name => dimButtonApi(buttons.filter(button => button.name === name)[0]),
channelKnobs: () => channelKnobs.map(knobApi),
channelSelectButtons: () => channelSelectButtons.map(roygButtonApi),
clearLCD: () => { [27, 26, 25, 24].forEach(row => { sendSysex([row, 0, 69, 0, ...new Array(68).fill(32)]); }); },
gridRow: y => zeroToSeven.map(x => x + (y * 8)).map(index => pads[index]).map(padApi),
gridCol: x => zeroToSeven.map(y => x + (y * 8)).map(index => pads[index]).map(padApi),
gridSelectButtons: () => gridSelectButtons.map(rgbButtonApi),
lcdSegmentsCol: x => zeroToSeven.map(y => lcdSegment(lcdSegmentSysex, x, y)),
lcdSegmentsRow: y => zeroToSeven.map(x => lcdSegment(lcdSegmentSysex, x, y)),
midiFromHardware: dispatchIncomingMidi(dispatchers),
onMidiToHardware: listener => { midiOutCallBacks.push(listener); return () => { midiOutCallBacks = midiOutCallBacks.filter(cb => cb !== listener); } },
timeDivisionButtons: name => roygButtonApi(timeDivisionButtons.filter(button => button.name === name)[0]),
masterKnob: () => knobApi(masterKnob),
swingKnob: () => knobApi(swingKnob),
tempoKnob: () => knobApi(tempoKnob),
touchstrip: () => compose(touchstrip, pressable, releaseable, pitchbendable)
}
}
};
var push_1 = push.webMIDIio;
var push_2 = push.push;
exports.default = push;
exports.push = push_2;
exports.webMIDIio = push_1;
Object.defineProperty(exports, '__esModule', { value: true });
})));