]> git.0d.be Git - nanofun.git/blobdiff - nanofun.js
ramp to values to avoid audio glitches
[nanofun.git] / nanofun.js
index 66e6147cb4655d5b84ee151371e5508221e7f295..b8dcaf96721bd3ad094f32bd6cd8a26b54cce0f5 100644 (file)
@@ -1,13 +1,9 @@
-var NANOPAD_TOUCHS = {
-  37:  0, 39:  1, 41:  2, 43:  3, 45:  4, 47:  5, 49:  6, 51:  7,
-  36:  8, 38:  9, 40: 10, 42: 11, 44: 12, 46: 13, 48: 14, 50: 15
-};
+var NANOPAD_TOUCHS = Array(37, 39, 41, 43, 45, 47, 49, 51,
+                           36, 38, 40, 42, 44, 46, 48, 50);
 
-var sample_buffers = Array(16);
-var samples = Array(16);
-var audioCtx = new window.AudioContext();
-var gainNode = audioCtx.createGain();
-gainNode.connect(audioCtx.destination);
+/* on French/Belgian keyboards, emulate pad touches with keypresses */
+var KEYBOARD_CODES = Array('a', 'z', 'e', 'r', 't', 'y', 'u', 'i',
+                           'q', 's', 'd', 'f', 'g', 'h', 'j', 'k');
 
 var midi = {
 
@@ -55,25 +51,29 @@ initPorts: function() {
 },
 
 onMIDIAccessChange: function(e) {
-    console.log(e);
+    console.log('on midi access change', e);
     //console.log(this);
     var port = e.port;
-    var portContainer = $("#midi" + port.type + "s");
-    if (portContainer.html().startsWith("<p>No connected")) {
-        portContainer.empty();
-    }
 
-    if (port.type == "input") {
-        if (this.inputs[port.name] === undefined) {
-            this.registerPort(port);
-        }
+    if (port.state == "disconnected") {
+      if (port.type == "input") {
+        this.inputs[port.name] = undefined;
+      } else {
+        this.outputs[port.name] = undefined;
+      }
+      if (port.name == 'nanoPAD2 MIDI 1' || port.name == 'nanoPAD2 2 PAD') { $('#devices .nanopad').removeClass('on'); }
+      if (port.name == 'nanoKONTROL2 MIDI 1' || port.name == 'nanoKONTROL2 2 SLIDER/KNOB') { $('#devices .nanokontrol').removeClass('on'); }
     } else {
-        if (this.outputs[port.name] === undefined) {
-            this.registerPort(port);
-        }
+      if (port.type == "input") {
+        if (this.inputs[port.name] === undefined) { this.registerPort(port); }
+      } else {
+        if (this.outputs[port.name] === undefined) { this.registerPort(port); }
+      }
+            console.log('hello:', port.name);
+      if (port.name == 'nanoPAD2 MIDI 1' || port.name == 'nanoPAD2 2 PAD') { $('#devices .nanopad').addClass('on'); }
+      if (port.name == 'nanoKONTROL2 MIDI 1' || port.name == 'nanoKONTROL2 2 SLIDER/KNOB') { $('#devices .nanokontrol').addClass('on'); }
+      this.renderPort(port);
     }
-
-    this.renderPort(port);
 },
 
 renderPort: function(port) {
@@ -89,6 +89,73 @@ registerPort: function(port) {
         port.onmidimessage = function(m) { self.onMIDIMessage(m); };
     } else {
         this.outputs[port.name] = port;
+
+        if (port.name == 'nanoKONTROL2 MIDI 1' || port.name == 'nanoKONTROL2 2 SLIDER/KNOB') {
+          var port_name = port.name;
+          /* turn "external leds" mode on.
+           *
+           * The sysex dump has been recorded from the official Korg kontrol
+           * editor by the Overtone project, original code at:
+           * https://github.com/overtone/overtone/blob/master/src/overtone/device/midi/nanoKONTROL2.clj */
+          device(port_name).raw([240, 126, 127, 6, 1, 247]);
+          device(port_name).raw([240, 66, 64, 0, 1, 19, 0, 31, 18, 0, 247]);
+          device(port_name).raw([240, 126, 127, 6, 1, 247]);
+
+          device(port_name).raw([ 240, 66, 64, 0, 1, 19, 0, 127, 127,
+                2, 3, 5, 64, 0, 0, 0, 1, 16, 1, 0, 0, 0, 0, 127, 0, 1, 0, 16,
+                0, 0, 127, 0, 1, 0, 32, 0, 127, 0, 0, 1, 0, 48, 0, 127, 0, 0,
+                1, 0, 64, 0, 127, 0, 16, 0, 1, 0, 1, 0, 127, 0, 1, 0, 0, 17, 0,
+                127, 0, 1, 0, 0, 33, 0, 127, 0, 1, 0, 49, 0, 0, 127, 0, 1, 0,
+                65, 0, 0, 127, 0, 16, 1, 0, 2, 0, 0, 127, 0, 1, 0, 18, 0, 127,
+                0, 0, 1, 0, 34, 0, 127, 0, 0, 1, 0, 50, 0, 127, 0, 1, 0, 0, 66,
+                0, 127, 0, 16, 1, 0, 0, 3, 0, 127, 0, 1, 0, 0, 19, 0, 127, 0,
+                1, 0, 35, 0, 0, 127, 0, 1, 0, 51, 0, 0, 127, 0, 1, 0, 67, 0,
+                127, 0, 0, 16, 1, 0, 4, 0, 127, 0, 0, 1, 0, 20, 0, 127, 0, 0,
+                1, 0, 36, 0, 127, 0, 1, 0, 0, 52, 0, 127, 0, 1, 0, 0, 68, 0,
+                127, 0, 16, 1, 0, 0, 5, 0, 127, 0, 1, 0, 21, 0, 0, 127, 0, 1,
+                0, 37, 0, 0, 127, 0, 1, 0, 53, 0, 127, 0, 0, 1, 0, 69, 0, 127,
+                0, 0, 16, 1, 0, 6, 0, 127, 0, 0, 1, 0, 22, 0, 127, 0, 1, 0, 0,
+                38, 0, 127, 0, 1, 0, 0, 54, 0, 127, 0, 1, 0, 70, 0, 0, 127, 0,
+                16, 1, 0, 7, 0, 0, 127, 0, 1, 0, 23, 0, 0, 127, 0, 1, 0, 39, 0,
+                127, 0, 0, 1, 0, 55, 0, 127, 0, 0, 1, 0, 71, 0, 127, 0, 16, 0,
+                1, 0, 58, 0, 127, 0, 1, 0, 0, 59, 0, 127, 0, 1, 0, 0, 46, 0,
+                127, 0, 1, 0, 60, 0, 0, 127, 0, 1, 0, 61, 0, 0, 127, 0, 1, 0,
+                62, 0, 127, 0, 0, 1, 0, 43, 0, 127, 0, 0, 1, 0, 44, 0, 127, 0,
+                1, 0, 0, 42, 0, 127, 0, 1, 0, 0, 41, 0, 127, 0, 1, 0, 45, 0, 0,
+                127, 0, 127, 127, 127, 127, 0, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                0, 0, 0, 0, 0, 0, 0, 0, 0, 247]);
+
+          device(port_name).raw([240, 126, 127, 6, 1, 247]);
+          device(port_name).raw([240, 66, 64, 0, 1, 19, 0, 31, 17, 0, 247]);
+
+          function on(note) { device(port_name).cc(note, 127); }
+          function off(note) { device(port_name).cc(note, 0); }
+          var leds = Array(43, 44, 42, 41, 45);
+          for (var i=0; i<8; i++) {
+            leds.push(64+i);
+            leds.push(48+i);
+            leds.push(32+i);
+            i += 1;
+            leds.push(32+i);
+            leds.push(48+i);
+            leds.push(64+i);
+          }
+                console.log(leds);
+          on(leds[0]);
+          on(leds[1]);
+          on(leds[2]);
+          var led_idx = 2;
+          var interval_id = setInterval(function() {
+            if (led_idx < leds.length) {
+              on(leds[led_idx+1]);
+            }
+            off(leds[led_idx-2]);
+            if (led_idx-1 == leds.length) {
+              clearInterval(interval_id);
+            }
+            led_idx += 1;
+          }, 50);
+        }
     }
 
     port.onstatechange = function(e) { self.onPortStateChange(e); };
@@ -105,33 +172,21 @@ onPortStateChange: function(event) {
 onMIDIMessage: function(message) {
     var port = message.target;
     var data = message.data;
+    console.log(message);
     if (data[0] == 144) { /* touch on */
-      var sample_idx = NANOPAD_TOUCHS[data[1]];
-      var sample_buffer = sample_buffers[sample_idx];
-      var nanotouch = $('.nanotouch')[sample_idx];
-      if (typeof(sample_buffer) != 'undefined') {
-        console.log(samples[sample_idx]);
-        if (typeof(samples[sample_idx]) != 'undefined' && samples[sample_idx].context.state == 'running') {
-          samples[sample_idx].stop(0);
-          samples[sample_idx] = undefined;
-        } else {
-          var sample = audioCtx.createBufferSource();
-          samples[sample_idx] = sample;
-          sample.loop = false;
-          sample.connect(gainNode);
-          sample.buffer = sample_buffer;
-          sample.onended = function() {
-            console.log('ended');
-            $(nanotouch).removeClass('playing');
-            samples[sample_idx] = undefined;
-          }
-          $(nanotouch).addClass('playing');
-          sample.start(0);
-        }
+      var sample_idx = NANOPAD_TOUCHS.indexOf(data[1]);
+      if (sample_idx != -1) {
+        this.onTouchOn(port, data, sample_idx);
       }
     }
+    if (data[0] == 176) { /* control change */
+      this.onControlChange(port, data, data[1], data[2]);
+    }
 },
 
+onTouchOn: function(port, data, sample_idx) {},
+onControlChange: function(port, data, number, value) {}
+
 };
 
 // status bytes on channel 1
@@ -236,34 +291,220 @@ var device = function(outputName) {
    return this;
 };
 
-$(function() {
-  var $nanopad = $('#nanopad');
-
-  $('.nanotouch input').on('change', function(ev) {
-    var nanotouch = $(this).parent();
-    var sample_idx = $nanopad.children().index(nanotouch);
-    var reader = new FileReader();
-    reader.onload = function(e) {
-      audioCtx.decodeAudioData(this.result, function(buffer) {
-        sample_buffers[sample_idx] = buffer;
-        $(nanotouch).find('span.duration').text(parseInt(buffer.duration) + 's');
-        $(nanotouch).removeClass('error').addClass('loaded');
-      }, function(e) {
-        $(nanotouch).find('span').text('');
-        $(nanotouch).removeClass('loaded').addClass('error');
-      });
+var nanofun = function() {
+  var self = this;
+
+  self.initAudio = function() {
+    self.sample_buffers = Array(NANOPAD_TOUCHS.length);
+    self.samples = Array(NANOPAD_TOUCHS.length);
+    self.sample_start_times = Array(NANOPAD_TOUCHS.length);
+    self.audioCtx = new window.AudioContext();
+    self.touchGainNodes = Array(NANOPAD_TOUCHS.length);
+    self.masterGainNode = self.audioCtx.createGain();
+    self.effectsGainNode = self.audioCtx.createGain();
+    for (var i=0; i<NANOPAD_TOUCHS.length; i++) {
+      self.touchGainNodes[i] = self.audioCtx.createGain();
+      self.touchGainNodes[i].connect(self.masterGainNode);
     }
-    reader.readAsArrayBuffer(this.files[0]);
-    $(nanotouch).find('span.name').text(this.files[0].name);
-  });
+    self.masterGainNode.connect(self.audioCtx.destination);
+
+    self.delay = self.audioCtx.createDelay(maxDelayTime=5);
+    self.delay.delayTime.value = 0.5;
+
+    self.feedback = self.audioCtx.createGain();
+    self.feedback.gain.value = 0.8;
+
+    self.filter = self.audioCtx.createBiquadFilter();
+    self.filter.frequency.value = 1000;
 
-  if (navigator.requestMIDIAccess) {
-    navigator.requestMIDIAccess({
-        sysex: true
-    }).then(
+    self.effectsGainNode.connect(self.delay);
+    self.delay.connect(self.feedback);
+    self.feedback.connect(self.filter);
+    self.filter.connect(self.delay);
+
+    self.filter.connect(self.masterGainNode);
+  }
+
+  self.initMIDI = function() {
+    if (navigator.requestMIDIAccess) {
+      navigator.requestMIDIAccess({sysex: true}).then(
         function(midiAccess) { midi.onMIDISuccess(midiAccess); },
         function(e) { midi.onMIDIFailure(e); }
-    );
+      );
+    }
+  }
+
+  self.initUI = function() {
+    var $nanopad = $('#nanopad');
+    var $nanotouch = $('.nanotouch');
+    for (var i=0; i<NANOPAD_TOUCHS.length; i++) {
+      var $new_touch = $nanotouch.clone();
+      $new_touch.attr('data-touch', i);
+      $new_touch.appendTo($nanopad);
+    }
+    $nanotouch.remove(); /* remove template */
+
+    $('.nanotouch input[type=file]').on('change', function(ev) {
+      var sample_idx = parseInt($(this).parent().data('touch'));
+      for (var i=0; i<this.files.length; i++) {
+        var reader = new FileReader();
+        var nanotouch = $('.nanotouch')[sample_idx + i];
+        reader.onload = function(e) {
+          var $nanotouch = $(this.nanotouch);
+          var sample_idx = this.sample_idx;
+          self.audioCtx.decodeAudioData(this.result, function(buffer) {
+            sample_buffers[sample_idx] = buffer;
+            $nanotouch.find('span.duration').data('duration', buffer.duration);
+            $nanotouch.find('span.duration').text(parseInt(buffer.duration) + 's');
+            $nanotouch.removeClass('error').addClass('loaded');
+          }, function(e) {
+            $nanotouch.find('span').text('');
+            $nanotouch.removeClass('loaded').addClass('error');
+          });
+        }
+        reader.nanotouch = nanotouch;
+        reader.sample_idx = sample_idx + i;
+        reader.readAsArrayBuffer(this.files[i]);
+        $(nanotouch).find('span.name').text(this.files[i].name);
+      }
+    });
+
+    midi.onTouchOn = function(port, data, sample_idx) {
+      self.startSample(sample_idx);
+    }
+
+    midi.onControlChange = function(port, data, control, value) {
+      if (control > 7 && control < 16) return; /* range between sliders and pots */
+      if (control >= 32 && control < 40) { /* "S" buttons */
+          var nanotouch = $('.nanotouch')[control-32];
+          if (value == 127) {
+            var checked = $(nanotouch).find('.loop input').prop('checked');
+            if (checked) {
+              $(nanotouch).find('.loop input').prop('checked', false);
+              device("nanoKONTROL2 MIDI 1").cc(control, 0);
+            } else {
+              $(nanotouch).find('.loop input').prop('checked', true);
+              device("nanoKONTROL2 MIDI 1").cc(control, 127);
+            }
+          }
+      }
+      if (control > 23) return; /* after pots */
+      if (control < 8) {
+        control += 8; /* sliders, control bottom pads (8-15) */
+      } else {
+        control -= 16; /* pots, control top pads (0-7) */
+      }
+      $('[data-touch=' + control + '] .touch-gain').val(value).trigger('change');
+    }
+
+    $(document).keypress(function(ev) {
+      var sample_idx = KEYBOARD_CODES.indexOf(ev.key);
+      if (sample_idx != -1) {
+        self.startSample(sample_idx);
+      }
+    });
+
+    $('.effects input').on('change', function() {
+      var effects = $(this).prop('checked');
+      var i = parseInt($(this).parents('.nanotouch').data('touch'));
+      if (effects) {
+        self.touchGainNodes[i].connect(self.effectsGainNode);
+      } else {
+        self.touchGainNodes[i].disconnect(self.effectsGainNode);
+      }
+    });
+
+    $('#master-gain').on('change', function() {
+      var fraction = parseInt(this.value) / parseInt(127);
+      var now = self.audioCtx.currentTime;
+      self.masterGainNode.gain.exponentialRampToValueAtTime(fraction * fraction, now + 0.015);
+    });
+
+    $('#delay').on('change', function() {
+      var value = this.value;
+      if (value == 0) {
+        self.delay.disconnect();
+      } else {
+        var now = self.audioCtx.currentTime;
+        self.delay.delayTime.exponentialRampToValueAtTime(value, now + 0.015);
+        self.delay.connect(self.audioCtx.destination);
+      }
+    });
+
+    $('#feedback').on('change', function() {
+      var now = self.audioCtx.currentTime;
+      self.feedback.gain.exponentialRampToValueAtTime(this.value, now + 0.015);
+    });
+
+    $('#filter').on('change', function() {
+      self.filter.frequency.value = this.value;
+    });
+
+    $('.touch-gain').on('change', function() {
+      var fraction = parseInt(this.value) / parseInt(127);
+      var touchIdx = parseInt($(this).parent().data('touch'));
+      var now = self.audioCtx.currentTime;
+      self.touchGainNodes[touchIdx].gain.exponentialRampToValueAtTime(fraction * fraction, now + 0.015);
+    });
+
+    self.time_interval_id = setInterval(function() {
+      for (var i=0; i<NANOPAD_TOUCHS.length; i++) {
+        var sample = self.samples[i];
+        if (sample !== undefined) {
+          var start_time = self.sample_start_times[i];
+          var current_position = sample.context.currentTime - start_time;
+          var nanotouch = $('.nanotouch')[i];
+          var duration = $(nanotouch).find('span.duration');
+          var total_duration = parseFloat($(duration).data('duration'))
+          $(duration).text(parseInt(total_duration - current_position) + 's');
+        }
+     }
+    }, 250);
   }
 
-});
+  self.startSample = function(sample_idx) {
+    var sample_buffer = self.sample_buffers[sample_idx];
+    var nanotouch = $('.nanotouch')[sample_idx];
+    if (typeof(sample_buffer) != 'undefined') {
+      if (typeof(samples[sample_idx]) != 'undefined' && samples[sample_idx].context.state == 'running') {
+        self.samples[sample_idx].stop(0);
+        self.samples[sample_idx] = undefined;
+      } else {
+        var sample = self.audioCtx.createBufferSource();
+        var gainNode = self.touchGainNodes[sample_idx];
+        self.samples[sample_idx] = sample;
+        sample.loop = ($(nanotouch).find('.loop input:checked').length == 1);
+        sample.connect(gainNode);
+        sample.buffer = sample_buffer;
+        sample.onended = function() {
+          $(nanotouch).removeClass('playing');
+          var duration = $(nanotouch).find('span.duration');
+          $(duration).text(parseInt($(duration).data('duration')) + 's');
+          self.samples[sample_idx] = undefined;
+        }
+        $(nanotouch).addClass('playing');
+        self.sample_start_times[sample_idx] = sample.context.currentTime;
+        sample.start(0);
+      }
+    }
+  }
+
+  self.initAudio();
+  self.initMIDI();
+  self.initUI();
+
+}
+
+$(function() { nanofun(); });
+
+if ('serviceWorker' in navigator) {
+  window.addEventListener('load', function() {
+    navigator.serviceWorker.register('service-worker.js').then(function(registration) {
+      // Registration was successful
+      console.log('ServiceWorker registration successful with scope: ', registration.scope);
+    }).catch(function(err) {
+      // registration failed :(
+      console.log('ServiceWorker registration failed: ', err);
+    });
+  });
+}