cssAudio - Activefile-genericCSS - ActiveGeneric - ActiveHTML - ActiveImage - ActiveJS - ActiveSVG - ActiveText - Activefile-genericVideo - ActiveLovehtmlicon-personicon-teamoctocatpop-outspinnerstartv

Pen Settings

CSS Base

Vendor Prefixing

Add External CSS

These stylesheets will be added in this order and before the code you write in the CSS editor. You can also add another Pen here, and it will pull the CSS from it. Try typing "font" or "ribbon" below.

Quick-add: + add another resource

Add External JavaScript

These scripts will run in this order and before the code in the JavaScript editor. You can also link to another Pen here, and it will run the JavaScript from it. Also try typing the name of any popular library.

Quick-add: + add another resource

Code Indentation

     

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.

            
              <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>
            
          
!
            
              // 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;
    .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;
  }
}
            
          
!
            
              // TODO ---------------------------------------------------------
// [ ] Add ability to record/save performances
// [ ] Make all options changeable via UI
// [ ] Add metronome (probably my Groove-O-Nome pen?)
// [ ] Figure out how to reduce latency (or at least hack the performance more cleanly to be less affected by latency issues)
// [ ] Browser test (note Chris's bug catch in comments)
// [ ] Can we redirect to Https://?

// 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 = 4; // 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. Make sure the URL starts with "https://" instead of "http://"!', 'OK. I\'ll try that!');

}

// 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
Close

Asset uploading is a PRO feature.

As a PRO member, you can drag-and-drop upload files here to use as resources. Images, Libraries, JSON data... anything you want. You can even edit them anytime, like any other code on CodePen.

Go PRO

Loading ..................

Console