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(16);
299 self.samples = Array(16);
300 self.sample_start_times = Array(16);
301 self.audioCtx = new window.AudioContext();
302 self.touchGainNodes = Array(16);
303 self.masterGainNode = self.audioCtx.createGain();
304 for (var i=0; i<16; i++) {
305 self.touchGainNodes[i] = self.audioCtx.createGain();
306 self.touchGainNodes[i].connect(self.masterGainNode);
308 self.masterGainNode.connect(self.audioCtx.destination);
310 self.delay = self.audioCtx.createDelay(maxDelayTime=5);
311 self.delay.delayTime.value = 0.5;
313 self.feedback = self.audioCtx.createGain();
314 self.feedback.gain.value = 0.8;
316 self.filter = self.audioCtx.createBiquadFilter();
317 self.filter.frequency.value = 1000;
319 self.delay.connect(self.feedback);
320 self.feedback.connect(self.filter);
321 self.filter.connect(self.delay);
323 self.masterGainNode.connect(self.delay);
326 self.initMIDI = function() {
327 if (navigator.requestMIDIAccess) {
328 navigator.requestMIDIAccess({sysex: true}).then(
329 function(midiAccess) { midi.onMIDISuccess(midiAccess); },
330 function(e) { midi.onMIDIFailure(e); }
335 self.initUI = function() {
336 var $nanopad = $('#nanopad');
337 var $nanotouch = $('.nanotouch');
338 for (var i=0; i<16; i++) {
339 var $new_touch = $nanotouch.clone();
340 $new_touch.attr('data-touch', i);
341 $new_touch.appendTo($nanopad);
343 $nanotouch.remove(); /* remove template */
345 $('.nanotouch input[type=file]').on('change', function(ev) {
346 var sample_idx = parseInt($(this).parent().data('touch'));
347 for (var i=0; i<this.files.length; i++) {
348 var reader = new FileReader();
349 var nanotouch = $('.nanotouch')[sample_idx + i];
350 reader.onload = function(e) {
351 var $nanotouch = $(this.nanotouch);
352 var sample_idx = this.sample_idx;
353 self.audioCtx.decodeAudioData(this.result, function(buffer) {
354 sample_buffers[sample_idx] = buffer;
355 $nanotouch.find('span.duration').data('duration', buffer.duration);
356 $nanotouch.find('span.duration').text(parseInt(buffer.duration) + 's');
357 $nanotouch.removeClass('error').addClass('loaded');
359 $nanotouch.find('span').text('');
360 $nanotouch.removeClass('loaded').addClass('error');
363 reader.nanotouch = nanotouch;
364 reader.sample_idx = sample_idx + i;
365 reader.readAsArrayBuffer(this.files[i]);
366 $(nanotouch).find('span.name').text(this.files[i].name);
370 midi.onTouchOn = function(port, data, sample_idx) {
371 self.startSample(sample_idx);
374 midi.onControlChange = function(port, data, control, value) {
375 if (control > 7 && control < 16) return; /* range between sliders and pots */
376 if (control >= 32 && control < 40) { /* "S" buttons */
377 var nanotouch = $('.nanotouch')[control-32];
379 var checked = $(nanotouch).find('.loop input').prop('checked');
381 $(nanotouch).find('.loop input').prop('checked', false);
382 device("nanoKONTROL2 MIDI 1").cc(control, 0);
384 $(nanotouch).find('.loop input').prop('checked', true);
385 device("nanoKONTROL2 MIDI 1").cc(control, 127);
389 if (control > 23) return; /* after pots */
391 control += 8; /* sliders, control bottom pads (8-15) */
393 control -= 16; /* pots, control top pads (0-7) */
395 $('[data-touch=' + control + '] .touch-gain').val(value).trigger('change');
398 $(document).keypress(function(ev) {
399 var sample_idx = KEYBOARD_CODES.indexOf(ev.key);
400 if (sample_idx != -1) {
401 self.startSample(sample_idx);
405 $('#master-gain').on('change', function() {
406 var fraction = parseInt(this.value) / parseInt(127);
407 self.masterGainNode.gain.value = fraction * fraction;
410 $('#delay').on('change', function() {
411 var value = this.value;
413 self.delay.disconnect();
415 self.delay.delayTime.value = value;
416 self.delay.connect(self.audioCtx.destination);
420 $('#feedback').on('change', function() {
421 self.feedback.gain.value = this.value;
424 $('#filter').on('change', function() {
425 self.filter.frequency.value = this.value;
428 $('.touch-gain').on('change', function() {
429 var fraction = parseInt(this.value) / parseInt(127);
430 var touchIdx = parseInt($(this).parent().data('touch'));
431 self.touchGainNodes[touchIdx].gain.value = fraction * fraction;
434 self.time_interval_id = setInterval(function() {
435 for (var i=0; i<16; i++) {
436 var sample = self.samples[i];
437 if (sample !== undefined) {
438 var start_time = self.sample_start_times[i];
439 var current_position = sample.context.currentTime - start_time;
440 var nanotouch = $('.nanotouch')[i];
441 var duration = $(nanotouch).find('span.duration');
442 var total_duration = parseFloat($(duration).data('duration'))
443 $(duration).text(parseInt(total_duration - current_position) + 's');
449 self.startSample = function(sample_idx) {
450 var sample_buffer = self.sample_buffers[sample_idx];
451 var nanotouch = $('.nanotouch')[sample_idx];
452 if (typeof(sample_buffer) != 'undefined') {
453 if (typeof(samples[sample_idx]) != 'undefined' && samples[sample_idx].context.state == 'running') {
454 self.samples[sample_idx].stop(0);
455 self.samples[sample_idx] = undefined;
457 var sample = self.audioCtx.createBufferSource();
458 var gainNode = self.touchGainNodes[sample_idx];
459 self.samples[sample_idx] = sample;
460 sample.loop = ($(nanotouch).find('.loop input:checked').length == 1);
461 sample.connect(gainNode);
462 sample.buffer = sample_buffer;
463 sample.onended = function() {
464 $(nanotouch).removeClass('playing');
465 var duration = $(nanotouch).find('span.duration');
466 $(duration).text(parseInt($(duration).data('duration')) + 's');
467 self.samples[sample_idx] = undefined;
469 $(nanotouch).addClass('playing');
470 self.sample_start_times[sample_idx] = sample.context.currentTime;
482 $(function() { nanofun(); });
484 if ('serviceWorker' in navigator) {
485 window.addEventListener('load', function() {
486 navigator.serviceWorker.register('service-worker.js').then(function(registration) {
487 // Registration was successful
488 console.log('ServiceWorker registration successful with scope: ', registration.scope);
489 }).catch(function(err) {
490 // registration failed :(
491 console.log('ServiceWorker registration failed: ', err);