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