A basic example of using the Web Audio API AudioContext with an AnalyserNode and frequency-domain analysis (FFT) to create a simple music visualiser.
A Pen by Justin Windle on CodePen.
#container | |
#warning1.message | |
%h1 | |
This experiment requires the | |
%a{ href:'https://dvcs.w3.org/hg/audio/raw-file/tip/webaudio/specification.html', target:'_blank' } Web Audio API | |
%h2 | |
Please try in one of | |
%a{ href:'http://caniuse.com/#feat=audio-api', target:'_blank' } these browsers | |
#warning2.message | |
%h1 | |
Safari users. You may hear audio but see no visuals. This is due to | |
%a{ href:'http://goo.gl/6WLx1', target:'_blank' } this bug | |
in Safari 6 | |
#intro.message | |
%h1 Simple music visualiser | |
%h2 Loading audio… |
### | |
Music is by The XX | |
@see http://thexx.info | |
This is best viewed in Chrome since there is a bug in Safari | |
when using getByteFrequencyData with MediaElementAudioSource | |
@see http://goo.gl/6WLx1 | |
### | |
# Config | |
NUM_PARTICLES = 150 | |
NUM_BANDS = 128 | |
SMOOTHING = 0.5 | |
MP3_PATH = 'http://s.cdpn.io/1715/the_xx_-_intro.mp3' | |
SCALE = MIN: 5.0, MAX: 80.0 | |
SPEED = MIN: 0.2, MAX: 1.0 | |
ALPHA = MIN: 0.8, MAX: 0.9 | |
SPIN = MIN: 0.001, MAX: 0.005 | |
SIZE = MIN: 0.5, MAX: 1.25 | |
COLORS = [ | |
'#69D2E7' | |
'#1B676B' | |
'#BEF202' | |
'#EBE54D' | |
'#00CDAC' | |
'#1693A5' | |
'#F9D423' | |
'#FF4E50' | |
'#E7204E' | |
'#0CCABA' | |
'#FF006F' | |
] | |
# Audio Analyser | |
class AudioAnalyser | |
@AudioContext: self.AudioContext or self.webkitAudioContext | |
@enabled: @AudioContext? | |
constructor: ( @audio = new Audio(), @numBands = 256, @smoothing = 0.3 ) -> | |
# construct audio object | |
if typeof @audio is 'string' | |
src = @audio | |
@audio = new Audio() | |
@audio.controls = yes | |
@audio.src = src | |
# setup audio context and nodes | |
@context = new AudioAnalyser.AudioContext() | |
# JavaScriptNode so we can hook onto updates | |
@jsNode = @context.createJavaScriptNode 2048, 1, 1 | |
# smoothed analyser with n bins for frequency-domain analysis | |
@analyser = @context.createAnalyser() | |
@analyser.smoothingTimeConstant = @smoothing | |
@analyser.fftSize = @numBands * 2 | |
# persistant bands array | |
@bands = new Uint8Array @analyser.frequencyBinCount | |
# circumvent http://crbug.com/112368 | |
@audio.addEventListener 'canplay', => | |
# media source | |
@source = @context.createMediaElementSource @audio | |
# wire up nodes | |
@source.connect @analyser | |
@analyser.connect @jsNode | |
@jsNode.connect @context.destination | |
@source.connect @context.destination | |
# update each time the JavaScriptNode is called | |
@jsNode.onaudioprocess = => | |
# retreive the data from the first channel | |
@analyser.getByteFrequencyData @bands | |
# fire callback | |
@onUpdate? @bands if not @audio.paused | |
start: -> | |
@audio.play() | |
stop: -> | |
@audio.pause() | |
# Particle | |
class Particle | |
constructor: ( @x = 0, @y = 0 ) -> | |
@reset() | |
reset: -> | |
@level = 1 + floor random 4 | |
@scale = random SCALE.MIN, SCALE.MAX | |
@alpha = random ALPHA.MIN, ALPHA.MAX | |
@speed = random SPEED.MIN, SPEED.MAX | |
@color = random COLORS | |
@size = random SIZE.MIN, SIZE.MAX | |
@spin = random SPIN.MAX, SPIN.MAX | |
@band = floor random NUM_BANDS | |
if random() < 0.5 then @spin = -@spin | |
@smoothedScale = 0.0 | |
@smoothedAlpha = 0.0 | |
@decayScale = 0.0 | |
@decayAlpha = 0.0 | |
@rotation = random TWO_PI | |
@energy = 0.0 | |
move: -> | |
@rotation += @spin | |
@y -= @speed * @level | |
draw: ( ctx ) -> | |
power = exp @energy | |
scale = @scale * power | |
alpha = @alpha * @energy * 1.5 | |
@decayScale = max @decayScale, scale | |
@decayAlpha = max @decayAlpha, alpha | |
@smoothedScale += ( @decayScale - @smoothedScale ) * 0.3 | |
@smoothedAlpha += ( @decayAlpha - @smoothedAlpha ) * 0.3 | |
@decayScale *= 0.985 | |
@decayAlpha *= 0.975 | |
ctx.save() | |
ctx.beginPath() | |
ctx.translate @x + cos( @rotation * @speed ) * 250, @y | |
ctx.rotate @rotation | |
ctx.scale @smoothedScale * @level, @smoothedScale * @level | |
ctx.moveTo @size * 0.5, 0 | |
ctx.lineTo @size * -0.5, 0 | |
ctx.lineWidth = 1 | |
ctx.lineCap = 'round' | |
ctx.globalAlpha = @smoothedAlpha / @level | |
ctx.strokeStyle = @color | |
ctx.stroke() | |
ctx.restore() | |
# Sketch | |
Sketch.create | |
particles: [] | |
setup: -> | |
# generate some particles | |
for i in [0..NUM_PARTICLES-1] by 1 | |
x = random @width | |
y = random @height * 2 | |
particle = new Particle x, y | |
particle.energy = random particle.band / 256 | |
@particles.push particle | |
if AudioAnalyser.enabled | |
try | |
# setup the audio analyser | |
analyser = new AudioAnalyser MP3_PATH, NUM_BANDS, SMOOTHING | |
# update particles based on fft transformed audio frequencies | |
analyser.onUpdate = ( bands ) => particle.energy = bands[ particle.band ] / 256 for particle in @particles | |
# start as soon as the audio is buffered | |
analyser.start(); | |
# show audio controls | |
document.body.appendChild analyser.audio | |
intro = document.getElementById 'intro' | |
intro.style.display = 'none' | |
# bug in Safari 6 when using getByteFrequencyData with MediaElementAudioSource | |
# @see http://goo.gl/6WLx1 | |
if /Safari/.test( navigator.userAgent ) and not /Chrome/.test( navigator.userAgent ) | |
warning = document.getElementById 'warning2' | |
warning.style.display = 'block' | |
catch error | |
else | |
# Web Audio API not detected | |
warning = document.getElementById 'warning1' | |
warning.style.display = 'block' | |
draw: -> | |
@globalCompositeOperation = 'lighter' | |
for particle in @particles | |
# recycle particles | |
if particle.y < -particle.size * particle.level * particle.scale * 2 | |
particle.reset(); | |
particle.x = random @width | |
particle.y = @height + particle.size * particle.scale * particle.level | |
particle.move() | |
particle.draw @ |
A basic example of using the Web Audio API AudioContext with an AnalyserNode and frequency-domain analysis (FFT) to create a simple music visualiser.
A Pen by Justin Windle on CodePen.
@import "compass" | |
@import url( http://fonts.googleapis.com/css?family=Lato:400,700 ) | |
html, body | |
font-family: 'Lato', sans-serif | |
background: #13242f | |
overflow: hidden | |
#container | |
&:before | |
@include background-image( radial-gradient( center, ellipse cover, rgba(0,0,0,0) 20%, rgba(0,0,0,1) 95% ) ) | |
position: absolute | |
content: '' | |
z-index: 0 | |
opacity: 0.9 | |
height: 100% | |
width: 100% | |
left: 0 | |
top: 0 | |
&:after | |
background: url( 'http://s.cdpn.io/1715/noise-1.png' ) | |
position: absolute | |
content: '' | |
z-index: 1 | |
opacity: 0.8 | |
height: 100% | |
width: 100% | |
left: 0 | |
top: 0 | |
audio | |
position: absolute | |
z-index: 2 | |
right: 0 | |
top: 0 | |
.message | |
$height: 60px | |
$width: 360px | |
box-shadow: 0 2px 4px rgba(0,0,0,0.2) | |
text-transform: uppercase | |
border-radius: 3px | |
text-align: center | |
line-height: 1.2 | |
background: rgba(0,0,0,0.8) | |
position: absolute | |
margin-left: $width * -0.5 | |
margin-top: $height * -0.5 | |
font-size: 13px | |
padding: 20px | |
display: none | |
z-index: 3 | |
height: $height | |
width: $width | |
color: #fff | |
left: 50% | |
top: 50% | |
h1, h2 | |
font-weight: 300 | |
margin: 10px 0 | |
a | |
text-decoration: none | |
font-weight: 700 | |
color: #1B676B | |
#intro | |
display: block |