|
// 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); |
|
}); |
|
|
|
} |