var NANOPAD_TOUCHS = Array(37, 39, 41, 43, 45, 47, 49, 51, 36, 38, 40, 42, 44, 46, 48, 50); /* on French/Belgian keyboards, emulate pad touches with keypresses */ var KEYBOARD_CODES = Array('a', 'z', 'e', 'r', 't', 'u', 'i', 'o', 'q', 's', 'd', 'f', 'g', 'h', 'j', 'k'); var midi = { onMIDISuccess: function(midiAccess) { console.log('MIDI Access Object', midiAccess); //console.log(this); var self = this; midiAccess.onstatechange = function(e) { self.onMIDIAccessChange(e); } this.midiAccess = midiAccess; this.inputs = {}; this.outputs = {}; this.initPorts(); }, initPorts: function() { var self = this; var inputs = this.midiAccess.inputs; if (inputs.size > 0) { inputs.forEach( function(port, key) { //console.log(port); self.registerPort(port); } ); } else { $("#midiinputs").append("
No connected inputs
"); } var outputs = this.midiAccess.outputs; if (outputs.size > 0) { outputs.forEach( function(port, key) { self.registerPort(port); self.renderPort(port); } ); } else { $("#midioutputs").append("No connected outputs
"); } }, onMIDIAccessChange: function(e) { console.log('on midi access change', e); //console.log(this); var port = e.port; var portContainer = $("#midi" + port.type + "s"); if (portContainer.html().startsWith("No connected")) { portContainer.empty(); } if (port.state == "disconnected") { if (port.type == "input") { this.inputs[port.name] = undefined; } else { this.outputs[port.name] = undefined; } } else { if (port.type == "input") { if (this.inputs[port.name] === undefined) { this.registerPort(port); } } else { if (this.outputs[port.name] === undefined) { this.registerPort(port); } } this.renderPort(port); } }, renderPort: function(port) { if (port.state == "connected") { $("#midi" + port.type + "s").append(port.name); } }, registerPort: function(port) { var self = this; if (port.type == "input") { this.inputs[port.name] = port; port.onmidimessage = function(m) { self.onMIDIMessage(m); }; } else { this.outputs[port.name] = port; if (port.name == 'nanoKONTROL2 MIDI 1') { /* turn "external leds" mode on. * * The sysex dump has been recorded from the official Korg kontrol * editor by the Overtone project, original code at: * https://github.com/overtone/overtone/blob/master/src/overtone/device/midi/nanoKONTROL2.clj */ device("nanoKONTROL2 MIDI 1").raw([240, 126, 127, 6, 1, 247]); device("nanoKONTROL2 MIDI 1").raw([240, 66, 64, 0, 1, 19, 0, 31, 18, 0, 247]); device("nanoKONTROL2 MIDI 1").raw([240, 126, 127, 6, 1, 247]); device("nanoKONTROL2 MIDI 1").raw([ 240, 66, 64, 0, 1, 19, 0, 127, 127, 2, 3, 5, 64, 0, 0, 0, 1, 16, 1, 0, 0, 0, 0, 127, 0, 1, 0, 16, 0, 0, 127, 0, 1, 0, 32, 0, 127, 0, 0, 1, 0, 48, 0, 127, 0, 0, 1, 0, 64, 0, 127, 0, 16, 0, 1, 0, 1, 0, 127, 0, 1, 0, 0, 17, 0, 127, 0, 1, 0, 0, 33, 0, 127, 0, 1, 0, 49, 0, 0, 127, 0, 1, 0, 65, 0, 0, 127, 0, 16, 1, 0, 2, 0, 0, 127, 0, 1, 0, 18, 0, 127, 0, 0, 1, 0, 34, 0, 127, 0, 0, 1, 0, 50, 0, 127, 0, 1, 0, 0, 66, 0, 127, 0, 16, 1, 0, 0, 3, 0, 127, 0, 1, 0, 0, 19, 0, 127, 0, 1, 0, 35, 0, 0, 127, 0, 1, 0, 51, 0, 0, 127, 0, 1, 0, 67, 0, 127, 0, 0, 16, 1, 0, 4, 0, 127, 0, 0, 1, 0, 20, 0, 127, 0, 0, 1, 0, 36, 0, 127, 0, 1, 0, 0, 52, 0, 127, 0, 1, 0, 0, 68, 0, 127, 0, 16, 1, 0, 0, 5, 0, 127, 0, 1, 0, 21, 0, 0, 127, 0, 1, 0, 37, 0, 0, 127, 0, 1, 0, 53, 0, 127, 0, 0, 1, 0, 69, 0, 127, 0, 0, 16, 1, 0, 6, 0, 127, 0, 0, 1, 0, 22, 0, 127, 0, 1, 0, 0, 38, 0, 127, 0, 1, 0, 0, 54, 0, 127, 0, 1, 0, 70, 0, 0, 127, 0, 16, 1, 0, 7, 0, 0, 127, 0, 1, 0, 23, 0, 0, 127, 0, 1, 0, 39, 0, 127, 0, 0, 1, 0, 55, 0, 127, 0, 0, 1, 0, 71, 0, 127, 0, 16, 0, 1, 0, 58, 0, 127, 0, 1, 0, 0, 59, 0, 127, 0, 1, 0, 0, 46, 0, 127, 0, 1, 0, 60, 0, 0, 127, 0, 1, 0, 61, 0, 0, 127, 0, 1, 0, 62, 0, 127, 0, 0, 1, 0, 43, 0, 127, 0, 0, 1, 0, 44, 0, 127, 0, 1, 0, 0, 42, 0, 127, 0, 1, 0, 0, 41, 0, 127, 0, 1, 0, 45, 0, 0, 127, 0, 127, 127, 127, 127, 0, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 247]); device("nanoKONTROL2 MIDI 1").raw([240, 126, 127, 6, 1, 247]); device("nanoKONTROL2 MIDI 1").raw([240, 66, 64, 0, 1, 19, 0, 31, 17, 0, 247]); function on(note) { device("nanoKONTROL2 MIDI 1").cc(note, 127); } function off(note) { device("nanoKONTROL2 MIDI 1").cc(note, 0); } var leds = Array(43, 44, 42, 41, 45); for (var i=0; i<8; i++) { leds.push(64+i); leds.push(48+i); leds.push(32+i); i += 1; leds.push(32+i); leds.push(48+i); leds.push(64+i); } console.log(leds); on(leds[0]); on(leds[1]); on(leds[2]); var led_idx = 2; var interval_id = setInterval(function() { if (led_idx < leds.length) { on(leds[led_idx+1]); } off(leds[led_idx-2]); if (led_idx-1 == leds.length) { clearInterval(interval_id); } led_idx += 1; }, 50); } } port.onstatechange = function(e) { self.onPortStateChange(e); }; }, onMIDIFailure: function(e) { alert("No access to MIDI devices or your browser doesn't support WebMIDI API. Please use WebMIDIAPIShim " + e); }, onPortStateChange: function(event) { console.log(event); }, onMIDIMessage: function(message) { var port = message.target; var data = message.data; console.log(message); if (data[0] == 144) { /* touch on */ var sample_idx = NANOPAD_TOUCHS.indexOf(data[1]); if (sample_idx != -1) { this.onTouchOn(port, data, sample_idx); } } if (data[0] == 176) { /* control change */ this.onControlChange(port, data, data[1], data[2]); } }, onTouchOn: function(port, data, sample_idx) {}, onControlChange: function(port, data, number, value) {} }; // status bytes on channel 1 var messages = { off: 128, on: 144, pp: 160, cc: 176, pc: 192, cp: 208, pb: 224 } var device = function(outputName) { this.current = midi.outputs[outputName]; this.channel = 1; // makes device visible inside of nested function defs var self = this; this._send = function(status, data) { var messageArr = [status + (self.channel - 1)].concat(data); console.log("sending " + messageArr + " to " + self.current.name); self.current.send(messageArr); return self; } this.ch = function(channel) { self.channel = channel; return self; } this.cc = function(b1, b2) { return self._send(messages.cc, [b1, b2]); } this.on = function(b1, b2) { return self._send(messages.on, [b1, b2]); } this.off = function(b1, b2) { return self._send(messages.off, [b1, b2]); } this.pp = function(b1, b2) { return self._send(messages.pp, [b1, b2]); } this.cp = function(b1) { return self._send(messages.cp, [b1]); } this.pb = function(b1) { return self._send( messages.pb, [ b1 & 127, b1 >> 7 ] ); } this.pc = function(b1) { return self._send(messages.pc, [b1]); } this.panic = function() { return self.cc(123, 0) } this.rpn = function(b1, b2) { return self.cc(101, b1 >> 7) .cc(100, b1 & 127) .cc(6, b2 >> 7) .cc(38, b2 & 127) .cc(101, 127) .cc(100, 127); } this.nrpn = function(b1, b2) { return self.cc(99, b1 >> 7) .cc(98, b1 & 127) .cc(6, b2 >> 7) .cc(38, b2 & 127) .cc(101, 127) .cc(100, 127); } this.raw = function(data) { console.log("sending raw data: " + data); self.current.send(data); return self; } this.toString = function() { var s = "no connected devices"; if (typeof this.current != 'undefined') { s = ""; } return s; } return this; }; var nanofun = function() { var self = this; self.initAudio = function() { self.sample_buffers = Array(16); self.samples = Array(16); self.audioCtx = new window.AudioContext(); self.touchGainNodes = Array(16); self.masterGainNode = self.audioCtx.createGain(); for (var i=0; i<16; i++) { self.touchGainNodes[i] = self.audioCtx.createGain(); self.touchGainNodes[i].connect(self.masterGainNode); } self.masterGainNode.connect(self.audioCtx.destination); } self.initMIDI = function() { if (navigator.requestMIDIAccess) { navigator.requestMIDIAccess({sysex: true}).then( function(midiAccess) { midi.onMIDISuccess(midiAccess); }, function(e) { midi.onMIDIFailure(e); } ); } } self.initUI = function() { var $nanopad = $('#nanopad'); var $nanotouch = $('.nanotouch'); for (var i=1; i<16; i++) { var $new_touch = $nanotouch.clone(); $new_touch.attr('data-touch', i); $new_touch.appendTo($nanopad); } $('.nanotouch input[type=file]').on('change', function(ev) { var nanotouch = $(this).parent(); var sample_idx = $nanopad.children().index(nanotouch); var reader = new FileReader(); reader.onload = function(e) { self.audioCtx.decodeAudioData(this.result, function(buffer) { sample_buffers[sample_idx] = buffer; $(nanotouch).find('span.duration').text(parseInt(buffer.duration) + 's'); $(nanotouch).removeClass('error').addClass('loaded'); }, function(e) { $(nanotouch).find('span').text(''); $(nanotouch).removeClass('loaded').addClass('error'); }); } reader.readAsArrayBuffer(this.files[0]); $(nanotouch).find('span.name').text(this.files[0].name); }); midi.onTouchOn = function(port, data, sample_idx) { self.startSample(sample_idx); } midi.onControlChange = function(port, data, control, value) { if (control > 7 && control < 16) return; /* range between sliders and pots */ if (control > 23) return; /* after pots */ if (control < 8) { control += 8; /* sliders, control bottom pads (8-15) */ } else { control -= 16; /* pots, control top pads (0-7) */ } $('[data-touch=' + control + '] .touch-gain').val(value).trigger('change'); } $(document).keypress(function(ev) { var sample_idx = KEYBOARD_CODES.indexOf(ev.key); if (sample_idx != -1) { self.startSample(sample_idx); } }); $('#master-gain').on('change', function() { var fraction = parseInt(this.value) / parseInt(127); self.masterGainNode.gain.value = fraction * fraction; }); $('.touch-gain').on('change', function() { var fraction = parseInt(this.value) / parseInt(127); var touchIdx = parseInt($(this).parent().data('touch')); self.touchGainNodes[touchIdx].gain.value = fraction * fraction; }); } self.startSample = function(sample_idx) { var sample_buffer = self.sample_buffers[sample_idx]; var nanotouch = $('.nanotouch')[sample_idx]; if (typeof(sample_buffer) != 'undefined') { if (typeof(samples[sample_idx]) != 'undefined' && samples[sample_idx].context.state == 'running') { self.samples[sample_idx].stop(0); self.samples[sample_idx] = undefined; } else { var sample = self.audioCtx.createBufferSource(); var gainNode = self.touchGainNodes[sample_idx]; self.samples[sample_idx] = sample; sample.loop = false; sample.connect(gainNode); sample.buffer = sample_buffer; sample.onended = function() { console.log('ended'); $(nanotouch).removeClass('playing'); self.samples[sample_idx] = undefined; } $(nanotouch).addClass('playing'); sample.start(0); } } } self.initAudio(); self.initMIDI(); self.initUI(); } $(function() { nanofun(); }); if ('serviceWorker' in navigator) { window.addEventListener('load', function() { navigator.serviceWorker.register('service-worker.js').then(function(registration) { // Registration was successful console.log('ServiceWorker registration successful with scope: ', registration.scope); }).catch(function(err) { // registration failed :( console.log('ServiceWorker registration failed: ', err); }); }); }