]> git.0d.be Git - nanofun.git/blob - nanofun.js
turn nanoKontrol external leds mode on (and animate leds a bit)
[nanofun.git] / nanofun.js
1 var NANOPAD_TOUCHS = Array(37, 39, 41, 43, 45, 47, 49, 51,
2                            36, 38, 40, 42, 44, 46, 48, 50);
3
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');
7
8 var midi = {
9
10 onMIDISuccess: function(midiAccess) {
11     console.log('MIDI Access Object', midiAccess);
12     //console.log(this);
13
14     var self = this;
15     midiAccess.onstatechange = function(e) {
16         self.onMIDIAccessChange(e);
17     }
18     this.midiAccess = midiAccess;
19     this.inputs = {};
20     this.outputs = {};
21
22     this.initPorts();
23 },
24
25 initPorts: function() {
26     var self = this;
27
28     var inputs = this.midiAccess.inputs;
29     if (inputs.size > 0) {
30         inputs.forEach(
31             function(port, key) {
32                 //console.log(port);
33                 self.registerPort(port);
34             }
35         );
36     } else {
37         $("#midiinputs").append("<p>No connected inputs</p>");
38     }
39
40     var outputs = this.midiAccess.outputs;
41     if (outputs.size > 0) {
42         outputs.forEach(
43             function(port, key) {
44                 self.registerPort(port);
45                 self.renderPort(port);
46             }
47         );
48     } else {
49         $("#midioutputs").append("<p>No connected outputs</p>"); 
50     }
51 },
52
53 onMIDIAccessChange: function(e) {
54     console.log('on midi access change', e);
55     //console.log(this);
56     var port = e.port;
57     var portContainer = $("#midi" + port.type + "s");
58     if (portContainer.html().startsWith("<p>No connected")) {
59         portContainer.empty();
60     }
61
62     if (port.state == "disconnected") {
63       if (port.type == "input") {
64         this.inputs[port.name] = undefined;
65       } else {
66         this.outputs[port.name] = undefined;
67       }
68     } else {
69       if (port.type == "input") {
70         if (this.inputs[port.name] === undefined) { this.registerPort(port); }
71       } else {
72         if (this.outputs[port.name] === undefined) { this.registerPort(port); }
73       }
74       this.renderPort(port);
75     }
76 },
77
78 renderPort: function(port) {
79     if (port.state == "connected") {
80       $("#midi" + port.type + "s").append(port.name);
81     }
82 },
83
84 registerPort: function(port) {
85     var self = this;
86     if (port.type == "input") {
87         this.inputs[port.name] = port;
88         port.onmidimessage = function(m) { self.onMIDIMessage(m); };
89     } else {
90         this.outputs[port.name] = port;
91
92         if (port.name == 'nanoKONTROL2 MIDI 1') {
93           /* turn "external leds" mode on.
94            *
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]);
101
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]);
125
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]);
128
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++) {
133             leds.push(64+i);
134             leds.push(48+i);
135             leds.push(32+i);
136             i += 1;
137             leds.push(32+i);
138             leds.push(48+i);
139             leds.push(64+i);
140           }
141                 console.log(leds);
142           on(leds[0]);
143           on(leds[1]);
144           on(leds[2]);
145           var led_idx = 2;
146           var interval_id = setInterval(function() {
147             if (led_idx < leds.length) {
148               on(leds[led_idx+1]);
149             }
150             off(leds[led_idx-2]);
151             if (led_idx-1 == leds.length) {
152               clearInterval(interval_id);
153             }
154             led_idx += 1;
155           }, 50);
156         }
157     }
158
159     port.onstatechange = function(e) { self.onPortStateChange(e); };
160 },
161
162 onMIDIFailure: function(e) {
163     alert("No access to MIDI devices or your browser doesn't support WebMIDI API. Please use WebMIDIAPIShim " + e);
164 },
165
166 onPortStateChange: function(event) {
167   console.log(event);
168 },
169
170 onMIDIMessage: function(message) {
171     var port = message.target;
172     var data = message.data;
173     if (data[0] == 144) { /* touch on */
174       var sample_idx = NANOPAD_TOUCHS.indexOf(data[1]);
175       if (sample_idx != -1) {
176         this.onTouchOn(port, data, sample_idx);
177       }
178     }
179 },
180
181 onTouchOn: function(port, data, sample_idx) {}
182
183 };
184
185 // status bytes on channel 1
186 var messages = {
187     off: 128,
188     on: 144,
189     pp: 160,
190     cc: 176,
191     pc: 192,
192     cp: 208,
193     pb: 224
194 }
195
196 var device = function(outputName) {
197    this.current = midi.outputs[outputName];
198    this.channel = 1;
199
200    // makes device visible inside of nested function defs
201    var self = this;
202
203    this._send = function(status, data) {
204     var messageArr = [status + (self.channel - 1)].concat(data);
205     console.log("sending " + messageArr + " to " + self.current.name);
206     self.current.send(messageArr);
207     return self;
208    }
209
210    this.ch = function(channel) {
211       self.channel = channel;
212       return self;
213    }
214
215    this.cc = function(b1, b2) {
216     return self._send(messages.cc, [b1, b2]);
217    }
218
219    this.on = function(b1, b2) {
220     return self._send(messages.on, [b1, b2]);
221    }
222
223    this.off = function(b1, b2) {
224     return self._send(messages.off, [b1, b2]);
225    }
226
227    this.pp = function(b1, b2) {
228     return self._send(messages.pp, [b1, b2]);
229    }
230
231    this.cp = function(b1) {
232     return self._send(messages.cp, [b1]);
233    }
234
235    this.pb = function(b1) {
236     return self._send(
237         messages.pb, [
238             b1 & 127,
239             b1 >> 7
240         ]
241     );
242    }
243
244    this.pc = function(b1) {
245     return self._send(messages.pc, [b1]);
246    }
247
248    this.panic = function() {
249     return self.cc(123, 0)
250    }
251
252    this.rpn = function(b1, b2) {
253     return self.cc(101, b1 >> 7)
254         .cc(100, b1 & 127)
255         .cc(6, b2 >> 7)
256         .cc(38, b2 & 127)
257         .cc(101, 127)
258         .cc(100, 127);
259    }
260
261    this.nrpn = function(b1, b2) {
262     return self.cc(99, b1 >> 7)
263         .cc(98, b1 & 127)
264         .cc(6, b2 >> 7)
265         .cc(38, b2 & 127)
266         .cc(101, 127)
267         .cc(100, 127);
268    }
269
270    this.raw = function(data) {
271     console.log("sending raw data: " + data);
272     self.current.send(data);
273     return self;
274    }
275
276    this.toString = function() {
277     var s = "no connected devices";
278     if (typeof this.current != 'undefined') {
279         s = "";
280     }
281     return s;
282    }
283
284    return this;
285 };
286
287 var nanofun = function() {
288   var self = this;
289
290   self.initAudio = function() {
291     self.sample_buffers = Array(16);
292     self.samples = Array(16);
293     self.audioCtx = new window.AudioContext();
294     self.gainNode = self.audioCtx.createGain();
295     self.gainNode.connect(self.audioCtx.destination);
296   }
297
298   self.initMIDI = function() {
299     if (navigator.requestMIDIAccess) {
300       navigator.requestMIDIAccess({sysex: true}).then(
301         function(midiAccess) { midi.onMIDISuccess(midiAccess); },
302         function(e) { midi.onMIDIFailure(e); }
303       );
304     }
305   }
306
307   self.initUI = function() {
308     var $nanopad = $('#nanopad');
309
310     $('.nanotouch input').on('change', function(ev) {
311       var nanotouch = $(this).parent();
312       var sample_idx = $nanopad.children().index(nanotouch);
313       var reader = new FileReader();
314       reader.onload = function(e) {
315         self.audioCtx.decodeAudioData(this.result, function(buffer) {
316           sample_buffers[sample_idx] = buffer;
317           $(nanotouch).find('span.duration').text(parseInt(buffer.duration) + 's');
318           $(nanotouch).removeClass('error').addClass('loaded');
319         }, function(e) {
320           $(nanotouch).find('span').text('');
321           $(nanotouch).removeClass('loaded').addClass('error');
322         });
323       }
324       reader.readAsArrayBuffer(this.files[0]);
325       $(nanotouch).find('span.name').text(this.files[0].name);
326     });
327
328     midi.onTouchOn = function(port, data, sample_idx) {
329       self.startSample(sample_idx);
330     }
331
332     $(document).keypress(function(ev) {
333       var sample_idx = KEYBOARD_CODES.indexOf(ev.key);
334       if (sample_idx != -1) {
335         self.startSample(sample_idx);
336       }
337     });
338   }
339
340   self.startSample = function(sample_idx) {
341     var sample_buffer = self.sample_buffers[sample_idx];
342     var nanotouch = $('.nanotouch')[sample_idx];
343     if (typeof(sample_buffer) != 'undefined') {
344       if (typeof(samples[sample_idx]) != 'undefined' && samples[sample_idx].context.state == 'running') {
345         self.samples[sample_idx].stop(0);
346         self.samples[sample_idx] = undefined;
347       } else {
348         var sample = self.audioCtx.createBufferSource();
349         self.samples[sample_idx] = sample;
350         sample.loop = false;
351         sample.connect(gainNode);
352         sample.buffer = sample_buffer;
353         sample.onended = function() {
354           console.log('ended');
355           $(nanotouch).removeClass('playing');
356           self.samples[sample_idx] = undefined;
357         }
358         $(nanotouch).addClass('playing');
359         sample.start(0);
360       }
361     }
362   }
363
364   self.initAudio();
365   self.initMIDI();
366   self.initUI();
367
368 }
369
370 $(function() { nanofun(); });