a graphic study of the Oscillators available on the Web Audio API.
Forked from nicolas barradeau's Pen Web Audio API: Oscillator study.
A Pen by Alex Vazquez on CodePen.
<div class="gradientOverlay"></div> | |
<div id="container"> | |
<canvas id="canvas"></canvas> | |
<div class="params"> | |
<div class="controls"> | |
<input type="radio" name="wave" value="0" id="w0" checked onchange="setWave(this.value)"onclick="setWave(this.value)"><label for="w0">sine</label><br/> | |
<input type="radio" name="wave" value="1" id="w1" onchange="setWave(this.value)"onclick="setWave(this.value)"><label for="w1">square</label><br/> | |
<input type="radio" name="wave" value="2" id="w2" onchange="setWave(this.value)"onclick="setWave(this.value)"><label for="w2">sawtooth</label><br/> | |
<input type="radio" name="wave" value="3" id="w3" onchange="setWave(this.value)"onclick="setWave(this.value)"><label for="w3">triangle</label> | |
</div> | |
<div class="controls"> | |
<input type="checkbox" value="0" id="p0" checked onchange="startStop(this.value)"><br/> | |
<input type="checkbox" value="1" id="p1" checked onchange="startStop(this.value)"><br/> | |
<input type="checkbox" value="2" id="p2" checked onchange="startStop(this.value)"><br/> | |
<input type="checkbox" value="3" id="p3" checked onchange="startStop(this.value)"> | |
</div> | |
<div class="controls" style="width:50%;"> | |
frequency | |
<br/> | |
<input type="range" id="frequencyInput" name="frequencyInput" min="1" max="440" value="1" step ="1" onchange="setFrequency(this)" style="width:90%"> | |
<label id="frequencyRate"></label> | |
<br/> | |
detune | |
<br/> | |
<input type="range" id="detuneInput" name="detuneInput" min="-5000" max="5000" value="0" step="0.01" onchange="setDetune(this)" style="width:90%"> | |
<label id="detuneRate"></label> | |
</div> | |
<div class="controls"> | |
gain | |
<br> | |
<input type="range" id="gainInput" name="gainInput" min="0.00" max="1" value=".5" step="0.01" onchange="setGain(this)" onclick="setGain(this)"> | |
<label id="gainRate"></label> | |
<br/> | |
<button id="reset" value="reset" onmousedown="reset()">reset</button> | |
<button id="randomize" value="randomize" onmousedown="randomize()">randomize</button> | |
</div> | |
</div> | |
</div> | |
<div class="blablah"> | |
<h1>oscillator study</h1> | |
<div style="width: 250px; text-align: justify;"> | |
<p> | |
testing the <a href="http://www.w3.org/TR/webaudio/">Web Audio API</a> | |
so this works on <a href="http://caniuse.com/#search=web%20audio%20api">a restricted set</a> of browsers. you should see the animations on recent browsers though (works on IE11, Chrome, FF, Opera & Safari) | |
</p> | |
<p> | |
the curves correspond to the default periodic functions available in the API. | |
more info and the formulas here: <a href="http://en.wikibooks.org/wiki/Sound_Synthesis_Theory/Oscillators_and_Wavetables">sound synthesis article on wikipedia</a> | |
</p> | |
<p> | |
use <input type="radio" id="r_gizmo"><label for="r_gizmo"> select this waveform</label><br/> | |
use <input type="checkbox" id="c_gizmo" checked ><label for="c_gizmo"> mute / unmute this waveform</label><br/> | |
</p> | |
<p> | |
you've got to love 8bit music to stay here for more than 10 seconds :) | |
</p> | |
</div> | |
</div> |
a graphic study of the Oscillators available on the Web Audio API.
Forked from nicolas barradeau's Pen Web Audio API: Oscillator study.
A Pen by Alex Vazquez on CodePen.
/** | |
* Created by nico on 25/01/14. | |
*/ | |
var actx, | |
c = document.getElementById('canvas'); | |
ctx = c.getContext('2d'), | |
size = c.width = Math.min( window.innerWidth, window.innerHeight ), | |
c.height = size; | |
waves = [], | |
PI = Math.PI, | |
PI2 = PI * 2; | |
var raf = window.requestAnimationFrame || | |
window.webkitRequestAnimationFrame || | |
window.mozRequestAnimationFrame || | |
window.msRequestAnimationFrame || | |
window.oRequestAnimationFrame || | |
function(func) { setTimeout( func, 1000 / 60 ); }; | |
var selectedWave; | |
var gain; | |
//background noise | |
var bg = document.createElement("canvas"); | |
bg.width = bg.height = 128; | |
var bg_ctx = bg.getContext( "2d" ); | |
bg_ctx.fillStyle = "#f00"; | |
bg_ctx.fillRect(0,0,100,100); | |
var img = bg_ctx.getImageData(0,0,128,128); | |
var data = img.data; | |
for( var i = 0; i < data.length; i += 4 ) | |
{ | |
var val = 0xCC + ( parseInt( Math.random() * 0x33 )); | |
data[ i ] = data[ i + 1 ] = data[ i + 2 ] = val; | |
data[ i + 3 ] = 255; | |
} | |
bg_ctx.putImageData( img,0,0 ); | |
document.body.style.backgroundImage = "url(" + bg.toDataURL("image/png")+ ")"; | |
///////////////////////////////// | |
var RGB = function( r,g,b ) | |
{ | |
this.r = r; | |
this.g = g; | |
this.b = b; | |
//from http://www.javascripter.net/faq/rgbtohex.htm | |
this.toHexString = function() | |
{ | |
return this.toHex( this.r ) + this.toHex( this.g ) + this.toHex( this.b ); | |
} | |
this.toHex = function toHex(n) | |
{ | |
n = parseInt( n, 10 ); | |
if ( n == 0 || isNaN( n )) return "00"; | |
n = Math.max( 0, Math.min( n, 255 ) ); | |
return "0123456789ABCDEF".charAt( ( n - n % 16 ) / 16 ) + "0123456789ABCDEF".charAt( n % 16 ); | |
} | |
} | |
var Wave = function( frequency, detune, gain, method ) | |
{ | |
this.x = 0; | |
this.y = 0; | |
this.dest = | |
{ | |
frequency: frequency, | |
detune: detune, | |
gain: gain | |
}; | |
this.color = null; | |
this.playing = false; | |
this.process = null; | |
this.method = method || 0; | |
switch( method ) | |
{ | |
default: | |
case Wave.SINE: | |
this.process = function ( n ){ return Math.sin( n * PI2 ); }; | |
this.color = new RGB( 255, 64,0 ); | |
break; | |
case Wave.SQUARE: | |
this.process = function ( n ){ return Math.sin( n * PI2 ) > 0 ? 1 : -1; }; | |
this.color = new RGB( 0, 99, 204 ); | |
break; | |
case Wave.SAWTOOTH: | |
this.process = function ( n ){ return ( n - Math.floor( n +.5 ) ) * 2; }; | |
this.color = new RGB( 255, 204, 0 ); | |
break; | |
case Wave.TRIANGLE: | |
this.process = function ( n ){ return ( 1 - Math.abs( n - Math.floor( n + .5 ) ) * 4 ); }; | |
this.color = new RGB( 0, 255, 102 ); | |
break; | |
case Wave.NOISE: | |
this.process = function ( n ){ return ( ( Math.random() -.5 ) * 2 ); }; | |
this.color = new RGB( 0, 102, 153 ); | |
break; | |
} | |
this.render = function( normalTime, ctx ) | |
{ | |
var t = normalTime; | |
var max = t + 1; | |
var step = 1 / 250; | |
var m = Math.sqrt( this.gain.gain.value ); | |
var radiusX = .45 + .35 * ( 1 - m ); | |
var radiusY = .12 * m; | |
ctx.beginPath(); | |
ctx.strokeStyle = "rgba( "+ this.color.r +","+ this.color.g +","+ this.color.b +","+ 1 +")"; | |
ctx.fillStyle = "rgba( "+ this.color.r +","+ this.color.g +","+ this.color.b +","+ this.gain.gain.value * .5 +")" | |
for( var t = normalTime; t < max; t += step ) | |
{ | |
this.x = Math.cos( t * PI2 ) * radiusX; | |
this.y = ( -.5 + this.method * ( 3 * .125 ) ) + ( this.process( ( normalTime + t ) * this.osc.frequency.value ) + Math.sin( t * PI2 ) ) * radiusY; | |
ctx.lineTo( this.x, this.y ); | |
} | |
ctx.closePath(); | |
ctx.fill(); | |
ctx.stroke(); | |
for( t = 0; t < 1; t += .25 ) | |
{ | |
this.x = Math.cos( t * PI2 ) * radiusX; | |
this.y = ( -.5 + this.method * ( 3 *.125 ) ) + ( this.process( ( normalTime + t ) * this.osc.frequency.value ) + Math.sin( t * PI2 ) ) * radiusY; | |
ctx.beginPath(); | |
ctx.arc( this.x, this.y, 0.01, 0, PI2 ); | |
ctx.stroke(); | |
} | |
} | |
//create the oscillator and gain objects | |
this.rebuildOSC = function() | |
{ | |
if( actx ) | |
{ | |
if( this.gain == null ) | |
{ | |
this.gain = actx.createGain(); | |
this.gain.connect( actx.destination ); | |
this.gain.gain.value = gain; | |
} | |
else | |
{ | |
this.osc.disconnect(); | |
} | |
this.osc = actx.createOscillator(); | |
this.osc.connect( this.gain ); | |
this.osc.type = method; | |
this.osc.frequency.value = frequency; | |
this.osc.detune.value = detune; | |
this.osc.start( 0 ); | |
this.playing = true; | |
} | |
//generic object to handle graphics only (unicorn-mode) | |
else | |
{ | |
this.osc = { | |
frequency: { value: frequency }, | |
detune: { value: frequency }, | |
start : function(value){}, | |
stop : function(value){} | |
}; | |
this.gain = { | |
gain: { value:gain } | |
}; | |
} | |
} | |
this.rebuildOSC(); | |
} | |
Wave.SINE = 0; | |
Wave.SQUARE = 1; | |
Wave.SAWTOOTH = 2; | |
Wave.TRIANGLE = 3; | |
///////////////////////////////// | |
function update() | |
{ | |
ctx.restore(); | |
size = c.width = Math.min( window.innerWidth, window.innerHeight ), | |
c.height = size; | |
container.style.width = size + "px"; | |
var time = Date.now() * 0.0001; | |
ctx.clearRect( 0,0, c.width, c.height ); | |
//normalized unit | |
var nu = 1 / size; | |
//normalized time : -1 >= NT >= 1 | |
normalTime = -1 + ( time % 1 ) * 2; | |
ctx.save(); | |
ctx.scale( c.width / 2 , c.height / 2 ); | |
ctx.translate( 1 + nu, 1 + nu ); | |
ctx.lineCap = "round"; | |
ctx.lineWidth = nu * 4; | |
for( var i =0; i < waves.length; i++) | |
{ | |
var w = waves[ i ]; | |
w.render( normalTime, ctx ); | |
w.osc.frequency.value += ( w.dest.frequency - w.osc.frequency.value ) *.1; | |
w.osc.detune.value += ( w.dest.detune - w.osc.detune.value ) *.1; | |
w.gain.gain.value += ( w.dest.gain - w.gain.gain.value ) *.1; | |
} | |
updateSettings(); | |
raf( update ); | |
} | |
function init() | |
{ | |
actx = window['AudioContext'] ? new AudioContext() : window['webkitAudioContext'] ? new webkitAudioContext() : null; | |
if( !actx ) | |
{ | |
console.log( 'NOOooooooooOOOOooooOOOOOHH !! ! !\n couldn\'t find the AUDIO CONTEXT....\n QUICK ! \n' ); | |
console.log( " / \n .7 \n \ , // \n |\.--._/|// \n /\ ) ) ).'/ \n /( \ // / \n /( J`((_/ \ \n / ) | _\ / \n /|) \ eJ L \n | \ L \ L L \n / \ J `. J L \n | ) L \/ \ \n / \ J (\ / \n _....___ | \ \ \``` \n ,.._.-' '''--...-||\ -. \ \ \n .'.=.' ` `.\ [ Y \n / / \] J \n Y / Y Y L \n | | | \ | L \n | | | Y A J \n | I | /I\ / \n | \ I \ ( |]/| \n J \ /._ / -tI/ | \n L ) / /'-------'J `'-:. \n J .' ,' ,' , \ `'-.__ \ \n \ T ,' ,' )\ /| ';'---7 / \n \| ,'L Y...-' / _.' / \ / / \n J Y | J .'-' / ,--.( / \n L | J L -' .' / | /\ \n | J. L J .-;.-/ | \ .' / \n J L`-J L____,.-'` | _.-' | \n L J L J `` J | \n J L | L J | \n L J L \ L \ \n | L ) _.'\ ) _.'\ \n L \('` \ ('` \ \n ) _.'\`-....' `-....' \n ('` \ \n `-.___/ sk "); | |
console.log( " UNICORN FALLBACK!!!\n\n\n "); | |
} | |
var container = document.getElementById( "container" ); | |
container.style.width = size + "px"; | |
ctx.fillStyle = "#FFF"; | |
ctx.fillRect( 0,0,size, size ); | |
waves.push( new Wave( 12, 2393, 1, Wave.SINE ) ); | |
waves.push( new Wave( 4, 0, .65, Wave.SQUARE ) ); | |
waves.push( new Wave( 1, 0, .25, Wave.SAWTOOTH ) ); | |
waves.push( new Wave( 240, 0, .3, Wave.TRIANGLE ) ); | |
setWave( 0 ); | |
} | |
///////////////////////////////////////// | |
// UI | |
///////////////////////////////////////// | |
function setWave( value ) | |
{ | |
selectedWave = waves[ parseInt( value ) ]; | |
updateSettings(); | |
} | |
function setFrequency( element ) | |
{ | |
selectedWave.osc.frequency.value = selectedWave.dest.frequency = element.value; | |
updateSettings(); | |
} | |
function setDetune( element ) | |
{ | |
selectedWave.osc.detune.value = selectedWave.dest.detune = element.value; | |
updateSettings(); | |
} | |
function setGain( element ) | |
{ | |
selectedWave.gain.gain.value = selectedWave.dest.gain = element.value; | |
updateSettings(); | |
} | |
function startStop( value ) | |
{ | |
//alternative : rebuild the oscillator | |
//they will run out of sync | |
if( waves[ value ].playing ) | |
{ | |
// waves[ value ].osc.stop(0); | |
waves[ value ].dest.gain = 0; | |
waves[ value ].playing = false; | |
} | |
else | |
{ | |
// waves[ value ].rebuildOSC(); | |
waves[ value ].dest.gain = 1; | |
waves[ value ].playing = true; | |
} | |
setWave( value ); | |
} | |
function reset() | |
{ | |
waves.forEach( function( w ) | |
{ | |
selectedWave = w; | |
selectedWave.osc.frequency.value = selectedWave.dest.frequency =1; | |
selectedWave.osc.detune.value = selectedWave.dest.detune =0; | |
selectedWave.gain.gain.value = selectedWave.dest.gain =1; | |
updateSettings(); | |
}); | |
setWave( 0 ); | |
} | |
function randomize() | |
{ | |
waves.forEach( function( w ) | |
{ | |
if( w.playing ) | |
{ | |
w.dest.frequency = Math.max( 1, Math.min( w.dest.frequency + parseInt( ( Math.random() -.5 ) * 10 ), 440 ) ); | |
w.dest.detune = Math.max( -5000, Math.min( w.dest.detune + parseInt( ( Math.random() -.5 ) * 20 ), 5000 ) ); | |
w.dest.gain += ( Math.random() -.5 ) * .1; | |
} | |
}); | |
} | |
function updateSettings() | |
{ | |
document.getElementById( "frequencyInput").value = selectedWave.osc.frequency.value; | |
document.getElementById( "frequencyRate").innerHTML = ""+ parseInt( selectedWave.osc.frequency.value, 10 ); | |
document.getElementById( "detuneInput").value = selectedWave.osc.detune.value; | |
document.getElementById( "detuneRate").innerHTML = ""+ parseInt( selectedWave.osc.detune.value, 10 ); | |
document.getElementById( "gainInput").value = selectedWave.gain.gain.value; | |
document.getElementById( "gainRate").innerHTML = ""+ parseFloat( selectedWave.gain.gain.value).toFixed( 2 ); | |
var radio = document.getElementsByName( "wave" ); | |
for( var i =0; i< radio.length; i++ ) | |
{ | |
radio.item( i ).checked = radio.item( i ).value == selectedWave.method; | |
} | |
}; | |
///////////////////////////////////////// | |
// GO UNICORN GO ! | |
///////////////////////////////////////// | |
init(); | |
raf( update ); |
html, body | |
{ | |
width:100%; | |
height:100%; | |
overflow:hidden; | |
background-color: #303030; | |
margin : 0; | |
} | |
.gradientOverlay | |
{ | |
background: -moz-linear-gradient(left, rgba(255,255,255,0.01) 0%, rgba(255,255,255,0.01) 1%, rgba(255,255,255,1) 35%, rgba(255,255,255,1) 65%, rgba(255,255,255,0) 100%); /* FF3.6+ */ | |
background: -webkit-gradient(linear, left top, right top, color-stop(0%,rgba(255,255,255,0.01)), color-stop(1%,rgba(255,255,255,0.01)), color-stop(35%,rgba(255,255,255,1)), color-stop(65%,rgba(255,255,255,1)), color-stop(100%,rgba(255,255,255,0))); /* Chrome,Safari4+ */ | |
background: -webkit-linear-gradient(left, rgba(255,255,255,0.01) 0%,rgba(255,255,255,0.01) 1%,rgba(255,255,255,1) 35%,rgba(255,255,255,1) 65%,rgba(255,255,255,0) 100%); /* Chrome10+,Safari5.1+ */ | |
background: -o-linear-gradient(left, rgba(255,255,255,0.01) 0%,rgba(255,255,255,0.01) 1%,rgba(255,255,255,1) 35%,rgba(255,255,255,1) 65%,rgba(255,255,255,0) 100%); /* Opera 11.10+ */ | |
background: -ms-linear-gradient(left, rgba(255,255,255,0.01) 0%,rgba(255,255,255,0.01) 1%,rgba(255,255,255,1) 35%,rgba(255,255,255,1) 65%,rgba(255,255,255,0) 100%); /* IE10+ */ | |
background: linear-gradient(to right, rgba(255,255,255,0.01) 0%,rgba(255,255,255,0.01) 1%,rgba(255,255,255,1) 35%,rgba(255,255,255,1) 65%,rgba(255,255,255,0) 100%); /* W3C */ | |
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#03ffffff', endColorstr='#00ffffff',GradientType=1 ); /* IE6-9 */ | |
position: absolute; | |
width : 100%; | |
height : 100%; | |
} | |
.blablah | |
{ | |
background-color: #FDFDFD; | |
padding: 2em; | |
font-family: "verdana"; | |
font-size: .8em; | |
position: absolute; | |
left: 2em; | |
-webkit-box-shadow: 0 12px 32px -8px black; | |
-moz-box-shadow: 0 12px 32px -8px black; | |
box-shadow: 0 12px 32px -8px black; | |
} | |
h1 | |
{ | |
text-align: center; | |
font-family: "verdana"; | |
font-size: 1.6em; | |
text-transform: uppercase; | |
} | |
#container | |
{ | |
font-family: "verdana"; | |
font-size: .7em; | |
display: block; | |
margin : auto; | |
position : absolute; | |
top : 0; | |
left : 0; | |
bottom : 0; | |
right : 0; | |
} | |
canvas | |
{ | |
display: block; | |
position: absolute; | |
pointer-events:none; | |
} | |
.params | |
{ | |
display: block; | |
position: absolute; | |
width: 100%; | |
bottom: 30px; | |
} | |
.controls | |
{ | |
margin:auto; | |
display: block; | |
position: relative; | |
float: left; | |
margin-right: 1em; | |
} | |
#frequencyInput, #detuneInput, #gainInput | |
{ | |
vertical-align: middle; | |
} | |
label | |
{ | |
cursor:pointer; | |
} |