]> git.0d.be Git - nanofun.git/blob - nanofun.js
display keyboard shortcut
[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.cycle_being_pressed = false;
300     self.sample_buffers = Array(NANOPAD_TOUCHS.length);
301     self.samples = Array(NANOPAD_TOUCHS.length);
302     self.sample_start_times = Array(NANOPAD_TOUCHS.length);
303     self.audioCtx = new window.AudioContext();
304     self.touchGainNodes = Array(NANOPAD_TOUCHS.length);
305     self.masterGainNode = self.audioCtx.createGain();
306     self.effectsGainNode = self.audioCtx.createGain();
307     for (var i=0; i<NANOPAD_TOUCHS.length; i++) {
308       self.touchGainNodes[i] = self.audioCtx.createGain();
309       self.touchGainNodes[i].connect(self.masterGainNode);
310     }
311     self.masterGainNode.connect(self.audioCtx.destination);
312
313     self.delay = self.audioCtx.createDelay(maxDelayTime=5);
314     self.delay.delayTime.value = 0.5;
315
316     self.feedback = self.audioCtx.createGain();
317     self.feedback.gain.value = 0.8;
318
319     self.filter = self.audioCtx.createBiquadFilter();
320     self.filter.frequency.value = 1000;
321
322     self.effectsGainNode.connect(self.delay);
323     self.delay.connect(self.feedback);
324     self.feedback.connect(self.filter);
325     self.filter.connect(self.delay);
326
327     self.filter.connect(self.masterGainNode);
328   }
329
330   self.initMIDI = function() {
331     if (navigator.requestMIDIAccess) {
332       navigator.requestMIDIAccess({sysex: true}).then(
333         function(midiAccess) { midi.onMIDISuccess(midiAccess); },
334         function(e) { midi.onMIDIFailure(e); }
335       );
336     }
337   }
338
339   self.initUI = function() {
340     var $nanopad = $('#nanopad');
341     var $nanotouch = $('.nanotouch');
342     for (var i=0; i<NANOPAD_TOUCHS.length; i++) {
343       var $new_touch = $nanotouch.clone();
344       $new_touch.attr('data-touch', i);
345       $new_touch.appendTo($nanopad);
346     }
347     $nanotouch.remove(); /* remove template */
348
349     $('.nanotouch input[type=file]').on('change', function(ev) {
350       var sample_idx = parseInt($(this).parent().data('touch'));
351       for (var i=0; i<this.files.length; i++) {
352         var reader = new FileReader();
353         var nanotouch = $('.nanotouch')[sample_idx + i];
354         reader.onload = function(e) {
355           var $nanotouch = $(this.nanotouch);
356           var sample_idx = this.sample_idx;
357           self.audioCtx.decodeAudioData(this.result, function(buffer) {
358             sample_buffers[sample_idx] = buffer;
359             $nanotouch.find('span.duration').data('duration', buffer.duration);
360             $nanotouch.find('span.duration').text(parseInt(buffer.duration) + 's');
361             $nanotouch.removeClass('error').addClass('loaded');
362           }, function(e) {
363             $nanotouch.find('span').text('');
364             $nanotouch.removeClass('loaded').addClass('error');
365           });
366         }
367         reader.nanotouch = nanotouch;
368         reader.sample_idx = sample_idx + i;
369         reader.readAsArrayBuffer(this.files[i]);
370         $(nanotouch).find('span.name').text(this.files[i].name);
371       }
372     });
373
374     midi.onTouchOn = function(port, data, sample_idx) {
375       self.toggleSample(sample_idx);
376     }
377
378     midi.onControlChange = function(port, data, control, value) {
379       if (control > 7 && control < 16) return; /* range between sliders and pots */
380       if (control == 46) {
381         /* cycle -> alternate mode, make faders control effects when pressed
382          * (moving faders when cycle is pressed) */
383         self.cycle_being_pressed = (value != 0);
384         return;
385       }
386       if (control >= 32 && control < 40) { /* "S" buttons -> loop */
387           var nanotouch = $('.nanotouch')[control-32];
388           if (value == 127) {
389             var checked = $(nanotouch).find('.loop input').prop('checked');
390             if (checked) {
391               $(nanotouch).find('.loop input').prop('checked', false);
392               device("nanoKONTROL2 MIDI 1").cc(control, 0);
393             } else {
394               $(nanotouch).find('.loop input').prop('checked', true);
395               device("nanoKONTROL2 MIDI 1").cc(control, 127);
396             }
397             $(nanotouch).find('.loop input').trigger('change');
398           }
399       }
400       if (control >= 48 && control < 55) { /* "M" buttons -> effects */
401           var nanotouch = $('.nanotouch')[control-48];
402           if (value == 127) {
403             var checked = $(nanotouch).find('.effects input').prop('checked');
404             if (checked) {
405               $(nanotouch).find('.effects input').prop('checked', false);
406               device("nanoKONTROL2 MIDI 1").cc(control, 0);
407             } else {
408               $(nanotouch).find('.effects input').prop('checked', true);
409               device("nanoKONTROL2 MIDI 1").cc(control, 127);
410             }
411             $(nanotouch).find('.effects input').trigger('change');
412           }
413
414       }
415       if ((control == 58 || control == 59) && value == 127) {
416         /* track < and > buttons: move focus between pads for special functions */
417         if (self.focused_pad === undefined) {
418           self.focused_pad = 0;
419         } else if (control == 58) {
420           self.focused_pad = self.focused_pad - 1;
421           if (self.focused_pad < 0) self.focused_pad = 15;
422         } else if (control == 59) {
423           self.focused_pad = self.focused_pad + 1;
424           if (self.focused_pad > 15) self.focused_pad = 0;
425         }
426         $('[data-touch]').removeClass('focus');
427         $('[data-touch=' + self.focused_pad + ']').addClass('focus');
428       }
429       if (control == 41 && value == 127 && self.focused_pad !== undefined) { /* play */
430         var nanotouch = $('.nanotouch')[self.focused_pad];
431         if ($(nanotouch).is('.playing')) {
432           self.samples[self.focused_pad].onended = function() {};  // disable callback
433           self.stopSample(self.focused_pad);
434         }
435         self.startSample(self.focused_pad);
436       }
437       if (control == 42 && value == 127 && self.focused_pad !== undefined) { /* stop */
438         self.stopSample(self.focused_pad);
439       }
440       if (control > 23) return; /* after pots */
441       if (self.cycle_being_pressed) {
442         /* 4 -> delay, 5 -> feedback, 6 -> filter, 7 -> master */
443         if (control == 4) {
444           $('#delay').val(value / 127 * 5).trigger('change');
445         } else if (control == 5) {
446           $('#feedback').val(value / 127 * 1).trigger('change');
447         } else if (control == 6) {
448           $('#filter').val(value / 127 * 5000).trigger('change');
449         } else if (control == 7) {
450           $('#master-gain').val(value).trigger('change');
451         }
452         return;
453       }
454       if (control < 8) {
455         control += 8; /* sliders, control bottom pads (8-15) */
456       } else {
457         control -= 16; /* pots, control top pads (0-7) */
458       }
459       $('[data-touch=' + control + '] .touch-gain').val(value).trigger('change');
460     }
461
462     $('.loop input').on('change', function() {
463       var sample_idx = parseInt($(this).parents('[data-touch]').data('touch'));
464       if (self.samples[sample_idx]) {
465         self.samples[sample_idx].loop = $(this).is(':checked');
466       }
467     });
468
469     $(document).keypress(function(ev) {
470       var sample_idx = KEYBOARD_CODES.indexOf(ev.key);
471       if (sample_idx != -1) {
472         self.toggleSample(sample_idx);
473       }
474     });
475
476     $('.effects input').on('change', function() {
477       var effects = $(this).prop('checked');
478       var i = parseInt($(this).parents('.nanotouch').data('touch'));
479       if (effects) {
480         self.touchGainNodes[i].connect(self.effectsGainNode);
481       } else {
482         self.touchGainNodes[i].disconnect(self.effectsGainNode);
483       }
484     });
485
486     $('#master-gain').on('change', function() {
487       var fraction = parseInt(this.value) / parseInt(127);
488       var now = self.audioCtx.currentTime;
489       self.masterGainNode.gain.exponentialRampToValueAtTime((fraction * fraction) || 0.0001, now + 0.015);
490     });
491
492     $('#delay').on('change', function() {
493       var value = this.value;
494       if (value == 0) {
495         self.delay.disconnect();
496       } else {
497         var now = self.audioCtx.currentTime;
498         self.delay.delayTime.exponentialRampToValueAtTime(value || 0.0001, now + 0.015);
499         self.delay.connect(self.audioCtx.destination);
500       }
501     });
502
503     $('#feedback').on('change', function() {
504       var now = self.audioCtx.currentTime;
505       self.feedback.gain.exponentialRampToValueAtTime(this.value || 0.0001, now + 0.015);
506     });
507
508     $('#filter').on('change', function() {
509       self.filter.frequency.value = this.value;
510     });
511
512     $('.touch-gain').on('change', function() {
513       var fraction = parseInt(this.value) / parseInt(127);
514       var touchIdx = parseInt($(this).parent().data('touch'));
515       var now = self.audioCtx.currentTime;
516       if (fraction == 0) {
517         self.touchGainNodes[touchIdx].gain.value = 0;
518       } else {
519         self.touchGainNodes[touchIdx].gain.exponentialRampToValueAtTime((fraction * fraction) || 0.0001, now + 0.015);
520       }
521     });
522
523     self.time_interval_id = setInterval(function() {
524       for (var i=0; i<NANOPAD_TOUCHS.length; i++) {
525         var sample = self.samples[i];
526         if (sample !== undefined) {
527           var start_time = self.sample_start_times[i];
528           var current_position = sample.context.currentTime - start_time;
529           var nanotouch = $('.nanotouch')[i];
530           var duration = $(nanotouch).find('span.duration');
531           var total_duration = parseFloat($(duration).data('duration'))
532           $(duration).text(parseInt(total_duration - current_position) + 's');
533         }
534      }
535     }, 250);
536   }
537
538   self.toggleSample = function(sample_idx) {
539     var nanotouch = $('.nanotouch')[sample_idx];
540     if ($(nanotouch).is('.playing')) {
541       self.stopSample(sample_idx);
542     } else {
543       self.startSample(sample_idx);
544     }
545   }
546
547   self.startSample = function(sample_idx) {
548     var sample_buffer = self.sample_buffers[sample_idx];
549     var nanotouch = $('.nanotouch')[sample_idx];
550     if (typeof(sample_buffer) != 'undefined') {
551       var sample = self.audioCtx.createBufferSource();
552       var gainNode = self.touchGainNodes[sample_idx];
553       self.samples[sample_idx] = sample;
554       sample.loop = ($(nanotouch).find('.loop input:checked').length == 1);
555       sample.connect(gainNode);
556       sample.buffer = sample_buffer;
557       sample.onended = function() {
558         $(nanotouch).removeClass('playing');
559         var duration = $(nanotouch).find('span.duration');
560         $(duration).text(parseInt($(duration).data('duration')) + 's');
561         self.samples[sample_idx] = undefined;
562       }
563       $(nanotouch).addClass('playing');
564       self.sample_start_times[sample_idx] = sample.context.currentTime;
565       sample.start(0);
566     }
567   }
568   self.stopSample = function(sample_idx) {
569     var sample_buffer = self.sample_buffers[sample_idx];
570     if (typeof(sample_buffer) != 'undefined') {
571       if (typeof(samples[sample_idx]) != 'undefined') {
572         self.samples[sample_idx].stop(0);
573         self.samples[sample_idx] = undefined;
574       }
575     }
576   }
577
578   self.initAudio();
579   self.initMIDI();
580   self.initUI();
581
582 }
583
584 $(function() { nanofun(); });
585
586 if ('serviceWorker' in navigator) {
587   window.addEventListener('load', function() {
588     navigator.serviceWorker.register('service-worker.js').then(function(registration) {
589       // Registration was successful
590       console.log('ServiceWorker registration successful with scope: ', registration.scope);
591     }).catch(function(err) {
592       // registration failed :(
593       console.log('ServiceWorker registration failed: ', err);
594     });
595   });
596 }