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.gainNode = self.audioCtx.createGain();
300 self.gainNode.connect(self.audioCtx.destination);
303 self.initMIDI = function() {
304 if (navigator.requestMIDIAccess) {
305 navigator.requestMIDIAccess({sysex: true}).then(
306 function(midiAccess) { midi.onMIDISuccess(midiAccess); },
307 function(e) { midi.onMIDIFailure(e); }
312 self.initUI = function() {
313 var $nanopad = $('#nanopad');
315 $('.nanotouch input').on('change', function(ev) {
316 var nanotouch = $(this).parent();
317 var sample_idx = $nanopad.children().index(nanotouch);
318 var reader = new FileReader();
319 reader.onload = function(e) {
320 self.audioCtx.decodeAudioData(this.result, function(buffer) {
321 sample_buffers[sample_idx] = buffer;
322 $(nanotouch).find('span.duration').text(parseInt(buffer.duration) + 's');
323 $(nanotouch).removeClass('error').addClass('loaded');
325 $(nanotouch).find('span').text('');
326 $(nanotouch).removeClass('loaded').addClass('error');
329 reader.readAsArrayBuffer(this.files[0]);
330 $(nanotouch).find('span.name').text(this.files[0].name);
333 midi.onTouchOn = function(port, data, sample_idx) {
334 self.startSample(sample_idx);
337 midi.onControlChange = function(port, data, control, value) {
339 $('#gain').val(value).trigger('change');
343 $(document).keypress(function(ev) {
344 var sample_idx = KEYBOARD_CODES.indexOf(ev.key);
345 if (sample_idx != -1) {
346 self.startSample(sample_idx);
350 $('#gain').on('change', function() {
351 var fraction = parseInt(this.value) / parseInt(127);
352 self.gainNode.gain.value = fraction * fraction;
356 self.startSample = function(sample_idx) {
357 var sample_buffer = self.sample_buffers[sample_idx];
358 var nanotouch = $('.nanotouch')[sample_idx];
359 if (typeof(sample_buffer) != 'undefined') {
360 if (typeof(samples[sample_idx]) != 'undefined' && samples[sample_idx].context.state == 'running') {
361 self.samples[sample_idx].stop(0);
362 self.samples[sample_idx] = undefined;
364 var sample = self.audioCtx.createBufferSource();
365 self.samples[sample_idx] = sample;
367 sample.connect(gainNode);
368 sample.buffer = sample_buffer;
369 sample.onended = function() {
370 console.log('ended');
371 $(nanotouch).removeClass('playing');
372 self.samples[sample_idx] = undefined;
374 $(nanotouch).addClass('playing');
386 $(function() { nanofun(); });