Skip to content

Instantly share code, notes, and snippets.

@oaubert
Created June 24, 2020 11:26
Show Gist options
  • Save oaubert/09d94dfc14724711ff5f55580a79a3b4 to your computer and use it in GitHub Desktop.
Save oaubert/09d94dfc14724711ff5f55580a79a3b4 to your computer and use it in GitHub Desktop.
Repeater Orchestra (1.0)
<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>

Repeater Orchestra (1.0)

Create an orchestra of yourself. An orchestra of Repeaters repeat The Conductor (live mic feed) each with a randomized delay time (N eighth notes), volume and spatial distribution. Used to perform Terry Riley's composition "In C" at Codepen meetups.

Performed live at codepen meetup: http://codepen.io/poopsplat/post/codepen-chicago-june-2016

NEEDS FINISHING! HALP ME!

A Pen by Bryant Smith on CodePen.

License.

// 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);
});
}
<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js"></script>
<script src="https://cdn.webrtc-experiment.com/MediaStreamRecorder.js"></script>
// 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;
}
}
<link href="https://code.jquery.com/ui/1.11.4/themes/smoothness/jquery-ui.css" rel="stylesheet" />
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment