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 var now = self.audioCtx.currentTime;
420 self.masterGainNode.gain.exponentialRampToValueAtTime(fraction * fraction, now + 0.015);
423 $('#delay').on('change', function() {
424 var value = this.value;
426 self.delay.disconnect();
428 var now = self.audioCtx.currentTime;
429 self.delay.delayTime.exponentialRampToValueAtTime(value, now + 0.015);
430 self.delay.connect(self.audioCtx.destination);
434 $('#feedback').on('change', function() {
435 var now = self.audioCtx.currentTime;
436 self.feedback.gain.exponentialRampToValueAtTime(this.value, now + 0.015);
439 $('#filter').on('change', function() {
440 self.filter.frequency.value = this.value;
443 $('.touch-gain').on('change', function() {
444 var fraction = parseInt(this.value) / parseInt(127);
445 var touchIdx = parseInt($(this).parent().data('touch'));
446 var now = self.audioCtx.currentTime;
447 self.touchGainNodes[touchIdx].gain.exponentialRampToValueAtTime(fraction * fraction, now + 0.015);
450 self.time_interval_id = setInterval(function() {
451 for (var i=0; i<NANOPAD_TOUCHS.length; i++) {
452 var sample = self.samples[i];
453 if (sample !== undefined) {
454 var start_time = self.sample_start_times[i];
455 var current_position = sample.context.currentTime - start_time;
456 var nanotouch = $('.nanotouch')[i];
457 var duration = $(nanotouch).find('span.duration');
458 var total_duration = parseFloat($(duration).data('duration'))
459 $(duration).text(parseInt(total_duration - current_position) + 's');
465 self.startSample = function(sample_idx) {
466 var sample_buffer = self.sample_buffers[sample_idx];
467 var nanotouch = $('.nanotouch')[sample_idx];
468 if (typeof(sample_buffer) != 'undefined') {
469 if (typeof(samples[sample_idx]) != 'undefined' && samples[sample_idx].context.state == 'running') {
470 self.samples[sample_idx].stop(0);
471 self.samples[sample_idx] = undefined;
473 var sample = self.audioCtx.createBufferSource();
474 var gainNode = self.touchGainNodes[sample_idx];
475 self.samples[sample_idx] = sample;
476 sample.loop = ($(nanotouch).find('.loop input:checked').length == 1);
477 sample.connect(gainNode);
478 sample.buffer = sample_buffer;
479 sample.onended = function() {
480 $(nanotouch).removeClass('playing');
481 var duration = $(nanotouch).find('span.duration');
482 $(duration).text(parseInt($(duration).data('duration')) + 's');
483 self.samples[sample_idx] = undefined;
485 $(nanotouch).addClass('playing');
486 self.sample_start_times[sample_idx] = sample.context.currentTime;
498 $(function() { nanofun(); });
500 if ('serviceWorker' in navigator) {
501 window.addEventListener('load', function() {
502 navigator.serviceWorker.register('service-worker.js').then(function(registration) {
503 // Registration was successful
504 console.log('ServiceWorker registration successful with scope: ', registration.scope);
505 }).catch(function(err) {
506 // registration failed :(
507 console.log('ServiceWorker registration failed: ', err);