]> git.0d.be Git - nanofun.git/blob - nanofun.js
bf32ff3f0d6f9f47034ff6d9b04ef19dc4d71bdc
[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' || port.name == 'nanoPAD2 2 PAD') { $('#devices .nanopad').removeClass('on'); }
65       if (port.name == 'nanoKONTROL2 MIDI 1' || port.name == 'nanoKONTROL2 2 SLIDER/KNOB') { $('#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' || port.name == 'nanoPAD2 2 PAD') { $('#devices .nanopad').addClass('on'); }
74       if (port.name == 'nanoKONTROL2 MIDI 1' || port.name == 'nanoKONTROL2 2 SLIDER/KNOB') { $('#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' || port.name == 'nanoKONTROL2 2 SLIDER/KNOB') {
94           var port_name = port.name;
95           /* turn "external leds" mode on.
96            *
97            * The sysex dump has been recorded from the official Korg kontrol
98            * editor by the Overtone project, original code at:
99            * https://github.com/overtone/overtone/blob/master/src/overtone/device/midi/nanoKONTROL2.clj */
100           device(port_name).raw([240, 126, 127, 6, 1, 247]);
101           device(port_name).raw([240, 66, 64, 0, 1, 19, 0, 31, 18, 0, 247]);
102           device(port_name).raw([240, 126, 127, 6, 1, 247]);
103
104           device(port_name).raw([ 240, 66, 64, 0, 1, 19, 0, 127, 127,
105                 2, 3, 5, 64, 0, 0, 0, 1, 16, 1, 0, 0, 0, 0, 127, 0, 1, 0, 16,
106                 0, 0, 127, 0, 1, 0, 32, 0, 127, 0, 0, 1, 0, 48, 0, 127, 0, 0,
107                 1, 0, 64, 0, 127, 0, 16, 0, 1, 0, 1, 0, 127, 0, 1, 0, 0, 17, 0,
108                 127, 0, 1, 0, 0, 33, 0, 127, 0, 1, 0, 49, 0, 0, 127, 0, 1, 0,
109                 65, 0, 0, 127, 0, 16, 1, 0, 2, 0, 0, 127, 0, 1, 0, 18, 0, 127,
110                 0, 0, 1, 0, 34, 0, 127, 0, 0, 1, 0, 50, 0, 127, 0, 1, 0, 0, 66,
111                 0, 127, 0, 16, 1, 0, 0, 3, 0, 127, 0, 1, 0, 0, 19, 0, 127, 0,
112                 1, 0, 35, 0, 0, 127, 0, 1, 0, 51, 0, 0, 127, 0, 1, 0, 67, 0,
113                 127, 0, 0, 16, 1, 0, 4, 0, 127, 0, 0, 1, 0, 20, 0, 127, 0, 0,
114                 1, 0, 36, 0, 127, 0, 1, 0, 0, 52, 0, 127, 0, 1, 0, 0, 68, 0,
115                 127, 0, 16, 1, 0, 0, 5, 0, 127, 0, 1, 0, 21, 0, 0, 127, 0, 1,
116                 0, 37, 0, 0, 127, 0, 1, 0, 53, 0, 127, 0, 0, 1, 0, 69, 0, 127,
117                 0, 0, 16, 1, 0, 6, 0, 127, 0, 0, 1, 0, 22, 0, 127, 0, 1, 0, 0,
118                 38, 0, 127, 0, 1, 0, 0, 54, 0, 127, 0, 1, 0, 70, 0, 0, 127, 0,
119                 16, 1, 0, 7, 0, 0, 127, 0, 1, 0, 23, 0, 0, 127, 0, 1, 0, 39, 0,
120                 127, 0, 0, 1, 0, 55, 0, 127, 0, 0, 1, 0, 71, 0, 127, 0, 16, 0,
121                 1, 0, 58, 0, 127, 0, 1, 0, 0, 59, 0, 127, 0, 1, 0, 0, 46, 0,
122                 127, 0, 1, 0, 60, 0, 0, 127, 0, 1, 0, 61, 0, 0, 127, 0, 1, 0,
123                 62, 0, 127, 0, 0, 1, 0, 43, 0, 127, 0, 0, 1, 0, 44, 0, 127, 0,
124                 1, 0, 0, 42, 0, 127, 0, 1, 0, 0, 41, 0, 127, 0, 1, 0, 45, 0, 0,
125                 127, 0, 127, 127, 127, 127, 0, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0,
126                 0, 0, 0, 0, 0, 0, 0, 0, 0, 247]);
127
128           device(port_name).raw([240, 126, 127, 6, 1, 247]);
129           device(port_name).raw([240, 66, 64, 0, 1, 19, 0, 31, 17, 0, 247]);
130
131           function on(note) { device(port_name).cc(note, 127); }
132           function off(note) { device(port_name).cc(note, 0); }
133           var leds = Array(43, 44, 42, 41, 45);
134           for (var i=0; i<8; i++) {
135             leds.push(64+i);
136             leds.push(48+i);
137             leds.push(32+i);
138             i += 1;
139             leds.push(32+i);
140             leds.push(48+i);
141             leds.push(64+i);
142           }
143                 console.log(leds);
144           on(leds[0]);
145           on(leds[1]);
146           on(leds[2]);
147           var led_idx = 2;
148           var interval_id = setInterval(function() {
149             if (led_idx < leds.length) {
150               on(leds[led_idx+1]);
151             }
152             off(leds[led_idx-2]);
153             if (led_idx-1 == leds.length) {
154               clearInterval(interval_id);
155             }
156             led_idx += 1;
157           }, 50);
158         }
159     }
160
161     port.onstatechange = function(e) { self.onPortStateChange(e); };
162 },
163
164 onMIDIFailure: function(e) {
165     alert("No access to MIDI devices or your browser doesn't support WebMIDI API. Please use WebMIDIAPIShim " + e);
166 },
167
168 onPortStateChange: function(event) {
169   console.log(event);
170 },
171
172 onMIDIMessage: function(message) {
173     var port = message.target;
174     var data = message.data;
175     console.log(message);
176     if (data[0] == 144) { /* touch on */
177       var sample_idx = NANOPAD_TOUCHS.indexOf(data[1]);
178       if (sample_idx != -1) {
179         this.onTouchOn(port, data, sample_idx);
180       }
181     }
182     if (data[0] == 176) { /* control change */
183       this.onControlChange(port, data, data[1], data[2]);
184     }
185 },
186
187 onTouchOn: function(port, data, sample_idx) {},
188 onControlChange: function(port, data, number, value) {}
189
190 };
191
192 // status bytes on channel 1
193 var messages = {
194     off: 128,
195     on: 144,
196     pp: 160,
197     cc: 176,
198     pc: 192,
199     cp: 208,
200     pb: 224
201 }
202
203 var device = function(outputName) {
204    this.current = midi.outputs[outputName];
205    this.channel = 1;
206
207    // makes device visible inside of nested function defs
208    var self = this;
209
210    this._send = function(status, data) {
211     var messageArr = [status + (self.channel - 1)].concat(data);
212     console.log("sending " + messageArr + " to " + self.current.name);
213     self.current.send(messageArr);
214     return self;
215    }
216
217    this.ch = function(channel) {
218       self.channel = channel;
219       return self;
220    }
221
222    this.cc = function(b1, b2) {
223     return self._send(messages.cc, [b1, b2]);
224    }
225
226    this.on = function(b1, b2) {
227     return self._send(messages.on, [b1, b2]);
228    }
229
230    this.off = function(b1, b2) {
231     return self._send(messages.off, [b1, b2]);
232    }
233
234    this.pp = function(b1, b2) {
235     return self._send(messages.pp, [b1, b2]);
236    }
237
238    this.cp = function(b1) {
239     return self._send(messages.cp, [b1]);
240    }
241
242    this.pb = function(b1) {
243     return self._send(
244         messages.pb, [
245             b1 & 127,
246             b1 >> 7
247         ]
248     );
249    }
250
251    this.pc = function(b1) {
252     return self._send(messages.pc, [b1]);
253    }
254
255    this.panic = function() {
256     return self.cc(123, 0)
257    }
258
259    this.rpn = function(b1, b2) {
260     return self.cc(101, b1 >> 7)
261         .cc(100, b1 & 127)
262         .cc(6, b2 >> 7)
263         .cc(38, b2 & 127)
264         .cc(101, 127)
265         .cc(100, 127);
266    }
267
268    this.nrpn = function(b1, b2) {
269     return self.cc(99, b1 >> 7)
270         .cc(98, b1 & 127)
271         .cc(6, b2 >> 7)
272         .cc(38, b2 & 127)
273         .cc(101, 127)
274         .cc(100, 127);
275    }
276
277    this.raw = function(data) {
278     console.log("sending raw data: " + data);
279     self.current.send(data);
280     return self;
281    }
282
283    this.toString = function() {
284     var s = "no connected devices";
285     if (typeof this.current != 'undefined') {
286         s = "";
287     }
288     return s;
289    }
290
291    return this;
292 };
293
294 var nanofun = function() {
295   var self = this;
296
297   self.initAudio = function() {
298     self.focused_pad = undefined;
299     self.sample_buffers = Array(NANOPAD_TOUCHS.length);
300     self.samples = Array(NANOPAD_TOUCHS.length);
301     self.sample_start_times = Array(NANOPAD_TOUCHS.length);
302     self.audioCtx = new window.AudioContext();
303     self.touchGainNodes = Array(NANOPAD_TOUCHS.length);
304     self.masterGainNode = self.audioCtx.createGain();
305     self.effectsGainNode = self.audioCtx.createGain();
306     for (var i=0; i<NANOPAD_TOUCHS.length; i++) {
307       self.touchGainNodes[i] = self.audioCtx.createGain();
308       self.touchGainNodes[i].connect(self.masterGainNode);
309     }
310     self.masterGainNode.connect(self.audioCtx.destination);
311
312     self.delay = self.audioCtx.createDelay(maxDelayTime=5);
313     self.delay.delayTime.value = 0.5;
314
315     self.feedback = self.audioCtx.createGain();
316     self.feedback.gain.value = 0.8;
317
318     self.filter = self.audioCtx.createBiquadFilter();
319     self.filter.frequency.value = 1000;
320
321     self.effectsGainNode.connect(self.delay);
322     self.delay.connect(self.feedback);
323     self.feedback.connect(self.filter);
324     self.filter.connect(self.delay);
325
326     self.filter.connect(self.masterGainNode);
327   }
328
329   self.initMIDI = function() {
330     if (navigator.requestMIDIAccess) {
331       navigator.requestMIDIAccess({sysex: true}).then(
332         function(midiAccess) { midi.onMIDISuccess(midiAccess); },
333         function(e) { midi.onMIDIFailure(e); }
334       );
335     }
336   }
337
338   self.initUI = function() {
339     var $nanopad = $('#nanopad');
340     var $nanotouch = $('.nanotouch');
341     for (var i=0; i<NANOPAD_TOUCHS.length; i++) {
342       var $new_touch = $nanotouch.clone();
343       $new_touch.attr('data-touch', i);
344       $new_touch.appendTo($nanopad);
345     }
346     $nanotouch.remove(); /* remove template */
347
348     $('.nanotouch input[type=file]').on('change', function(ev) {
349       var sample_idx = parseInt($(this).parent().data('touch'));
350       for (var i=0; i<this.files.length; i++) {
351         var reader = new FileReader();
352         var nanotouch = $('.nanotouch')[sample_idx + i];
353         reader.onload = function(e) {
354           var $nanotouch = $(this.nanotouch);
355           var sample_idx = this.sample_idx;
356           self.audioCtx.decodeAudioData(this.result, function(buffer) {
357             sample_buffers[sample_idx] = buffer;
358             $nanotouch.find('span.duration').data('duration', buffer.duration);
359             $nanotouch.find('span.duration').text(parseInt(buffer.duration) + 's');
360             $nanotouch.removeClass('error').addClass('loaded');
361           }, function(e) {
362             $nanotouch.find('span').text('');
363             $nanotouch.removeClass('loaded').addClass('error');
364           });
365         }
366         reader.nanotouch = nanotouch;
367         reader.sample_idx = sample_idx + i;
368         reader.readAsArrayBuffer(this.files[i]);
369         $(nanotouch).find('span.name').text(this.files[i].name);
370       }
371     });
372
373     midi.onTouchOn = function(port, data, sample_idx) {
374       self.toggleSample(sample_idx);
375     }
376
377     midi.onControlChange = function(port, data, control, value) {
378       if (control > 7 && control < 16) return; /* range between sliders and pots */
379       if (control >= 32 && control < 40) { /* "S" buttons -> loop */
380           var nanotouch = $('.nanotouch')[control-32];
381           if (value == 127) {
382             var checked = $(nanotouch).find('.loop input').prop('checked');
383             if (checked) {
384               $(nanotouch).find('.loop input').prop('checked', false);
385               device("nanoKONTROL2 MIDI 1").cc(control, 0);
386             } else {
387               $(nanotouch).find('.loop input').prop('checked', true);
388               device("nanoKONTROL2 MIDI 1").cc(control, 127);
389             }
390             $(nanotouch).find('.loop input').trigger('change');
391           }
392       }
393       if (control >= 48 && control < 55) { /* "M" buttons -> effects */
394           var nanotouch = $('.nanotouch')[control-48];
395           if (value == 127) {
396             var checked = $(nanotouch).find('.effects input').prop('checked');
397             if (checked) {
398               $(nanotouch).find('.effects input').prop('checked', false);
399               device("nanoKONTROL2 MIDI 1").cc(control, 0);
400             } else {
401               $(nanotouch).find('.effects input').prop('checked', true);
402               device("nanoKONTROL2 MIDI 1").cc(control, 127);
403             }
404             $(nanotouch).find('.effects input').trigger('change');
405           }
406
407       }
408       if ((control == 58 || control == 59) && value == 127) {
409         /* track < and > buttons: move focus between pads for special functions */
410         if (self.focused_pad === undefined) {
411           self.focused_pad = 0;
412         } else if (control == 58) {
413           self.focused_pad = self.focused_pad - 1;
414           if (self.focused_pad < 0) self.focused_pad = 15;
415         } else if (control == 59) {
416           self.focused_pad = self.focused_pad + 1;
417           if (self.focused_pad > 15) self.focused_pad = 0;
418         }
419         $('[data-touch]').removeClass('focus');
420         $('[data-touch=' + self.focused_pad + ']').addClass('focus');
421       }
422       if (control == 41 && value == 127 && self.focused_pad !== undefined) { /* play */
423         var nanotouch = $('.nanotouch')[self.focused_pad];
424         if ($(nanotouch).is('.playing')) {
425           self.samples[self.focused_pad].onended = function() {};  // disable callback
426           self.stopSample(self.focused_pad);
427         }
428         self.startSample(self.focused_pad);
429       }
430       if (control == 42 && value == 127 && self.focused_pad !== undefined) { /* stop */
431         self.stopSample(self.focused_pad);
432       }
433       if (control > 23) return; /* after pots */
434       if (control < 8) {
435         control += 8; /* sliders, control bottom pads (8-15) */
436       } else {
437         control -= 16; /* pots, control top pads (0-7) */
438       }
439       $('[data-touch=' + control + '] .touch-gain').val(value).trigger('change');
440     }
441
442     $('.loop input').on('change', function() {
443       var sample_idx = parseInt($(this).parents('[data-touch]').data('touch'));
444       if (self.samples[sample_idx]) {
445         self.samples[sample_idx].loop = $(this).is(':checked');
446       }
447     });
448
449     $(document).keypress(function(ev) {
450       var sample_idx = KEYBOARD_CODES.indexOf(ev.key);
451       if (sample_idx != -1) {
452         self.startSample(sample_idx);
453       }
454     });
455
456     $('.effects input').on('change', function() {
457       var effects = $(this).prop('checked');
458       var i = parseInt($(this).parents('.nanotouch').data('touch'));
459       if (effects) {
460         self.touchGainNodes[i].connect(self.effectsGainNode);
461       } else {
462         self.touchGainNodes[i].disconnect(self.effectsGainNode);
463       }
464     });
465
466     $('#master-gain').on('change', function() {
467       var fraction = parseInt(this.value) / parseInt(127);
468       var now = self.audioCtx.currentTime;
469       self.masterGainNode.gain.exponentialRampToValueAtTime((fraction * fraction) || 0.01, now + 0.015);
470     });
471
472     $('#delay').on('change', function() {
473       var value = this.value;
474       if (value == 0) {
475         self.delay.disconnect();
476       } else {
477         var now = self.audioCtx.currentTime;
478         self.delay.delayTime.exponentialRampToValueAtTime(value || 0.01, now + 0.015);
479         self.delay.connect(self.audioCtx.destination);
480       }
481     });
482
483     $('#feedback').on('change', function() {
484       var now = self.audioCtx.currentTime;
485       self.feedback.gain.exponentialRampToValueAtTime(this.value || 0.01, now + 0.015);
486     });
487
488     $('#filter').on('change', function() {
489       self.filter.frequency.value = this.value;
490     });
491
492     $('.touch-gain').on('change', function() {
493       var fraction = parseInt(this.value) / parseInt(127);
494       var touchIdx = parseInt($(this).parent().data('touch'));
495       var now = self.audioCtx.currentTime;
496       self.touchGainNodes[touchIdx].gain.exponentialRampToValueAtTime((fraction * fraction) || 0.01, now + 0.015);
497     });
498
499     self.time_interval_id = setInterval(function() {
500       for (var i=0; i<NANOPAD_TOUCHS.length; i++) {
501         var sample = self.samples[i];
502         if (sample !== undefined) {
503           var start_time = self.sample_start_times[i];
504           var current_position = sample.context.currentTime - start_time;
505           var nanotouch = $('.nanotouch')[i];
506           var duration = $(nanotouch).find('span.duration');
507           var total_duration = parseFloat($(duration).data('duration'))
508           $(duration).text(parseInt(total_duration - current_position) + 's');
509         }
510      }
511     }, 250);
512   }
513
514   self.toggleSample = function(sample_idx) {
515     var nanotouch = $('.nanotouch')[sample_idx];
516     if ($(nanotouch).is('.playing')) {
517       self.stopSample(sample_idx);
518     } else {
519       self.startSample(sample_idx);
520     }
521   }
522
523   self.startSample = function(sample_idx) {
524     var sample_buffer = self.sample_buffers[sample_idx];
525     var nanotouch = $('.nanotouch')[sample_idx];
526     if (typeof(sample_buffer) != 'undefined') {
527       var sample = self.audioCtx.createBufferSource();
528       var gainNode = self.touchGainNodes[sample_idx];
529       self.samples[sample_idx] = sample;
530       sample.loop = ($(nanotouch).find('.loop input:checked').length == 1);
531       sample.connect(gainNode);
532       sample.buffer = sample_buffer;
533       sample.onended = function() {
534         $(nanotouch).removeClass('playing');
535         var duration = $(nanotouch).find('span.duration');
536         $(duration).text(parseInt($(duration).data('duration')) + 's');
537         self.samples[sample_idx] = undefined;
538       }
539       $(nanotouch).addClass('playing');
540       self.sample_start_times[sample_idx] = sample.context.currentTime;
541       sample.start(0);
542     }
543   }
544   self.stopSample = function(sample_idx) {
545     var sample_buffer = self.sample_buffers[sample_idx];
546     if (typeof(sample_buffer) != 'undefined') {
547       if (typeof(samples[sample_idx]) != 'undefined') {
548         self.samples[sample_idx].stop(0);
549         self.samples[sample_idx] = undefined;
550       }
551     }
552   }
553
554   self.initAudio();
555   self.initMIDI();
556   self.initUI();
557
558 }
559
560 $(function() { nanofun(); });
561
562 if ('serviceWorker' in navigator) {
563   window.addEventListener('load', function() {
564     navigator.serviceWorker.register('service-worker.js').then(function(registration) {
565       // Registration was successful
566       console.log('ServiceWorker registration successful with scope: ', registration.scope);
567     }).catch(function(err) {
568       // registration failed :(
569       console.log('ServiceWorker registration failed: ', err);
570     });
571   });
572 }