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.sample_buffers = Array(NANOPAD_TOUCHS.length);
300 self.samples = Array(NANOPAD_TOUCHS.length);
301 self.sample_start_times = Array(NANOPAD_TOUCHS.length);
302 self.audioCtx = new window.AudioContext();
303 self.touchGainNodes = Array(NANOPAD_TOUCHS.length);
304 self.masterGainNode = self.audioCtx.createGain();
305 self.effectsGainNode = self.audioCtx.createGain();
306 for (var i=0; i<NANOPAD_TOUCHS.length; i++) {
307 self.touchGainNodes[i] = self.audioCtx.createGain();
308 self.touchGainNodes[i].connect(self.masterGainNode);
310 self.masterGainNode.connect(self.audioCtx.destination);
312 self.delay = self.audioCtx.createDelay(maxDelayTime=5);
313 self.delay.delayTime.value = 0.5;
315 self.feedback = self.audioCtx.createGain();
316 self.feedback.gain.value = 0.8;
318 self.filter = self.audioCtx.createBiquadFilter();
319 self.filter.frequency.value = 1000;
321 self.effectsGainNode.connect(self.delay);
322 self.delay.connect(self.feedback);
323 self.feedback.connect(self.filter);
324 self.filter.connect(self.delay);
326 self.filter.connect(self.masterGainNode);
329 self.initMIDI = function() {
330 if (navigator.requestMIDIAccess) {
331 navigator.requestMIDIAccess({sysex: true}).then(
332 function(midiAccess) { midi.onMIDISuccess(midiAccess); },
333 function(e) { midi.onMIDIFailure(e); }
338 self.initUI = function() {
339 var $nanopad = $('#nanopad');
340 var $nanotouch = $('.nanotouch');
341 for (var i=0; i<NANOPAD_TOUCHS.length; i++) {
342 var $new_touch = $nanotouch.clone();
343 $new_touch.attr('data-touch', i);
344 $new_touch.appendTo($nanopad);
346 $nanotouch.remove(); /* remove template */
348 $('.nanotouch input[type=file]').on('change', function(ev) {
349 var sample_idx = parseInt($(this).parent().data('touch'));
350 for (var i=0; i<this.files.length; i++) {
351 var reader = new FileReader();
352 var nanotouch = $('.nanotouch')[sample_idx + i];
353 reader.onload = function(e) {
354 var $nanotouch = $(this.nanotouch);
355 var sample_idx = this.sample_idx;
356 self.audioCtx.decodeAudioData(this.result, function(buffer) {
357 sample_buffers[sample_idx] = buffer;
358 $nanotouch.find('span.duration').data('duration', buffer.duration);
359 $nanotouch.find('span.duration').text(parseInt(buffer.duration) + 's');
360 $nanotouch.removeClass('error').addClass('loaded');
362 $nanotouch.find('span').text('');
363 $nanotouch.removeClass('loaded').addClass('error');
366 reader.nanotouch = nanotouch;
367 reader.sample_idx = sample_idx + i;
368 reader.readAsArrayBuffer(this.files[i]);
369 $(nanotouch).find('span.name').text(this.files[i].name);
373 midi.onTouchOn = function(port, data, sample_idx) {
374 self.toggleSample(sample_idx);
377 midi.onControlChange = function(port, data, control, value) {
378 if (control > 7 && control < 16) return; /* range between sliders and pots */
379 if (control >= 32 && control < 40) { /* "S" buttons -> loop */
380 var nanotouch = $('.nanotouch')[control-32];
382 var checked = $(nanotouch).find('.loop input').prop('checked');
384 $(nanotouch).find('.loop input').prop('checked', false);
385 device("nanoKONTROL2 MIDI 1").cc(control, 0);
387 $(nanotouch).find('.loop input').prop('checked', true);
388 device("nanoKONTROL2 MIDI 1").cc(control, 127);
390 $(nanotouch).find('.loop input').trigger('change');
393 if (control >= 48 && control < 55) { /* "M" buttons -> effects */
394 var nanotouch = $('.nanotouch')[control-48];
396 var checked = $(nanotouch).find('.effects input').prop('checked');
398 $(nanotouch).find('.effects input').prop('checked', false);
399 device("nanoKONTROL2 MIDI 1").cc(control, 0);
401 $(nanotouch).find('.effects input').prop('checked', true);
402 device("nanoKONTROL2 MIDI 1").cc(control, 127);
404 $(nanotouch).find('.effects input').trigger('change');
408 if ((control == 58 || control == 59) && value == 127) {
409 /* track < and > buttons: move focus between pads for special functions */
410 if (self.focused_pad === undefined) {
411 self.focused_pad = 0;
412 } else if (control == 58) {
413 self.focused_pad = self.focused_pad - 1;
414 if (self.focused_pad < 0) self.focused_pad = 15;
415 } else if (control == 59) {
416 self.focused_pad = self.focused_pad + 1;
417 if (self.focused_pad > 15) self.focused_pad = 0;
419 $('[data-touch]').removeClass('focus');
420 $('[data-touch=' + self.focused_pad + ']').addClass('focus');
422 if (control == 41 && value == 127 && self.focused_pad !== undefined) { /* play */
423 var nanotouch = $('.nanotouch')[self.focused_pad];
424 if ($(nanotouch).is('.playing')) {
425 self.samples[self.focused_pad].onended = function() {}; // disable callback
426 self.stopSample(self.focused_pad);
428 self.startSample(self.focused_pad);
430 if (control == 42 && value == 127 && self.focused_pad !== undefined) { /* stop */
431 self.stopSample(self.focused_pad);
433 if (control > 23) return; /* after pots */
435 control += 8; /* sliders, control bottom pads (8-15) */
437 control -= 16; /* pots, control top pads (0-7) */
439 $('[data-touch=' + control + '] .touch-gain').val(value).trigger('change');
442 $('.loop input').on('change', function() {
443 var sample_idx = parseInt($(this).parents('[data-touch]').data('touch'));
444 if (self.samples[sample_idx]) {
445 self.samples[sample_idx].loop = $(this).is(':checked');
449 $(document).keypress(function(ev) {
450 var sample_idx = KEYBOARD_CODES.indexOf(ev.key);
451 if (sample_idx != -1) {
452 self.startSample(sample_idx);
456 $('.effects input').on('change', function() {
457 var effects = $(this).prop('checked');
458 var i = parseInt($(this).parents('.nanotouch').data('touch'));
460 self.touchGainNodes[i].connect(self.effectsGainNode);
462 self.touchGainNodes[i].disconnect(self.effectsGainNode);
466 $('#master-gain').on('change', function() {
467 var fraction = parseInt(this.value) / parseInt(127);
468 var now = self.audioCtx.currentTime;
469 self.masterGainNode.gain.exponentialRampToValueAtTime((fraction * fraction) || 0.01, now + 0.015);
472 $('#delay').on('change', function() {
473 var value = this.value;
475 self.delay.disconnect();
477 var now = self.audioCtx.currentTime;
478 self.delay.delayTime.exponentialRampToValueAtTime(value || 0.01, now + 0.015);
479 self.delay.connect(self.audioCtx.destination);
483 $('#feedback').on('change', function() {
484 var now = self.audioCtx.currentTime;
485 self.feedback.gain.exponentialRampToValueAtTime(this.value || 0.01, now + 0.015);
488 $('#filter').on('change', function() {
489 self.filter.frequency.value = this.value;
492 $('.touch-gain').on('change', function() {
493 var fraction = parseInt(this.value) / parseInt(127);
494 var touchIdx = parseInt($(this).parent().data('touch'));
495 var now = self.audioCtx.currentTime;
496 self.touchGainNodes[touchIdx].gain.exponentialRampToValueAtTime((fraction * fraction) || 0.01, now + 0.015);
499 self.time_interval_id = setInterval(function() {
500 for (var i=0; i<NANOPAD_TOUCHS.length; i++) {
501 var sample = self.samples[i];
502 if (sample !== undefined) {
503 var start_time = self.sample_start_times[i];
504 var current_position = sample.context.currentTime - start_time;
505 var nanotouch = $('.nanotouch')[i];
506 var duration = $(nanotouch).find('span.duration');
507 var total_duration = parseFloat($(duration).data('duration'))
508 $(duration).text(parseInt(total_duration - current_position) + 's');
514 self.toggleSample = function(sample_idx) {
515 var nanotouch = $('.nanotouch')[sample_idx];
516 if ($(nanotouch).is('.playing')) {
517 self.stopSample(sample_idx);
519 self.startSample(sample_idx);
523 self.startSample = function(sample_idx) {
524 var sample_buffer = self.sample_buffers[sample_idx];
525 var nanotouch = $('.nanotouch')[sample_idx];
526 if (typeof(sample_buffer) != 'undefined') {
527 var sample = self.audioCtx.createBufferSource();
528 var gainNode = self.touchGainNodes[sample_idx];
529 self.samples[sample_idx] = sample;
530 sample.loop = ($(nanotouch).find('.loop input:checked').length == 1);
531 sample.connect(gainNode);
532 sample.buffer = sample_buffer;
533 sample.onended = function() {
534 $(nanotouch).removeClass('playing');
535 var duration = $(nanotouch).find('span.duration');
536 $(duration).text(parseInt($(duration).data('duration')) + 's');
537 self.samples[sample_idx] = undefined;
539 $(nanotouch).addClass('playing');
540 self.sample_start_times[sample_idx] = sample.context.currentTime;
544 self.stopSample = function(sample_idx) {
545 var sample_buffer = self.sample_buffers[sample_idx];
546 if (typeof(sample_buffer) != 'undefined') {
547 if (typeof(samples[sample_idx]) != 'undefined') {
548 self.samples[sample_idx].stop(0);
549 self.samples[sample_idx] = undefined;
560 $(function() { nanofun(); });
562 if ('serviceWorker' in navigator) {
563 window.addEventListener('load', function() {
564 navigator.serviceWorker.register('service-worker.js').then(function(registration) {
565 // Registration was successful
566 console.log('ServiceWorker registration successful with scope: ', registration.scope);
567 }).catch(function(err) {
568 // registration failed :(
569 console.log('ServiceWorker registration failed: ', err);