├── sources └── screencapture.png ├── README.md ├── style └── style.css ├── License.txt ├── index.html ├── js └── html5_audio_visualizer.js └── audio_visualizer_single_page_version.html /sources/screencapture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wayou/HTML5_Audio_Visualizer/HEAD/sources/screencapture.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | HTML5 Audio Visualizer 2 | ====================== 3 | 4 | An audio spectrum visualizer built with HTML5 Audio API 5 | 6 | Demo 7 | --- 8 | [See it in action](http://wayou.github.io/HTML5_Audio_Visualizer/). 9 | 10 | Screen Capture 11 | --- 12 | 13 | ![alt tag](https://raw.github.com/Wayou/HTML5_Audio_Visualizer/master/sources/screencapture.png) 14 | -------------------------------------------------------------------------------- /style/style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | font-family: arial, "Microsoft YaHei"; 4 | background-color: #272822; 5 | color: #FEFEFE; 6 | } 7 | #fileWrapper{ 8 | transition:all 0.5s ease; 9 | } 10 | #fileWrapper:hover{ 11 | opacity: 1!important; 12 | } 13 | 14 | #visualizer_wrapper{ 15 | text-align: center; 16 | } 17 | footer{ 18 | position: fixed; 19 | bottom: 2px; 20 | color:#aaa; 21 | } -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Wayou Liu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | HTML5 Audio API showcase | Audio visualizer 7 | 8 | 9 | 10 |
11 |
12 |
13 | HTML5 Audio API showcase | An Audio Viusalizer 14 |
15 | 16 | 17 |
18 |
19 | 20 |
21 |
22 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /js/html5_audio_visualizer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * An audio spectrum visualizer built with HTML5 Audio API 3 | * Author:Wayou 4 | * License: MIT 5 | * Feb 15, 2014 6 | */ 7 | window.onload = function() { 8 | new Visualizer().ini(); 9 | }; 10 | var Visualizer = function() { 11 | this.file = null; //the current file 12 | this.fileName = null; //the current file name 13 | this.audioContext = null; 14 | this.source = null; //the audio source 15 | this.info = document.getElementById('info').innerHTML; //used to upgrade the UI information 16 | this.infoUpdateId = null; //to store the setTimeout ID and clear the interval 17 | this.animationId = null; 18 | this.status = 0; //flag for sound is playing 1 or stopped 0 19 | this.forceStop = false; 20 | this.allCapsReachBottom = false; 21 | }; 22 | Visualizer.prototype = { 23 | ini: function() { 24 | this._prepareAPI(); 25 | this._addEventListner(); 26 | }, 27 | _prepareAPI: function() { 28 | //fix browser vender for AudioContext and requestAnimationFrame 29 | window.AudioContext = window.AudioContext || window.webkitAudioContext || window.mozAudioContext || window.msAudioContext; 30 | window.requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame; 31 | window.cancelAnimationFrame = window.cancelAnimationFrame || window.webkitCancelAnimationFrame || window.mozCancelAnimationFrame || window.msCancelAnimationFrame; 32 | try { 33 | this.audioContext = new AudioContext(); 34 | } catch (e) { 35 | this._updateInfo('!Your browser does not support AudioContext', false); 36 | console.log(e); 37 | } 38 | }, 39 | _addEventListner: function() { 40 | var that = this, 41 | audioInput = document.getElementById('uploadedFile'), 42 | dropContainer = document.getElementsByTagName("canvas")[0]; 43 | //listen the file upload 44 | audioInput.onchange = function() { 45 | if (that.audioContext===null) {return;}; 46 | 47 | //the if statement fixes the file selction cancle, because the onchange will trigger even the file selection been canceled 48 | if (audioInput.files.length !== 0) { 49 | //only process the first file 50 | that.file = audioInput.files[0]; 51 | that.fileName = that.file.name; 52 | if (that.status === 1) { 53 | //the sound is still playing but we upload another file, so set the forceStop flag to true 54 | that.forceStop = true; 55 | }; 56 | document.getElementById('fileWrapper').style.opacity = 1; 57 | that._updateInfo('Uploading', true); 58 | //once the file is ready,start the visualizer 59 | that._start(); 60 | }; 61 | }; 62 | //listen the drag & drop 63 | dropContainer.addEventListener("dragenter", function() { 64 | document.getElementById('fileWrapper').style.opacity = 1; 65 | that._updateInfo('Drop it on the page', true); 66 | }, false); 67 | dropContainer.addEventListener("dragover", function(e) { 68 | e.stopPropagation(); 69 | e.preventDefault(); 70 | //set the drop mode 71 | e.dataTransfer.dropEffect = 'copy'; 72 | }, false); 73 | dropContainer.addEventListener("dragleave", function() { 74 | document.getElementById('fileWrapper').style.opacity = 0.2; 75 | that._updateInfo(that.info, false); 76 | }, false); 77 | dropContainer.addEventListener("drop", function(e) { 78 | e.stopPropagation(); 79 | e.preventDefault(); 80 | if (that.audioContext===null) {return;}; 81 | document.getElementById('fileWrapper').style.opacity = 1; 82 | that._updateInfo('Uploading', true); 83 | //get the dropped file 84 | that.file = e.dataTransfer.files[0]; 85 | if (that.status === 1) { 86 | document.getElementById('fileWrapper').style.opacity = 1; 87 | that.forceStop = true; 88 | }; 89 | that.fileName = that.file.name; 90 | //once the file is ready,start the visualizer 91 | that._start(); 92 | }, false); 93 | }, 94 | _start: function() { 95 | //read and decode the file into audio array buffer 96 | var that = this, 97 | file = this.file, 98 | fr = new FileReader(); 99 | fr.onload = function(e) { 100 | var fileResult = e.target.result; 101 | var audioContext = that.audioContext; 102 | if (audioContext === null) { 103 | return; 104 | }; 105 | that._updateInfo('Decoding the audio', true); 106 | audioContext.decodeAudioData(fileResult, function(buffer) { 107 | that._updateInfo('Decode succussfully,start the visualizer', true); 108 | that._visualize(audioContext, buffer); 109 | }, function(e) { 110 | that._updateInfo('!Fail to decode the file', false); 111 | console.error(e); 112 | }); 113 | }; 114 | fr.onerror = function(e) { 115 | that._updateInfo('!Fail to read the file', false); 116 | console.error(e); 117 | }; 118 | //assign the file to the reader 119 | this._updateInfo('Starting read the file', true); 120 | fr.readAsArrayBuffer(file); 121 | }, 122 | _visualize: function(audioContext, buffer) { 123 | var audioBufferSouceNode = audioContext.createBufferSource(), 124 | analyser = audioContext.createAnalyser(), 125 | that = this; 126 | //connect the source to the analyser 127 | audioBufferSouceNode.connect(analyser); 128 | //connect the analyser to the destination(the speaker), or we won't hear the sound 129 | analyser.connect(audioContext.destination); 130 | //then assign the buffer to the buffer source node 131 | audioBufferSouceNode.buffer = buffer; 132 | //play the source 133 | if (!audioBufferSouceNode.start) { 134 | audioBufferSouceNode.start = audioBufferSouceNode.noteOn //in old browsers use noteOn method 135 | audioBufferSouceNode.stop = audioBufferSouceNode.noteOff //in old browsers use noteOff method 136 | }; 137 | //stop the previous sound if any 138 | if (this.animationId !== null) { 139 | cancelAnimationFrame(this.animationId); 140 | } 141 | if (this.source !== null) { 142 | this.source.stop(0); 143 | } 144 | audioBufferSouceNode.start(0); 145 | this.status = 1; 146 | this.source = audioBufferSouceNode; 147 | audioBufferSouceNode.onended = function() { 148 | that._audioEnd(that); 149 | }; 150 | this._updateInfo('Playing ' + this.fileName, false); 151 | this.info = 'Playing ' + this.fileName; 152 | document.getElementById('fileWrapper').style.opacity = 0.2; 153 | this._drawSpectrum(analyser); 154 | }, 155 | _drawSpectrum: function(analyser) { 156 | var that = this, 157 | canvas = document.getElementById('canvas'), 158 | cwidth = canvas.width, 159 | cheight = canvas.height - 2, 160 | meterWidth = 10, //width of the meters in the spectrum 161 | gap = 2, //gap between meters 162 | capHeight = 2, 163 | capStyle = '#fff', 164 | meterNum = 800 / (10 + 2), //count of the meters 165 | capYPositionArray = []; ////store the vertical position of hte caps for the preivous frame 166 | ctx = canvas.getContext('2d'), 167 | gradient = ctx.createLinearGradient(0, 0, 0, 300); 168 | gradient.addColorStop(1, '#0f0'); 169 | gradient.addColorStop(0.5, '#ff0'); 170 | gradient.addColorStop(0, '#f00'); 171 | var drawMeter = function() { 172 | var array = new Uint8Array(analyser.frequencyBinCount); 173 | analyser.getByteFrequencyData(array); 174 | if (that.status === 0) { 175 | //fix when some sounds end the value still not back to zero 176 | for (var i = array.length - 1; i >= 0; i--) { 177 | array[i] = 0; 178 | }; 179 | allCapsReachBottom = true; 180 | for (var i = capYPositionArray.length - 1; i >= 0; i--) { 181 | allCapsReachBottom = allCapsReachBottom && (capYPositionArray[i] === 0); 182 | }; 183 | if (allCapsReachBottom) { 184 | cancelAnimationFrame(that.animationId); //since the sound is stoped and animation finished, stop the requestAnimation to prevent potential memory leak,THIS IS VERY IMPORTANT! 185 | return; 186 | }; 187 | }; 188 | var step = Math.round(array.length / meterNum); //sample limited data from the total array 189 | ctx.clearRect(0, 0, cwidth, cheight); 190 | for (var i = 0; i < meterNum; i++) { 191 | var value = array[i * step]; 192 | if (capYPositionArray.length < Math.round(meterNum)) { 193 | capYPositionArray.push(value); 194 | }; 195 | ctx.fillStyle = capStyle; 196 | //draw the cap, with transition effect 197 | if (value < capYPositionArray[i]) { 198 | ctx.fillRect(i * 12, cheight - (--capYPositionArray[i]), meterWidth, capHeight); 199 | } else { 200 | ctx.fillRect(i * 12, cheight - value, meterWidth, capHeight); 201 | capYPositionArray[i] = value; 202 | }; 203 | ctx.fillStyle = gradient; //set the filllStyle to gradient for a better look 204 | ctx.fillRect(i * 12 /*meterWidth+gap*/ , cheight - value + capHeight, meterWidth, cheight); //the meter 205 | } 206 | that.animationId = requestAnimationFrame(drawMeter); 207 | } 208 | this.animationId = requestAnimationFrame(drawMeter); 209 | }, 210 | _audioEnd: function(instance) { 211 | if (this.forceStop) { 212 | this.forceStop = false; 213 | this.status = 1; 214 | return; 215 | }; 216 | this.status = 0; 217 | var text = 'HTML5 Audio API showcase | An Audio Viusalizer'; 218 | document.getElementById('fileWrapper').style.opacity = 1; 219 | document.getElementById('info').innerHTML = text; 220 | instance.info = text; 221 | document.getElementById('uploadedFile').value = ''; 222 | }, 223 | _updateInfo: function(text, processing) { 224 | var infoBar = document.getElementById('info'), 225 | dots = '...', 226 | i = 0, 227 | that = this; 228 | infoBar.innerHTML = text + dots.substring(0, i++); 229 | if (this.infoUpdateId !== null) { 230 | clearTimeout(this.infoUpdateId); 231 | }; 232 | if (processing) { 233 | //animate dots at the end of the info text 234 | var animateDot = function() { 235 | if (i > 3) { 236 | i = 0 237 | }; 238 | infoBar.innerHTML = text + dots.substring(0, i++); 239 | that.infoUpdateId = setTimeout(animateDot, 250); 240 | } 241 | this.infoUpdateId = setTimeout(animateDot, 250); 242 | }; 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /audio_visualizer_single_page_version.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | HTML5 Audio API showcase | Audio visualizer 6 | 28 | 29 | 30 |
31 |
32 |
33 | HTML5 Audio API showcase | An Audio Viusalizer 34 |
35 | 36 | 37 |
38 |
39 | 40 |
41 |
42 | 45 | 291 | 299 | 300 | --------------------------------------------------------------------------------