]> git.0d.be Git - nanofun.git/blob - nanofun.js
add some knobs for effects
[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     var portContainer = $("#midi" + port.type + "s");
58     if (portContainer.html().startsWith("<p>No connected")) {
59         portContainer.empty();
60     }
61
62     if (port.state == "disconnected") {
63       if (port.type == "input") {
64         this.inputs[port.name] = undefined;
65       } else {
66         this.outputs[port.name] = undefined;
67       }
68     } else {
69       if (port.type == "input") {
70         if (this.inputs[port.name] === undefined) { this.registerPort(port); }
71       } else {
72         if (this.outputs[port.name] === undefined) { this.registerPort(port); }
73       }
74       this.renderPort(port);
75     }
76 },
77
78 renderPort: function(port) {
79     if (port.state == "connected") {
80       $("#midi" + port.type + "s").append(port.name);
81     }
82 },
83
84 registerPort: function(port) {
85     var self = this;
86     if (port.type == "input") {
87         this.inputs[port.name] = port;
88         port.onmidimessage = function(m) { self.onMIDIMessage(m); };
89     } else {
90         this.outputs[port.name] = port;
91
92         if (port.name == 'nanoKONTROL2 MIDI 1') {
93           /* turn "external leds" mode on.
94            *
95            * The sysex dump has been recorded from the official Korg kontrol
96            * editor by the Overtone project, original code at:
97            * https://github.com/overtone/overtone/blob/master/src/overtone/device/midi/nanoKONTROL2.clj */
98           device("nanoKONTROL2 MIDI 1").raw([240, 126, 127, 6, 1, 247]);
99           device("nanoKONTROL2 MIDI 1").raw([240, 66, 64, 0, 1, 19, 0, 31, 18, 0, 247]);
100           device("nanoKONTROL2 MIDI 1").raw([240, 126, 127, 6, 1, 247]);
101
102           device("nanoKONTROL2 MIDI 1").raw([ 240, 66, 64, 0, 1, 19, 0, 127, 127,
103                 2, 3, 5, 64, 0, 0, 0, 1, 16, 1, 0, 0, 0, 0, 127, 0, 1, 0, 16,
104                 0, 0, 127, 0, 1, 0, 32, 0, 127, 0, 0, 1, 0, 48, 0, 127, 0, 0,
105                 1, 0, 64, 0, 127, 0, 16, 0, 1, 0, 1, 0, 127, 0, 1, 0, 0, 17, 0,
106                 127, 0, 1, 0, 0, 33, 0, 127, 0, 1, 0, 49, 0, 0, 127, 0, 1, 0,
107                 65, 0, 0, 127, 0, 16, 1, 0, 2, 0, 0, 127, 0, 1, 0, 18, 0, 127,
108                 0, 0, 1, 0, 34, 0, 127, 0, 0, 1, 0, 50, 0, 127, 0, 1, 0, 0, 66,
109                 0, 127, 0, 16, 1, 0, 0, 3, 0, 127, 0, 1, 0, 0, 19, 0, 127, 0,
110                 1, 0, 35, 0, 0, 127, 0, 1, 0, 51, 0, 0, 127, 0, 1, 0, 67, 0,
111                 127, 0, 0, 16, 1, 0, 4, 0, 127, 0, 0, 1, 0, 20, 0, 127, 0, 0,
112                 1, 0, 36, 0, 127, 0, 1, 0, 0, 52, 0, 127, 0, 1, 0, 0, 68, 0,
113                 127, 0, 16, 1, 0, 0, 5, 0, 127, 0, 1, 0, 21, 0, 0, 127, 0, 1,
114                 0, 37, 0, 0, 127, 0, 1, 0, 53, 0, 127, 0, 0, 1, 0, 69, 0, 127,
115                 0, 0, 16, 1, 0, 6, 0, 127, 0, 0, 1, 0, 22, 0, 127, 0, 1, 0, 0,
116                 38, 0, 127, 0, 1, 0, 0, 54, 0, 127, 0, 1, 0, 70, 0, 0, 127, 0,
117                 16, 1, 0, 7, 0, 0, 127, 0, 1, 0, 23, 0, 0, 127, 0, 1, 0, 39, 0,
118                 127, 0, 0, 1, 0, 55, 0, 127, 0, 0, 1, 0, 71, 0, 127, 0, 16, 0,
119                 1, 0, 58, 0, 127, 0, 1, 0, 0, 59, 0, 127, 0, 1, 0, 0, 46, 0,
120                 127, 0, 1, 0, 60, 0, 0, 127, 0, 1, 0, 61, 0, 0, 127, 0, 1, 0,
121                 62, 0, 127, 0, 0, 1, 0, 43, 0, 127, 0, 0, 1, 0, 44, 0, 127, 0,
122                 1, 0, 0, 42, 0, 127, 0, 1, 0, 0, 41, 0, 127, 0, 1, 0, 45, 0, 0,
123                 127, 0, 127, 127, 127, 127, 0, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0,
124                 0, 0, 0, 0, 0, 0, 0, 0, 0, 247]);
125
126           device("nanoKONTROL2 MIDI 1").raw([240, 126, 127, 6, 1, 247]);
127           device("nanoKONTROL2 MIDI 1").raw([240, 66, 64, 0, 1, 19, 0, 31, 17, 0, 247]);
128
129           function on(note) { device("nanoKONTROL2 MIDI 1").cc(note, 127); }
130           function off(note) { device("nanoKONTROL2 MIDI 1").cc(note, 0); }
131           var leds = Array(43, 44, 42, 41, 45);
132           for (var i=0; i<8; i++) {
133             leds.push(64+i);
134             leds.push(48+i);
135             leds.push(32+i);
136             i += 1;
137             leds.push(32+i);
138             leds.push(48+i);
139             leds.push(64+i);
140           }
141                 console.log(leds);
142           on(leds[0]);
143           on(leds[1]);
144           on(leds[2]);
145           var led_idx = 2;
146           var interval_id = setInterval(function() {
147             if (led_idx < leds.length) {
148               on(leds[led_idx+1]);
149             }
150             off(leds[led_idx-2]);
151             if (led_idx-1 == leds.length) {
152               clearInterval(interval_id);
153             }
154             led_idx += 1;
155           }, 50);
156         }
157     }
158
159     port.onstatechange = function(e) { self.onPortStateChange(e); };
160 },
161
162 onMIDIFailure: function(e) {
163     alert("No access to MIDI devices or your browser doesn't support WebMIDI API. Please use WebMIDIAPIShim " + e);
164 },
165
166 onPortStateChange: function(event) {
167   console.log(event);
168 },
169
170 onMIDIMessage: function(message) {
171     var port = message.target;
172     var data = message.data;
173     console.log(message);
174     if (data[0] == 144) { /* touch on */
175       var sample_idx = NANOPAD_TOUCHS.indexOf(data[1]);
176       if (sample_idx != -1) {
177         this.onTouchOn(port, data, sample_idx);
178       }
179     }
180     if (data[0] == 176) { /* control change */
181       this.onControlChange(port, data, data[1], data[2]);
182     }
183 },
184
185 onTouchOn: function(port, data, sample_idx) {},
186 onControlChange: function(port, data, number, value) {}
187
188 };
189
190 // status bytes on channel 1
191 var messages = {
192     off: 128,
193     on: 144,
194     pp: 160,
195     cc: 176,
196     pc: 192,
197     cp: 208,
198     pb: 224
199 }
200
201 var device = function(outputName) {
202    this.current = midi.outputs[outputName];
203    this.channel = 1;
204
205    // makes device visible inside of nested function defs
206    var self = this;
207
208    this._send = function(status, data) {
209     var messageArr = [status + (self.channel - 1)].concat(data);
210     console.log("sending " + messageArr + " to " + self.current.name);
211     self.current.send(messageArr);
212     return self;
213    }
214
215    this.ch = function(channel) {
216       self.channel = channel;
217       return self;
218    }
219
220    this.cc = function(b1, b2) {
221     return self._send(messages.cc, [b1, b2]);
222    }
223
224    this.on = function(b1, b2) {
225     return self._send(messages.on, [b1, b2]);
226    }
227
228    this.off = function(b1, b2) {
229     return self._send(messages.off, [b1, b2]);
230    }
231
232    this.pp = function(b1, b2) {
233     return self._send(messages.pp, [b1, b2]);
234    }
235
236    this.cp = function(b1) {
237     return self._send(messages.cp, [b1]);
238    }
239
240    this.pb = function(b1) {
241     return self._send(
242         messages.pb, [
243             b1 & 127,
244             b1 >> 7
245         ]
246     );
247    }
248
249    this.pc = function(b1) {
250     return self._send(messages.pc, [b1]);
251    }
252
253    this.panic = function() {
254     return self.cc(123, 0)
255    }
256
257    this.rpn = function(b1, b2) {
258     return self.cc(101, b1 >> 7)
259         .cc(100, b1 & 127)
260         .cc(6, b2 >> 7)
261         .cc(38, b2 & 127)
262         .cc(101, 127)
263         .cc(100, 127);
264    }
265
266    this.nrpn = function(b1, b2) {
267     return self.cc(99, b1 >> 7)
268         .cc(98, b1 & 127)
269         .cc(6, b2 >> 7)
270         .cc(38, b2 & 127)
271         .cc(101, 127)
272         .cc(100, 127);
273    }
274
275    this.raw = function(data) {
276     console.log("sending raw data: " + data);
277     self.current.send(data);
278     return self;
279    }
280
281    this.toString = function() {
282     var s = "no connected devices";
283     if (typeof this.current != 'undefined') {
284         s = "";
285     }
286     return s;
287    }
288
289    return this;
290 };
291
292 var nanofun = function() {
293   var self = this;
294
295   self.initAudio = function() {
296     self.sample_buffers = Array(16);
297     self.samples = Array(16);
298     self.audioCtx = new window.AudioContext();
299     self.touchGainNodes = Array(16);
300     self.masterGainNode = self.audioCtx.createGain();
301     for (var i=0; i<16; i++) {
302       self.touchGainNodes[i] = self.audioCtx.createGain();
303       self.touchGainNodes[i].connect(self.masterGainNode);
304     }
305     self.masterGainNode.connect(self.audioCtx.destination);
306
307     self.delay = self.audioCtx.createDelay(maxDelayTime=5);
308     self.delay.delayTime.value = 0.5;
309
310     self.feedback = self.audioCtx.createGain();
311     self.feedback.gain.value = 0.8;
312
313     self.filter = self.audioCtx.createBiquadFilter();
314     self.filter.frequency.value = 1000;
315
316     self.delay.connect(self.feedback);
317     self.feedback.connect(self.filter);
318     self.filter.connect(self.delay);
319
320     self.masterGainNode.connect(self.delay);
321   }
322
323   self.initMIDI = function() {
324     if (navigator.requestMIDIAccess) {
325       navigator.requestMIDIAccess({sysex: true}).then(
326         function(midiAccess) { midi.onMIDISuccess(midiAccess); },
327         function(e) { midi.onMIDIFailure(e); }
328       );
329     }
330   }
331
332   self.initUI = function() {
333     var $nanopad = $('#nanopad');
334     var $nanotouch = $('.nanotouch');
335     for (var i=1; i<16; i++) {
336       var $new_touch = $nanotouch.clone();
337       $new_touch.attr('data-touch', i);
338       $new_touch.appendTo($nanopad);
339     }
340
341     $('.nanotouch input[type=file]').on('change', function(ev) {
342       var sample_idx = parseInt($(this).parent().data('touch'));
343       for (var i=0; i<this.files.length; i++) {
344         var reader = new FileReader();
345         var nanotouch = $('.nanotouch')[sample_idx + i];
346         reader.onload = function(e) {
347           var $nanotouch = $(this.nanotouch);
348           var sample_idx = this.sample_idx;
349           self.audioCtx.decodeAudioData(this.result, function(buffer) {
350             sample_buffers[sample_idx] = buffer;
351             $nanotouch.find('span.duration').text(parseInt(buffer.duration) + 's');
352             $nanotouch.removeClass('error').addClass('loaded');
353           }, function(e) {
354             $nanotouch.find('span').text('');
355             $nanotouch.removeClass('loaded').addClass('error');
356           });
357         }
358         reader.nanotouch = nanotouch;
359         reader.sample_idx = sample_idx + i;
360         reader.readAsArrayBuffer(this.files[i]);
361         $(nanotouch).find('span.name').text(this.files[i].name);
362       }
363     });
364
365     midi.onTouchOn = function(port, data, sample_idx) {
366       self.startSample(sample_idx);
367     }
368
369     midi.onControlChange = function(port, data, control, value) {
370       if (control > 7 && control < 16) return; /* range between sliders and pots */
371       if (control > 23) return; /* after pots */
372       if (control < 8) {
373         control += 8; /* sliders, control bottom pads (8-15) */
374       } else {
375         control -= 16; /* pots, control top pads (0-7) */
376       }
377       $('[data-touch=' + control + '] .touch-gain').val(value).trigger('change');
378     }
379
380     $(document).keypress(function(ev) {
381       var sample_idx = KEYBOARD_CODES.indexOf(ev.key);
382       if (sample_idx != -1) {
383         self.startSample(sample_idx);
384       }
385     });
386
387     $('#master-gain').on('change', function() {
388       var fraction = parseInt(this.value) / parseInt(127);
389       self.masterGainNode.gain.value = fraction * fraction;
390     });
391
392     $('#delay').on('change', function() {
393       var value = this.value;
394       if (value == 0) {
395         self.delay.disconnect();
396       } else {
397         self.delay.delayTime.value = value;
398         self.delay.connect(self.audioCtx.destination);
399       }
400     });
401
402     $('#feedback').on('change', function() {
403       self.feedback.gain.value = this.value;
404     });
405
406     $('#filter').on('change', function() {
407       self.filter.frequency.value = this.value;
408     });
409
410     $('.touch-gain').on('change', function() {
411       var fraction = parseInt(this.value) / parseInt(127);
412       var touchIdx = parseInt($(this).parent().data('touch'));
413       self.touchGainNodes[touchIdx].gain.value = fraction * fraction;
414     });
415   }
416
417   self.startSample = function(sample_idx) {
418     var sample_buffer = self.sample_buffers[sample_idx];
419     var nanotouch = $('.nanotouch')[sample_idx];
420     if (typeof(sample_buffer) != 'undefined') {
421       if (typeof(samples[sample_idx]) != 'undefined' && samples[sample_idx].context.state == 'running') {
422         self.samples[sample_idx].stop(0);
423         self.samples[sample_idx] = undefined;
424       } else {
425         var sample = self.audioCtx.createBufferSource();
426         var gainNode = self.touchGainNodes[sample_idx];
427         self.samples[sample_idx] = sample;
428         sample.loop = false;
429         sample.connect(gainNode);
430         sample.buffer = sample_buffer;
431         sample.onended = function() {
432           $(nanotouch).removeClass('playing');
433           self.samples[sample_idx] = undefined;
434         }
435         $(nanotouch).addClass('playing');
436         sample.start(0);
437       }
438     }
439   }
440
441   self.initAudio();
442   self.initMIDI();
443   self.initUI();
444
445 }
446
447 $(function() { nanofun(); });
448
449 if ('serviceWorker' in navigator) {
450   window.addEventListener('load', function() {
451     navigator.serviceWorker.register('service-worker.js').then(function(registration) {
452       // Registration was successful
453       console.log('ServiceWorker registration successful with scope: ', registration.scope);
454     }).catch(function(err) {
455       // registration failed :(
456       console.log('ServiceWorker registration failed: ', err);
457     });
458   });
459 }