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.audioCtx = new window.AudioContext();
300 self.touchGainNodes = Array(16);
301 self.masterGainNode = self.audioCtx.createGain();
302 for (var i=0; i<16; i++) {
303 self.touchGainNodes[i] = self.audioCtx.createGain();
304 self.touchGainNodes[i].connect(self.masterGainNode);
306 self.masterGainNode.connect(self.audioCtx.destination);
308 self.delay = self.audioCtx.createDelay(maxDelayTime=5);
309 self.delay.delayTime.value = 0.5;
311 self.feedback = self.audioCtx.createGain();
312 self.feedback.gain.value = 0.8;
314 self.filter = self.audioCtx.createBiquadFilter();
315 self.filter.frequency.value = 1000;
317 self.delay.connect(self.feedback);
318 self.feedback.connect(self.filter);
319 self.filter.connect(self.delay);
321 self.masterGainNode.connect(self.delay);
324 self.initMIDI = function() {
325 if (navigator.requestMIDIAccess) {
326 navigator.requestMIDIAccess({sysex: true}).then(
327 function(midiAccess) { midi.onMIDISuccess(midiAccess); },
328 function(e) { midi.onMIDIFailure(e); }
333 self.initUI = function() {
334 var $nanopad = $('#nanopad');
335 var $nanotouch = $('.nanotouch');
336 for (var i=0; i<16; i++) {
337 var $new_touch = $nanotouch.clone();
338 $new_touch.attr('data-touch', i);
339 $new_touch.appendTo($nanopad);
341 $nanotouch.remove(); /* remove template */
343 $('.nanotouch input[type=file]').on('change', function(ev) {
344 var sample_idx = parseInt($(this).parent().data('touch'));
345 for (var i=0; i<this.files.length; i++) {
346 var reader = new FileReader();
347 var nanotouch = $('.nanotouch')[sample_idx + i];
348 reader.onload = function(e) {
349 var $nanotouch = $(this.nanotouch);
350 var sample_idx = this.sample_idx;
351 self.audioCtx.decodeAudioData(this.result, function(buffer) {
352 sample_buffers[sample_idx] = buffer;
353 $nanotouch.find('span.duration').text(parseInt(buffer.duration) + 's');
354 $nanotouch.removeClass('error').addClass('loaded');
356 $nanotouch.find('span').text('');
357 $nanotouch.removeClass('loaded').addClass('error');
360 reader.nanotouch = nanotouch;
361 reader.sample_idx = sample_idx + i;
362 reader.readAsArrayBuffer(this.files[i]);
363 $(nanotouch).find('span.name').text(this.files[i].name);
367 midi.onTouchOn = function(port, data, sample_idx) {
368 self.startSample(sample_idx);
371 midi.onControlChange = function(port, data, control, value) {
372 if (control > 7 && control < 16) return; /* range between sliders and pots */
373 if (control > 23) return; /* after pots */
375 control += 8; /* sliders, control bottom pads (8-15) */
377 control -= 16; /* pots, control top pads (0-7) */
379 $('[data-touch=' + control + '] .touch-gain').val(value).trigger('change');
382 $(document).keypress(function(ev) {
383 var sample_idx = KEYBOARD_CODES.indexOf(ev.key);
384 if (sample_idx != -1) {
385 self.startSample(sample_idx);
389 $('#master-gain').on('change', function() {
390 var fraction = parseInt(this.value) / parseInt(127);
391 self.masterGainNode.gain.value = fraction * fraction;
394 $('#delay').on('change', function() {
395 var value = this.value;
397 self.delay.disconnect();
399 self.delay.delayTime.value = value;
400 self.delay.connect(self.audioCtx.destination);
404 $('#feedback').on('change', function() {
405 self.feedback.gain.value = this.value;
408 $('#filter').on('change', function() {
409 self.filter.frequency.value = this.value;
412 $('.touch-gain').on('change', function() {
413 var fraction = parseInt(this.value) / parseInt(127);
414 var touchIdx = parseInt($(this).parent().data('touch'));
415 self.touchGainNodes[touchIdx].gain.value = fraction * fraction;
419 self.startSample = function(sample_idx) {
420 var sample_buffer = self.sample_buffers[sample_idx];
421 var nanotouch = $('.nanotouch')[sample_idx];
422 if (typeof(sample_buffer) != 'undefined') {
423 if (typeof(samples[sample_idx]) != 'undefined' && samples[sample_idx].context.state == 'running') {
424 self.samples[sample_idx].stop(0);
425 self.samples[sample_idx] = undefined;
427 var sample = self.audioCtx.createBufferSource();
428 var gainNode = self.touchGainNodes[sample_idx];
429 self.samples[sample_idx] = sample;
431 sample.connect(gainNode);
432 sample.buffer = sample_buffer;
433 sample.onended = function() {
434 $(nanotouch).removeClass('playing');
435 self.samples[sample_idx] = undefined;
437 $(nanotouch).addClass('playing');
449 $(function() { nanofun(); });
451 if ('serviceWorker' in navigator) {
452 window.addEventListener('load', function() {
453 navigator.serviceWorker.register('service-worker.js').then(function(registration) {
454 // Registration was successful
455 console.log('ServiceWorker registration successful with scope: ', registration.scope);
456 }).catch(function(err) {
457 // registration failed :(
458 console.log('ServiceWorker registration failed: ', err);