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', 'u', 'i', 'o',
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);
308 self.initMIDI = function() {
309 if (navigator.requestMIDIAccess) {
310 navigator.requestMIDIAccess({sysex: true}).then(
311 function(midiAccess) { midi.onMIDISuccess(midiAccess); },
312 function(e) { midi.onMIDIFailure(e); }
317 self.initUI = function() {
318 var $nanopad = $('#nanopad');
320 $('.nanotouch input').on('change', function(ev) {
321 var nanotouch = $(this).parent();
322 var sample_idx = $nanopad.children().index(nanotouch);
323 var reader = new FileReader();
324 reader.onload = function(e) {
325 self.audioCtx.decodeAudioData(this.result, function(buffer) {
326 sample_buffers[sample_idx] = buffer;
327 $(nanotouch).find('span.duration').text(parseInt(buffer.duration) + 's');
328 $(nanotouch).removeClass('error').addClass('loaded');
330 $(nanotouch).find('span').text('');
331 $(nanotouch).removeClass('loaded').addClass('error');
334 reader.readAsArrayBuffer(this.files[0]);
335 $(nanotouch).find('span.name').text(this.files[0].name);
338 midi.onTouchOn = function(port, data, sample_idx) {
339 self.startSample(sample_idx);
342 midi.onControlChange = function(port, data, control, value) {
343 if (control > 7 && control < 16) return; /* range between sliders and pots */
344 if (control > 23) return; /* after pots */
345 if (control >= 16) { control -= 8; }
346 $('.touch-gain[data-touch=' + control + ']').val(value).trigger('change');
349 $(document).keypress(function(ev) {
350 var sample_idx = KEYBOARD_CODES.indexOf(ev.key);
351 if (sample_idx != -1) {
352 self.startSample(sample_idx);
356 $('#master-gain').on('change', function() {
357 var fraction = parseInt(this.value) / parseInt(127);
358 self.masterGainNode.gain.value = fraction * fraction;
361 $('.touch-gain').on('change', function() {
362 var fraction = parseInt(this.value) / parseInt(127);
363 var touchIdx = parseInt($(this).data('touch'));
364 self.touchGainNodes[touchIdx].gain.value = fraction * fraction;
368 self.startSample = function(sample_idx) {
369 var sample_buffer = self.sample_buffers[sample_idx];
370 var nanotouch = $('.nanotouch')[sample_idx];
371 if (typeof(sample_buffer) != 'undefined') {
372 if (typeof(samples[sample_idx]) != 'undefined' && samples[sample_idx].context.state == 'running') {
373 self.samples[sample_idx].stop(0);
374 self.samples[sample_idx] = undefined;
376 var sample = self.audioCtx.createBufferSource();
377 var gainNode = self.touchGainNodes[sample_idx];
378 self.samples[sample_idx] = sample;
380 sample.connect(gainNode);
381 sample.buffer = sample_buffer;
382 sample.onended = function() {
383 console.log('ended');
384 $(nanotouch).removeClass('playing');
385 self.samples[sample_idx] = undefined;
387 $(nanotouch).addClass('playing');
399 $(function() { nanofun(); });