Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ add another resource

JavaScript

Babel is required to process package imports. If you need a different preprocessor remove all packages first.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Behavior

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <div class="page-wrap"> 
  <h1>The Repeater Orchestra</h1>
  <div class="orchestra">
    <div class="label -conductor">The Conductor (You)</div>
    <div class="label -repeaters">Repeaters</div>
  </div>
</div>

<div class="controls">
  <h2>Controls</h2>
  <button class="gate-open">Turn Mic On/Off</button>
  <div class="slider-wrap">
    <div class="slider micGain"></div>
    <div class="rot -left">Mic</div>
  </div>
  <div class="slider-wrap">
    <div class="slider masterGain"></div>
    <div class="rot -right">Orch</div>
  </div>
  <button class="clear-delays">Restart Repeaters</button>
</div>

<a href="https://codepen.io/poopsplat/post/codepen-chicago-june-2016" target="_blank" class="example">Video Example <span class="fake-link">here</span></a>

<div class="loading"><h1>The Repeater Orchestra (1.0)</h1></div>
              
            
!

CSS

              
                // Box-sizing
*, *::before, *::after {
  box-sizing: inherit;
}
html {
  box-sizing: border-box;
  font-size: 16px; //rem
}

// Fonts
@import url(https://fonts.googleapis.com/css?family=Share+Tech+Mono);

// Colors
$blue: rgb(18, 104, 207);
$black: rgb(20,20,20);
$gray: rgb(150,150,150);
$lite-gray: rgb(180,180,180);
$white: rgb(255,255,255);

// Base
body {
  background: $blue;
  text-align: center;
  text-transform: uppercase;
  @extend %font;
}
%font {
  color: $white;
  font-family: 'Share Tech Mono';
  letter-spacing: -0.05em;
}
%stroke {
    text-shadow:
   -2px -2px 0 $black,  
    2px -2px 0 $black,
    -2px 2px 0 $black,
     2px 2px 0 $black;
}
h1, h2 {
  @extend %stroke;
  margin-top: 0;
}
.page-wrap {
  padding-top: 1rem;
  width: 40rem;
  margin: auto;
}

// Orchestra
.orchestra {
  position: relative;
  margin: auto;
  width: 100%;
  height: 20rem;
  background: rgba($black,.2);
  margin: 8rem 0 10rem;
  .label {
    position: absolute;
    &.-repeaters{
      top: 1rem;
      left: 41rem;
    }
    &.-conductor{
      top: -4rem;
      left: 26rem;
    }
  }
}
canvas {
  background: $blue;
  transform: translate(-50%,0); // Thus, our bottom/left refers to the center
  position: absolute;
  border: 2px solid $white;
  border-radius: 100%;
  @extend %stroke;
}
#conductor{
  top: 0;
  left: 50%;
  transform: translate(-50%,-50%);
}

// UI
.controls {
  position: fixed;
  padding: 1em;
  left: 0;
  bottom: 0;
  background: $blue;
  border-top: 2px solid $white;
  border-right: 2px solid $white;
  opacity: 0.5;
  &:hover { opacity: 1; }
}

// Buttons
%button-hover {
  &:hover{
    cursor: pointer;
    background: $lite-gray;
  }
}
button{
  position: relative;
  border: $white 2px solid;
  background-color: $black;
  color: $white;
  font-family: 'Share Tech Mono';
  letter-spacing: -0.1em;
  text-align: center;
  text-transform: uppercase;
  @extend %button-hover;
  display: block;
  margin: auto;
  padding: .4rem;
  &.on{
    background-color: $white;
    color: black;
    border-color: $black;
  }
}

// Sliders
.slider-wrap {
  display: inline-block;
  margin: 1rem .5rem;
}
.slider {
  border: $black 2px solid;
  border-radius: 0;
  background-color: $black;
  background-image: none;
  display: inline-block;
  height: 6rem;
  &:hover{
    cursor: pointer;
  }
  .ui-slider-range {
    border-radius: 0;
    background-color: $white;
    background-image: none;
  }
  .ui-slider-handle {
    border: $black 2px solid;
    border-radius: 0;
    background-color: $white;
    background-image: none;
    // transition: transform 0.2s;
    &:hover{
      cursor: pointer;
    }
  }
}

// Alert
.alert {
  position: fixed;
  top: 0;
  right: 0;
  left: 0;
  height: 100%;
  background: rgba($white, 0.8);
  .content {
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%,-50%);
    width: 500px;
    background: $blue;
    border: 2px solid $black;
    padding: 2em;
    z-index: 100;
    a {
      color: $white;
    }
    .message {
      // float: left;
      text-align: left;
      display: inline-block;
      width: 300px;
    }
    .icon {
      float: left;
      width: 100px;
      height: auto;
      margin-right: 2em;
    }
    button {
      clear: both;
      display: block;
      margin: 2em auto 0;
    }
  }
}

// Example Performance
.example {
  position: fixed;
  bottom: 0;
  left: 100%;
  padding: 1rem;
  width: 200px;
  background: $blue;
  display: inline-block;
  color: white;
  text-decoration: none;
  .fake-link {
    text-decoration: underline;
  }
  border-top: 2px $white solid;
  border-right: 2px $white solid;
  transform: rotate(270deg);
  transform-origin: 0% 100%;
  opacity: 0.5;
  &:hover{ opacity: 1; }
}

//Loading
.loading{
  position: fixed;
  z-index: 100;
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 100%;
  background-color: $blue;
  transition: opacity 0.2s, visibility 0s 0.2s;
  h1 {
    position: absolute;
    top: 40%;
    left: 50%;
    transform: translate(-50%,-50%);
    text-shadow: none;
    color: $black;
  }
}
              
            
!

JS

              
                // TODO ---------------------------------------------------------
// [ ] Make all options changeable via UI: n-repeaters, tempo, latency
// [ ] Latency reduction should be additive not multiplicative, right?
// [ ] Add links for how to record yourself!

// GLOBALS ---------------------------------------------------------

// Options
_nDelays = 50; // Number of Repeaters
_tempo = 80; // Global tempo (bpm)
_maxEighths = 60; // Max eigth notes a repeater can be delayed.
_minGain = 0.2; // Min gain multiple for a repeater.
_maxGain = 2; // Max gain multiple for a repeater.
_gateOpen = false; // Is the mic "on"?
_micGainStart = 0.5; // Starting mic gain
_masterGainStart = 0.5; // Starting gain of Orchestra
_monitorGainStart = 4; // How much louder should the mic feed be than the repeaters? (Monitor)
_latencyTune = 0; // A slight crunching of our delay times to help account for latency

// Calculations
var eighthTime = (60 / _tempo / 2)*(1-_latencyTune/1000.0);  // This _latencyTune thing is HACKY
var gainRange = _maxGain - _minGain;

// CUSTOM USER ALERTS ---------------------------------------------------------  
function customAlert(msg, btn, svg) {
  var btn = btn || 'Word';
  var svg = svg || 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/385326/achtung.svg';
  if ($('.alert').length) { // If there is already an alert, we're gonna try again with this message in a short while.
    setTimeout(function() {
      customAlert(msg, btn);
    }, 100);
  } else {
    $('<div class="alert"><div class="content"><img src="' + svg + '" class="icon"><div class="message">' + msg + '</div><button>' + btn + '</button></div></div>').appendTo('body');
    $('.alert button').click(function(e) {
      e.preventDefault();
      $('.alert').remove();
    })
  }
}

// REQUEST USER AUDIO INPUT STREAM ---------------------------------------------------------
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia;
navigator.getUserMedia({
  audio: {
    latency: 0, //Does this do anything?
    sampleSize: 128 //Does this do anything? Probably not....
  }
}, gotStream, streamErr);

// If error getting input...
function streamErr() {

  // Clear loading screen
  $('.loading').remove();

  // Clear the stage, because nothing will work.
  $('body *:not(h1)').remove();
  
  // Give the user some advice
  customAlert('Couldn\'t get the mic. Likely, you need to give this site permission to use your mic.  You may have been prompted.  <a href="https://support.google.com/chrome/answer/2693767?hl=en-GB">This article</a> might help. Then refresh the page.', 'OK. I\'ll figure it out and refresh!');

}

// If success...
function gotStream(stream) {

  // Clear loading screen
  $('.loading').remove();

  // Alert user to wear headphones!!!
  customAlert('If your mic is near your speakers (like on most computers), this thing is gonna make gnarly feedback.  Unless you have a fancy schmancy mic/speaker setup, you should put on headphones to enjoy this.', 'Word. I put on headphones!', 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/385326/headphones.svg');

  // AUDIO CONTEXT ---------------------------------------------------------  
  // Build the audio context.  This is from where all the magic of web audio stems.
  window.AudioContext = window.AudioContext || window.webkitAudioContext;
  _cxt = new AudioContext();

  // PRIMARY INPUT/OUTPUT CHAIN ---------------------------------------------------------
  // _mic -> _micGain -> _monitorGain -> _masterGain -> _compressor -> destination

  // Create + connect _mic
  _mic = _cxt.createMediaStreamSource(stream);

  // Create + connect _micGain -- This controls mic volume (it will also be how we "turn the mic off")
  _micGain = _cxt.createGain(); // Create
  _micGain.gain.value = _micGainStart * _gateOpen;
  _mic.connect(_micGain); // Chain _mic to _micGain

  // Create + connect _monitorGain -- Makes the mic chain louder than the delay chains so you can better monitor.
  _monitorGain = _cxt.createGain(); 
  _monitorGain.gain.value = _monitorGainStart;
  _micGain.connect(_monitorGain); // Chain

  // Create + connect _masterGain -- Volume of entire orchestra
  _masterGain = _cxt.createGain();
  _masterGain.gain.value = _masterGainStart;
  _monitorGain.connect(_masterGain); // Chain

  // Create + connect _compressor -- Compress me because I hate clipping
  _compressor = _cxt.createDynamicsCompressor(); // The defaults for this are good.
  _masterGain.connect(_compressor); // Chain

  // Finally, connect us to the destination (where the audio is outputted)
  _compressor.connect(_cxt.destination);

  // DELAY CHAINS ---------------------------------------------------------
  // We need to another chain for each of our _delays:
  // _mic -> _micGain -> _delays[i] -> _gains[i] -> _panners[i] -> _masterGain -> _compressor -> destination

  // Make some empty arrays to fill
  _delays = new Array();
  _delayTimes = new Array();
  _gains = new Array();
  _panners = new Array();

  for (i = 0; i < _nDelays; i++) {

    // Create delay object
    _delays[i] = _cxt.createDelay(_maxEighths * eighthTime * 1.1);
    // Asign a delayTime of some random integer (< _maxEighths) number of eighth notes
    var nEighths = Math.ceil(Math.random() * _maxEighths); // integer num of eights up to max Eighths
    _delayTimes[i] = (nEighths) * eighthTime; // Calculate time for this delay and store it
    _delays[i].delayTime.value = _delayTimes[i]; // Apply delay time.
    // Chain (_mic is already connected _micGain)
    _micGain.connect(_delays[i]);

    // Create gain node
    _gains[i] = _cxt.createGain();
    // Set random gain up to within gain r
    _gains[i].gain.value = Math.random() * gainRange + _minGain;
    // Chain
    _delays[i].connect(_gains[i]);

    // Create pan node
    _panners[i] = _cxt.createStereoPanner();
    // Set random pan
    _panners[i].pan.value = (i % 10) / 5 - 1;
    // Chain
    _gains[i].connect(_panners[i]);

    // Connect _masterGain (and therefore out to _compressor and destination)
    _panners[i].connect(_masterGain);
  }

  // INIT VISUALIZER ---------------------------------------------------------
  // With more than a little help from: https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Visualizations_with_Web_Audio_API

  // Objects / Object Arrays
  var analysers = new Array();
  var canvases = new Array();
  
  orchW = $('.orchestra').width();
  orchH = $('.orchestra').height();
  numInRow = 10;
  repW = (orchW / numInRow);

  // Initiate and place canvases for all repeaters
  for (i = 0; i < _nDelays; i++) {

    // Use the awesome power of arithmetic to put things in places
    $('.orchestra').append('<canvas id="repeater-' + i + '" width="' + (repW - 10) + '" height="' + (repW - 10) + '">');
    var $canvas = $('#repeater-' + i);
    var x = (((1 / numInRow) / 2) + (i % numInRow) * (1 / numInRow)) * orchW + (Math.floor(Math.random() * 8) - 4);
    var y = (-Math.sin(x * Math.PI / orchW) + 1) * (orchH / 2) - ((Math.floor(i / numInRow) - 2) * repW); // Who remembers high school trig? This guy remembers high school trig.
    $canvas.css('left', x);
    $canvas.css('bottom', y); // Todo -- don't do this with JS, SCSS can probably handle this.  And then everyone will be happier.
    canvases[i] = $canvas[0].getContext('2d');

    // Create analyzer and connect to _micGain
    analysers[i] = _cxt.createAnalyser(); // Create
    _gains[i].connect(analysers[i]); // Chain 

    // Set up an array to house data collected from analyser.
    analysers[i].fftSize = 1024;
  }

  // Do same for conductor (but he's bigger, and we'll just place him in CSS)
  $('.orchestra').append('<canvas id="conductor" width="' + (repW * 3) + '" height="' + (repW * 3) + '">');
  conductorCanvas = $('#conductor')[0].getContext('2d');
  conductorAnalyser = _cxt.createAnalyser();
  _micGain.connect(conductorAnalyser);

  // Build array to house analysis data
  var bufferLength = analysers[0].frequencyBinCount;
  var dataArray = new Float32Array(bufferLength);

  // ANIMATE OSCILLOSCOPES
  function draw() { //Our drawing function to be called every frame

    drawVisual = requestAnimationFrame(draw); // Keep calling this from now on.

    for (j = 0; j < _nDelays + 1; j++) {

      if (j < _nDelays) {
        analysers[j].getFloatTimeDomainData(dataArray); // Get analyser data
        var canvasToDraw = canvases[j];
        var canvasWidth = (repW - 10); // Get width and height
        var canvasHeight = (repW - 10);
      } else {
        conductorAnalyser.getFloatTimeDomainData(dataArray); // Get analyser data
        var canvasToDraw = conductorCanvas;
        var canvasWidth = (repW * 3); // Get width and height
        var canvasHeight = (repW * 3);
      }

      canvasToDraw.clearRect(0, 0, canvasWidth, canvasHeight); // Clear canvas
      canvasToDraw.strokeStyle = 'rgb(255,255,255)'; // Style the line
      canvasToDraw.lineWidth = 2;

      canvasToDraw.beginPath(); // Draw the line
      var sliceWidth = canvasWidth * 1.0 / bufferLength;
      var x = 0;
      for (var i = 0; i < bufferLength; i++) {
        var v = dataArray[i] * (canvasHeight / 2) * 40;
        var y = v + canvasHeight / 2;
        if (i === 0) { //Either start or continue line
          canvasToDraw.moveTo(x, y);
        } else {
          canvasToDraw.lineTo(x, y);
        }
        x += sliceWidth;
      }
      canvasToDraw.lineTo(canvasWidth, canvasHeight / 2); // Finish in the right spot
      canvasToDraw.stroke(); // Stroke it (T.W.S.S.)
    }
  }
  draw(); // Start her up! Now we're drawing!

  // UI ---------------------------------------------------------
  // Make sliders
  // For DRY sake, these are the base options:
  baseOptions = {
      min: 0,
      max: 1,
      range: 'min',
      step: 0.01,
      orientation: 'vertical'
    }
    // Make slider to control _masterGain's level (for options, duplicate and extend baseOptions object to include slide callback)
  $('.slider.masterGain').slider($.extend(true, {}, baseOptions, {
    value: _masterGain.gain.value,
    change: function(event, ui) {
      _masterGain.gain.linearRampToValueAtTime(ui.value, _cxt.currentTime + 0.2); //We want to set this with a ramp, to make the change gradual and remove potential pops
    }
  }));
  // Ditto for _micGain
  $('.slider.micGain').slider($.extend(true, {}, baseOptions, {
    value: _micGainStart,
    change: function(event, ui) {
      _micGain.gain.linearRampToValueAtTime(ui.value * _gateOpen, _cxt.currentTime + 0.2);
    }
  }));

  // Gate Open Button
  _gateOpenToggle = function() {
    if (_gateOpen) {
      $('.gate-open').removeClass('on');
      _gateOpen = false;
    } else {
      $('.gate-open').addClass('on');
      _gateOpen = true;
    }
    setTimeout(function() {
      _micGain.gain.linearRampToValueAtTime($('.slider.micGain').slider('value') * _gateOpen, _cxt.currentTime + 0.2);
    }, 500);
  }
  $('.gate-open').click(function(e) {
    e.preventDefault();
    _gateOpenToggle();
  });
  $(window).keyup(function(e) {
    e.preventDefault();
    if (e.which === 32) _gateOpenToggle();
  });

  // Clear Delays
  $('.clear-delays').click(function(e) {
    // Force Mute.  This will help us avoid pops from delays coming in and out of being
    _micGain.gain.linearRampToValueAtTime(0, _cxt.currentTime + 0.05);
    setTimeout(function() { // A bit after we mute
      for (i = 0; i < _nDelays; i++) {
        // Close mic gate
        _gateOpen = false;
        $('.gate-open').removeClass('on');
        // Disconnect old gain object
        _delays[i].disconnect();
        _delays[i] = null; // I'm doing this in the hopes that it helps JS collect my garbage.  I don't know if it's working.  I worry these delay objects are just piling up somewhere.
        // Create new delay object to replace it.
        _delays[i] = _cxt.createDelay(_maxEighths * eighthTime * 1.1);
        // Give it its old delay time
        _delays[i].delayTime.value = _delayTimes[i];
        // Rechain
        _micGain.connect(_delays[i]);
        _delays[i].connect(_gains[i]);
      }
    }, 200);
    // Schedule unmuting (but gate stays closed)
    setTimeout(function() {
      _micGain.gain.linearRampToValueAtTime($('.slider.micGain').slider('value') * _gateOpen * 1, _cxt.currentTime + 1);
    }, 500);
  });

}
              
            
!
999px

Console