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', 'y', 'u', 'i',
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);
58 if (port.state == "disconnected") {
59 if (port.type == "input") {
60 this.inputs[port.name] = undefined;
62 this.outputs[port.name] = undefined;
64 if (port.name == 'nanoPAD2 MIDI 1' || port.name == 'nanoPAD2 2 PAD') { $('#devices .nanopad').removeClass('on'); }
65 if (port.name == 'nanoKONTROL2 MIDI 1' || port.name == 'nanoKONTROL2 2 SLIDER/KNOB') { $('#devices .nanokontrol').removeClass('on'); }
67 if (port.type == "input") {
68 if (this.inputs[port.name] === undefined) { this.registerPort(port); }
70 if (this.outputs[port.name] === undefined) { this.registerPort(port); }
72 console.log('hello:', port.name);
73 if (port.name == 'nanoPAD2 MIDI 1' || port.name == 'nanoPAD2 2 PAD') { $('#devices .nanopad').addClass('on'); }
74 if (port.name == 'nanoKONTROL2 MIDI 1' || port.name == 'nanoKONTROL2 2 SLIDER/KNOB') { $('#devices .nanokontrol').addClass('on'); }
75 this.renderPort(port);
79 renderPort: function(port) {
80 if (port.state == "connected") {
81 $("#midi" + port.type + "s").append(port.name);
85 registerPort: function(port) {
87 if (port.type == "input") {
88 this.inputs[port.name] = port;
89 port.onmidimessage = function(m) { self.onMIDIMessage(m); };
91 this.outputs[port.name] = port;
93 if (port.name == 'nanoKONTROL2 MIDI 1' || port.name == 'nanoKONTROL2 2 SLIDER/KNOB') {
94 var port_name = port.name;
95 /* turn "external leds" mode on.
97 * The sysex dump has been recorded from the official Korg kontrol
98 * editor by the Overtone project, original code at:
99 * https://github.com/overtone/overtone/blob/master/src/overtone/device/midi/nanoKONTROL2.clj */
100 device(port_name).raw([240, 126, 127, 6, 1, 247]);
101 device(port_name).raw([240, 66, 64, 0, 1, 19, 0, 31, 18, 0, 247]);
102 device(port_name).raw([240, 126, 127, 6, 1, 247]);
104 device(port_name).raw([ 240, 66, 64, 0, 1, 19, 0, 127, 127,
105 2, 3, 5, 64, 0, 0, 0, 1, 16, 1, 0, 0, 0, 0, 127, 0, 1, 0, 16,
106 0, 0, 127, 0, 1, 0, 32, 0, 127, 0, 0, 1, 0, 48, 0, 127, 0, 0,
107 1, 0, 64, 0, 127, 0, 16, 0, 1, 0, 1, 0, 127, 0, 1, 0, 0, 17, 0,
108 127, 0, 1, 0, 0, 33, 0, 127, 0, 1, 0, 49, 0, 0, 127, 0, 1, 0,
109 65, 0, 0, 127, 0, 16, 1, 0, 2, 0, 0, 127, 0, 1, 0, 18, 0, 127,
110 0, 0, 1, 0, 34, 0, 127, 0, 0, 1, 0, 50, 0, 127, 0, 1, 0, 0, 66,
111 0, 127, 0, 16, 1, 0, 0, 3, 0, 127, 0, 1, 0, 0, 19, 0, 127, 0,
112 1, 0, 35, 0, 0, 127, 0, 1, 0, 51, 0, 0, 127, 0, 1, 0, 67, 0,
113 127, 0, 0, 16, 1, 0, 4, 0, 127, 0, 0, 1, 0, 20, 0, 127, 0, 0,
114 1, 0, 36, 0, 127, 0, 1, 0, 0, 52, 0, 127, 0, 1, 0, 0, 68, 0,
115 127, 0, 16, 1, 0, 0, 5, 0, 127, 0, 1, 0, 21, 0, 0, 127, 0, 1,
116 0, 37, 0, 0, 127, 0, 1, 0, 53, 0, 127, 0, 0, 1, 0, 69, 0, 127,
117 0, 0, 16, 1, 0, 6, 0, 127, 0, 0, 1, 0, 22, 0, 127, 0, 1, 0, 0,
118 38, 0, 127, 0, 1, 0, 0, 54, 0, 127, 0, 1, 0, 70, 0, 0, 127, 0,
119 16, 1, 0, 7, 0, 0, 127, 0, 1, 0, 23, 0, 0, 127, 0, 1, 0, 39, 0,
120 127, 0, 0, 1, 0, 55, 0, 127, 0, 0, 1, 0, 71, 0, 127, 0, 16, 0,
121 1, 0, 58, 0, 127, 0, 1, 0, 0, 59, 0, 127, 0, 1, 0, 0, 46, 0,
122 127, 0, 1, 0, 60, 0, 0, 127, 0, 1, 0, 61, 0, 0, 127, 0, 1, 0,
123 62, 0, 127, 0, 0, 1, 0, 43, 0, 127, 0, 0, 1, 0, 44, 0, 127, 0,
124 1, 0, 0, 42, 0, 127, 0, 1, 0, 0, 41, 0, 127, 0, 1, 0, 45, 0, 0,
125 127, 0, 127, 127, 127, 127, 0, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0,
126 0, 0, 0, 0, 0, 0, 0, 0, 0, 247]);
128 device(port_name).raw([240, 126, 127, 6, 1, 247]);
129 device(port_name).raw([240, 66, 64, 0, 1, 19, 0, 31, 17, 0, 247]);
131 function on(note) { device(port_name).cc(note, 127); }
132 function off(note) { device(port_name).cc(note, 0); }
133 var leds = Array(43, 44, 42, 41, 45);
134 for (var i=0; i<8; i++) {
148 var interval_id = setInterval(function() {
149 if (led_idx < leds.length) {
152 off(leds[led_idx-2]);
153 if (led_idx-1 == leds.length) {
154 clearInterval(interval_id);
161 port.onstatechange = function(e) { self.onPortStateChange(e); };
164 onMIDIFailure: function(e) {
165 alert("No access to MIDI devices or your browser doesn't support WebMIDI API. Please use WebMIDIAPIShim " + e);
168 onPortStateChange: function(event) {
172 onMIDIMessage: function(message) {
173 var port = message.target;
174 var data = message.data;
175 console.log(message);
176 if (data[0] == 144) { /* touch on */
177 var sample_idx = NANOPAD_TOUCHS.indexOf(data[1]);
178 if (sample_idx != -1) {
179 this.onTouchOn(port, data, sample_idx);
182 if (data[0] == 176) { /* control change */
183 this.onControlChange(port, data, data[1], data[2]);
187 onTouchOn: function(port, data, sample_idx) {},
188 onControlChange: function(port, data, number, value) {}
192 // status bytes on channel 1
203 var device = function(outputName) {
204 this.current = midi.outputs[outputName];
207 // makes device visible inside of nested function defs
210 this._send = function(status, data) {
211 var messageArr = [status + (self.channel - 1)].concat(data);
212 console.log("sending " + messageArr + " to " + self.current.name);
213 self.current.send(messageArr);
217 this.ch = function(channel) {
218 self.channel = channel;
222 this.cc = function(b1, b2) {
223 return self._send(messages.cc, [b1, b2]);
226 this.on = function(b1, b2) {
227 return self._send(messages.on, [b1, b2]);
230 this.off = function(b1, b2) {
231 return self._send(messages.off, [b1, b2]);
234 this.pp = function(b1, b2) {
235 return self._send(messages.pp, [b1, b2]);
238 this.cp = function(b1) {
239 return self._send(messages.cp, [b1]);
242 this.pb = function(b1) {
251 this.pc = function(b1) {
252 return self._send(messages.pc, [b1]);
255 this.panic = function() {
256 return self.cc(123, 0)
259 this.rpn = function(b1, b2) {
260 return self.cc(101, b1 >> 7)
268 this.nrpn = function(b1, b2) {
269 return self.cc(99, b1 >> 7)
277 this.raw = function(data) {
278 console.log("sending raw data: " + data);
279 self.current.send(data);
283 this.toString = function() {
284 var s = "no connected devices";
285 if (typeof this.current != 'undefined') {
294 var nanofun = function() {
297 self.initAudio = function() {
298 self.focused_pad = undefined;
299 self.cycle_being_pressed = false;
300 self.sample_buffers = Array(NANOPAD_TOUCHS.length);
301 self.samples = Array(NANOPAD_TOUCHS.length);
302 self.sample_start_times = Array(NANOPAD_TOUCHS.length);
303 self.audioCtx = new window.AudioContext();
304 self.touchGainNodes = Array(NANOPAD_TOUCHS.length);
305 self.masterGainNode = self.audioCtx.createGain();
306 self.effectsGainNode = self.audioCtx.createGain();
307 for (var i=0; i<NANOPAD_TOUCHS.length; i++) {
308 self.touchGainNodes[i] = self.audioCtx.createGain();
309 self.touchGainNodes[i].connect(self.masterGainNode);
311 self.masterGainNode.connect(self.audioCtx.destination);
313 self.delay = self.audioCtx.createDelay(maxDelayTime=5);
314 self.delay.delayTime.value = 0.5;
316 self.feedback = self.audioCtx.createGain();
317 self.feedback.gain.value = 0.8;
319 self.filter = self.audioCtx.createBiquadFilter();
320 self.filter.frequency.value = 1000;
322 self.effectsGainNode.connect(self.delay);
323 self.delay.connect(self.feedback);
324 self.feedback.connect(self.filter);
325 self.filter.connect(self.delay);
327 self.filter.connect(self.masterGainNode);
330 self.initMIDI = function() {
331 if (navigator.requestMIDIAccess) {
332 navigator.requestMIDIAccess({sysex: true}).then(
333 function(midiAccess) { midi.onMIDISuccess(midiAccess); },
334 function(e) { midi.onMIDIFailure(e); }
339 self.initUI = function() {
340 var $nanopad = $('#nanopad');
341 var $nanotouch = $('.nanotouch');
342 for (var i=0; i<NANOPAD_TOUCHS.length; i++) {
343 var $new_touch = $nanotouch.clone();
344 $new_touch.attr('data-touch', i);
345 $new_touch.find('.key').text(KEYBOARD_CODES[i]);
346 $new_touch.appendTo($nanopad);
348 $nanotouch.remove(); /* remove template */
350 $('.nanotouch input[type=file]').on('change', function(ev) {
351 var sample_idx = parseInt($(this).parent().data('touch'));
352 for (var i=0; i<this.files.length; i++) {
353 var reader = new FileReader();
354 var nanotouch = $('.nanotouch')[sample_idx + i];
355 reader.onload = function(e) {
356 var $nanotouch = $(this.nanotouch);
357 var sample_idx = this.sample_idx;
358 self.audioCtx.decodeAudioData(this.result, function(buffer) {
359 sample_buffers[sample_idx] = buffer;
360 $nanotouch.find('span.duration').data('duration', buffer.duration);
361 $nanotouch.find('span.duration').text(parseInt(buffer.duration) + 's');
362 $nanotouch.removeClass('error').addClass('loaded');
364 $nanotouch.find('span').text('');
365 $nanotouch.removeClass('loaded').addClass('error');
368 reader.nanotouch = nanotouch;
369 reader.sample_idx = sample_idx + i;
370 reader.readAsArrayBuffer(this.files[i]);
371 $(nanotouch).find('span.name').text(this.files[i].name);
375 midi.onTouchOn = function(port, data, sample_idx) {
376 self.toggleSample(sample_idx);
379 midi.onControlChange = function(port, data, control, value) {
380 if (control > 7 && control < 16) return; /* range between sliders and pots */
382 /* cycle -> alternate mode, make faders control effects when pressed
383 * (moving faders when cycle is pressed) */
384 self.cycle_being_pressed = (value != 0);
387 if (control >= 32 && control < 40) { /* "S" buttons -> loop */
388 var nanotouch = $('.nanotouch')[control-32];
390 var checked = $(nanotouch).find('.loop input').prop('checked');
392 $(nanotouch).find('.loop input').prop('checked', false);
393 device("nanoKONTROL2 MIDI 1").cc(control, 0);
395 $(nanotouch).find('.loop input').prop('checked', true);
396 device("nanoKONTROL2 MIDI 1").cc(control, 127);
398 $(nanotouch).find('.loop input').trigger('change');
401 if (control >= 48 && control < 55) { /* "M" buttons -> effects */
402 var nanotouch = $('.nanotouch')[control-48];
404 var checked = $(nanotouch).find('.effects input').prop('checked');
406 $(nanotouch).find('.effects input').prop('checked', false);
407 device("nanoKONTROL2 MIDI 1").cc(control, 0);
409 $(nanotouch).find('.effects input').prop('checked', true);
410 device("nanoKONTROL2 MIDI 1").cc(control, 127);
412 $(nanotouch).find('.effects input').trigger('change');
416 if ((control == 58 || control == 59) && value == 127) {
417 /* track < and > buttons: move focus between pads for special functions */
418 if (self.focused_pad === undefined) {
419 self.focused_pad = 0;
420 } else if (control == 58) {
421 self.focused_pad = self.focused_pad - 1;
422 if (self.focused_pad < 0) self.focused_pad = 15;
423 } else if (control == 59) {
424 self.focused_pad = self.focused_pad + 1;
425 if (self.focused_pad > 15) self.focused_pad = 0;
427 $('[data-touch]').removeClass('focus');
428 $('[data-touch=' + self.focused_pad + ']').addClass('focus');
430 if (control == 41 && value == 127 && self.focused_pad !== undefined) { /* play */
431 var nanotouch = $('.nanotouch')[self.focused_pad];
432 if ($(nanotouch).is('.playing')) {
433 self.samples[self.focused_pad].onended = function() {}; // disable callback
434 self.stopSample(self.focused_pad);
436 self.startSample(self.focused_pad);
438 if (control == 42 && value == 127 && self.focused_pad !== undefined) { /* stop */
439 self.stopSample(self.focused_pad);
441 if (control > 23) return; /* after pots */
442 if (self.cycle_being_pressed) {
443 /* 4 -> delay, 5 -> feedback, 6 -> filter, 7 -> master */
445 $('#delay').val(value / 127 * 5).trigger('change');
446 } else if (control == 5) {
447 $('#feedback').val(value / 127 * 1).trigger('change');
448 } else if (control == 6) {
449 $('#filter').val(value / 127 * 5000).trigger('change');
450 } else if (control == 7) {
451 $('#master-gain').val(value).trigger('change');
456 control += 8; /* sliders, control bottom pads (8-15) */
458 control -= 16; /* pots, control top pads (0-7) */
460 $('[data-touch=' + control + '] .touch-gain').val(value).trigger('change');
463 $('.loop input').on('change', function() {
464 var sample_idx = parseInt($(this).parents('[data-touch]').data('touch'));
465 if (self.samples[sample_idx]) {
466 self.samples[sample_idx].loop = $(this).is(':checked');
470 $(document).keypress(function(ev) {
471 var sample_idx = KEYBOARD_CODES.indexOf(ev.key);
472 if (sample_idx != -1) {
473 self.toggleSample(sample_idx);
477 $('.effects input').on('change', function() {
478 var effects = $(this).prop('checked');
479 var i = parseInt($(this).parents('.nanotouch').data('touch'));
481 self.touchGainNodes[i].connect(self.effectsGainNode);
483 self.touchGainNodes[i].disconnect(self.effectsGainNode);
487 $('#master-gain').on('change', function() {
488 var fraction = parseInt(this.value) / parseInt(127);
489 var now = self.audioCtx.currentTime;
490 self.masterGainNode.gain.exponentialRampToValueAtTime((fraction * fraction) || 0.0001, now + 0.015);
493 $('#delay').on('change', function() {
494 var value = this.value;
496 self.delay.disconnect();
498 var now = self.audioCtx.currentTime;
499 self.delay.delayTime.exponentialRampToValueAtTime(value || 0.0001, now + 0.015);
500 self.delay.connect(self.audioCtx.destination);
504 $('#feedback').on('change', function() {
505 var now = self.audioCtx.currentTime;
506 self.feedback.gain.exponentialRampToValueAtTime(this.value || 0.0001, now + 0.015);
509 $('#filter').on('change', function() {
510 self.filter.frequency.value = this.value;
513 $('.touch-gain').on('change', function() {
514 var fraction = parseInt(this.value) / parseInt(127);
515 var touchIdx = parseInt($(this).parent().data('touch'));
516 var now = self.audioCtx.currentTime;
518 self.touchGainNodes[touchIdx].gain.value = 0;
520 self.touchGainNodes[touchIdx].gain.exponentialRampToValueAtTime((fraction * fraction) || 0.0001, now + 0.015);
524 self.time_interval_id = setInterval(function() {
525 for (var i=0; i<NANOPAD_TOUCHS.length; i++) {
526 var sample = self.samples[i];
527 if (sample !== undefined) {
528 var start_time = self.sample_start_times[i];
529 var current_position = sample.context.currentTime - start_time;
530 var nanotouch = $('.nanotouch')[i];
531 var duration = $(nanotouch).find('span.duration');
532 var total_duration = parseFloat($(duration).data('duration'))
533 $(duration).text(parseInt(total_duration - current_position) + 's');
539 self.toggleSample = function(sample_idx) {
540 var nanotouch = $('.nanotouch')[sample_idx];
541 if ($(nanotouch).is('.playing')) {
542 self.stopSample(sample_idx);
544 self.startSample(sample_idx);
548 self.startSample = function(sample_idx) {
549 var sample_buffer = self.sample_buffers[sample_idx];
550 var nanotouch = $('.nanotouch')[sample_idx];
551 if (typeof(sample_buffer) != 'undefined') {
552 var sample = self.audioCtx.createBufferSource();
553 var gainNode = self.touchGainNodes[sample_idx];
554 self.samples[sample_idx] = sample;
555 sample.loop = ($(nanotouch).find('.loop input:checked').length == 1);
556 sample.connect(gainNode);
557 sample.buffer = sample_buffer;
558 sample.onended = function() {
559 $(nanotouch).removeClass('playing');
560 var duration = $(nanotouch).find('span.duration');
561 $(duration).text(parseInt($(duration).data('duration')) + 's');
562 self.samples[sample_idx] = undefined;
564 $(nanotouch).addClass('playing');
565 self.sample_start_times[sample_idx] = sample.context.currentTime;
569 self.stopSample = function(sample_idx) {
570 var sample_buffer = self.sample_buffers[sample_idx];
571 if (typeof(sample_buffer) != 'undefined') {
572 if (typeof(samples[sample_idx]) != 'undefined') {
573 self.samples[sample_idx].stop(0);
574 self.samples[sample_idx] = undefined;
585 $(function() { nanofun(); });
587 if ('serviceWorker' in navigator) {
588 window.addEventListener('load', function() {
589 navigator.serviceWorker.register('service-worker.js').then(function(registration) {
590 // Registration was successful
591 console.log('ServiceWorker registration successful with scope: ', registration.scope);
592 }).catch(function(err) {
593 // registration failed :(
594 console.log('ServiceWorker registration failed: ', err);