dynamically create touches
[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', 'u', 'i', 'o',
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
308   self.initMIDI = function() {
309     if (navigator.requestMIDIAccess) {
310       navigator.requestMIDIAccess({sysex: true}).then(
311         function(midiAccess) { midi.onMIDISuccess(midiAccess); },
312         function(e) { midi.onMIDIFailure(e); }
313       );
314     }
315   }
316
317   self.initUI = function() {
318     var $nanopad = $('#nanopad');
319     var $nanotouch = $('.nanotouch');
320     for (var i=1; i<16; i++) {
321       var $new_touch = $nanotouch.clone();
322       $new_touch.attr('data-touch', i);
323       $new_touch.appendTo($nanopad);
324     }
325
326     $('.nanotouch input[type=file]').on('change', function(ev) {
327       var nanotouch = $(this).parent();
328       var sample_idx = $nanopad.children().index(nanotouch);
329       var reader = new FileReader();
330       reader.onload = function(e) {
331         self.audioCtx.decodeAudioData(this.result, function(buffer) {
332           sample_buffers[sample_idx] = buffer;
333           $(nanotouch).find('span.duration').text(parseInt(buffer.duration) + 's');
334           $(nanotouch).removeClass('error').addClass('loaded');
335         }, function(e) {
336           $(nanotouch).find('span').text('');
337           $(nanotouch).removeClass('loaded').addClass('error');
338         });
339       }
340       reader.readAsArrayBuffer(this.files[0]);
341       $(nanotouch).find('span.name').text(this.files[0].name);
342     });
343
344     midi.onTouchOn = function(port, data, sample_idx) {
345       self.startSample(sample_idx);
346     }
347
348     midi.onControlChange = function(port, data, control, value) {
349       if (control > 7 && control < 16) return; /* range between sliders and pots */
350       if (control > 23) return; /* after pots */
351       if (control >= 16) { control -= 8; }
352       $('[data-touch=' + control + '] .touch-gain').val(value).trigger('change');
353     }
354
355     $(document).keypress(function(ev) {
356       var sample_idx = KEYBOARD_CODES.indexOf(ev.key);
357       if (sample_idx != -1) {
358         self.startSample(sample_idx);
359       }
360     });
361
362     $('#master-gain').on('change', function() {
363       var fraction = parseInt(this.value) / parseInt(127);
364       self.masterGainNode.gain.value = fraction * fraction;
365     });
366
367     $('.touch-gain').on('change', function() {
368       var fraction = parseInt(this.value) / parseInt(127);
369       var touchIdx = parseInt($(this).parent().data('touch'));
370       self.touchGainNodes[touchIdx].gain.value = fraction * fraction;
371     });
372   }
373
374   self.startSample = function(sample_idx) {
375     var sample_buffer = self.sample_buffers[sample_idx];
376     var nanotouch = $('.nanotouch')[sample_idx];
377     if (typeof(sample_buffer) != 'undefined') {
378       if (typeof(samples[sample_idx]) != 'undefined' && samples[sample_idx].context.state == 'running') {
379         self.samples[sample_idx].stop(0);
380         self.samples[sample_idx] = undefined;
381       } else {
382         var sample = self.audioCtx.createBufferSource();
383         var gainNode = self.touchGainNodes[sample_idx];
384         self.samples[sample_idx] = sample;
385         sample.loop = false;
386         sample.connect(gainNode);
387         sample.buffer = sample_buffer;
388         sample.onended = function() {
389           console.log('ended');
390           $(nanotouch).removeClass('playing');
391           self.samples[sample_idx] = undefined;
392         }
393         $(nanotouch).addClass('playing');
394         sample.start(0);
395       }
396     }
397   }
398
399   self.initAudio();
400   self.initMIDI();
401   self.initUI();
402
403 }
404
405 $(function() { nanofun(); });