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.sample_buffers = Array(NANOPAD_TOUCHS.length);
299 self.samples = Array(NANOPAD_TOUCHS.length);
300 self.sample_start_times = Array(NANOPAD_TOUCHS.length);
301 self.audioCtx = new window.AudioContext();
302 self.touchGainNodes = Array(NANOPAD_TOUCHS.length);
303 self.masterGainNode = self.audioCtx.createGain();
304 self.effectsGainNode = self.audioCtx.createGain();
305 for (var i=0; i<NANOPAD_TOUCHS.length; i++) {
306 self.touchGainNodes[i] = self.audioCtx.createGain();
307 self.touchGainNodes[i].connect(self.masterGainNode);
309 self.masterGainNode.connect(self.audioCtx.destination);
311 self.delay = self.audioCtx.createDelay(maxDelayTime=5);
312 self.delay.delayTime.value = 0.5;
314 self.feedback = self.audioCtx.createGain();
315 self.feedback.gain.value = 0.8;
317 self.filter = self.audioCtx.createBiquadFilter();
318 self.filter.frequency.value = 1000;
320 self.effectsGainNode.connect(self.delay);
321 self.delay.connect(self.feedback);
322 self.feedback.connect(self.filter);
323 self.filter.connect(self.delay);
325 self.filter.connect(self.masterGainNode);
328 self.initMIDI = function() {
329 if (navigator.requestMIDIAccess) {
330 navigator.requestMIDIAccess({sysex: true}).then(
331 function(midiAccess) { midi.onMIDISuccess(midiAccess); },
332 function(e) { midi.onMIDIFailure(e); }
337 self.initUI = function() {
338 var $nanopad = $('#nanopad');
339 var $nanotouch = $('.nanotouch');
340 for (var i=0; i<NANOPAD_TOUCHS.length; i++) {
341 var $new_touch = $nanotouch.clone();
342 $new_touch.attr('data-touch', i);
343 $new_touch.appendTo($nanopad);
345 $nanotouch.remove(); /* remove template */
347 $('.nanotouch input[type=file]').on('change', function(ev) {
348 var sample_idx = parseInt($(this).parent().data('touch'));
349 for (var i=0; i<this.files.length; i++) {
350 var reader = new FileReader();
351 var nanotouch = $('.nanotouch')[sample_idx + i];
352 reader.onload = function(e) {
353 var $nanotouch = $(this.nanotouch);
354 var sample_idx = this.sample_idx;
355 self.audioCtx.decodeAudioData(this.result, function(buffer) {
356 sample_buffers[sample_idx] = buffer;
357 $nanotouch.find('span.duration').data('duration', buffer.duration);
358 $nanotouch.find('span.duration').text(parseInt(buffer.duration) + 's');
359 $nanotouch.removeClass('error').addClass('loaded');
361 $nanotouch.find('span').text('');
362 $nanotouch.removeClass('loaded').addClass('error');
365 reader.nanotouch = nanotouch;
366 reader.sample_idx = sample_idx + i;
367 reader.readAsArrayBuffer(this.files[i]);
368 $(nanotouch).find('span.name').text(this.files[i].name);
372 midi.onTouchOn = function(port, data, sample_idx) {
373 self.startSample(sample_idx);
376 midi.onControlChange = function(port, data, control, value) {
377 if (control > 7 && control < 16) return; /* range between sliders and pots */
378 if (control >= 32 && control < 40) { /* "S" buttons */
379 var nanotouch = $('.nanotouch')[control-32];
381 var checked = $(nanotouch).find('.loop input').prop('checked');
383 $(nanotouch).find('.loop input').prop('checked', false);
384 device("nanoKONTROL2 MIDI 1").cc(control, 0);
386 $(nanotouch).find('.loop input').prop('checked', true);
387 device("nanoKONTROL2 MIDI 1").cc(control, 127);
391 if (control > 23) return; /* after pots */
393 control += 8; /* sliders, control bottom pads (8-15) */
395 control -= 16; /* pots, control top pads (0-7) */
397 $('[data-touch=' + control + '] .touch-gain').val(value).trigger('change');
400 $(document).keypress(function(ev) {
401 var sample_idx = KEYBOARD_CODES.indexOf(ev.key);
402 if (sample_idx != -1) {
403 self.startSample(sample_idx);
407 $('.effects input').on('change', function() {
408 var effects = $(this).prop('checked');
409 var i = parseInt($(this).parents('.nanotouch').data('touch'));
411 self.touchGainNodes[i].connect(self.effectsGainNode);
413 self.touchGainNodes[i].disconnect(self.effectsGainNode);
417 $('#master-gain').on('change', function() {
418 var fraction = parseInt(this.value) / parseInt(127);
419 self.masterGainNode.gain.value = fraction * fraction;
422 $('#delay').on('change', function() {
423 var value = this.value;
425 self.delay.disconnect();
427 self.delay.delayTime.value = value;
428 self.delay.connect(self.audioCtx.destination);
432 $('#feedback').on('change', function() {
433 self.feedback.gain.value = this.value;
436 $('#filter').on('change', function() {
437 self.filter.frequency.value = this.value;
440 $('.touch-gain').on('change', function() {
441 var fraction = parseInt(this.value) / parseInt(127);
442 var touchIdx = parseInt($(this).parent().data('touch'));
443 self.touchGainNodes[touchIdx].gain.value = fraction * fraction;
446 self.time_interval_id = setInterval(function() {
447 for (var i=0; i<NANOPAD_TOUCHS.length; i++) {
448 var sample = self.samples[i];
449 if (sample !== undefined) {
450 var start_time = self.sample_start_times[i];
451 var current_position = sample.context.currentTime - start_time;
452 var nanotouch = $('.nanotouch')[i];
453 var duration = $(nanotouch).find('span.duration');
454 var total_duration = parseFloat($(duration).data('duration'))
455 $(duration).text(parseInt(total_duration - current_position) + 's');
461 self.startSample = function(sample_idx) {
462 var sample_buffer = self.sample_buffers[sample_idx];
463 var nanotouch = $('.nanotouch')[sample_idx];
464 if (typeof(sample_buffer) != 'undefined') {
465 if (typeof(samples[sample_idx]) != 'undefined' && samples[sample_idx].context.state == 'running') {
466 self.samples[sample_idx].stop(0);
467 self.samples[sample_idx] = undefined;
469 var sample = self.audioCtx.createBufferSource();
470 var gainNode = self.touchGainNodes[sample_idx];
471 self.samples[sample_idx] = sample;
472 sample.loop = ($(nanotouch).find('.loop input:checked').length == 1);
473 sample.connect(gainNode);
474 sample.buffer = sample_buffer;
475 sample.onended = function() {
476 $(nanotouch).removeClass('playing');
477 var duration = $(nanotouch).find('span.duration');
478 $(duration).text(parseInt($(duration).data('duration')) + 's');
479 self.samples[sample_idx] = undefined;
481 $(nanotouch).addClass('playing');
482 self.sample_start_times[sample_idx] = sample.context.currentTime;
494 $(function() { nanofun(); });
496 if ('serviceWorker' in navigator) {
497 window.addEventListener('load', function() {
498 navigator.serviceWorker.register('service-worker.js').then(function(registration) {
499 // Registration was successful
500 console.log('ServiceWorker registration successful with scope: ', registration.scope);
501 }).catch(function(err) {
502 // registration failed :(
503 console.log('ServiceWorker registration failed: ', err);