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.appendTo($nanopad);
347 $nanotouch.remove(); /* remove template */
349 $('.nanotouch input[type=file]').on('change', function(ev) {
350 var sample_idx = parseInt($(this).parent().data('touch'));
351 for (var i=0; i<this.files.length; i++) {
352 var reader = new FileReader();
353 var nanotouch = $('.nanotouch')[sample_idx + i];
354 reader.onload = function(e) {
355 var $nanotouch = $(this.nanotouch);
356 var sample_idx = this.sample_idx;
357 self.audioCtx.decodeAudioData(this.result, function(buffer) {
358 sample_buffers[sample_idx] = buffer;
359 $nanotouch.find('span.duration').data('duration', buffer.duration);
360 $nanotouch.find('span.duration').text(parseInt(buffer.duration) + 's');
361 $nanotouch.removeClass('error').addClass('loaded');
363 $nanotouch.find('span').text('');
364 $nanotouch.removeClass('loaded').addClass('error');
367 reader.nanotouch = nanotouch;
368 reader.sample_idx = sample_idx + i;
369 reader.readAsArrayBuffer(this.files[i]);
370 $(nanotouch).find('span.name').text(this.files[i].name);
374 midi.onTouchOn = function(port, data, sample_idx) {
375 self.toggleSample(sample_idx);
378 midi.onControlChange = function(port, data, control, value) {
379 if (control > 7 && control < 16) return; /* range between sliders and pots */
381 /* cycle -> alternate mode, make faders control effects when pressed
382 * (moving faders when cycle is pressed) */
383 self.cycle_being_pressed = (value != 0);
386 if (control >= 32 && control < 40) { /* "S" buttons -> loop */
387 var nanotouch = $('.nanotouch')[control-32];
389 var checked = $(nanotouch).find('.loop input').prop('checked');
391 $(nanotouch).find('.loop input').prop('checked', false);
392 device("nanoKONTROL2 MIDI 1").cc(control, 0);
394 $(nanotouch).find('.loop input').prop('checked', true);
395 device("nanoKONTROL2 MIDI 1").cc(control, 127);
397 $(nanotouch).find('.loop input').trigger('change');
400 if (control >= 48 && control < 55) { /* "M" buttons -> effects */
401 var nanotouch = $('.nanotouch')[control-48];
403 var checked = $(nanotouch).find('.effects input').prop('checked');
405 $(nanotouch).find('.effects input').prop('checked', false);
406 device("nanoKONTROL2 MIDI 1").cc(control, 0);
408 $(nanotouch).find('.effects input').prop('checked', true);
409 device("nanoKONTROL2 MIDI 1").cc(control, 127);
411 $(nanotouch).find('.effects input').trigger('change');
415 if ((control == 58 || control == 59) && value == 127) {
416 /* track < and > buttons: move focus between pads for special functions */
417 if (self.focused_pad === undefined) {
418 self.focused_pad = 0;
419 } else if (control == 58) {
420 self.focused_pad = self.focused_pad - 1;
421 if (self.focused_pad < 0) self.focused_pad = 15;
422 } else if (control == 59) {
423 self.focused_pad = self.focused_pad + 1;
424 if (self.focused_pad > 15) self.focused_pad = 0;
426 $('[data-touch]').removeClass('focus');
427 $('[data-touch=' + self.focused_pad + ']').addClass('focus');
429 if (control == 41 && value == 127 && self.focused_pad !== undefined) { /* play */
430 var nanotouch = $('.nanotouch')[self.focused_pad];
431 if ($(nanotouch).is('.playing')) {
432 self.samples[self.focused_pad].onended = function() {}; // disable callback
433 self.stopSample(self.focused_pad);
435 self.startSample(self.focused_pad);
437 if (control == 42 && value == 127 && self.focused_pad !== undefined) { /* stop */
438 self.stopSample(self.focused_pad);
440 if (control > 23) return; /* after pots */
441 if (self.cycle_being_pressed) {
442 /* 4 -> delay, 5 -> feedback, 6 -> filter, 7 -> master */
444 $('#delay').val(value / 127 * 5).trigger('change');
445 } else if (control == 5) {
446 $('#feedback').val(value / 127 * 1).trigger('change');
447 } else if (control == 6) {
448 $('#filter').val(value / 127 * 5000).trigger('change');
449 } else if (control == 7) {
450 $('#master-gain').val(value).trigger('change');
455 control += 8; /* sliders, control bottom pads (8-15) */
457 control -= 16; /* pots, control top pads (0-7) */
459 $('[data-touch=' + control + '] .touch-gain').val(value).trigger('change');
462 $('.loop input').on('change', function() {
463 var sample_idx = parseInt($(this).parents('[data-touch]').data('touch'));
464 if (self.samples[sample_idx]) {
465 self.samples[sample_idx].loop = $(this).is(':checked');
469 $(document).keypress(function(ev) {
470 var sample_idx = KEYBOARD_CODES.indexOf(ev.key);
471 if (sample_idx != -1) {
472 self.toggleSample(sample_idx);
476 $('.effects input').on('change', function() {
477 var effects = $(this).prop('checked');
478 var i = parseInt($(this).parents('.nanotouch').data('touch'));
480 self.touchGainNodes[i].connect(self.effectsGainNode);
482 self.touchGainNodes[i].disconnect(self.effectsGainNode);
486 $('#master-gain').on('change', function() {
487 var fraction = parseInt(this.value) / parseInt(127);
488 var now = self.audioCtx.currentTime;
489 self.masterGainNode.gain.exponentialRampToValueAtTime((fraction * fraction) || 0.0001, now + 0.015);
492 $('#delay').on('change', function() {
493 var value = this.value;
495 self.delay.disconnect();
497 var now = self.audioCtx.currentTime;
498 self.delay.delayTime.exponentialRampToValueAtTime(value || 0.0001, now + 0.015);
499 self.delay.connect(self.audioCtx.destination);
503 $('#feedback').on('change', function() {
504 var now = self.audioCtx.currentTime;
505 self.feedback.gain.exponentialRampToValueAtTime(this.value || 0.0001, now + 0.015);
508 $('#filter').on('change', function() {
509 self.filter.frequency.value = this.value;
512 $('.touch-gain').on('change', function() {
513 var fraction = parseInt(this.value) / parseInt(127);
514 var touchIdx = parseInt($(this).parent().data('touch'));
515 var now = self.audioCtx.currentTime;
517 self.touchGainNodes[touchIdx].gain.value = 0;
519 self.touchGainNodes[touchIdx].gain.exponentialRampToValueAtTime((fraction * fraction) || 0.0001, now + 0.015);
523 self.time_interval_id = setInterval(function() {
524 for (var i=0; i<NANOPAD_TOUCHS.length; i++) {
525 var sample = self.samples[i];
526 if (sample !== undefined) {
527 var start_time = self.sample_start_times[i];
528 var current_position = sample.context.currentTime - start_time;
529 var nanotouch = $('.nanotouch')[i];
530 var duration = $(nanotouch).find('span.duration');
531 var total_duration = parseFloat($(duration).data('duration'))
532 $(duration).text(parseInt(total_duration - current_position) + 's');
538 self.toggleSample = function(sample_idx) {
539 var nanotouch = $('.nanotouch')[sample_idx];
540 if ($(nanotouch).is('.playing')) {
541 self.stopSample(sample_idx);
543 self.startSample(sample_idx);
547 self.startSample = function(sample_idx) {
548 var sample_buffer = self.sample_buffers[sample_idx];
549 var nanotouch = $('.nanotouch')[sample_idx];
550 if (typeof(sample_buffer) != 'undefined') {
551 var sample = self.audioCtx.createBufferSource();
552 var gainNode = self.touchGainNodes[sample_idx];
553 self.samples[sample_idx] = sample;
554 sample.loop = ($(nanotouch).find('.loop input:checked').length == 1);
555 sample.connect(gainNode);
556 sample.buffer = sample_buffer;
557 sample.onended = function() {
558 $(nanotouch).removeClass('playing');
559 var duration = $(nanotouch).find('span.duration');
560 $(duration).text(parseInt($(duration).data('duration')) + 's');
561 self.samples[sample_idx] = undefined;
563 $(nanotouch).addClass('playing');
564 self.sample_start_times[sample_idx] = sample.context.currentTime;
568 self.stopSample = function(sample_idx) {
569 var sample_buffer = self.sample_buffers[sample_idx];
570 if (typeof(sample_buffer) != 'undefined') {
571 if (typeof(samples[sample_idx]) != 'undefined') {
572 self.samples[sample_idx].stop(0);
573 self.samples[sample_idx] = undefined;
584 $(function() { nanofun(); });
586 if ('serviceWorker' in navigator) {
587 window.addEventListener('load', function() {
588 navigator.serviceWorker.register('service-worker.js').then(function(registration) {
589 // Registration was successful
590 console.log('ServiceWorker registration successful with scope: ', registration.scope);
591 }).catch(function(err) {
592 // registration failed :(
593 console.log('ServiceWorker registration failed: ', err);