├── README.md ├── css └── style.css ├── docs ├── docco.css └── local_audio_visualizer.html ├── index.html └── js └── local_audio_visualizer.js /README.md: -------------------------------------------------------------------------------- 1 | # local_audio_visualizer 2 | 3 | An HTML5 experiment. 4 | 5 | Drag an audio file to the browser window. It will be played directly from your hard disk, and you'll have a neat spectrum visualization. 6 | 7 | Here is [the demo](http://cbrandolino.github.com/local-audio-visualizer) 8 | 9 | Here is the docco-generated [documentation, which makes for a decent tutorial](http://cbrandolino.github.com/local-audio-visualizer/docs/local_audio_visualizer) 10 | 11 | ## HTML5 features showcased 12 | 13 | - **Web Audio Api** (for playback and analysis) 14 | - **File access** (for local playback) 15 | - **Native drag and drop** (for, you know, drag and drop) 16 | - **Canvas and css transforms** (for the things you see) 17 | -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 |   font-family: 'Droid Sans'; 3 |   font-style: normal; 4 |   font-weight: bold; 5 |   src: local('Droid Sans'), url('http://playground.html5rocks.com/samples/html5_misc/Droid_Sans.ttf') format('truetype'); 6 | } 7 | 8 | #instructions { 9 | display: block; 10 | position: absolute; 11 | width: 100%; 12 | text-align: center; 13 | top: 50%; 14 | margin-top: -100px; 15 | font-family: 'Droid Sans'; 16 | color: #fff; 17 | } 18 | 19 | #bottom-bar { 20 | position: absolute; 21 | bottom: 20px; 22 | left: 10px; 23 | height: 160px; 24 | font-family: 'Droid Sans'; 25 | color: #fff; 26 | } 27 | 28 | #info { 29 | float: left; 30 | } 31 | 32 | #info a { 33 | color: #ddd; 34 | } 35 | 36 | #container { 37 | display: block; 38 | position: absolute; 39 | top: 0; 40 | left: 0; 41 | width: 100%; 42 | height: 100%; 43 | background: radial-gradient( 44 | ellipse farthest-corner, 45 | #7d7d7d 0%, 46 | #0e0e0e 100%); 47 | } 48 | 49 | #canvas-container { 50 | width: 800px; 51 | height: 600px; 52 | margin: auto; 53 | position: relative; 54 | top: 50%; 55 | margin-top: -300px; 56 | } 57 | #canvas-copy { 58 | opacity: 0.05; 59 | -webkit-transform: scaleY(-1); 60 | margin-top: -6px; 61 | } 62 | -------------------------------------------------------------------------------- /docs/docco.css: -------------------------------------------------------------------------------- 1 | /*--------------------- Layout and Typography ----------------------------*/ 2 | body { 3 | font-family: 'Palatino Linotype', 'Book Antiqua', Palatino, FreeSerif, serif; 4 | font-size: 15px; 5 | line-height: 22px; 6 | color: #252519; 7 | margin: 0; padding: 0; 8 | } 9 | a { 10 | color: #261a3b; 11 | } 12 | a:visited { 13 | color: #261a3b; 14 | } 15 | p { 16 | margin: 0 0 15px 0; 17 | } 18 | h1, h2, h3, h4, h5, h6 { 19 | margin: 0px 0 15px 0; 20 | } 21 | h1 { 22 | margin-top: 40px; 23 | } 24 | hr { 25 | border: 0 none; 26 | border-top: 1px solid #e5e5ee; 27 | height: 1px; 28 | margin: 20px 0; 29 | } 30 | #container { 31 | position: relative; 32 | } 33 | #background { 34 | position: fixed; 35 | top: 0; left: 525px; right: 0; bottom: 0; 36 | background: #f5f5ff; 37 | border-left: 1px solid #e5e5ee; 38 | z-index: -1; 39 | } 40 | #jump_to, #jump_page { 41 | background: white; 42 | -webkit-box-shadow: 0 0 25px #777; -moz-box-shadow: 0 0 25px #777; 43 | -webkit-border-bottom-left-radius: 5px; -moz-border-radius-bottomleft: 5px; 44 | font: 10px Arial; 45 | text-transform: uppercase; 46 | cursor: pointer; 47 | text-align: right; 48 | } 49 | #jump_to, #jump_wrapper { 50 | position: fixed; 51 | right: 0; top: 0; 52 | padding: 5px 10px; 53 | } 54 | #jump_wrapper { 55 | padding: 0; 56 | display: none; 57 | } 58 | #jump_to:hover #jump_wrapper { 59 | display: block; 60 | } 61 | #jump_page { 62 | padding: 5px 0 3px; 63 | margin: 0 0 25px 25px; 64 | } 65 | #jump_page .source { 66 | display: block; 67 | padding: 5px 10px; 68 | text-decoration: none; 69 | border-top: 1px solid #eee; 70 | } 71 | #jump_page .source:hover { 72 | background: #f5f5ff; 73 | } 74 | #jump_page .source:first-child { 75 | } 76 | table td { 77 | border: 0; 78 | outline: 0; 79 | } 80 | td.docs, th.docs { 81 | max-width: 450px; 82 | min-width: 450px; 83 | min-height: 5px; 84 | padding: 10px 25px 1px 50px; 85 | overflow-x: hidden; 86 | vertical-align: top; 87 | text-align: left; 88 | } 89 | .docs pre { 90 | margin: 15px 0 15px; 91 | padding-left: 15px; 92 | } 93 | .docs p tt, .docs p code { 94 | background: #f8f8ff; 95 | border: 1px solid #dedede; 96 | font-size: 12px; 97 | padding: 0 0.2em; 98 | } 99 | .pilwrap { 100 | position: relative; 101 | } 102 | .pilcrow { 103 | font: 12px Arial; 104 | text-decoration: none; 105 | color: #454545; 106 | position: absolute; 107 | top: 3px; left: -20px; 108 | padding: 1px 2px; 109 | opacity: 0; 110 | -webkit-transition: opacity 0.2s linear; 111 | } 112 | td.docs:hover .pilcrow { 113 | opacity: 1; 114 | } 115 | td.code, th.code { 116 | padding: 14px 15px 16px 25px; 117 | width: 100%; 118 | vertical-align: top; 119 | background: #f5f5ff; 120 | border-left: 1px solid #e5e5ee; 121 | } 122 | pre, tt, code { 123 | font-size: 12px; line-height: 18px; 124 | font-family: Menlo, Monaco, Consolas, "Lucida Console", monospace; 125 | margin: 0; padding: 0; 126 | } 127 | 128 | 129 | /*---------------------- Syntax Highlighting -----------------------------*/ 130 | td.linenos { background-color: #f0f0f0; padding-right: 10px; } 131 | span.lineno { background-color: #f0f0f0; padding: 0 5px 0 5px; } 132 | body .hll { background-color: #ffffcc } 133 | body .c { color: #408080; font-style: italic } /* Comment */ 134 | body .err { border: 1px solid #FF0000 } /* Error */ 135 | body .k { color: #954121 } /* Keyword */ 136 | body .o { color: #666666 } /* Operator */ 137 | body .cm { color: #408080; font-style: italic } /* Comment.Multiline */ 138 | body .cp { color: #BC7A00 } /* Comment.Preproc */ 139 | body .c1 { color: #408080; font-style: italic } /* Comment.Single */ 140 | body .cs { color: #408080; font-style: italic } /* Comment.Special */ 141 | body .gd { color: #A00000 } /* Generic.Deleted */ 142 | body .ge { font-style: italic } /* Generic.Emph */ 143 | body .gr { color: #FF0000 } /* Generic.Error */ 144 | body .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 145 | body .gi { color: #00A000 } /* Generic.Inserted */ 146 | body .go { color: #808080 } /* Generic.Output */ 147 | body .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ 148 | body .gs { font-weight: bold } /* Generic.Strong */ 149 | body .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 150 | body .gt { color: #0040D0 } /* Generic.Traceback */ 151 | body .kc { color: #954121 } /* Keyword.Constant */ 152 | body .kd { color: #954121; font-weight: bold } /* Keyword.Declaration */ 153 | body .kn { color: #954121; font-weight: bold } /* Keyword.Namespace */ 154 | body .kp { color: #954121 } /* Keyword.Pseudo */ 155 | body .kr { color: #954121; font-weight: bold } /* Keyword.Reserved */ 156 | body .kt { color: #B00040 } /* Keyword.Type */ 157 | body .m { color: #666666 } /* Literal.Number */ 158 | body .s { color: #219161 } /* Literal.String */ 159 | body .na { color: #7D9029 } /* Name.Attribute */ 160 | body .nb { color: #954121 } /* Name.Builtin */ 161 | body .nc { color: #0000FF; font-weight: bold } /* Name.Class */ 162 | body .no { color: #880000 } /* Name.Constant */ 163 | body .nd { color: #AA22FF } /* Name.Decorator */ 164 | body .ni { color: #999999; font-weight: bold } /* Name.Entity */ 165 | body .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ 166 | body .nf { color: #0000FF } /* Name.Function */ 167 | body .nl { color: #A0A000 } /* Name.Label */ 168 | body .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ 169 | body .nt { color: #954121; font-weight: bold } /* Name.Tag */ 170 | body .nv { color: #19469D } /* Name.Variable */ 171 | body .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ 172 | body .w { color: #bbbbbb } /* Text.Whitespace */ 173 | body .mf { color: #666666 } /* Literal.Number.Float */ 174 | body .mh { color: #666666 } /* Literal.Number.Hex */ 175 | body .mi { color: #666666 } /* Literal.Number.Integer */ 176 | body .mo { color: #666666 } /* Literal.Number.Oct */ 177 | body .sb { color: #219161 } /* Literal.String.Backtick */ 178 | body .sc { color: #219161 } /* Literal.String.Char */ 179 | body .sd { color: #219161; font-style: italic } /* Literal.String.Doc */ 180 | body .s2 { color: #219161 } /* Literal.String.Double */ 181 | body .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ 182 | body .sh { color: #219161 } /* Literal.String.Heredoc */ 183 | body .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ 184 | body .sx { color: #954121 } /* Literal.String.Other */ 185 | body .sr { color: #BB6688 } /* Literal.String.Regex */ 186 | body .s1 { color: #219161 } /* Literal.String.Single */ 187 | body .ss { color: #19469D } /* Literal.String.Symbol */ 188 | body .bp { color: #954121 } /* Name.Builtin.Pseudo */ 189 | body .vc { color: #19469D } /* Name.Variable.Class */ 190 | body .vg { color: #19469D } /* Name.Variable.Global */ 191 | body .vi { color: #19469D } /* Name.Variable.Instance */ 192 | body .il { color: #666666 } /* Literal.Number.Integer.Long */ -------------------------------------------------------------------------------- /docs/local_audio_visualizer.html: -------------------------------------------------------------------------------- 1 | local_audio_visualizer.js

local_audio_visualizer.js

window.onload = function() {
 2 |   var element = document.getElementById('container')
 3 |   dropAndLoad(element, init, "ArrayBuffer")
 4 | }

Reusable dropAndLoad function: it reads a local file dropped on a 5 | dropElement in the DOM in the specified readFormat 6 | (In this case, we want an arrayBuffer)

function dropAndLoad(dropElement, callback, readFormat) {
 7 |   var readFormat = readFormat || "DataUrl"
 8 | 
 9 |   dropElement.addEventListener('dragover', function(e) {
10 |     e.stopPropagation()
11 |     e.preventDefault()
12 |     e.dataTransfer.dropEffect = 'copy'
13 |   }, false)
14 | 
15 |   dropElement.addEventListener('drop', function(e) {
16 |     e.stopPropagation()
17 |     e.preventDefault()
18 |     loadFile(e.dataTransfer.files[0])
19 |   }, false) 
20 | 
21 |   function loadFile(files) {
22 |     var file = files
23 |     var reader = new FileReader()
24 |     reader.onload = function(e) {
25 |       callback(e.target.result)
26 |     }
27 |     reader['readAs'+readFormat](file)
28 |   }
29 | }

Once the file is loaded, we start getting our hands dirty.

function init(arrayBuffer) {
30 |   document.getElementById('instructions').innerHTML = 'Loading ...'

Create a new audioContext and its analyser

  window.audioCtx = new webkitAudioContext()
31 |   window.analyser = audioCtx.createAnalyser()

If a sound is still playing, stop it.

  if (window.source)
32 |     source.noteOff(0)

Decode the data in our array into an audio buffer

  audioCtx.decodeAudioData(arrayBuffer, function(buffer) {

Use the audio buffer with as our audio source

    window.source = audioCtx.createBufferSource()   
33 |     source.buffer = buffer

Connect to the analyser ...

    source.connect(analyser)

and back to the destination, to play the sound after the analysis.

    analyser.connect(audioCtx.destination)

Start playing the buffer.

    source.noteOn(0)

Initialize a visualizer object

    var viz = new simpleViz()

Finally, initialize the visualizer.

    new visualizer(viz['update'], analyser)
34 |     document.getElementById('instructions').innerHTML = ''
35 |   })
36 | }

The visualizer object. 37 | Calls the visualization function every time a new frame 38 | is available. 39 | Is passed an analyser (audioContext analyser).

function visualizer(visualization, analyser) {
40 |   var self = this
41 |   this.visualization = visualization  
42 |   var last = Date.now()
43 |   var loop = function() {
44 |     var dt = Date.now() - last

we get the current byteFreq data from our analyser

    var byteFreq = new Uint8Array(analyser.frequencyBinCount)
45 |     analyser.getByteFrequencyData(byteFreq)
46 |     last = Date.now()

We might want to use a delta time (dt) too for our visualization.

    self.visualization(byteFreq, dt)
47 |     webkitRequestAnimationFrame(loop)
48 |   }
49 |   webkitRequestAnimationFrame(loop)
50 | }

A simple visualization. Its update function illustrates how to use 51 | the byte frequency data from an audioContext analyser.

function simpleViz(canvas) {
52 |   var self = this
53 |   this.canvas = document.getElementById('canvas')
54 |   this.ctx = this.canvas.getContext("2d")
55 |   this.copyCtx = document.getElementById('canvas-copy').getContext("2d")
56 |   this.ctx.fillStyle = '#fff' 
57 |   this.barWidth = 10
58 |   this.barGap = 4

We get the total number of bars to display

  this.bars = Math.floor(this.canvas.width / (this.barWidth + this.barGap))

This function is launched for each frame, together with the byte frequency data.

  this.update = function(byteFreq) {
59 |     self.ctx.clearRect(0, 0, self.canvas.width, self.canvas.height)

We take an element from the byteFreq array for each of the bars. 60 | Let's pretend our byteFreq contains 20 elements, and we have five bars...

    var step = Math.floor(byteFreq.length / self.bars)

|||||||||||||||||||| elements 61 | | | | | | elements we'll use for our bars

    for (var i = 0; i < self.bars; i ++) {

Draw each bar

      var barHeight = byteFreq[i*step]
62 |       self.ctx.fillRect(
63 |         i * (self.barWidth + self.barGap), 
64 |         self.canvas.height - barHeight, 
65 |         self.barWidth, 
66 |         barHeight)
67 |       self.copyCtx.clearRect(0, 0, self.canvas.width, self.canvas.height)
68 |       self.copyCtx.drawImage(self.canvas, 0, 0)
69 |     }
70 |   }
71 | }
72 | 
73 | 
-------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | local_audio_visualizer 6 | 7 | 8 | 9 | 10 |
11 | Fork me on GitHub 12 |
13 | 14 | 15 |
16 |
17 |

18 | Drag an audio file to play it locally
19 | and visualize it 20 |

21 |
22 |
23 |
24 |
25 |
26 |

Local Music Visualizer

27 |

Uses the following HTML5 featrures: 28 | Web Audio API, 29 | drop events, 30 | file access, and 31 | canvas. 32 |

Only works on the most recent versions of Chrome (and possibly Safari). 33 |

Here's the documentation, and here's 34 | cbrandolino's github. 35 |

36 |
37 |
38 | 39 | 40 | -------------------------------------------------------------------------------- /js/local_audio_visualizer.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || 3 | window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; 4 | window.requestAnimationFrame = requestAnimationFrame; 5 | })(); 6 | 7 | window.onload = function() { 8 | var element = document.getElementById('container') 9 | dropAndLoad(element, init, "ArrayBuffer") 10 | } 11 | 12 | 13 | // Reusable dropAndLoad function: it reads a local file dropped on a 14 | // `dropElement` in the DOM in the specified `readFormat` 15 | // (In this case, we want an arrayBuffer) 16 | function dropAndLoad(dropElement, callback, readFormat) { 17 | var readFormat = readFormat || "DataUrl" 18 | 19 | dropElement.addEventListener('dragover', function(e) { 20 | e.stopPropagation() 21 | e.preventDefault() 22 | e.dataTransfer.dropEffect = 'copy' 23 | }, false) 24 | 25 | dropElement.addEventListener('drop', function(e) { 26 | e.stopPropagation() 27 | e.preventDefault() 28 | loadFile(e.dataTransfer.files[0]) 29 | }, false) 30 | 31 | function loadFile(files) { 32 | var file = files 33 | var reader = new FileReader() 34 | reader.onload = function(e) { 35 | callback(e.target.result) 36 | } 37 | reader['readAs'+readFormat](file) 38 | } 39 | } 40 | 41 | // Once the file is loaded, we start getting our hands dirty. 42 | function init(arrayBuffer) { 43 | document.getElementById('instructions').innerHTML = 'Loading ...' 44 | // Create a new `audioContext` and its `analyser` 45 | window.audioCtx = new AudioContext() 46 | window.analyser = audioCtx.createAnalyser() 47 | // If a sound is still playing, stop it. 48 | if (window.source) 49 | source.noteOff(0) 50 | // Decode the data in our array into an audio buffer 51 | audioCtx.decodeAudioData(arrayBuffer, function(buffer) { 52 | // Use the audio buffer with as our audio source 53 | window.source = audioCtx.createBufferSource() 54 | source.buffer = buffer 55 | // Connect to the analyser ... 56 | source.connect(analyser) 57 | // and back to the destination, to play the sound after the analysis. 58 | analyser.connect(audioCtx.destination) 59 | // Start playing the buffer. 60 | source.start(0) 61 | // Initialize a visualizer object 62 | var viz = new simpleViz() 63 | // Finally, initialize the visualizer. 64 | new visualizer(viz['update'], analyser) 65 | document.getElementById('instructions').innerHTML = '' 66 | }) 67 | } 68 | 69 | // The visualizer object. 70 | // Calls the `visualization` function every time a new frame 71 | // is available. 72 | // Is passed an `analyser` (audioContext analyser). 73 | function visualizer(visualization, analyser) { 74 | var self = this 75 | this.visualization = visualization 76 | var last = Date.now() 77 | var loop = function() { 78 | var dt = Date.now() - last 79 | // we get the current byteFreq data from our analyser 80 | var byteFreq = new Uint8Array(analyser.frequencyBinCount) 81 | analyser.getByteFrequencyData(byteFreq) 82 | last = Date.now() 83 | // We might want to use a delta time (`dt`) too for our visualization. 84 | self.visualization(byteFreq, dt) 85 | requestAnimationFrame(loop) 86 | } 87 | requestAnimationFrame(loop) 88 | } 89 | 90 | // A simple visualization. Its update function illustrates how to use 91 | // the byte frequency data from an audioContext analyser. 92 | function simpleViz(canvas) { 93 | var self = this 94 | this.canvas = document.getElementById('canvas') 95 | this.ctx = this.canvas.getContext("2d") 96 | this.copyCtx = document.getElementById('canvas-copy').getContext("2d") 97 | this.ctx.fillStyle = '#fff' 98 | this.barWidth = 10 99 | this.barGap = 4 100 | // We get the total number of bars to display 101 | this.bars = Math.floor(this.canvas.width / (this.barWidth + this.barGap)) 102 | // This function is launched for each frame, together with the byte frequency data. 103 | this.update = function(byteFreq) { 104 | self.ctx.clearRect(0, 0, self.canvas.width, self.canvas.height) 105 | // We take an element from the byteFreq array for each of the bars. 106 | // Let's pretend our byteFreq contains 20 elements, and we have five bars... 107 | var step = Math.floor(byteFreq.length / self.bars) 108 | // `||||||||||||||||||||` elements 109 | // `| | | | | ` elements we'll use for our bars 110 | for (var i = 0; i < self.bars; i ++) { 111 | // Draw each bar 112 | var barHeight = byteFreq[i*step] 113 | self.ctx.fillRect( 114 | i * (self.barWidth + self.barGap), 115 | self.canvas.height - barHeight, 116 | self.barWidth, 117 | barHeight) 118 | self.copyCtx.clearRect(0, 0, self.canvas.width, self.canvas.height) 119 | self.copyCtx.drawImage(self.canvas, 0, 0) 120 | } 121 | } 122 | } 123 | 124 | 125 | --------------------------------------------------------------------------------