Synth From Hell
Crazy experiment involving Audio API and CSS3 3D
*turn on "override software rendering list" option in "chrome://flags" for a better view.
See https://github.com/bartaz/impress.js/issues/40
<script class="cssdeck" src="//cdnjs.cloudflare.com/ajax/libs/jquery/1.8.0/jquery.min.js"></script>
<div class="menu">
<span class="title">Synth From Hell</span>
<div class="options left">
<label for="instrument">Instruments</label>
<div class="dropdown">
<select id='instrument' class="dropdown-select">
<option value="1">Church Organ</option>
<option value="2">Cloud Song</option>
<option value="3">Tom Crazy</option>
<option value="4">Phone</option>
</select>
</div>
<span>
<input type="checkbox" value='vibrato' id="vibrato">
<label for="vibrato">Vibrato</label>
</span>
</div>
<div class="options right">
<input type="checkbox" checked value='animate' id="animate">
<label for="animate">Animate</label>
<input type="checkbox" value='notes' id="notes">
<label for="notes">Notes</label>
<input type="checkbox" value='keymap' id="keymap">
<label for="keymap">Key Map</label>
</div>
</div>
<div class="volume">
<input id="volume" type="range" min="0" max="1" step="0.1" value="0.1"/>
<label for="volume">Gain</label>
</div>
<div class="octave">
<p>Octave</p>
<div id="oct1" class="box b-active">1</div>
<div id="oct2" class="box">2</div>
</div>
<div class="scene">
<div class="piano animate" id="piano">
</div>
</div>
<div class="info">
<p>
Use the keyboard to play the notes. <br> Press key <strong>1</strong> or <strong>2</strong> to change the octaves.
</p>
</div>
<div class="credits">
<p>
Allan Esquina
</p>
</div>
* {
* {
padding: 0;
margin: 0;
}
$colorMenu: #191426;
body {
background-image: radial-gradient(#473962 15%, #2E2344 60%);
font: 20px/20px "Lucida Grande", Tahoma, Verdana, sans-serif;
color:#C1B8B7;
overflow: hidden;
}
html, body {
height: 100%;
}
createKey( $colorLeft, $colorRight, $colorTop, $colorFront, $borderColor, $keyWidth, $keyHeight, $keyDeep ) {
transform-style:preserve-3d;
width: $keyWidth;
height: $keyHeight;
position: absolute;
transform-origin: 0% 0% (-$keyDeep);
backface-visibility: hidden;
transition:.2s;
.size-lr {
width: $keyDeep;
height: $keyHeight;
}
.size-t {
width: $keyWidth;
height: $keyDeep;
}
.tec {
position: absolute;
top: 0;
left: 0;
transform-origin:0% 0%;
border:1px solid $borderColor;
}
.y90-left {
transform: rotateY(90deg);
background: $colorLeft;
background-image: linear-gradient(to bottom right, $colorTop, $colorLeft);
}
.y90-right {
transform: rotateY(90deg);
left: $keyWidth;
background: $colorRight;
background-image: linear-gradient(to bottom right, $colorTop, $colorRight);
}
.x90-top {
transform: rotateX(-90deg);
background: $colorTop;
background-image: linear-gradient(to bottom right, $colorFront, $colorTop);
}
.x90-front {
transform: rotateX(0deg);
width: $keyWidth;
height: $keyHeight;
background: $colorFront;
background-image: linear-gradient(to top bottom, $colorTop, $colorFront);
position: relative;
}
.x90-front2 {
transform: rotateY(0deg);
margin-left: $keyWidth;
width: $keyWidth;
height: $keyHeight;
background: $colorFront;
background-image: linear-gradient(to bottom right, $colorRight, $colorFront);
}
}
.pressed {
transform: rotateX(-10deg);
}
.pressed-black {
transform: rotateX(-10deg) translateZ(-150px) !important;
margin-top: -20px !important;
}
.piano {
transform-style:preserve-3d;
perspective:1500;
position: absolute;
width: 1260px;
height: 70px;
top:0;
left:0;
right: 0;
bottom: 0;
margin:auto;
margin-top: 300px;
transform:translateZ(10px) rotateY(0deg) rotateX(-50deg);
}
.white-key-group {
z-index: 1;
createKey( #999, #999, #fff, #eee, #ccc, 60px, 70px, 400px );
}
.black-key-group {
margin-top: -42px;
transform: translateZ(-150px);
z-index: 100;
createKey( #222, #222, #111, #333, #222, 30px, 40px, 250px );
.f-notes {
color:#eee;
top:5%;
}
.f-keymap {
color:#eee;
bottom:5%;
}
}
.active {
box-shadow: 0px 0px 150px 10px #72ECFC
}
.scene {
position: absolute;
width: 1200px;
height: 200px;
top:-20%;
left:-5%;
right: 0;
bottom:0;
margin: auto;
}
.menu {
position: fixed;
top: 0;
width: 100%;
height: 70px;
background: $colorMenu;
text-align: center;
color:#C1B8B7;
font-size: 14px;
line-height: 70px;
.options {
border-left: 1px solid lighten( $colorMenu, 10 );
float: right;
height: 70px;
line-height: 70px;
&.left {
width: 400px;
}
&.right {
width: 350px;
}
}
.title {
float: left;
font-size: 20px;
margin-left: 7%;
}
label {
margin: 0 15px;
vertical-align: middle;
padding-top: 3px;
line-height: 20px;
}
}
.f-notes {
text-align: center;
font-weight: bold;
font-size: 16px;
color:#222;
position: absolute;
top: 10%;
width: 100%;
text-align: center;
visibility: hidden;
}
.f-keymap {
text-align: center;
font-weight: bold;
font-size: 12px;
color:#222;
position: absolute;
bottom: 10%;
width: 100%;
text-align: center;
visibility: hidden;
}
.info-active {
visibility: visible;
}
.info {
position: absolute;
left: 50px;
bottom:30px;
font-size: 14px;
}
.credits {
position: absolute;
right: 50px;
bottom: 30px;
font-size: 14px;
}
.octave {
position: absolute;
top: 100px;
left:25px;
z-index:300;
p {
font-size: 14px;
margin-bottom: 5px;
}
.box {
float: left;
line-height: 50px;
text-align: center;
width: 50px;
height: 50px;
border:1px solid #ccc;
border-radius: 7px;
margin-right: 10px;
transition:.4s;
cursor:pointer;
&.b-active {
background: #ccc;
color:#191426;
&:hover {
background: #ccc;
}
}
&:hover {
background: lighten( $colorMenu, 20 );
}
}
}
.volume {
position: absolute;
top: 130px;
right: -30px;
height: 50px;
font-size: 14px;
transform: rotate(-90deg);
opacity: 0.2;
z-index: 1000;
transition:.2s;
&:hover {
opacity: 0.9;
}
label {
display: block;
text-align: center;
}
}
//Animate
.animate {
animation-name: expand;
animation-duration: 10s;
animation-iteration-count: infinite;
animation-direction: alternate;
}
expand {
0% {
// transform:translateZ(-100px) rotateY(-10deg) rotateX(-10deg);
transform: rotateY(-30deg) rotateX(-40deg);
}
100% {
transform: rotateY(20deg) rotateX(-35deg);
}
}
/*
* CUSTOM DROP
* http://codepen.io/Thibaut/pen/Jasci
*/
.dropdown {
display: inline-block;
position: relative;
overflow: hidden;
vertical-align: middle;
height: 28px;
width: 150px;
background: #f2f2f2;
line-height: 20px;
border: 1px solid;
border-color: white #f7f7f7 whitesmoke;
border-radius: 3px;
background-image: -webkit-linear-gradient(top, transparent, rgba(0, 0, 0, 0.06));
background-image: -moz-linear-gradient(top, transparent, rgba(0, 0, 0, 0.06));
background-image: -o-linear-gradient(top, transparent, rgba(0, 0, 0, 0.06));
background-image: linear-gradient(to bottom, transparent, rgba(0, 0, 0, 0.06));
-webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.08);
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.08);
}
.dropdown:before, .dropdown:after {
content: '';
position: absolute;
z-index: 2;
top: 9px;
right: 10px;
width: 0;
height: 0;
border: 4px dashed;
border-color: #888888 transparent;
pointer-events: none;
}
.dropdown:before {
border-bottom-style: solid;
border-top: none;
}
.dropdown:after {
margin-top: 7px;
border-top-style: solid;
border-bottom: none;
}
.dropdown-select {
position: relative;
width: 130%;
margin: 0;
padding: 6px 8px 6px 10px;
height: 28px;
line-height: 14px;
font-size: 16px;
color: #62717a;
text-shadow: 0 1px white;
background: #f2f2f2; /* Fallback for IE 8 */
background: rgba(0, 0, 0, 0) !important; /* "transparent" doesn't work with Opera */
border: 0;
border-radius: 0;
-webkit-appearance: none;
}
.dropdown-select:focus {
z-index: 3;
width: 100%;
color: #394349;
outline: 2px solid #49aff2;
outline: 2px solid -webkit-focus-ring-color;
outline-offset: -2px;
}
.dropdown-select > option {
margin: 3px;
padding: 6px 8px;
text-shadow: none;
background: #f2f2f2;
border-radius: 3px;
cursor: pointer;
}
/* Fix for IE 8 putting the arrows behind the select element. */
.lt-ie9 .dropdown {
z-index: 1;
}
.lt-ie9 .dropdown-select {
z-index: -1;
}
.lt-ie9 .dropdown-select:focus {
z-index: 3;
}
/* Dirty fix for Firefox adding padding where it shouldn't. */
url-prefix() {
.dropdown-select {
padding-left: 6px;
}
}
/*
* CUSTOM CHECKBOX
* http://codepen.io/CreativeJuiz/pen/BiHzp
*/
/* Base for label styling */
[type="checkbox"]:not(:checked),
[type="checkbox"]:checked {
position: absolute;
left: -9999px;
}
[type="checkbox"]:not(:checked) + label,
[type="checkbox"]:checked + label {
position: relative;
padding-left: 25px;
cursor: pointer;
}
/* checkbox aspect */
[type="checkbox"]:not(:checked) + label:before,
[type="checkbox"]:checked + label:before {
content: '';
position: absolute;
left:0; top: 2px;
width: 17px; height: 17px;
border: 1px solid #aaa;
background: #f8f8f8;
border-radius: 3px;
box-shadow: inset 0 1px 3px rgba(0,0,0,.3)
}
/* checked mark aspect */
[type="checkbox"]:not(:checked) + label:after,
[type="checkbox"]:checked + label:after {
content: 'X';
position: absolute;
top: 0;
left: -2px;
font-size: 35px;
color: #47AFC4;
transition: all .2s;
}
/* checked mark aspect changes */
[type="checkbox"]:not(:checked) + label:after {
opacity: 0;
transform: scale(0);
}
[type="checkbox"]:checked + label:after {
opacity: 1;
transform: scale(1);
}
/* disabled checkbox */
[type="checkbox"]:disabled:not(:checked) + label:before,
[type="checkbox"]:disabled:checked + label:before {
box-shadow: none;
border-color: #bbb;
background-color: #ddd;
}
[type="checkbox"]:disabled:checked + label:after {
color: #999;
}
[type="checkbox"]:disabled + label {
color: #aaa;
}
/* accessibility */
[type="checkbox"]:checked:focus + label:before,
[type="checkbox"]:not(:checked):focus + label:before {
border: 1px dotted blue;
}
;(function( win, doc, $ ) {
;(function( win, doc, $ ){
'use strict';
var __KEYSPRESSED = [],
__PLAYINGNOTES = [],
__STOPPEDNOTES = [],
__INSTRUMENT = 1,
__VOLUME = 0.1,
__VIBRATO = false,
__OCTAVE = 3,
audio_context,
oscillator;
function Piano( octave, target ) {
this.octave = octave;
this.notes = ['c', 'd', 'e', 'f', 'g', 'a', 'b'];
this.susNotes = ['c', 'd', 'f', 'g', 'a'];
this.mapNotes = ['c', 'd', 'e', 'f', 'g', 'a', 'b','c#', 'd#', 'f#', 'g#', 'a#', 'c', 'd', 'e', 'c#', 'd#', 'f#', 'g#'];
this.keymap = ['a', 's', 'd', 'f', 'g','h', 'j','w', 'e', 'r', 't', 'y', 'k', 'l', 'ç', 'u', 'i', 'o', 'p'];
this.target = target;
this.init();
}
Piano.prototype = {
init: function() {
this.$target = $( this.target );
this.createKeys();
this.bindEvents();
this.changeOctave();
},
createKeys: function() {
var key,whiteW=60, blackW = ( whiteW / 2 ), o = this.octave, wWidth=0, bWidth=blackW + (blackW/2);
for( var k=0; k<o; k+=1 ) {
for( var i=0, l=this.notes.length; i<l; i+=1 ) {
key = this.createKey();
key.find( '.f-notes' ).html( this.notes[ i ].toUpperCase() );
key.addClass( 'white-key-group' );
key.addClass( 'key' );
key.addClass( 'tom-' + this.notes[ i ] );
key.attr('data-note', this.notes[ i ].toUpperCase() + ( k + 3 ) );
key.css('left', wWidth + 'px' );
this.$target.append( key );
wWidth += whiteW;
if( i !== 2 && i !== 6 ) {
key = this.createKey();
key.find( '.f-notes' ).html( this.notes[ i ].toUpperCase() + '#' );
key.addClass( 'black-key-group' );
key.addClass( 'key' );
key.addClass( 'tom-s' + this.notes[ i ] );
key.attr('data-note', this.notes[ i ].toUpperCase() + '#' + ( k + 3 ) );
key.css('left', bWidth + 'px' );
this.$target.append( key );
}
bWidth += whiteW;
}
}
},
changeOctave: function( octave ) {
var self = this;
self.$target.find( '.f-keymap' ).html( '' );
var i=0, j=0, oc = octave || __OCTAVE;
this.mapNotes.forEach( function( e ) {
if( i===12 ) {
oc++ ; i=0;
}
self.$target.find( 'div[data-note='+ e.toUpperCase() + oc + ']').find('.f-keymap').html( self.keymap[ j ].toUpperCase() );
i++;
j++;
});
},
createKey: function() {
return $('<div><div class="tec y90-left size-lr"></div><div class="tec y90-right size-lr"></div><div class="tec x90-top keyTop size-t"></div><div class="tec x90-front"><span class="f-keymap"></span><span class="f-notes"></span></div> </div> ');
},
bindEvents: function() {
var $keys = $( '.key' );
$keys.on( 'mouseenter', function( e ) {
var $el = $( e.currentTarget ), freqs, freq;
if( $el.hasClass('white-key-group') ) {
$el.addClass( 'pressed' );
} else {
$el.addClass( 'pressed-black ' );
}
$el.addClass( 'active' );
freq = mplay.getFrequency( $el.attr( 'data-note' ) );
freqs = mplay.getInstrument( freq, __INSTRUMENT, __VIBRATO );
mplay.play( freqs );
});
$keys.on( 'mouseout', function( e ) {
var $el = $( e.currentTarget );
if( $el.hasClass('white-key-group') ) {
$el.removeClass( 'pressed' );
} else {
$el.removeClass( 'pressed-black ' );
}
$el.removeClass( 'active' );
mplay.stop();
});
}
};
$( doc ).ready(function() {
var $oct1 = $( '#oct1' ),$oct2 = $( '#oct2' );
var _changeOctave = function( oct ) {
if( oct ) {
$oct1.addClass( 'b-active' );
$oct2.removeClass( 'b-active' );
} else {
$oct1.removeClass( 'b-active' );
$oct2.addClass( 'b-active' );
}
};
try {
var Tmp = win.AudioContext || win.webkitAudioContext;
audio_context = new Tmp();
} catch (e) {
alert('No web audio oscillator support in this browser');
}
$( doc ).keydown(function( e ) {
e.preventDefault();
if( __KEYSPRESSED.indexOf( e.which ) === -1 ) {
__KEYSPRESSED.push( e.which );
}
if( e.which === 49 ) {
__OCTAVE = 3;
piano.changeOctave();
_changeOctave( true );
}
if( e.which === 50 ) {
__OCTAVE = 4;
piano.changeOctave();
_changeOctave( false );
}
});
$( doc ).keyup(function( e ) {
e.preventDefault();
var io = __KEYSPRESSED.indexOf( e.which );
if( io !== -1 ) {
__KEYSPRESSED.splice( io, 1 );
__STOPPEDNOTES.push( e.which );
}
});
$( '#vibrato').on( 'change', function( e ) {
__VIBRATO = $( e.currentTarget ).is(':checked') ? true : false;
});
$( '#animate').on( 'change', function( e ) {
if( $( e.currentTarget ).is(':checked') ) {
win.piano.$target.addClass( 'animate' );
} else {
win.piano.$target.removeClass( 'animate' );
}
});
$( '#instrument').on( 'change', function( e ) {
__INSTRUMENT = +e.currentTarget.selectedIndex + 1;
});
$( '#volume').on( 'change', function( e ) {
__VOLUME = +e.currentTarget.value;
});
$( '#notes' ).on( 'change', function( e ) {
if( $( e.currentTarget ).is(':checked') ) {
win.piano.$target.find( '.f-notes' ).addClass( 'info-active' );
} else {
win.piano.$target.find( '.f-notes' ).removeClass( 'info-active' );
}
});
$( '#keymap' ).on( 'change', function( e ) {
if( $( e.currentTarget ).is(':checked') ) {
win.piano.$target.find( '.f-keymap' ).addClass( 'info-active' );
} else {
win.piano.$target.find( '.f-keymap' ).removeClass( 'info-active' );
}
});
$( '.box' ).on( 'click', function( e ) {
if( $( e.currentTarget ).attr( 'id' ) === 'oct1' ) {
__OCTAVE = 3;
piano.changeOctave();
_changeOctave( true );
} else {
__OCTAVE = 4;
piano.changeOctave();
_changeOctave( false );
}
});
win.piano = new Piano(3, '#piano');
setInterval( mplay.render, 0 );
});
var mplay = {};
var fq = [];
mplay.play = function ( freqs, key ) {
var oscs = [], o, i, g;
freqs.forEach( function( freq ) {
g = createGain();
g.gain.value = __VOLUME;
o = audio_context.createOscillator();
o.frequency.value = freq;
o.connect( g );
g.connect( audio_context.destination );
_play( 0 );
oscs.push( o );
});
fq[ key ] = oscs;
function createGain() {
var out;
if( audio_context.createGain ) {
out = audio_context.createGain();
} else if ( audio_context.createGainNode ) {
out = audio_context.createGainNode();
}
return out;
}
function _play( arg ) {
if( o.noteOn ) {
o.noteOn( arg );
} else if ( o.start ) {
o.start( arg );
}
}
};
mplay.stop = function ( key ) {
fq[ key ].forEach( function( o ) {
_stop( 0, o );
});
function _stop( arg, o ) {
if( o.noteOff ) {
o.noteOff( arg );
} else if ( o.stop ) {
o.stop( arg );
}
}
};
mplay.getKeyMap = function( map ) {
switch( map ) {
case 65:
return 'C' + __OCTAVE;
case 83:
return 'D' + __OCTAVE;
case 68:
return 'E' + __OCTAVE;
case 70:
return 'F' + __OCTAVE;
case 71:
return 'G' + __OCTAVE;
case 72:
return 'A' + __OCTAVE;
case 74:
return 'B' + __OCTAVE;
case 87:
return 'C#' + __OCTAVE;
case 69:
return 'D#' + __OCTAVE;
case 82:
return 'F#' + __OCTAVE;
case 84:
return 'G#' + __OCTAVE;
case 89:
return 'A#' + __OCTAVE;
case 75:
return 'C' + ( __OCTAVE + 1 );
case 76:
return 'D' + ( __OCTAVE + 1 );
case 186:
return 'E' + ( __OCTAVE + 1 );
case 220:
return 'F' + ( __OCTAVE + 1 );
case 85:
return 'C#' + ( __OCTAVE + 1 );
case 73:
return 'D#' + ( __OCTAVE + 1 );
case 79:
return 'F#' + ( __OCTAVE + 1 );
case 80:
return 'G#' + ( __OCTAVE + 1 );
case 65:
return 'A#' + ( __OCTAVE + 1 );
default:
return false;
}
};
mplay.render = function() {
var freq, keyMap, $el;
__KEYSPRESSED.forEach( function( key ) {
if( __PLAYINGNOTES.indexOf( key ) === -1 ) {
keyMap = mplay.getKeyMap( key );
if( keyMap ) {
freq = mplay.getFrequency( keyMap );
mplay.play( mplay.getInstrument( freq, __INSTRUMENT, __VIBRATO ), key );
__PLAYINGNOTES.push( key );
$el = $('div[data-note=' + keyMap + ']');
if( $el.hasClass('white-key-group') ) {
$el.addClass( 'pressed' );
} else {
$el.addClass( 'pressed-black ' );
}
$el.addClass( 'active' );
}
}
});
__STOPPEDNOTES.forEach( function( key ) {
keyMap = mplay.getKeyMap( key );
if( keyMap ) {
mplay.stop( key );
__STOPPEDNOTES.splice( __STOPPEDNOTES.indexOf( key ), 1 );
__PLAYINGNOTES.splice( __PLAYINGNOTES.indexOf( key ), 1 );
$el = $('div[data-note=' + keyMap + ']');
if( $el.hasClass('white-key-group') ) {
$el.removeClass( 'pressed' );
} else {
$el.removeClass( 'pressed-black ' );
}
$el.removeClass( 'active' );
}
});
};
// From Retrojs
// https://github.com/eshiota/retro-audio-js
mplay.getFrequency= function (note) {
var notes = ["A", "A#", "B", "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#"], octave, keyNumber;
if (note.length === 3) {
octave = note.charAt(2);
} else {
octave = note.charAt(1);
}
octave = +octave + 1;
keyNumber = notes.indexOf(note.slice(0, -1));
if (keyNumber < 3) {
keyNumber = keyNumber + 12 + ((octave - 1) * 12) + 1;
} else {
keyNumber = keyNumber + ((octave - 1) * 12) + 1;
}
// Return frequency of note
return Math.floor(440 * Math.pow(2, (keyNumber - 49) / 12));
};
mplay.getInstrument = function( freq, instrument, vibrato ) {
var freqs = [];
switch( instrument ) {
case 1:
freqs.push( freq );
freqs.push( freq * 2 );
freqs.push( freq * 4 );
freqs.push( freq * 8.5);
freqs.push( freq * 19 );
freqs.push( freq / 2 );
break;
case 2:
freqs.push( freq );
freqs.push( freq * (freq / ( freq -1 ) ) );
freqs.push( freq * (freq / ( freq -5 ) ) );
freqs.push( freq * (freq * ( freq -22 ) ) );
freqs.push( freq * 4 );
freqs.push( freq / 2 );
freqs.push( freq / 3 );
break;
case 3:
freqs.push( freq );
freqs.push( freq * (freq * ( freq -122 ) ) );
freqs.push( freq * (freq * ( freq -22 ) ) );
freqs.push( freq * (freq * ( freq -2 ) ) );
freqs.push( freq * 23 );
freqs.push( freq * 1.221 );
freqs.push( freq / 2 );
freqs.push( freq / 2.2 );
freqs.push( freq / 3 );
break;
case 4:
freqs.push( freq * 2 );
freqs.push( freq / 2 );
break;
}
if( vibrato ) {
freqs.push( freq * (freq / ( freq -5 ) ) );
}
return freqs;
};
}( window, document, $ ));