]> git.0d.be Git - nanofun.git/blob - nanofun.js
display remaining time when playing 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 > 23) return; /* after pots */
376       if (control < 8) {
377         control += 8; /* sliders, control bottom pads (8-15) */
378       } else {
379         control -= 16; /* pots, control top pads (0-7) */
380       }
381       $('[data-touch=' + control + '] .touch-gain').val(value).trigger('change');
382     }
383
384     $(document).keypress(function(ev) {
385       var sample_idx = KEYBOARD_CODES.indexOf(ev.key);
386       if (sample_idx != -1) {
387         self.startSample(sample_idx);
388       }
389     });
390
391     $('#master-gain').on('change', function() {
392       var fraction = parseInt(this.value) / parseInt(127);
393       self.masterGainNode.gain.value = fraction * fraction;
394     });
395
396     $('#delay').on('change', function() {
397       var value = this.value;
398       if (value == 0) {
399         self.delay.disconnect();
400       } else {
401         self.delay.delayTime.value = value;
402         self.delay.connect(self.audioCtx.destination);
403       }
404     });
405
406     $('#feedback').on('change', function() {
407       self.feedback.gain.value = this.value;
408     });
409
410     $('#filter').on('change', function() {
411       self.filter.frequency.value = this.value;
412     });
413
414     $('.touch-gain').on('change', function() {
415       var fraction = parseInt(this.value) / parseInt(127);
416       var touchIdx = parseInt($(this).parent().data('touch'));
417       self.touchGainNodes[touchIdx].gain.value = fraction * fraction;
418     });
419
420     self.time_interval_id = setInterval(function() {
421       for (var i=0; i<16; i++) {
422         var sample = self.samples[i];
423         if (sample !== undefined) {
424           var start_time = self.sample_start_times[i];
425           var current_position = sample.context.currentTime - start_time;
426           var nanotouch = $('.nanotouch')[i];
427           var duration = $(nanotouch).find('span.duration');
428           var total_duration = parseFloat($(duration).data('duration'))
429           $(duration).text(parseInt(total_duration - current_position) + 's');
430         }
431      }
432     }, 250);
433   }
434
435   self.startSample = function(sample_idx) {
436     var sample_buffer = self.sample_buffers[sample_idx];
437     var nanotouch = $('.nanotouch')[sample_idx];
438     if (typeof(sample_buffer) != 'undefined') {
439       if (typeof(samples[sample_idx]) != 'undefined' && samples[sample_idx].context.state == 'running') {
440         self.samples[sample_idx].stop(0);
441         self.samples[sample_idx] = undefined;
442       } else {
443         var sample = self.audioCtx.createBufferSource();
444         var gainNode = self.touchGainNodes[sample_idx];
445         self.samples[sample_idx] = sample;
446         sample.loop = false;
447         sample.connect(gainNode);
448         sample.buffer = sample_buffer;
449         sample.onended = function() {
450           $(nanotouch).removeClass('playing');
451           var duration = $(nanotouch).find('span.duration');
452           $(duration).text(parseInt($(duration).data('duration')) + 's');
453           self.samples[sample_idx] = undefined;
454         }
455         $(nanotouch).addClass('playing');
456         self.sample_start_times[sample_idx] = sample.context.currentTime;
457         sample.start(0);
458       }
459     }
460   }
461
462   self.initAudio();
463   self.initMIDI();
464   self.initUI();
465
466 }
467
468 $(function() { nanofun(); });
469
470 if ('serviceWorker' in navigator) {
471   window.addEventListener('load', function() {
472     navigator.serviceWorker.register('service-worker.js').then(function(registration) {
473       // Registration was successful
474       console.log('ServiceWorker registration successful with scope: ', registration.scope);
475     }).catch(function(err) {
476       // registration failed :(
477       console.log('ServiceWorker registration failed: ', err);
478     });
479   });
480 }