]> git.0d.be Git - nanofun.git/blob - nanofun.js
make loop value change via nanokontrol work when playing
[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.sample_buffers = Array(NANOPAD_TOUCHS.length);
299     self.samples = Array(NANOPAD_TOUCHS.length);
300     self.sample_start_times = Array(NANOPAD_TOUCHS.length);
301     self.audioCtx = new window.AudioContext();
302     self.touchGainNodes = Array(NANOPAD_TOUCHS.length);
303     self.masterGainNode = self.audioCtx.createGain();
304     self.effectsGainNode = self.audioCtx.createGain();
305     for (var i=0; i<NANOPAD_TOUCHS.length; i++) {
306       self.touchGainNodes[i] = self.audioCtx.createGain();
307       self.touchGainNodes[i].connect(self.masterGainNode);
308     }
309     self.masterGainNode.connect(self.audioCtx.destination);
310
311     self.delay = self.audioCtx.createDelay(maxDelayTime=5);
312     self.delay.delayTime.value = 0.5;
313
314     self.feedback = self.audioCtx.createGain();
315     self.feedback.gain.value = 0.8;
316
317     self.filter = self.audioCtx.createBiquadFilter();
318     self.filter.frequency.value = 1000;
319
320     self.effectsGainNode.connect(self.delay);
321     self.delay.connect(self.feedback);
322     self.feedback.connect(self.filter);
323     self.filter.connect(self.delay);
324
325     self.filter.connect(self.masterGainNode);
326   }
327
328   self.initMIDI = function() {
329     if (navigator.requestMIDIAccess) {
330       navigator.requestMIDIAccess({sysex: true}).then(
331         function(midiAccess) { midi.onMIDISuccess(midiAccess); },
332         function(e) { midi.onMIDIFailure(e); }
333       );
334     }
335   }
336
337   self.initUI = function() {
338     var $nanopad = $('#nanopad');
339     var $nanotouch = $('.nanotouch');
340     for (var i=0; i<NANOPAD_TOUCHS.length; i++) {
341       var $new_touch = $nanotouch.clone();
342       $new_touch.attr('data-touch', i);
343       $new_touch.appendTo($nanopad);
344     }
345     $nanotouch.remove(); /* remove template */
346
347     $('.nanotouch input[type=file]').on('change', function(ev) {
348       var sample_idx = parseInt($(this).parent().data('touch'));
349       for (var i=0; i<this.files.length; i++) {
350         var reader = new FileReader();
351         var nanotouch = $('.nanotouch')[sample_idx + i];
352         reader.onload = function(e) {
353           var $nanotouch = $(this.nanotouch);
354           var sample_idx = this.sample_idx;
355           self.audioCtx.decodeAudioData(this.result, function(buffer) {
356             sample_buffers[sample_idx] = buffer;
357             $nanotouch.find('span.duration').data('duration', buffer.duration);
358             $nanotouch.find('span.duration').text(parseInt(buffer.duration) + 's');
359             $nanotouch.removeClass('error').addClass('loaded');
360           }, function(e) {
361             $nanotouch.find('span').text('');
362             $nanotouch.removeClass('loaded').addClass('error');
363           });
364         }
365         reader.nanotouch = nanotouch;
366         reader.sample_idx = sample_idx + i;
367         reader.readAsArrayBuffer(this.files[i]);
368         $(nanotouch).find('span.name').text(this.files[i].name);
369       }
370     });
371
372     midi.onTouchOn = function(port, data, sample_idx) {
373       self.startSample(sample_idx);
374     }
375
376     midi.onControlChange = function(port, data, control, value) {
377       if (control > 7 && control < 16) return; /* range between sliders and pots */
378       if (control >= 32 && control < 40) { /* "S" buttons */
379           var nanotouch = $('.nanotouch')[control-32];
380           if (value == 127) {
381             var checked = $(nanotouch).find('.loop input').prop('checked');
382             if (checked) {
383               $(nanotouch).find('.loop input').prop('checked', false);
384               device("nanoKONTROL2 MIDI 1").cc(control, 0);
385             } else {
386               $(nanotouch).find('.loop input').prop('checked', true);
387               device("nanoKONTROL2 MIDI 1").cc(control, 127);
388             }
389             $(nanotouch).find('.loop input').trigger('change');
390           }
391       }
392       if (control > 23) return; /* after pots */
393       if (control < 8) {
394         control += 8; /* sliders, control bottom pads (8-15) */
395       } else {
396         control -= 16; /* pots, control top pads (0-7) */
397       }
398       $('[data-touch=' + control + '] .touch-gain').val(value).trigger('change');
399     }
400
401     $('.loop input').on('change', function() {
402       var sample_idx = parseInt($(this).parents('[data-touch]').data('touch'));
403       if (self.samples[sample_idx]) {
404         self.samples[sample_idx].loop = $(this).is(':checked');
405       }
406     });
407
408     $(document).keypress(function(ev) {
409       var sample_idx = KEYBOARD_CODES.indexOf(ev.key);
410       if (sample_idx != -1) {
411         self.startSample(sample_idx);
412       }
413     });
414
415     $('.effects input').on('change', function() {
416       var effects = $(this).prop('checked');
417       var i = parseInt($(this).parents('.nanotouch').data('touch'));
418       if (effects) {
419         self.touchGainNodes[i].connect(self.effectsGainNode);
420       } else {
421         self.touchGainNodes[i].disconnect(self.effectsGainNode);
422       }
423     });
424
425     $('#master-gain').on('change', function() {
426       var fraction = parseInt(this.value) / parseInt(127);
427       var now = self.audioCtx.currentTime;
428       self.masterGainNode.gain.exponentialRampToValueAtTime(fraction * fraction, now + 0.015);
429     });
430
431     $('#delay').on('change', function() {
432       var value = this.value;
433       if (value == 0) {
434         self.delay.disconnect();
435       } else {
436         var now = self.audioCtx.currentTime;
437         self.delay.delayTime.exponentialRampToValueAtTime(value, now + 0.015);
438         self.delay.connect(self.audioCtx.destination);
439       }
440     });
441
442     $('#feedback').on('change', function() {
443       var now = self.audioCtx.currentTime;
444       self.feedback.gain.exponentialRampToValueAtTime(this.value, now + 0.015);
445     });
446
447     $('#filter').on('change', function() {
448       self.filter.frequency.value = this.value;
449     });
450
451     $('.touch-gain').on('change', function() {
452       var fraction = parseInt(this.value) / parseInt(127);
453       var touchIdx = parseInt($(this).parent().data('touch'));
454       var now = self.audioCtx.currentTime;
455       self.touchGainNodes[touchIdx].gain.exponentialRampToValueAtTime(fraction * fraction, now + 0.015);
456     });
457
458     self.time_interval_id = setInterval(function() {
459       for (var i=0; i<NANOPAD_TOUCHS.length; i++) {
460         var sample = self.samples[i];
461         if (sample !== undefined) {
462           var start_time = self.sample_start_times[i];
463           var current_position = sample.context.currentTime - start_time;
464           var nanotouch = $('.nanotouch')[i];
465           var duration = $(nanotouch).find('span.duration');
466           var total_duration = parseFloat($(duration).data('duration'))
467           $(duration).text(parseInt(total_duration - current_position) + 's');
468         }
469      }
470     }, 250);
471   }
472
473   self.startSample = function(sample_idx) {
474     var sample_buffer = self.sample_buffers[sample_idx];
475     var nanotouch = $('.nanotouch')[sample_idx];
476     if (typeof(sample_buffer) != 'undefined') {
477       if (typeof(samples[sample_idx]) != 'undefined' && samples[sample_idx].context.state == 'running') {
478         self.samples[sample_idx].stop(0);
479         self.samples[sample_idx] = undefined;
480       } else {
481         var sample = self.audioCtx.createBufferSource();
482         var gainNode = self.touchGainNodes[sample_idx];
483         self.samples[sample_idx] = sample;
484         sample.loop = ($(nanotouch).find('.loop input:checked').length == 1);
485         sample.connect(gainNode);
486         sample.buffer = sample_buffer;
487         sample.onended = function() {
488           $(nanotouch).removeClass('playing');
489           var duration = $(nanotouch).find('span.duration');
490           $(duration).text(parseInt($(duration).data('duration')) + 's');
491           self.samples[sample_idx] = undefined;
492         }
493         $(nanotouch).addClass('playing');
494         self.sample_start_times[sample_idx] = sample.context.currentTime;
495         sample.start(0);
496       }
497     }
498   }
499
500   self.initAudio();
501   self.initMIDI();
502   self.initUI();
503
504 }
505
506 $(function() { nanofun(); });
507
508 if ('serviceWorker' in navigator) {
509   window.addEventListener('load', function() {
510     navigator.serviceWorker.register('service-worker.js').then(function(registration) {
511       // Registration was successful
512       console.log('ServiceWorker registration successful with scope: ', registration.scope);
513     }).catch(function(err) {
514       // registration failed :(
515       console.log('ServiceWorker registration failed: ', err);
516     });
517   });
518 }