#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:'https://goo.gl/6WLx1', target:'_blank' } this bug
    in Safari 6

#intro.message

  %h1 Simple music visualiser
  %h2 Loading audio…
View Compiled
@import compass

@import url( https://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( 'https://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
View Compiled
###

  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 https://goo.gl/6WLx1

###

# Config

NUM_PARTICLES = 150
NUM_BANDS = 128
SMOOTHING = 0.5
MP3_PATH = 'https://api.soundcloud.com/tracks/42328219/stream?client_id=b1495e39071bd7081a74093816f77ddb'

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.crossOrigin = "anonymous"
      @audio.controls = yes
      @audio.src = src
  
    # setup audio context and nodes
    @context = new AudioAnalyser.AudioContext()
    
    # createScriptProcessor so we can hook onto updates
    @jsNode = @context.createScriptProcessor 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 https://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 @
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://soulwire.github.io/sketch.js/js/sketch.min.js