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