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=1; i<16; i++) {
337 var $new_touch = $nanotouch.clone();
338 $new_touch.attr('data-touch', i);
339 $new_touch.appendTo($nanopad);
342 $('.nanotouch input[type=file]').on('change', function(ev) {
343 var sample_idx = parseInt($(this).parent().data('touch'));
344 for (var i=0; i<this.files.length; i++) {
345 var reader = new FileReader();
346 var nanotouch = $('.nanotouch')[sample_idx + i];
347 reader.onload = function(e) {
348 var $nanotouch = $(this.nanotouch);
349 var sample_idx = this.sample_idx;
350 self.audioCtx.decodeAudioData(this.result, function(buffer) {
351 sample_buffers[sample_idx] = buffer;
352 $nanotouch.find('span.duration').text(parseInt(buffer.duration) + 's');
353 $nanotouch.removeClass('error').addClass('loaded');
355 $nanotouch.find('span').text('');
356 $nanotouch.removeClass('loaded').addClass('error');
359 reader.nanotouch = nanotouch;
360 reader.sample_idx = sample_idx + i;
361 reader.readAsArrayBuffer(this.files[i]);
362 $(nanotouch).find('span.name').text(this.files[i].name);
366 midi.onTouchOn = function(port, data, sample_idx) {
367 self.startSample(sample_idx);
370 midi.onControlChange = function(port, data, control, value) {
371 if (control > 7 && control < 16) return; /* range between sliders and pots */
372 if (control > 23) return; /* after pots */
374 control += 8; /* sliders, control bottom pads (8-15) */
376 control -= 16; /* pots, control top pads (0-7) */
378 $('[data-touch=' + control + '] .touch-gain').val(value).trigger('change');
381 $(document).keypress(function(ev) {
382 var sample_idx = KEYBOARD_CODES.indexOf(ev.key);
383 if (sample_idx != -1) {
384 self.startSample(sample_idx);
388 $('#master-gain').on('change', function() {
389 var fraction = parseInt(this.value) / parseInt(127);
390 self.masterGainNode.gain.value = fraction * fraction;
393 $('#delay').on('change', function() {
394 var value = this.value;
396 self.delay.disconnect();
398 self.delay.delayTime.value = value;
399 self.delay.connect(self.audioCtx.destination);
403 $('#feedback').on('change', function() {
404 self.feedback.gain.value = this.value;
407 $('#filter').on('change', function() {
408 self.filter.frequency.value = this.value;
411 $('.touch-gain').on('change', function() {
412 var fraction = parseInt(this.value) / parseInt(127);
413 var touchIdx = parseInt($(this).parent().data('touch'));
414 self.touchGainNodes[touchIdx].gain.value = fraction * fraction;
418 self.startSample = function(sample_idx) {
419 var sample_buffer = self.sample_buffers[sample_idx];
420 var nanotouch = $('.nanotouch')[sample_idx];
421 if (typeof(sample_buffer) != 'undefined') {
422 if (typeof(samples[sample_idx]) != 'undefined' && samples[sample_idx].context.state == 'running') {
423 self.samples[sample_idx].stop(0);
424 self.samples[sample_idx] = undefined;
426 var sample = self.audioCtx.createBufferSource();
427 var gainNode = self.touchGainNodes[sample_idx];
428 self.samples[sample_idx] = sample;
430 sample.connect(gainNode);
431 sample.buffer = sample_buffer;
432 sample.onended = function() {
433 $(nanotouch).removeClass('playing');
434 self.samples[sample_idx] = undefined;
436 $(nanotouch).addClass('playing');
448 $(function() { nanofun(); });
450 if ('serviceWorker' in navigator) {
451 window.addEventListener('load', function() {
452 navigator.serviceWorker.register('service-worker.js').then(function(registration) {
453 // Registration was successful
454 console.log('ServiceWorker registration successful with scope: ', registration.scope);
455 }).catch(function(err) {
456 // registration failed :(
457 console.log('ServiceWorker registration failed: ', err);