-
Notifications
You must be signed in to change notification settings - Fork 24
/
launchpad-mini.js
399 lines (340 loc) · 12.8 KB
/
launchpad-mini.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
'use strict';
const
util = require( 'util' ),
EventEmitter = require( 'events' ),
midi = require( 'midi' ),
brightnessSteps = require( './lib/brightness' ),
Buttons = require( './lib/button-list' ),
buttons = require( './lib/buttons' ),
colors = require( './lib/colors' );
const
/**
* @param port MIDI port object
* @returns {Array.<{portNumber:Number, portName:String}>}>}
*/
findLaunchpadPorts = function ( port ) {
return (new Array( port.getPortCount() )).fill( 0 )
.map( ( nil, portNumber ) => ({ portNumber: portNumber, portName: port.getPortName( portNumber ) }) )
.filter( desc => desc.portName.indexOf( 'Launchpad' ) >= 0 );
},
connectFirstPort = function ( port ) {
return findLaunchpadPorts( port ).some( desc => {
port.openPort( desc.portNumber );
return true;
} );
},
or = function ( test, alternative ) {
return test === undefined ? !!alternative : !!test;
};
class Launchpad extends EventEmitter {
constructor() {
super();
this.midiIn = new midi.input();
this.midiOut = new midi.output();
this.midiIn.on( 'message', ( dt, msg ) => this._processMessage( dt, msg ) );
/**
* Storage format: [ {x0 y0}, {x1 y0}, ...{x9 y0}, {x0 y1}, {x1 y1}, ... ]
* @type {Array.<{pressed:Boolean, x:Number, y:Number, cmd:Number, key:Number, id:Symbol}>}
*/
this._buttons = Buttons.All
.map( b => ({
x: b[ 0 ],
y: b[ 1 ],
id: b.id
}) )
.map( b => {
b.cmd = b.y >= 8 ? 0xb0 : 0x90;
b.key = b.y >= 8 ? 0x68 + b.x : 0x10 * b.y + b.x;
return b;
} );
/** @type {Number} */
this._writeBuffer = 0;
/** @type {Number} */
this._displayBuffer = 0;
/** @type {Boolean} */
this._flashing = false;
/** @type {Color} */
this.red = colors.red;
/** @type {Color} */
this.green = colors.green;
/** @type {Color} */
this.amber = colors.amber;
/**
* Due to limitations in LED levels, only full brightness is available for yellow,
* the other modifier versions have no effect.
* @type {Color}
*/
this.yellow = colors.yellow;
/** @type {Color} */
this.off = colors.off;
return this;
}
/**
* @param {Number=} port MIDI port number to use. By default, the first MIDI port where a Launchpad is found
* will be used. See availablePorts for a list of Launchpad ports (in case more than one is connected).
* @param {Number} outPort MIDI output port to use, if defined, port is MIDI input port, otherwise port is both input and output port.
*/
connect( port, outPort ) {
return new Promise( ( res, rej ) => {
if ( port !== undefined ) {
if( outPort === undefined){
// User has not specified outPort, use port also as output MIDI port.
outPort = port;
}
// User has specified a port, use it
try {
this.midiIn.openPort( port );
this.midiOut.openPort( outPort );
this.emit( 'connect' );
res( 'Launchpad connected' );
} catch ( e ) {
rej( `Cannot connect on port ${port}: ` + e );
}
} else {
// Search for Launchpad and use its port
let iOk = connectFirstPort( this.midiIn ),
oOk = connectFirstPort( this.midiOut );
if ( iOk && oOk ) {
this.emit( 'connect' );
res( 'Launchpad connected.' );
} else {
rej( `No Launchpad on MIDI ports found.` );
}
}
} );
}
/**
* Close the MIDI ports so the program can exit.
*/
disconnect() {
this.midiIn.closePort();
this.midiOut.closePort();
this.emit( 'disconnect' );
}
/**
* Reset mapping mode, buffer settings, and duty cycle. Also turn all LEDs on or off.
*
* @param {Number=} brightness If given, all LEDs will be set to the brightness level (1 = low, 3 = high).
* If undefined (or any other number), all LEDs will be turned off.
*/
reset( brightness ) {
brightness = brightness > 0 && brightness <= 3 ? brightness + 0x7c : 0;
this.sendRaw( [ 0xb0, 0x00, brightness ] )
}
sendRaw( data ) {
this.midiOut.sendMessage( data );
}
/**
* Can be used if multiple Launchpads are connected.
* @returns {{input: Array.<{portNumber:Number, portName:String}>, output: Array.<{portNumber:Number, portName:String}>}}
* Available input and output ports with a connected Launchpad; no other MIDI devices are shown.
*/
get availablePorts() {
return {
input: findLaunchpadPorts( this.midiIn ),
output: findLaunchpadPorts( this.midiOut )
}
}
/**
* Get a list of buttons which are currently pressed.
* @returns {Array.<Array.<Number>>} Array containing [x,y] pairs of pressed buttons
*/
get pressedButtons() {
return this._buttons.filter( b => b.pressed )
.map( b => Buttons.byXy( b.x, b.y ) );
}
/**
* Check if a button is pressed.
* @param {Array.<Number>} button [x,y] coordinates of the button to test
* @returns {boolean}
*/
isPressed( button ) {
return this._buttons.some( b => b.pressed && b.x === button[ 0 ] && b.y === button[ 1 ] );
}
/**
* Set the specified color for the given LED(s).
* @param {Number|Color} color A color code, or one of the pre-defined colors.
* @param {Array.<Number>|Array.<Array.<Number>>} buttons [x,y] value pair, or array of pairs
* @return {Promise} Resolves as soon as the Launchpad has processed all data.
*/
col( color, buttons ) {
// Code would look much better with the Rest operator ...
if ( buttons.length > 0 && buttons[ 0 ] instanceof Array ) {
buttons.forEach( btn => this.col( color, btn ) );
return new Promise( ( res, rej ) => setTimeout( res, buttons.length / 20 ) );
} else {
let b = this._button( buttons );
if ( b ) {
this.sendRaw( [ b.cmd, b.key, color.code || color ] );
}
return Promise.resolve( !!b );
}
}
/**
* Set colors for multiple buttons.
* @param {Array.<Array.<>>} buttonsWithColor Array containing entries of the form [x,y,color].
* @returns {Promise}
*/
setColors( buttonsWithColor ) {
buttonsWithColor.forEach( btn => this.setSingleButtonColor( btn, btn[ 2 ] ) );
return new Promise( ( res ) => setTimeout( res, buttonsWithColor.length / 20 ) );
}
setSingleButtonColor( xy, color ) {
let b = this._button( xy );
if ( b ) {
this.sendRaw( [ b.cmd, b.key, color.code || color ] );
}
return !!b;
}
/**
* @return {Number} Current buffer (0 or 1) that is written to
*/
get writeBuffer() {
return this._writeBuffer;
}
/**
* @return {Number} Current buffer (0 or 1) that is displayed
*/
get displayBuffer() {
return this._displayBuffer;
}
/**
* Select the buffer to which LED colors are written. Default buffer of an unconfigured Launchpad is 0.
* @param {Number} bufferNumber
*/
set writeBuffer( bufferNumber ) {
this.setBuffers( { write: bufferNumber } );
}
/**
* Select which buffer the Launchpad uses for the LED button colors. Default is 0.
* Also disables flashing.
* @param {Number} bufferNumber
*/
set displayBuffer( bufferNumber ) {
this.setBuffers( { display: bufferNumber, flash: false } );
}
/**
* Enable flashing. This essentially tells Launchpad to alternate the display buffer
* at a pre-defined speed.
* @param {Boolean} flash
*/
set flash( flash ) {
this.setBuffers( { flash: flash } );
}
/**
* @param {{write:Number=, display:Number=, copyToDisplay:Boolean=, flash:Boolean=}=} args
*/
setBuffers( args ) {
args = args || {};
this._flashing = or( args.flash, this._flashing );
this._writeBuffer = 1 * or( args.write, this._writeBuffer );
this._displayBuffer = 1 * or( args.display, this._displayBuffer );
let cmd =
0b100000 +
0b010000 * or( args.copyToDisplay, 0 ) +
0b001000 * this._flashing +
0b000100 * this.writeBuffer +
0b000001 * this.displayBuffer;
this.sendRaw( [ 0xb0, 0x00, cmd ] );
}
/**
* Set the low/medium button brightness. Low brightness buttons are about `num/den` times as bright
* as full brightness buttons. Medium brightness buttons are twice as bright as low brightness.
* @param {Number=} num Numerator, between 1 and 16, default=1
* @param {Number=} den Denominator, between 3 and 18, default=5
*/
multiplexing( num, den ) {
let data,
cmd;
num = Math.max( 1, Math.min( num || 1, 16 ) );
den = Math.max( 3, Math.min( den || 5, 18 ) );
if ( num < 9 ) {
cmd = 0x1e;
data = 0x10 * (num - 1) + (den - 3);
} else {
cmd = 0x1f;
data = 0x10 * (num - 9) + (den - 3);
}
this.sendRaw( [ 0xb0, cmd, data ] );
}
/**
* Set the button brightness for buttons with non-full brightness.
* Lower brightness increases contrast since the full-brightness buttons will not change.
*
* @param {Number} brightness Brightness between 0 (dark) and 1 (bright)
*/
brightness( brightness ) {
this.multiplexing.apply( this, brightnessSteps.getNumDen( brightness ) );
}
/**
* Generate an array of coordinate pairs from a string “painting”. The input string is 9×9 characters big
* and starts with the first button row (including the scene buttons on the right). The last row is for the
* Automap buttons which are in reality on top on the Launchpad.
*
* Any character which is a lowercase 'x' will be returned in the coordinate array.
*
* The generated array can be used for setting button colours, for example.
*
* @param {String} map
* @returns {Array.<Array.<Number>>} Array containing [x,y] coordinate pairs.
*/
fromMap( map ) {
return Array.prototype.map.call( map, ( char, ix ) => ({
x: ix % 9,
y: (ix - (ix % 9)) / 9,
c: char
}) )
.filter( data => data.c === 'x' )
.map( data => Buttons.byXy( data.x, data.y ) );
}
/**
* Converts a string describing a row or column to button coordinates.
* @param {String|Array.<String>} pattern String pattern, or array of string patterns.
* String format is 'mod:pattern', with *mod* being one of rN (row N, e.g. r4), cN (column N), am (Automap), sc (Scene).
* *pattern* are buttons from 0 to 8, where an 'x' or 'X' marks the button as selected,
* and any other character is ignored; for example: 'x..xx' or 'X XX'.
*/
fromPattern( pattern ) {
if ( pattern instanceof Array ) {
return buttons.decodeStrings( pattern );
}
return buttons.decodeString( pattern )
.map( xy => Buttons.byXy( xy[ 0 ], xy[ 1 ] ) );
}
/**
* @returns {{pressed: Boolean, x: Number, y: Number, cmd:Number, key:Number, id:Symbol}} Button at given coordinates
*/
_button( xy ) {
return this._buttons[ 9 * xy[ 1 ] + xy[ 0 ] ];
}
_processMessage( deltaTime, message ) {
let x, y, pressed;
if ( message[ 0 ] === 0x90 ) {
// Grid pressed
x = message[ 1 ] % 0x10;
y = (message[ 1 ] - x) / 0x10;
pressed = message[ 2 ] > 0;
} else if ( message[ 0 ] === 0xb0 ) {
// Automap/Live button
x = message[ 1 ] - 0x68;
y = 8;
pressed = message[ 2 ] > 0;
} else {
console.log( `Unknown message: ${message} at ${deltaTime}` );
return;
}
let button = this._button( [ x, y ] );
button.pressed = pressed;
this.emit( 'key', {
x: x, y: y, pressed: pressed, id: button.id,
// Pretend to be an array so the returned object
// can be fed back to .col()
0: x, 1: y, length: 2
} );
}
}
util.inherits( Launchpad, EventEmitter );
// Button Groups
Launchpad.Buttons = Buttons;
Launchpad.Colors = colors;
module.exports = Launchpad;