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') { $('#devices .nanopad').removeClass('on'); }
65 if (port.name == 'nanoKONTROL2 MIDI 1') { $('#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') { $('#devices .nanopad').addClass('on'); }
74 if (port.name == 'nanoKONTROL2 MIDI 1') { $('#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') {
94 /* turn "external leds" mode on.
96 * The sysex dump has been recorded from the official Korg kontrol
97 * editor by the Overtone project, original code at:
98 * https://github.com/overtone/overtone/blob/master/src/overtone/device/midi/nanoKONTROL2.clj */
99 device("nanoKONTROL2 MIDI 1").raw([240, 126, 127, 6, 1, 247]);
100 device("nanoKONTROL2 MIDI 1").raw([240, 66, 64, 0, 1, 19, 0, 31, 18, 0, 247]);
101 device("nanoKONTROL2 MIDI 1").raw([240, 126, 127, 6, 1, 247]);
103 device("nanoKONTROL2 MIDI 1").raw([ 240, 66, 64, 0, 1, 19, 0, 127, 127,
104 2, 3, 5, 64, 0, 0, 0, 1, 16, 1, 0, 0, 0, 0, 127, 0, 1, 0, 16,
105 0, 0, 127, 0, 1, 0, 32, 0, 127, 0, 0, 1, 0, 48, 0, 127, 0, 0,
106 1, 0, 64, 0, 127, 0, 16, 0, 1, 0, 1, 0, 127, 0, 1, 0, 0, 17, 0,
107 127, 0, 1, 0, 0, 33, 0, 127, 0, 1, 0, 49, 0, 0, 127, 0, 1, 0,
108 65, 0, 0, 127, 0, 16, 1, 0, 2, 0, 0, 127, 0, 1, 0, 18, 0, 127,
109 0, 0, 1, 0, 34, 0, 127, 0, 0, 1, 0, 50, 0, 127, 0, 1, 0, 0, 66,
110 0, 127, 0, 16, 1, 0, 0, 3, 0, 127, 0, 1, 0, 0, 19, 0, 127, 0,
111 1, 0, 35, 0, 0, 127, 0, 1, 0, 51, 0, 0, 127, 0, 1, 0, 67, 0,
112 127, 0, 0, 16, 1, 0, 4, 0, 127, 0, 0, 1, 0, 20, 0, 127, 0, 0,
113 1, 0, 36, 0, 127, 0, 1, 0, 0, 52, 0, 127, 0, 1, 0, 0, 68, 0,
114 127, 0, 16, 1, 0, 0, 5, 0, 127, 0, 1, 0, 21, 0, 0, 127, 0, 1,
115 0, 37, 0, 0, 127, 0, 1, 0, 53, 0, 127, 0, 0, 1, 0, 69, 0, 127,
116 0, 0, 16, 1, 0, 6, 0, 127, 0, 0, 1, 0, 22, 0, 127, 0, 1, 0, 0,
117 38, 0, 127, 0, 1, 0, 0, 54, 0, 127, 0, 1, 0, 70, 0, 0, 127, 0,
118 16, 1, 0, 7, 0, 0, 127, 0, 1, 0, 23, 0, 0, 127, 0, 1, 0, 39, 0,
119 127, 0, 0, 1, 0, 55, 0, 127, 0, 0, 1, 0, 71, 0, 127, 0, 16, 0,
120 1, 0, 58, 0, 127, 0, 1, 0, 0, 59, 0, 127, 0, 1, 0, 0, 46, 0,
121 127, 0, 1, 0, 60, 0, 0, 127, 0, 1, 0, 61, 0, 0, 127, 0, 1, 0,
122 62, 0, 127, 0, 0, 1, 0, 43, 0, 127, 0, 0, 1, 0, 44, 0, 127, 0,
123 1, 0, 0, 42, 0, 127, 0, 1, 0, 0, 41, 0, 127, 0, 1, 0, 45, 0, 0,
124 127, 0, 127, 127, 127, 127, 0, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0,
125 0, 0, 0, 0, 0, 0, 0, 0, 0, 247]);
127 device("nanoKONTROL2 MIDI 1").raw([240, 126, 127, 6, 1, 247]);
128 device("nanoKONTROL2 MIDI 1").raw([240, 66, 64, 0, 1, 19, 0, 31, 17, 0, 247]);
130 function on(note) { device("nanoKONTROL2 MIDI 1").cc(note, 127); }
131 function off(note) { device("nanoKONTROL2 MIDI 1").cc(note, 0); }
132 var leds = Array(43, 44, 42, 41, 45);
133 for (var i=0; i<8; i++) {
147 var interval_id = setInterval(function() {
148 if (led_idx < leds.length) {
151 off(leds[led_idx-2]);
152 if (led_idx-1 == leds.length) {
153 clearInterval(interval_id);
160 port.onstatechange = function(e) { self.onPortStateChange(e); };
163 onMIDIFailure: function(e) {
164 alert("No access to MIDI devices or your browser doesn't support WebMIDI API. Please use WebMIDIAPIShim " + e);
167 onPortStateChange: function(event) {
171 onMIDIMessage: function(message) {
172 var port = message.target;
173 var data = message.data;
174 console.log(message);
175 if (data[0] == 144) { /* touch on */
176 var sample_idx = NANOPAD_TOUCHS.indexOf(data[1]);
177 if (sample_idx != -1) {
178 this.onTouchOn(port, data, sample_idx);
181 if (data[0] == 176) { /* control change */
182 this.onControlChange(port, data, data[1], data[2]);
186 onTouchOn: function(port, data, sample_idx) {},
187 onControlChange: function(port, data, number, value) {}
191 // status bytes on channel 1
202 var device = function(outputName) {
203 this.current = midi.outputs[outputName];
206 // makes device visible inside of nested function defs
209 this._send = function(status, data) {
210 var messageArr = [status + (self.channel - 1)].concat(data);
211 console.log("sending " + messageArr + " to " + self.current.name);
212 self.current.send(messageArr);
216 this.ch = function(channel) {
217 self.channel = channel;
221 this.cc = function(b1, b2) {
222 return self._send(messages.cc, [b1, b2]);
225 this.on = function(b1, b2) {
226 return self._send(messages.on, [b1, b2]);
229 this.off = function(b1, b2) {
230 return self._send(messages.off, [b1, b2]);
233 this.pp = function(b1, b2) {
234 return self._send(messages.pp, [b1, b2]);
237 this.cp = function(b1) {
238 return self._send(messages.cp, [b1]);
241 this.pb = function(b1) {
250 this.pc = function(b1) {
251 return self._send(messages.pc, [b1]);
254 this.panic = function() {
255 return self.cc(123, 0)
258 this.rpn = function(b1, b2) {
259 return self.cc(101, b1 >> 7)
267 this.nrpn = function(b1, b2) {
268 return self.cc(99, b1 >> 7)
276 this.raw = function(data) {
277 console.log("sending raw data: " + data);
278 self.current.send(data);
282 this.toString = function() {
283 var s = "no connected devices";
284 if (typeof this.current != 'undefined') {
293 var nanofun = function() {
296 self.initAudio = function() {
297 self.sample_buffers = Array(16);
298 self.samples = Array(16);
299 self.sample_start_times = Array(16);
300 self.audioCtx = new window.AudioContext();
301 self.touchGainNodes = Array(16);
302 self.masterGainNode = self.audioCtx.createGain();
303 for (var i=0; i<16; i++) {
304 self.touchGainNodes[i] = self.audioCtx.createGain();
305 self.touchGainNodes[i].connect(self.masterGainNode);
307 self.masterGainNode.connect(self.audioCtx.destination);
309 self.delay = self.audioCtx.createDelay(maxDelayTime=5);
310 self.delay.delayTime.value = 0.5;
312 self.feedback = self.audioCtx.createGain();
313 self.feedback.gain.value = 0.8;
315 self.filter = self.audioCtx.createBiquadFilter();
316 self.filter.frequency.value = 1000;
318 self.delay.connect(self.feedback);
319 self.feedback.connect(self.filter);
320 self.filter.connect(self.delay);
322 self.masterGainNode.connect(self.delay);
325 self.initMIDI = function() {
326 if (navigator.requestMIDIAccess) {
327 navigator.requestMIDIAccess({sysex: true}).then(
328 function(midiAccess) { midi.onMIDISuccess(midiAccess); },
329 function(e) { midi.onMIDIFailure(e); }
334 self.initUI = function() {
335 var $nanopad = $('#nanopad');
336 var $nanotouch = $('.nanotouch');
337 for (var i=0; i<16; i++) {
338 var $new_touch = $nanotouch.clone();
339 $new_touch.attr('data-touch', i);
340 $new_touch.appendTo($nanopad);
342 $nanotouch.remove(); /* remove template */
344 $('.nanotouch input[type=file]').on('change', function(ev) {
345 var sample_idx = parseInt($(this).parent().data('touch'));
346 for (var i=0; i<this.files.length; i++) {
347 var reader = new FileReader();
348 var nanotouch = $('.nanotouch')[sample_idx + i];
349 reader.onload = function(e) {
350 var $nanotouch = $(this.nanotouch);
351 var sample_idx = this.sample_idx;
352 self.audioCtx.decodeAudioData(this.result, function(buffer) {
353 sample_buffers[sample_idx] = buffer;
354 $nanotouch.find('span.duration').data('duration', buffer.duration);
355 $nanotouch.find('span.duration').text(parseInt(buffer.duration) + 's');
356 $nanotouch.removeClass('error').addClass('loaded');
358 $nanotouch.find('span').text('');
359 $nanotouch.removeClass('loaded').addClass('error');
362 reader.nanotouch = nanotouch;
363 reader.sample_idx = sample_idx + i;
364 reader.readAsArrayBuffer(this.files[i]);
365 $(nanotouch).find('span.name').text(this.files[i].name);
369 midi.onTouchOn = function(port, data, sample_idx) {
370 self.startSample(sample_idx);
373 midi.onControlChange = function(port, data, control, value) {
374 if (control > 7 && control < 16) return; /* range between sliders and pots */
375 if (control >= 32 && control < 40) { /* "S" buttons */
376 var nanotouch = $('.nanotouch')[control-32];
378 var checked = $(nanotouch).find('.loop input').prop('checked');
380 $(nanotouch).find('.loop input').prop('checked', false);
381 device("nanoKONTROL2 MIDI 1").cc(control, 0);
383 $(nanotouch).find('.loop input').prop('checked', true);
384 device("nanoKONTROL2 MIDI 1").cc(control, 127);
388 if (control > 23) return; /* after pots */
390 control += 8; /* sliders, control bottom pads (8-15) */
392 control -= 16; /* pots, control top pads (0-7) */
394 $('[data-touch=' + control + '] .touch-gain').val(value).trigger('change');
397 $(document).keypress(function(ev) {
398 var sample_idx = KEYBOARD_CODES.indexOf(ev.key);
399 if (sample_idx != -1) {
400 self.startSample(sample_idx);
404 $('#master-gain').on('change', function() {
405 var fraction = parseInt(this.value) / parseInt(127);
406 self.masterGainNode.gain.value = fraction * fraction;
409 $('#delay').on('change', function() {
410 var value = this.value;
412 self.delay.disconnect();
414 self.delay.delayTime.value = value;
415 self.delay.connect(self.audioCtx.destination);
419 $('#feedback').on('change', function() {
420 self.feedback.gain.value = this.value;
423 $('#filter').on('change', function() {
424 self.filter.frequency.value = this.value;
427 $('.touch-gain').on('change', function() {
428 var fraction = parseInt(this.value) / parseInt(127);
429 var touchIdx = parseInt($(this).parent().data('touch'));
430 self.touchGainNodes[touchIdx].gain.value = fraction * fraction;
433 self.time_interval_id = setInterval(function() {
434 for (var i=0; i<16; i++) {
435 var sample = self.samples[i];
436 if (sample !== undefined) {
437 var start_time = self.sample_start_times[i];
438 var current_position = sample.context.currentTime - start_time;
439 var nanotouch = $('.nanotouch')[i];
440 var duration = $(nanotouch).find('span.duration');
441 var total_duration = parseFloat($(duration).data('duration'))
442 $(duration).text(parseInt(total_duration - current_position) + 's');
448 self.startSample = function(sample_idx) {
449 var sample_buffer = self.sample_buffers[sample_idx];
450 var nanotouch = $('.nanotouch')[sample_idx];
451 if (typeof(sample_buffer) != 'undefined') {
452 if (typeof(samples[sample_idx]) != 'undefined' && samples[sample_idx].context.state == 'running') {
453 self.samples[sample_idx].stop(0);
454 self.samples[sample_idx] = undefined;
456 var sample = self.audioCtx.createBufferSource();
457 var gainNode = self.touchGainNodes[sample_idx];
458 self.samples[sample_idx] = sample;
459 sample.loop = ($(nanotouch).find('.loop input:checked').length == 1);
460 sample.connect(gainNode);
461 sample.buffer = sample_buffer;
462 sample.onended = function() {
463 $(nanotouch).removeClass('playing');
464 var duration = $(nanotouch).find('span.duration');
465 $(duration).text(parseInt($(duration).data('duration')) + 's');
466 self.samples[sample_idx] = undefined;
468 $(nanotouch).addClass('playing');
469 self.sample_start_times[sample_idx] = sample.context.currentTime;
481 $(function() { nanofun(); });
483 if ('serviceWorker' in navigator) {
484 window.addEventListener('load', function() {
485 navigator.serviceWorker.register('service-worker.js').then(function(registration) {
486 // Registration was successful
487 console.log('ServiceWorker registration successful with scope: ', registration.scope);
488 }).catch(function(err) {
489 // registration failed :(
490 console.log('ServiceWorker registration failed: ', err);