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 > 23) return; /* after pots */
377 control += 8; /* sliders, control bottom pads (8-15) */
379 control -= 16; /* pots, control top pads (0-7) */
381 $('[data-touch=' + control + '] .touch-gain').val(value).trigger('change');
384 $(document).keypress(function(ev) {
385 var sample_idx = KEYBOARD_CODES.indexOf(ev.key);
386 if (sample_idx != -1) {
387 self.startSample(sample_idx);
391 $('#master-gain').on('change', function() {
392 var fraction = parseInt(this.value) / parseInt(127);
393 self.masterGainNode.gain.value = fraction * fraction;
396 $('#delay').on('change', function() {
397 var value = this.value;
399 self.delay.disconnect();
401 self.delay.delayTime.value = value;
402 self.delay.connect(self.audioCtx.destination);
406 $('#feedback').on('change', function() {
407 self.feedback.gain.value = this.value;
410 $('#filter').on('change', function() {
411 self.filter.frequency.value = this.value;
414 $('.touch-gain').on('change', function() {
415 var fraction = parseInt(this.value) / parseInt(127);
416 var touchIdx = parseInt($(this).parent().data('touch'));
417 self.touchGainNodes[touchIdx].gain.value = fraction * fraction;
420 self.time_interval_id = setInterval(function() {
421 for (var i=0; i<16; i++) {
422 var sample = self.samples[i];
423 if (sample !== undefined) {
424 var start_time = self.sample_start_times[i];
425 var current_position = sample.context.currentTime - start_time;
426 var nanotouch = $('.nanotouch')[i];
427 var duration = $(nanotouch).find('span.duration');
428 var total_duration = parseFloat($(duration).data('duration'))
429 $(duration).text(parseInt(total_duration - current_position) + 's');
435 self.startSample = function(sample_idx) {
436 var sample_buffer = self.sample_buffers[sample_idx];
437 var nanotouch = $('.nanotouch')[sample_idx];
438 if (typeof(sample_buffer) != 'undefined') {
439 if (typeof(samples[sample_idx]) != 'undefined' && samples[sample_idx].context.state == 'running') {
440 self.samples[sample_idx].stop(0);
441 self.samples[sample_idx] = undefined;
443 var sample = self.audioCtx.createBufferSource();
444 var gainNode = self.touchGainNodes[sample_idx];
445 self.samples[sample_idx] = sample;
447 sample.connect(gainNode);
448 sample.buffer = sample_buffer;
449 sample.onended = function() {
450 $(nanotouch).removeClass('playing');
451 var duration = $(nanotouch).find('span.duration');
452 $(duration).text(parseInt($(duration).data('duration')) + 's');
453 self.samples[sample_idx] = undefined;
455 $(nanotouch).addClass('playing');
456 self.sample_start_times[sample_idx] = sample.context.currentTime;
468 $(function() { nanofun(); });
470 if ('serviceWorker' in navigator) {
471 window.addEventListener('load', function() {
472 navigator.serviceWorker.register('service-worker.js').then(function(registration) {
473 // Registration was successful
474 console.log('ServiceWorker registration successful with scope: ', registration.scope);
475 }).catch(function(err) {
476 // registration failed :(
477 console.log('ServiceWorker registration failed: ', err);