add a gain controller, mapped to fader 7 of the nanoKontrol
[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.gainNode = self.audioCtx.createGain();
300     self.gainNode.connect(self.audioCtx.destination);
301   }
302
303   self.initMIDI = function() {
304     if (navigator.requestMIDIAccess) {
305       navigator.requestMIDIAccess({sysex: true}).then(
306         function(midiAccess) { midi.onMIDISuccess(midiAccess); },
307         function(e) { midi.onMIDIFailure(e); }
308       );
309     }
310   }
311
312   self.initUI = function() {
313     var $nanopad = $('#nanopad');
314
315     $('.nanotouch input').on('change', function(ev) {
316       var nanotouch = $(this).parent();
317       var sample_idx = $nanopad.children().index(nanotouch);
318       var reader = new FileReader();
319       reader.onload = function(e) {
320         self.audioCtx.decodeAudioData(this.result, function(buffer) {
321           sample_buffers[sample_idx] = buffer;
322           $(nanotouch).find('span.duration').text(parseInt(buffer.duration) + 's');
323           $(nanotouch).removeClass('error').addClass('loaded');
324         }, function(e) {
325           $(nanotouch).find('span').text('');
326           $(nanotouch).removeClass('loaded').addClass('error');
327         });
328       }
329       reader.readAsArrayBuffer(this.files[0]);
330       $(nanotouch).find('span.name').text(this.files[0].name);
331     });
332
333     midi.onTouchOn = function(port, data, sample_idx) {
334       self.startSample(sample_idx);
335     }
336
337     midi.onControlChange = function(port, data, control, value) {
338       if (control == 7) {
339         $('#gain').val(value).trigger('change');
340       }
341     }
342
343     $(document).keypress(function(ev) {
344       var sample_idx = KEYBOARD_CODES.indexOf(ev.key);
345       if (sample_idx != -1) {
346         self.startSample(sample_idx);
347       }
348     });
349
350     $('#gain').on('change', function() {
351       var fraction = parseInt(this.value) / parseInt(127);
352       self.gainNode.gain.value = fraction * fraction;
353     });
354   }
355
356   self.startSample = function(sample_idx) {
357     var sample_buffer = self.sample_buffers[sample_idx];
358     var nanotouch = $('.nanotouch')[sample_idx];
359     if (typeof(sample_buffer) != 'undefined') {
360       if (typeof(samples[sample_idx]) != 'undefined' && samples[sample_idx].context.state == 'running') {
361         self.samples[sample_idx].stop(0);
362         self.samples[sample_idx] = undefined;
363       } else {
364         var sample = self.audioCtx.createBufferSource();
365         self.samples[sample_idx] = sample;
366         sample.loop = false;
367         sample.connect(gainNode);
368         sample.buffer = sample_buffer;
369         sample.onended = function() {
370           console.log('ended');
371           $(nanotouch).removeClass('playing');
372           self.samples[sample_idx] = undefined;
373         }
374         $(nanotouch).addClass('playing');
375         sample.start(0);
376       }
377     }
378   }
379
380   self.initAudio();
381   self.initMIDI();
382   self.initUI();
383
384 }
385
386 $(function() { nanofun(); });