]> git.0d.be Git - nanofun.git/blob - nanofun.js
34e1dcc2184a1f675f4092afac24778c9776cccf
[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', 'y', 'u', 'i',
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
58     if (port.state == "disconnected") {
59       if (port.type == "input") {
60         this.inputs[port.name] = undefined;
61       } else {
62         this.outputs[port.name] = undefined;
63       }
64       if (port.name == 'nanoPAD2 MIDI 1') { $('#devices .nanopad').removeClass('on'); }
65       if (port.name == 'nanoKONTROL2 MIDI 1') { $('#devices .nanokontrol').removeClass('on'); }
66     } else {
67       if (port.type == "input") {
68         if (this.inputs[port.name] === undefined) { this.registerPort(port); }
69       } else {
70         if (this.outputs[port.name] === undefined) { this.registerPort(port); }
71       }
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);
76     }
77 },
78
79 renderPort: function(port) {
80     if (port.state == "connected") {
81       $("#midi" + port.type + "s").append(port.name);
82     }
83 },
84
85 registerPort: function(port) {
86     var self = this;
87     if (port.type == "input") {
88         this.inputs[port.name] = port;
89         port.onmidimessage = function(m) { self.onMIDIMessage(m); };
90     } else {
91         this.outputs[port.name] = port;
92
93         if (port.name == 'nanoKONTROL2 MIDI 1') {
94           /* turn "external leds" mode on.
95            *
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]);
102
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]);
126
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]);
129
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++) {
134             leds.push(64+i);
135             leds.push(48+i);
136             leds.push(32+i);
137             i += 1;
138             leds.push(32+i);
139             leds.push(48+i);
140             leds.push(64+i);
141           }
142                 console.log(leds);
143           on(leds[0]);
144           on(leds[1]);
145           on(leds[2]);
146           var led_idx = 2;
147           var interval_id = setInterval(function() {
148             if (led_idx < leds.length) {
149               on(leds[led_idx+1]);
150             }
151             off(leds[led_idx-2]);
152             if (led_idx-1 == leds.length) {
153               clearInterval(interval_id);
154             }
155             led_idx += 1;
156           }, 50);
157         }
158     }
159
160     port.onstatechange = function(e) { self.onPortStateChange(e); };
161 },
162
163 onMIDIFailure: function(e) {
164     alert("No access to MIDI devices or your browser doesn't support WebMIDI API. Please use WebMIDIAPIShim " + e);
165 },
166
167 onPortStateChange: function(event) {
168   console.log(event);
169 },
170
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);
179       }
180     }
181     if (data[0] == 176) { /* control change */
182       this.onControlChange(port, data, data[1], data[2]);
183     }
184 },
185
186 onTouchOn: function(port, data, sample_idx) {},
187 onControlChange: function(port, data, number, value) {}
188
189 };
190
191 // status bytes on channel 1
192 var messages = {
193     off: 128,
194     on: 144,
195     pp: 160,
196     cc: 176,
197     pc: 192,
198     cp: 208,
199     pb: 224
200 }
201
202 var device = function(outputName) {
203    this.current = midi.outputs[outputName];
204    this.channel = 1;
205
206    // makes device visible inside of nested function defs
207    var self = this;
208
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);
213     return self;
214    }
215
216    this.ch = function(channel) {
217       self.channel = channel;
218       return self;
219    }
220
221    this.cc = function(b1, b2) {
222     return self._send(messages.cc, [b1, b2]);
223    }
224
225    this.on = function(b1, b2) {
226     return self._send(messages.on, [b1, b2]);
227    }
228
229    this.off = function(b1, b2) {
230     return self._send(messages.off, [b1, b2]);
231    }
232
233    this.pp = function(b1, b2) {
234     return self._send(messages.pp, [b1, b2]);
235    }
236
237    this.cp = function(b1) {
238     return self._send(messages.cp, [b1]);
239    }
240
241    this.pb = function(b1) {
242     return self._send(
243         messages.pb, [
244             b1 & 127,
245             b1 >> 7
246         ]
247     );
248    }
249
250    this.pc = function(b1) {
251     return self._send(messages.pc, [b1]);
252    }
253
254    this.panic = function() {
255     return self.cc(123, 0)
256    }
257
258    this.rpn = function(b1, b2) {
259     return self.cc(101, b1 >> 7)
260         .cc(100, b1 & 127)
261         .cc(6, b2 >> 7)
262         .cc(38, b2 & 127)
263         .cc(101, 127)
264         .cc(100, 127);
265    }
266
267    this.nrpn = function(b1, b2) {
268     return self.cc(99, b1 >> 7)
269         .cc(98, b1 & 127)
270         .cc(6, b2 >> 7)
271         .cc(38, b2 & 127)
272         .cc(101, 127)
273         .cc(100, 127);
274    }
275
276    this.raw = function(data) {
277     console.log("sending raw data: " + data);
278     self.current.send(data);
279     return self;
280    }
281
282    this.toString = function() {
283     var s = "no connected devices";
284     if (typeof this.current != 'undefined') {
285         s = "";
286     }
287     return s;
288    }
289
290    return this;
291 };
292
293 var nanofun = function() {
294   var self = this;
295
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);
305     }
306     self.masterGainNode.connect(self.audioCtx.destination);
307
308     self.delay = self.audioCtx.createDelay(maxDelayTime=5);
309     self.delay.delayTime.value = 0.5;
310
311     self.feedback = self.audioCtx.createGain();
312     self.feedback.gain.value = 0.8;
313
314     self.filter = self.audioCtx.createBiquadFilter();
315     self.filter.frequency.value = 1000;
316
317     self.delay.connect(self.feedback);
318     self.feedback.connect(self.filter);
319     self.filter.connect(self.delay);
320
321     self.masterGainNode.connect(self.delay);
322   }
323
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); }
329       );
330     }
331   }
332
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);
340     }
341
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');
354           }, function(e) {
355             $nanotouch.find('span').text('');
356             $nanotouch.removeClass('loaded').addClass('error');
357           });
358         }
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);
363       }
364     });
365
366     midi.onTouchOn = function(port, data, sample_idx) {
367       self.startSample(sample_idx);
368     }
369
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 */
373       if (control < 8) {
374         control += 8; /* sliders, control bottom pads (8-15) */
375       } else {
376         control -= 16; /* pots, control top pads (0-7) */
377       }
378       $('[data-touch=' + control + '] .touch-gain').val(value).trigger('change');
379     }
380
381     $(document).keypress(function(ev) {
382       var sample_idx = KEYBOARD_CODES.indexOf(ev.key);
383       if (sample_idx != -1) {
384         self.startSample(sample_idx);
385       }
386     });
387
388     $('#master-gain').on('change', function() {
389       var fraction = parseInt(this.value) / parseInt(127);
390       self.masterGainNode.gain.value = fraction * fraction;
391     });
392
393     $('#delay').on('change', function() {
394       var value = this.value;
395       if (value == 0) {
396         self.delay.disconnect();
397       } else {
398         self.delay.delayTime.value = value;
399         self.delay.connect(self.audioCtx.destination);
400       }
401     });
402
403     $('#feedback').on('change', function() {
404       self.feedback.gain.value = this.value;
405     });
406
407     $('#filter').on('change', function() {
408       self.filter.frequency.value = this.value;
409     });
410
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;
415     });
416   }
417
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;
425       } else {
426         var sample = self.audioCtx.createBufferSource();
427         var gainNode = self.touchGainNodes[sample_idx];
428         self.samples[sample_idx] = sample;
429         sample.loop = false;
430         sample.connect(gainNode);
431         sample.buffer = sample_buffer;
432         sample.onended = function() {
433           $(nanotouch).removeClass('playing');
434           self.samples[sample_idx] = undefined;
435         }
436         $(nanotouch).addClass('playing');
437         sample.start(0);
438       }
439     }
440   }
441
442   self.initAudio();
443   self.initMIDI();
444   self.initUI();
445
446 }
447
448 $(function() { nanofun(); });
449
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);
458     });
459   });
460 }