1 var NANOPAD_TOUCHS = Array(37, 39, 41, 43, 45, 47, 49, 51,
2 36, 38, 40, 42, 44, 46, 48, 50);
4 /* on French/Belgian keyboards, emulate pad touches with keypresses */
5 var KEYBOARD_CODES = Array('a', 'z', 'e', 'r', 't', 'u', 'i', 'o',
6 'q', 's', 'd', 'f', 'g', 'h', 'j', 'k');
10 onMIDISuccess: function(midiAccess) {
11 console.log('MIDI Access Object', midiAccess);
15 midiAccess.onstatechange = function(e) {
16 self.onMIDIAccessChange(e);
18 this.midiAccess = midiAccess;
25 initPorts: function() {
28 var inputs = this.midiAccess.inputs;
29 if (inputs.size > 0) {
33 self.registerPort(port);
37 $("#midiinputs").append("<p>No connected inputs</p>");
40 var outputs = this.midiAccess.outputs;
41 if (outputs.size > 0) {
44 self.registerPort(port);
45 self.renderPort(port);
49 $("#midioutputs").append("<p>No connected outputs</p>");
53 onMIDIAccessChange: function(e) {
54 console.log('on midi access change', e);
57 var portContainer = $("#midi" + port.type + "s");
58 if (portContainer.html().startsWith("<p>No connected")) {
59 portContainer.empty();
62 if (port.state == "disconnected") {
63 if (port.type == "input") {
64 this.inputs[port.name] = undefined;
66 this.outputs[port.name] = undefined;
69 if (port.type == "input") {
70 if (this.inputs[port.name] === undefined) { this.registerPort(port); }
72 if (this.outputs[port.name] === undefined) { this.registerPort(port); }
74 this.renderPort(port);
78 renderPort: function(port) {
79 if (port.state == "connected") {
80 $("#midi" + port.type + "s").append(port.name);
84 registerPort: function(port) {
86 if (port.type == "input") {
87 this.inputs[port.name] = port;
88 port.onmidimessage = function(m) { self.onMIDIMessage(m); };
90 this.outputs[port.name] = port;
92 if (port.name == 'nanoKONTROL2 MIDI 1') {
93 /* turn "external leds" mode on.
95 * The sysex dump has been recorded from the official Korg kontrol
96 * editor by the Overtone project, original code at:
97 * https://github.com/overtone/overtone/blob/master/src/overtone/device/midi/nanoKONTROL2.clj */
98 device("nanoKONTROL2 MIDI 1").raw([240, 126, 127, 6, 1, 247]);
99 device("nanoKONTROL2 MIDI 1").raw([240, 66, 64, 0, 1, 19, 0, 31, 18, 0, 247]);
100 device("nanoKONTROL2 MIDI 1").raw([240, 126, 127, 6, 1, 247]);
102 device("nanoKONTROL2 MIDI 1").raw([ 240, 66, 64, 0, 1, 19, 0, 127, 127,
103 2, 3, 5, 64, 0, 0, 0, 1, 16, 1, 0, 0, 0, 0, 127, 0, 1, 0, 16,
104 0, 0, 127, 0, 1, 0, 32, 0, 127, 0, 0, 1, 0, 48, 0, 127, 0, 0,
105 1, 0, 64, 0, 127, 0, 16, 0, 1, 0, 1, 0, 127, 0, 1, 0, 0, 17, 0,
106 127, 0, 1, 0, 0, 33, 0, 127, 0, 1, 0, 49, 0, 0, 127, 0, 1, 0,
107 65, 0, 0, 127, 0, 16, 1, 0, 2, 0, 0, 127, 0, 1, 0, 18, 0, 127,
108 0, 0, 1, 0, 34, 0, 127, 0, 0, 1, 0, 50, 0, 127, 0, 1, 0, 0, 66,
109 0, 127, 0, 16, 1, 0, 0, 3, 0, 127, 0, 1, 0, 0, 19, 0, 127, 0,
110 1, 0, 35, 0, 0, 127, 0, 1, 0, 51, 0, 0, 127, 0, 1, 0, 67, 0,
111 127, 0, 0, 16, 1, 0, 4, 0, 127, 0, 0, 1, 0, 20, 0, 127, 0, 0,
112 1, 0, 36, 0, 127, 0, 1, 0, 0, 52, 0, 127, 0, 1, 0, 0, 68, 0,
113 127, 0, 16, 1, 0, 0, 5, 0, 127, 0, 1, 0, 21, 0, 0, 127, 0, 1,
114 0, 37, 0, 0, 127, 0, 1, 0, 53, 0, 127, 0, 0, 1, 0, 69, 0, 127,
115 0, 0, 16, 1, 0, 6, 0, 127, 0, 0, 1, 0, 22, 0, 127, 0, 1, 0, 0,
116 38, 0, 127, 0, 1, 0, 0, 54, 0, 127, 0, 1, 0, 70, 0, 0, 127, 0,
117 16, 1, 0, 7, 0, 0, 127, 0, 1, 0, 23, 0, 0, 127, 0, 1, 0, 39, 0,
118 127, 0, 0, 1, 0, 55, 0, 127, 0, 0, 1, 0, 71, 0, 127, 0, 16, 0,
119 1, 0, 58, 0, 127, 0, 1, 0, 0, 59, 0, 127, 0, 1, 0, 0, 46, 0,
120 127, 0, 1, 0, 60, 0, 0, 127, 0, 1, 0, 61, 0, 0, 127, 0, 1, 0,
121 62, 0, 127, 0, 0, 1, 0, 43, 0, 127, 0, 0, 1, 0, 44, 0, 127, 0,
122 1, 0, 0, 42, 0, 127, 0, 1, 0, 0, 41, 0, 127, 0, 1, 0, 45, 0, 0,
123 127, 0, 127, 127, 127, 127, 0, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0,
124 0, 0, 0, 0, 0, 0, 0, 0, 0, 247]);
126 device("nanoKONTROL2 MIDI 1").raw([240, 126, 127, 6, 1, 247]);
127 device("nanoKONTROL2 MIDI 1").raw([240, 66, 64, 0, 1, 19, 0, 31, 17, 0, 247]);
129 function on(note) { device("nanoKONTROL2 MIDI 1").cc(note, 127); }
130 function off(note) { device("nanoKONTROL2 MIDI 1").cc(note, 0); }
131 var leds = Array(43, 44, 42, 41, 45);
132 for (var i=0; i<8; i++) {
146 var interval_id = setInterval(function() {
147 if (led_idx < leds.length) {
150 off(leds[led_idx-2]);
151 if (led_idx-1 == leds.length) {
152 clearInterval(interval_id);
159 port.onstatechange = function(e) { self.onPortStateChange(e); };
162 onMIDIFailure: function(e) {
163 alert("No access to MIDI devices or your browser doesn't support WebMIDI API. Please use WebMIDIAPIShim " + e);
166 onPortStateChange: function(event) {
170 onMIDIMessage: function(message) {
171 var port = message.target;
172 var data = message.data;
173 console.log(message);
174 if (data[0] == 144) { /* touch on */
175 var sample_idx = NANOPAD_TOUCHS.indexOf(data[1]);
176 if (sample_idx != -1) {
177 this.onTouchOn(port, data, sample_idx);
180 if (data[0] == 176) { /* control change */
181 this.onControlChange(port, data, data[1], data[2]);
185 onTouchOn: function(port, data, sample_idx) {},
186 onControlChange: function(port, data, number, value) {}
190 // status bytes on channel 1
201 var device = function(outputName) {
202 this.current = midi.outputs[outputName];
205 // makes device visible inside of nested function defs
208 this._send = function(status, data) {
209 var messageArr = [status + (self.channel - 1)].concat(data);
210 console.log("sending " + messageArr + " to " + self.current.name);
211 self.current.send(messageArr);
215 this.ch = function(channel) {
216 self.channel = channel;
220 this.cc = function(b1, b2) {
221 return self._send(messages.cc, [b1, b2]);
224 this.on = function(b1, b2) {
225 return self._send(messages.on, [b1, b2]);
228 this.off = function(b1, b2) {
229 return self._send(messages.off, [b1, b2]);
232 this.pp = function(b1, b2) {
233 return self._send(messages.pp, [b1, b2]);
236 this.cp = function(b1) {
237 return self._send(messages.cp, [b1]);
240 this.pb = function(b1) {
249 this.pc = function(b1) {
250 return self._send(messages.pc, [b1]);
253 this.panic = function() {
254 return self.cc(123, 0)
257 this.rpn = function(b1, b2) {
258 return self.cc(101, b1 >> 7)
266 this.nrpn = function(b1, b2) {
267 return self.cc(99, b1 >> 7)
275 this.raw = function(data) {
276 console.log("sending raw data: " + data);
277 self.current.send(data);
281 this.toString = function() {
282 var s = "no connected devices";
283 if (typeof this.current != 'undefined') {
292 var nanofun = function() {
295 self.initAudio = function() {
296 self.sample_buffers = Array(16);
297 self.samples = Array(16);
298 self.audioCtx = new window.AudioContext();
299 self.touchGainNodes = Array(16);
300 self.masterGainNode = self.audioCtx.createGain();
301 for (var i=0; i<16; i++) {
302 self.touchGainNodes[i] = self.audioCtx.createGain();
303 self.touchGainNodes[i].connect(self.masterGainNode);
305 self.masterGainNode.connect(self.audioCtx.destination);
308 self.initMIDI = function() {
309 if (navigator.requestMIDIAccess) {
310 navigator.requestMIDIAccess({sysex: true}).then(
311 function(midiAccess) { midi.onMIDISuccess(midiAccess); },
312 function(e) { midi.onMIDIFailure(e); }
317 self.initUI = function() {
318 var $nanopad = $('#nanopad');
319 var $nanotouch = $('.nanotouch');
320 for (var i=1; i<16; i++) {
321 var $new_touch = $nanotouch.clone();
322 $new_touch.attr('data-touch', i);
323 $new_touch.appendTo($nanopad);
326 $('.nanotouch input[type=file]').on('change', function(ev) {
327 var nanotouch = $(this).parent();
328 var sample_idx = $nanopad.children().index(nanotouch);
329 var reader = new FileReader();
330 reader.onload = function(e) {
331 self.audioCtx.decodeAudioData(this.result, function(buffer) {
332 sample_buffers[sample_idx] = buffer;
333 $(nanotouch).find('span.duration').text(parseInt(buffer.duration) + 's');
334 $(nanotouch).removeClass('error').addClass('loaded');
336 $(nanotouch).find('span').text('');
337 $(nanotouch).removeClass('loaded').addClass('error');
340 reader.readAsArrayBuffer(this.files[0]);
341 $(nanotouch).find('span.name').text(this.files[0].name);
344 midi.onTouchOn = function(port, data, sample_idx) {
345 self.startSample(sample_idx);
348 midi.onControlChange = function(port, data, control, value) {
349 if (control > 7 && control < 16) return; /* range between sliders and pots */
350 if (control > 23) return; /* after pots */
352 control += 8; /* sliders, control bottom pads (8-15) */
354 control -= 16; /* pots, control top pads (0-7) */
356 $('[data-touch=' + control + '] .touch-gain').val(value).trigger('change');
359 $(document).keypress(function(ev) {
360 var sample_idx = KEYBOARD_CODES.indexOf(ev.key);
361 if (sample_idx != -1) {
362 self.startSample(sample_idx);
366 $('#master-gain').on('change', function() {
367 var fraction = parseInt(this.value) / parseInt(127);
368 self.masterGainNode.gain.value = fraction * fraction;
371 $('.touch-gain').on('change', function() {
372 var fraction = parseInt(this.value) / parseInt(127);
373 var touchIdx = parseInt($(this).parent().data('touch'));
374 self.touchGainNodes[touchIdx].gain.value = fraction * fraction;
378 self.startSample = function(sample_idx) {
379 var sample_buffer = self.sample_buffers[sample_idx];
380 var nanotouch = $('.nanotouch')[sample_idx];
381 if (typeof(sample_buffer) != 'undefined') {
382 if (typeof(samples[sample_idx]) != 'undefined' && samples[sample_idx].context.state == 'running') {
383 self.samples[sample_idx].stop(0);
384 self.samples[sample_idx] = undefined;
386 var sample = self.audioCtx.createBufferSource();
387 var gainNode = self.touchGainNodes[sample_idx];
388 self.samples[sample_idx] = sample;
390 sample.connect(gainNode);
391 sample.buffer = sample_buffer;
392 sample.onended = function() {
393 console.log('ended');
394 $(nanotouch).removeClass('playing');
395 self.samples[sample_idx] = undefined;
397 $(nanotouch).addClass('playing');
409 $(function() { nanofun(); });