]> git.0d.be Git - nanofun.git/blob - nanofun.js
3fd7203ae2a86a4c8bf013364d71b9258e72edc6
[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=0; i<16; i++) {
337       var $new_touch = $nanotouch.clone();
338       $new_touch.attr('data-touch', i);
339       $new_touch.appendTo($nanopad);
340     }
341     $nanotouch.remove(); /* remove template */
342
343     $('.nanotouch input[type=file]').on('change', function(ev) {
344       var sample_idx = parseInt($(this).parent().data('touch'));
345       for (var i=0; i<this.files.length; i++) {
346         var reader = new FileReader();
347         var nanotouch = $('.nanotouch')[sample_idx + i];
348         reader.onload = function(e) {
349           var $nanotouch = $(this.nanotouch);
350           var sample_idx = this.sample_idx;
351           self.audioCtx.decodeAudioData(this.result, function(buffer) {
352             sample_buffers[sample_idx] = buffer;
353             $nanotouch.find('span.duration').text(parseInt(buffer.duration) + 's');
354             $nanotouch.removeClass('error').addClass('loaded');
355           }, function(e) {
356             $nanotouch.find('span').text('');
357             $nanotouch.removeClass('loaded').addClass('error');
358           });
359         }
360         reader.nanotouch = nanotouch;
361         reader.sample_idx = sample_idx + i;
362         reader.readAsArrayBuffer(this.files[i]);
363         $(nanotouch).find('span.name').text(this.files[i].name);
364       }
365     });
366
367     midi.onTouchOn = function(port, data, sample_idx) {
368       self.startSample(sample_idx);
369     }
370
371     midi.onControlChange = function(port, data, control, value) {
372       if (control > 7 && control < 16) return; /* range between sliders and pots */
373       if (control > 23) return; /* after pots */
374       if (control < 8) {
375         control += 8; /* sliders, control bottom pads (8-15) */
376       } else {
377         control -= 16; /* pots, control top pads (0-7) */
378       }
379       $('[data-touch=' + control + '] .touch-gain').val(value).trigger('change');
380     }
381
382     $(document).keypress(function(ev) {
383       var sample_idx = KEYBOARD_CODES.indexOf(ev.key);
384       if (sample_idx != -1) {
385         self.startSample(sample_idx);
386       }
387     });
388
389     $('#master-gain').on('change', function() {
390       var fraction = parseInt(this.value) / parseInt(127);
391       self.masterGainNode.gain.value = fraction * fraction;
392     });
393
394     $('#delay').on('change', function() {
395       var value = this.value;
396       if (value == 0) {
397         self.delay.disconnect();
398       } else {
399         self.delay.delayTime.value = value;
400         self.delay.connect(self.audioCtx.destination);
401       }
402     });
403
404     $('#feedback').on('change', function() {
405       self.feedback.gain.value = this.value;
406     });
407
408     $('#filter').on('change', function() {
409       self.filter.frequency.value = this.value;
410     });
411
412     $('.touch-gain').on('change', function() {
413       var fraction = parseInt(this.value) / parseInt(127);
414       var touchIdx = parseInt($(this).parent().data('touch'));
415       self.touchGainNodes[touchIdx].gain.value = fraction * fraction;
416     });
417   }
418
419   self.startSample = function(sample_idx) {
420     var sample_buffer = self.sample_buffers[sample_idx];
421     var nanotouch = $('.nanotouch')[sample_idx];
422     if (typeof(sample_buffer) != 'undefined') {
423       if (typeof(samples[sample_idx]) != 'undefined' && samples[sample_idx].context.state == 'running') {
424         self.samples[sample_idx].stop(0);
425         self.samples[sample_idx] = undefined;
426       } else {
427         var sample = self.audioCtx.createBufferSource();
428         var gainNode = self.touchGainNodes[sample_idx];
429         self.samples[sample_idx] = sample;
430         sample.loop = false;
431         sample.connect(gainNode);
432         sample.buffer = sample_buffer;
433         sample.onended = function() {
434           $(nanotouch).removeClass('playing');
435           self.samples[sample_idx] = undefined;
436         }
437         $(nanotouch).addClass('playing');
438         sample.start(0);
439       }
440     }
441   }
442
443   self.initAudio();
444   self.initMIDI();
445   self.initUI();
446
447 }
448
449 $(function() { nanofun(); });
450
451 if ('serviceWorker' in navigator) {
452   window.addEventListener('load', function() {
453     navigator.serviceWorker.register('service-worker.js').then(function(registration) {
454       // Registration was successful
455       console.log('ServiceWorker registration successful with scope: ', registration.scope);
456     }).catch(function(err) {
457       // registration failed :(
458       console.log('ServiceWorker registration failed: ', err);
459     });
460   });
461 }