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);
57 var portContainer = $("#midi" + port.type + "s");
58 if (portContainer.html().startsWith("<p>No connected")) {
59 portContainer.empty();
62 if (port.state == "disconnected") {
63 if (port.type == "input") {
64 this.inputs[port.name] = undefined;
66 this.outputs[port.name] = undefined;
69 if (port.type == "input") {
70 if (this.inputs[port.name] === undefined) { this.registerPort(port); }
72 if (this.outputs[port.name] === undefined) { this.registerPort(port); }
74 this.renderPort(port);
78 renderPort: function(port) {
79 if (port.state == "connected") {
80 $("#midi" + port.type + "s").append(port.name);
84 registerPort: function(port) {
86 if (port.type == "input") {
87 this.inputs[port.name] = port;
88 port.onmidimessage = function(m) { self.onMIDIMessage(m); };
90 this.outputs[port.name] = port;
92 if (port.name == 'nanoKONTROL2 MIDI 1') {
93 /* turn "external leds" mode on.
95 * The sysex dump has been recorded from the official Korg kontrol
96 * editor by the Overtone project, original code at:
97 * https://github.com/overtone/overtone/blob/master/src/overtone/device/midi/nanoKONTROL2.clj */
98 device("nanoKONTROL2 MIDI 1").raw([240, 126, 127, 6, 1, 247]);
99 device("nanoKONTROL2 MIDI 1").raw([240, 66, 64, 0, 1, 19, 0, 31, 18, 0, 247]);
100 device("nanoKONTROL2 MIDI 1").raw([240, 126, 127, 6, 1, 247]);
102 device("nanoKONTROL2 MIDI 1").raw([ 240, 66, 64, 0, 1, 19, 0, 127, 127,
103 2, 3, 5, 64, 0, 0, 0, 1, 16, 1, 0, 0, 0, 0, 127, 0, 1, 0, 16,
104 0, 0, 127, 0, 1, 0, 32, 0, 127, 0, 0, 1, 0, 48, 0, 127, 0, 0,
105 1, 0, 64, 0, 127, 0, 16, 0, 1, 0, 1, 0, 127, 0, 1, 0, 0, 17, 0,
106 127, 0, 1, 0, 0, 33, 0, 127, 0, 1, 0, 49, 0, 0, 127, 0, 1, 0,
107 65, 0, 0, 127, 0, 16, 1, 0, 2, 0, 0, 127, 0, 1, 0, 18, 0, 127,
108 0, 0, 1, 0, 34, 0, 127, 0, 0, 1, 0, 50, 0, 127, 0, 1, 0, 0, 66,
109 0, 127, 0, 16, 1, 0, 0, 3, 0, 127, 0, 1, 0, 0, 19, 0, 127, 0,
110 1, 0, 35, 0, 0, 127, 0, 1, 0, 51, 0, 0, 127, 0, 1, 0, 67, 0,
111 127, 0, 0, 16, 1, 0, 4, 0, 127, 0, 0, 1, 0, 20, 0, 127, 0, 0,
112 1, 0, 36, 0, 127, 0, 1, 0, 0, 52, 0, 127, 0, 1, 0, 0, 68, 0,
113 127, 0, 16, 1, 0, 0, 5, 0, 127, 0, 1, 0, 21, 0, 0, 127, 0, 1,
114 0, 37, 0, 0, 127, 0, 1, 0, 53, 0, 127, 0, 0, 1, 0, 69, 0, 127,
115 0, 0, 16, 1, 0, 6, 0, 127, 0, 0, 1, 0, 22, 0, 127, 0, 1, 0, 0,
116 38, 0, 127, 0, 1, 0, 0, 54, 0, 127, 0, 1, 0, 70, 0, 0, 127, 0,
117 16, 1, 0, 7, 0, 0, 127, 0, 1, 0, 23, 0, 0, 127, 0, 1, 0, 39, 0,
118 127, 0, 0, 1, 0, 55, 0, 127, 0, 0, 1, 0, 71, 0, 127, 0, 16, 0,
119 1, 0, 58, 0, 127, 0, 1, 0, 0, 59, 0, 127, 0, 1, 0, 0, 46, 0,
120 127, 0, 1, 0, 60, 0, 0, 127, 0, 1, 0, 61, 0, 0, 127, 0, 1, 0,
121 62, 0, 127, 0, 0, 1, 0, 43, 0, 127, 0, 0, 1, 0, 44, 0, 127, 0,
122 1, 0, 0, 42, 0, 127, 0, 1, 0, 0, 41, 0, 127, 0, 1, 0, 45, 0, 0,
123 127, 0, 127, 127, 127, 127, 0, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0,
124 0, 0, 0, 0, 0, 0, 0, 0, 0, 247]);
126 device("nanoKONTROL2 MIDI 1").raw([240, 126, 127, 6, 1, 247]);
127 device("nanoKONTROL2 MIDI 1").raw([240, 66, 64, 0, 1, 19, 0, 31, 17, 0, 247]);
129 function on(note) { device("nanoKONTROL2 MIDI 1").cc(note, 127); }
130 function off(note) { device("nanoKONTROL2 MIDI 1").cc(note, 0); }
131 var leds = Array(43, 44, 42, 41, 45);
132 for (var i=0; i<8; i++) {
146 var interval_id = setInterval(function() {
147 if (led_idx < leds.length) {
150 off(leds[led_idx-2]);
151 if (led_idx-1 == leds.length) {
152 clearInterval(interval_id);
159 port.onstatechange = function(e) { self.onPortStateChange(e); };
162 onMIDIFailure: function(e) {
163 alert("No access to MIDI devices or your browser doesn't support WebMIDI API. Please use WebMIDIAPIShim " + e);
166 onPortStateChange: function(event) {
170 onMIDIMessage: function(message) {
171 var port = message.target;
172 var data = message.data;
173 console.log(message);
174 if (data[0] == 144) { /* touch on */
175 var sample_idx = NANOPAD_TOUCHS.indexOf(data[1]);
176 if (sample_idx != -1) {
177 this.onTouchOn(port, data, sample_idx);
180 if (data[0] == 176) { /* control change */
181 this.onControlChange(port, data, data[1], data[2]);
185 onTouchOn: function(port, data, sample_idx) {},
186 onControlChange: function(port, data, number, value) {}
190 // status bytes on channel 1
201 var device = function(outputName) {
202 this.current = midi.outputs[outputName];
205 // makes device visible inside of nested function defs
208 this._send = function(status, data) {
209 var messageArr = [status + (self.channel - 1)].concat(data);
210 console.log("sending " + messageArr + " to " + self.current.name);
211 self.current.send(messageArr);
215 this.ch = function(channel) {
216 self.channel = channel;
220 this.cc = function(b1, b2) {
221 return self._send(messages.cc, [b1, b2]);
224 this.on = function(b1, b2) {
225 return self._send(messages.on, [b1, b2]);
228 this.off = function(b1, b2) {
229 return self._send(messages.off, [b1, b2]);
232 this.pp = function(b1, b2) {
233 return self._send(messages.pp, [b1, b2]);
236 this.cp = function(b1) {
237 return self._send(messages.cp, [b1]);
240 this.pb = function(b1) {
249 this.pc = function(b1) {
250 return self._send(messages.pc, [b1]);
253 this.panic = function() {
254 return self.cc(123, 0)
257 this.rpn = function(b1, b2) {
258 return self.cc(101, b1 >> 7)
266 this.nrpn = function(b1, b2) {
267 return self.cc(99, b1 >> 7)
275 this.raw = function(data) {
276 console.log("sending raw data: " + data);
277 self.current.send(data);
281 this.toString = function() {
282 var s = "no connected devices";
283 if (typeof this.current != 'undefined') {
292 var nanofun = function() {
295 self.initAudio = function() {
296 self.sample_buffers = Array(16);
297 self.samples = Array(16);
298 self.audioCtx = new window.AudioContext();
299 self.touchGainNodes = Array(16);
300 self.masterGainNode = self.audioCtx.createGain();
301 for (var i=0; i<16; i++) {
302 self.touchGainNodes[i] = self.audioCtx.createGain();
303 self.touchGainNodes[i].connect(self.masterGainNode);
305 self.masterGainNode.connect(self.audioCtx.destination);
307 self.delay = self.audioCtx.createDelay(maxDelayTime=5);
308 self.delay.delayTime.value = 0.5;
310 self.feedback = self.audioCtx.createGain();
311 self.feedback.gain.value = 0.8;
313 self.filter = self.audioCtx.createBiquadFilter();
314 self.filter.frequency.value = 1000;
316 self.delay.connect(self.feedback);
317 self.feedback.connect(self.filter);
318 self.filter.connect(self.delay);
320 self.masterGainNode.connect(self.delay);
323 self.initMIDI = function() {
324 if (navigator.requestMIDIAccess) {
325 navigator.requestMIDIAccess({sysex: true}).then(
326 function(midiAccess) { midi.onMIDISuccess(midiAccess); },
327 function(e) { midi.onMIDIFailure(e); }
332 self.initUI = function() {
333 var $nanopad = $('#nanopad');
334 var $nanotouch = $('.nanotouch');
335 for (var i=1; i<16; i++) {
336 var $new_touch = $nanotouch.clone();
337 $new_touch.attr('data-touch', i);
338 $new_touch.appendTo($nanopad);
341 $('.nanotouch input[type=file]').on('change', function(ev) {
342 var sample_idx = parseInt($(this).parent().data('touch'));
343 for (var i=0; i<this.files.length; i++) {
344 var reader = new FileReader();
345 var nanotouch = $('.nanotouch')[sample_idx + i];
346 reader.onload = function(e) {
347 var $nanotouch = $(this.nanotouch);
348 var sample_idx = this.sample_idx;
349 self.audioCtx.decodeAudioData(this.result, function(buffer) {
350 sample_buffers[sample_idx] = buffer;
351 $nanotouch.find('span.duration').text(parseInt(buffer.duration) + 's');
352 $nanotouch.removeClass('error').addClass('loaded');
354 $nanotouch.find('span').text('');
355 $nanotouch.removeClass('loaded').addClass('error');
358 reader.nanotouch = nanotouch;
359 reader.sample_idx = sample_idx + i;
360 reader.readAsArrayBuffer(this.files[i]);
361 $(nanotouch).find('span.name').text(this.files[i].name);
365 midi.onTouchOn = function(port, data, sample_idx) {
366 self.startSample(sample_idx);
369 midi.onControlChange = function(port, data, control, value) {
370 if (control > 7 && control < 16) return; /* range between sliders and pots */
371 if (control > 23) return; /* after pots */
373 control += 8; /* sliders, control bottom pads (8-15) */
375 control -= 16; /* pots, control top pads (0-7) */
377 $('[data-touch=' + control + '] .touch-gain').val(value).trigger('change');
380 $(document).keypress(function(ev) {
381 var sample_idx = KEYBOARD_CODES.indexOf(ev.key);
382 if (sample_idx != -1) {
383 self.startSample(sample_idx);
387 $('#master-gain').on('change', function() {
388 var fraction = parseInt(this.value) / parseInt(127);
389 self.masterGainNode.gain.value = fraction * fraction;
392 $('#delay').on('change', function() {
393 var value = this.value;
395 self.delay.disconnect();
397 self.delay.delayTime.value = value;
398 self.delay.connect(self.audioCtx.destination);
402 $('#feedback').on('change', function() {
403 self.feedback.gain.value = this.value;
406 $('#filter').on('change', function() {
407 self.filter.frequency.value = this.value;
410 $('.touch-gain').on('change', function() {
411 var fraction = parseInt(this.value) / parseInt(127);
412 var touchIdx = parseInt($(this).parent().data('touch'));
413 self.touchGainNodes[touchIdx].gain.value = fraction * fraction;
417 self.startSample = function(sample_idx) {
418 var sample_buffer = self.sample_buffers[sample_idx];
419 var nanotouch = $('.nanotouch')[sample_idx];
420 if (typeof(sample_buffer) != 'undefined') {
421 if (typeof(samples[sample_idx]) != 'undefined' && samples[sample_idx].context.state == 'running') {
422 self.samples[sample_idx].stop(0);
423 self.samples[sample_idx] = undefined;
425 var sample = self.audioCtx.createBufferSource();
426 var gainNode = self.touchGainNodes[sample_idx];
427 self.samples[sample_idx] = sample;
429 sample.connect(gainNode);
430 sample.buffer = sample_buffer;
431 sample.onended = function() {
432 $(nanotouch).removeClass('playing');
433 self.samples[sample_idx] = undefined;
435 $(nanotouch).addClass('playing');
447 $(function() { nanofun(); });
449 if ('serviceWorker' in navigator) {
450 window.addEventListener('load', function() {
451 navigator.serviceWorker.register('service-worker.js').then(function(registration) {
452 // Registration was successful
453 console.log('ServiceWorker registration successful with scope: ', registration.scope);
454 }).catch(function(err) {
455 // registration failed :(
456 console.log('ServiceWorker registration failed: ', err);