├── .project
├── LICENSE
├── README.md
├── css
└── index.css
├── index.html
├── js
├── loader.js
└── main.js
└── resource
├── Screenshot.gif
└── cd_blue.png
/.project:
--------------------------------------------------------------------------------
1 |
2 |
3 | AudioVisualizer
4 |
5 |
6 |
7 |
8 |
9 | com.aptana.ide.core.unifiedBuilder
10 |
11 |
12 |
13 |
14 |
15 | com.aptana.projects.webnature
16 |
17 |
18 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Qieguo Chow
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | 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, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | AudioVisualizer
2 | ---
3 | This is a demo to use HTML5 Audio Context API.
4 |
5 | Now there are only two kinds very simple visualizer effects, but I will add some interesting effect in future.
6 |
7 | For more infomation, please visit my blog: http://www.cnblogs.com/qieguo/p/5405303.html
8 |
9 | Demo
10 | ---
11 | See it in action: http://qieguo2016.github.io/AudioVisualizer/
12 |
13 | If you visit this demo on PC, Move Your Mouse to capture the balls!
14 |
15 | If you visit this demo on mobile device, touch the ball and move them!
16 |
17 | Reference:
18 | ---
19 | 1. http://wayou.github.io/MeowmeowPlayer/
20 |
21 | 2. https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Visualizations_with_Web_Audio_API
22 |
23 |
24 | Screenshot
25 | ---
26 | 
27 |
--------------------------------------------------------------------------------
/css/index.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | margin: 0;
4 | padding: 0;
5 | font-family: arial, "Microsoft YaHei";
6 | font-size: 16px;
7 | background-color: #272822;
8 | color: #FEFEFE;
9 | }
10 |
11 | div.msg {
12 | position: fixed;
13 | left: 5px;
14 | top: 5px;
15 | }
16 |
17 | #info {
18 | padding: 0;
19 | margin: 1px;
20 | -webkit-user-select:none
21 | }
22 |
23 | #fileIn {
24 | text-decoration: underline;
25 | color: #36B953;
26 | -webkit-user-select:none
27 | }
28 |
29 | #fileIn:hover {
30 | cursor: pointer;
31 | color: #FF00B4;
32 | }
33 |
34 | #uploader {
35 | display: none;
36 | }
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | HTML5 Web Audio Demo
11 |
12 |
13 |
14 |
15 |
16 |
17 |
HTML5 Audio visualizer
18 |
19 | Click me to select an audio file.
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/js/loader.js:
--------------------------------------------------------------------------------
1 | var lightLoader = function(c, cw, ch){
2 |
3 | var that = this;
4 | this.c = c;
5 | this.ctx = c.getContext('2d');
6 | this.cw = cw;
7 | this.ch = ch;
8 | this.raf = null;
9 |
10 | this.loaded = 0;
11 | this.loaderSpeed = .6;
12 | this.loaderWidth = cw * 0.8;
13 | this.loaderHeight = 20;
14 | this.loader = {
15 | x: (this.cw/2) - (this.loaderWidth/2),
16 | y: (this.ch/2) - (this.loaderHeight/2)
17 | };
18 | this.particles = [];
19 | this.particleLift = 220;
20 | this.hueStart = 0
21 | this.hueEnd = 120;
22 | this.hue = 0;
23 | this.gravity = .15;
24 | this.particleRate = 4;
25 |
26 | /*========================================================*/
27 | /* Initialize
28 | /*========================================================*/
29 | this.init = function(){
30 | this.loaded = 0;
31 | this.particles = [];
32 | this.loop();
33 | };
34 |
35 | /*========================================================*/
36 | /* Utility Functions
37 | /*========================================================*/
38 | this.rand = function(rMi, rMa){return ~~((Math.random()*(rMa-rMi+1))+rMi);};
39 | this.hitTest = function(x1, y1, w1, h1, x2, y2, w2, h2){return !(x1 + w1 < x2 || x2 + w2 < x1 || y1 + h1 < y2 || y2 + h2 < y1);};
40 |
41 | /*========================================================*/
42 | /* Update Loader
43 | /*========================================================*/
44 | this.updateLoader = function(){
45 | if(this.loaded < 100){
46 | this.loaded += this.loaderSpeed;
47 | } else {
48 | this.loaded = 0;
49 | }
50 | };
51 |
52 | /*========================================================*/
53 | /* Render Loader
54 | /*========================================================*/
55 | this.renderLoader = function(){
56 | this.ctx.fillStyle = '#000';
57 | this.ctx.fillRect(this.loader.x, this.loader.y, this.loaderWidth, this.loaderHeight);
58 |
59 | this.hue = this.hueStart + (this.loaded/100)*(this.hueEnd - this.hueStart);
60 |
61 | var newWidth = (this.loaded/100)*this.loaderWidth;
62 | this.ctx.fillStyle = 'hsla('+this.hue+', 100%, 40%, 1)';
63 | this.ctx.fillRect(this.loader.x, this.loader.y, newWidth, this.loaderHeight);
64 |
65 | this.ctx.fillStyle = '#222';
66 | this.ctx.fillRect(this.loader.x, this.loader.y, newWidth, this.loaderHeight/2);
67 | };
68 |
69 | /*========================================================*/
70 | /* Particles
71 | /*========================================================*/
72 | this.Particle = function(){
73 | this.x = that.loader.x + ((that.loaded/100)*that.loaderWidth) - that.rand(0, 1);
74 | this.y = that.ch/2 + that.rand(0,that.loaderHeight)-that.loaderHeight/2;
75 | this.vx = (that.rand(0,4)-2)/100;
76 | this.vy = (that.rand(0,that.particleLift)-that.particleLift*2)/100;
77 | this.width = that.rand(2,6)/2;
78 | this.height = that.rand(2,6)/2;
79 | this.hue = that.hue;
80 | };
81 |
82 | this.Particle.prototype.update = function(i){
83 | this.vx += (that.rand(0,6)-3)/100;
84 | this.vy += that.gravity;
85 | this.x += this.vx;
86 | this.y += this.vy;
87 |
88 | if(this.y > that.ch){
89 | that.particles.splice(i, 1);
90 | }
91 | };
92 |
93 | this.Particle.prototype.render = function(){
94 | that.ctx.fillStyle = 'hsla('+this.hue+', 100%, '+that.rand(50,70)+'%, '+that.rand(20,100)/100+')';
95 | that.ctx.fillRect(this.x, this.y, this.width, this.height);
96 | };
97 |
98 | this.createParticles = function(){
99 | var i = this.particleRate;
100 | while(i--){
101 | this.particles.push(new this.Particle());
102 | };
103 | };
104 |
105 | this.updateParticles = function(){
106 | var i = this.particles.length;
107 | while(i--){
108 | var p = this.particles[i];
109 | p.update(i);
110 | };
111 | };
112 |
113 | this.renderParticles = function(){
114 | var i = this.particles.length;
115 | while(i--){
116 | var p = this.particles[i];
117 | p.render();
118 | };
119 | };
120 |
121 |
122 | /*========================================================*/
123 | /* Clear Canvas
124 | /*========================================================*/
125 | this.clearCanvas = function(){
126 | this.ctx.globalCompositeOperation = 'source-over';
127 | this.ctx.clearRect(0,0,this.cw,this.ch);
128 | this.ctx.globalCompositeOperation = 'lighter';
129 | };
130 |
131 | /*========================================================*/
132 | /* Animation Loop
133 | /*========================================================*/
134 | this.loop = function(){
135 | var loopIt = function(){
136 | that.raf = requestAnimationFrame(loopIt);
137 | that.clearCanvas();
138 |
139 | that.createParticles();
140 |
141 | that.updateLoader();
142 | that.updateParticles();
143 |
144 | that.renderLoader();
145 | that.renderParticles();
146 |
147 | };
148 | loopIt();
149 | };
150 |
151 |
152 | this.stop = function(){
153 | this.clearCanvas();
154 | window.cancelAnimationFrame(this.raf);
155 | }
156 |
157 | };
158 |
159 |
160 | /*========================================================*/
161 | /* Setup requestAnimationFrame when it is unavailable.
162 | /*========================================================*/
163 | var setupRAF = function(){
164 | var lastTime = 0;
165 | var vendors = ['ms', 'moz', 'webkit', 'o'];
166 | for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x){
167 | window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
168 | window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame'];
169 | };
170 |
171 | if(!window.requestAnimationFrame){
172 | window.requestAnimationFrame = function(callback, element){
173 | var currTime = new Date().getTime();
174 | var timeToCall = Math.max(0, 16 - (currTime - lastTime));
175 | var id = window.setTimeout(function() { callback(currTime + timeToCall); }, timeToCall);
176 | lastTime = currTime + timeToCall;
177 | return id;
178 | };
179 | };
180 |
181 | if (!window.cancelAnimationFrame){
182 | window.cancelAnimationFrame = function(id){
183 | clearTimeout(id);
184 | };
185 | };
186 | };
--------------------------------------------------------------------------------
/js/main.js:
--------------------------------------------------------------------------------
1 | /*-----------------------------【程序说明】-----------------------------
2 | * 程序说明: 使用HTML5 Web Audio API实现音乐可视化效果
3 | * 程序描述: 有常规频谱和能量球两种方式,能量球方式还提供了捕捉能量球的交互效果,
4 | * 并支持PC端的鼠标捕捉和移动端的触屏捕捉。
5 | * 浏览器支持:Chrome、Firefox、Safari(若有问题,请更新浏览器到最新版)
6 | * 2016年04月 Created by @Qieguo
7 | * 更多信息请关注我的博客:http://www.cnblogs.com/qieguo/p/5405303.html
8 | *
9 | * Reference: 1)参考了刘哇勇的文章并使用了一部分代码。http://www.cnblogs.com/Wayou/p/html5_audio_api_visualizer.html
10 | * 2)Visualizations with Web Audio API(官方原版,强力推荐)
11 | * https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Visualizations_with_Web_Audio_API
12 | * Licensed under the MIT,转载使用请注明出处!http://www.cnblogs.com/qieguo/p/5405303.html
13 | *--------------------------------------------------------------------
14 | */
15 |
16 |
17 | "use strict"
18 | var mymv;
19 | function resizeCanvas () {
20 | var canvas = document.getElementById('drawCanvas');
21 | canvas.width = window.clientWidth
22 | || document.documentElement.clientWidth
23 | || document.body.clientWidth;
24 | canvas.height = window.clientHeight
25 | || document.documentElement.clientHeight
26 | || document.body.clientHeight;
27 | }
28 |
29 | window.onload = function() {
30 | //根据浏览器尺寸设置画布的尺寸
31 | resizeCanvas();
32 |
33 | //浏览器加载时初始化MV对象
34 | mymv = new MV();
35 | mymv.init();
36 |
37 | var canvas = document.getElementById('drawCanvas');
38 | //鼠标捕捉能量球
39 | canvas.onmousemove = function (e) {
40 | if (mymv.status != 0) {
41 | for (var n = 0; n < mymv.visualizer.length; n++) {
42 | var s = mymv.visualizer[n];
43 | if (Math.sqrt(Math.pow(s.x-e.pageX,2) + Math.pow(s.y-e.pageY,2)) < s.radius) {
44 | s.x = e.pageX;
45 | s.y = e.pageY;
46 | }
47 | }
48 | }
49 | };
50 |
51 | /*触屏设备单指拖动能量球*/
52 | canvas.addEventListener('touchmove', function(event) {
53 | //判断是否播放状态
54 | if (mymv.status != 0) {
55 | // 如果画布内只有一个手指的话
56 | if (event.targetTouches.length == 1) {
57 | event.preventDefault();// 阻止浏览器默认事件,重要
58 | var touch = event.targetTouches[0];
59 | // 把能量球放在手指所在的位置()
60 | for (var n = 0; n < mymv.visualizer.length; n++) {
61 | var s = mymv.visualizer[n];
62 | if (Math.sqrt(Math.pow(s.x-touch.pageX,2) + Math.pow(s.y-touch.pageY,2)) < 30) {
63 | s.x = touch.pageX;
64 | s.y = touch.pageY;
65 | }
66 | }
67 | }
68 | }
69 | }, false);
70 |
71 | //测试绘图样式
72 | canvas.onclick = function (e) {
73 | //判断播放状态,不播放的时候才触发
74 | if (mymv.status === 0) {
75 | var ctx = canvas.getContext('2d');
76 | var gradient = ctx.createRadialGradient(e.pageX, e.pageY, 0, e.pageX, e.pageY, 30);
77 | var random = function (m, n) { return Math.round(Math.random()*(n - m) + m); };
78 | //内发光,圆内变色
79 | var color = 'hsla(' + random(0, 360) + ',' + '100%,' + random(50, 60) + '%,1)';
80 | gradient.addColorStop(0, 'hsla(0,0%,100%,0.8)');
81 | gradient.addColorStop(0.6, color);
82 | gradient.addColorStop(1, 'hsla(0,0%,100%,0)');
83 | //内发光,圆外变色
84 | // var color = 'hsla(' + random(0, 360) + ',' + '100%,' + random(50, 60) + '%,0)';
85 | // gradient.addColorStop(0, 'hsla(0,0%,100%,1)');
86 | // gradient.addColorStop(0.6, 'hsla(0,5%,98%,0.8)');
87 | // gradient.addColorStop(1, color);
88 | ctx.fillStyle = gradient;
89 | ctx.beginPath();
90 | ctx.arc(e.pageX, e.pageY, 30, 0, Math.PI*2, true);
91 | ctx.fill();
92 | }
93 | };
94 |
95 | };
96 |
97 | //根据浏览器尺寸设置画布的尺寸
98 | window.onresize = function () { resizeCanvas(); };
99 |
100 | //定义MV对象的属性
101 | var MV = function() {
102 | this.files = null; //Music Visualize对象的文件
103 | this.fileName = null; //Music Visualize对象的文件名
104 | this.ac = null; //Music Visualize对象的AudioContext
105 | this.status = 0; //Music Visualize对象的状态(播放/停止状态)
106 | this.forceStop = false; //强制终止播放状态
107 | this.animationId = null; //动画ID
108 | this.source = null; //流媒体的源
109 | this.visualizer = []; //频谱表现形式,包含x,y,dy,color,radius
110 | this.canvas = null; //画布
111 | this.loader = null; //进度条
112 | };
113 |
114 | //定义MV对象方法,原型方式
115 | MV.prototype = {
116 | init: function() {
117 | //浏览器兼容设置
118 | window.AudioContext = window.AudioContext || window.webkitAudioContext || window.mozAudioContext || window.msAudioContext;
119 | window.requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame;
120 | window.cancelAnimationFrame = window.cancelAnimationFrame || window.webkitCancelAnimationFrame || window.mozCancelAnimationFrame || window.msCancelAnimationFrame;
121 | try {
122 | this.ac = new AudioContext();
123 | } catch (err) {
124 | alert('!Your browser does not support AudioContext, Please change to Chrome or Firefox!');
125 | console.log(err);
126 | };
127 |
128 | //隐藏文件输入控件,通过fileIn文本的click事件调用文件输入控件
129 | var that = this,
130 | btn = document.getElementById('fileIn'),
131 | audioInput = document.getElementById('uploader');
132 |
133 | this.canvas = document.getElementById('drawCanvas');
134 | this.loader = new lightLoader(this.canvas, this.canvas.width, this.canvas.height - 1);
135 | setupRAF(); //当浏览器不支持requestAnimationFrame的时候设置个备胎
136 |
137 | btn.onclick = function () { audioInput.click(); };
138 | //监控文本输入控件的改变,判断是否读入文件或者切换文件
139 | audioInput.onchange = function() {
140 | if (that.ac === null) {return;}; //new AudioContext失败,则退出函数
141 | //判断是否真正选中文件,因为取消也可以触发onchange事件
142 | if (audioInput.files.length !== 0) {
143 | //仅获取文件列中的第一个文件
144 | that.files = audioInput.files[0];
145 | that.fileName = audioInput.files[0].name;
146 | if (that.status === 1) {
147 | //正在播放的时候切换文件,需要强制停止,将forceStop置为true
148 | that.forceStop = true;
149 | //停止前一首歌曲
150 | if (that.animationId !== null) {
151 | cancelAnimationFrame(that.animationId);
152 | }
153 | if (that.source !== null) {
154 | that.source.stop(0);
155 | }
156 | };
157 | //当文件准备好的时候,开始读入
158 | that._read();
159 | that.loader.init();
160 | };
161 | };
162 | },
163 |
164 | _read: function() {
165 | //读取文件,并进行解码
166 | var that = this,
167 | rfile = that.files,
168 | fr = new FileReader();
169 | fr.onload = function(e) {
170 | if (that.ac === null) {
171 | return;
172 | };
173 | that._updateInfo('Decoding the audio', true);
174 | //AudioContext.decodeAudioData解码Audio文件,第一个参数为缓冲数列
175 | var fileResult = e.target.result;
176 | that.ac.decodeAudioData(fileResult, function(buffer) {
177 | that._updateInfo('Decode succussfully,start the visualizer', true);
178 | //转到播放和分析环节
179 | that.loader.stop();
180 | that._control(that.ac, buffer);
181 | }, function(err) {
182 | alert('!Fail to decode the file');
183 | console.log(err);
184 | });
185 | };
186 | fr.onerror = function(err) {
187 | alert('!Fail to read the file');
188 | console.log(err);
189 | };
190 | that._updateInfo('Starting read the file', true);
191 | //ArrayBuffer方式读取
192 | fr.readAsArrayBuffer(rfile);
193 | },
194 |
195 | _control: function(audioContext, buffer) {
196 | //创建BufferSource来保存解码出来的数据流
197 | var bufferSouceNode = audioContext.createBufferSource(),
198 | analyser = audioContext.createAnalyser(),
199 | that = this;
200 | //将音源连接起来,音源>分析器>输出
201 | bufferSouceNode.connect(analyser);
202 | analyser.connect(audioContext.destination);
203 | bufferSouceNode.buffer = buffer;
204 | bufferSouceNode.loop = true;
205 | //启动,也就是启动音频读入>分析>输出这个流程
206 | if (!bufferSouceNode.start) {
207 | bufferSouceNode.start = bufferSouceNode.noteOn //旧版本语法:noteOn
208 | bufferSouceNode.stop = bufferSouceNode.noteOff //旧版本语法:noteOn
209 | };
210 | //停止前一首歌曲
211 | if (this.animationId !== null) {
212 | cancelAnimationFrame(this.animationId);
213 | }
214 | if (this.source !== null) {
215 | this.source.stop(0);
216 | }
217 | //启动的新版本语法
218 | bufferSouceNode.start(0);
219 | this.status = 1;
220 | this.source = bufferSouceNode;
221 | //音频结束事件,绑定_audioEnd函数
222 | bufferSouceNode.onended = function() {
223 | that._audioEnd(that);
224 | };
225 | this._updateInfo('Playing ' + this.fileName, false);
226 | this._visualize_flow(analyser); //能量球样式的可视化效果
227 | //this._visualize(analyser); //柱状图样式的可视化效果
228 | },
229 |
230 | _visualize: function(analyser) {
231 | var that = this,
232 | cwidth = this.canvas.width,
233 | cheight = this.canvas.height - 2, //底部留一点余白
234 | meterWidth = 10, //能量条的宽度
235 | gap = 2, //能量条间的间距
236 | capHeight = 2,
237 | capStyle = '#fff',
238 | meterNum = Math.round(cwidth / (meterWidth + gap)),
239 | capYPositionArray = [], //保存能力柱帽子的先前位置
240 | ctx = this.canvas.getContext('2d');
241 |
242 | //定义一个渐变样式用于画图
243 | var gradient = ctx.createLinearGradient(0, 0, 0, cheight);
244 | gradient.addColorStop(1, '#0f0');
245 | gradient.addColorStop(0.5, '#ff0');
246 | gradient.addColorStop(0, '#f00');
247 | ctx.fillStyle = gradient;
248 |
249 | var drawMeter = function() {
250 | var array = new Uint8Array(analyser.frequencyBinCount);
251 | analyser.getByteFrequencyData(array);
252 | if (that.status === 0) {
253 | //曲终时能量帽的归零
254 | for (var i = array.length - 1; i >= 0; i--) {
255 | array[i] = 0;
256 | };
257 | allCapsReachBottom = true;
258 | for (var i = capYPositionArray.length - 1; i >= 0; i--) {
259 | allCapsReachBottom = allCapsReachBottom && (capYPositionArray[i] === 0);
260 | };
261 | if (allCapsReachBottom) {
262 | //!!!音频播完动画结束了!必须手动停止动画以防内存泄露!非常重要!!!
263 | cancelAnimationFrame(that.animationId);
264 | return;
265 | };
266 | };
267 | //计算步长
268 | var step = Math.round(array.length / meterNum);
269 | ctx.clearRect(0, 0, cwidth, cheight);
270 | for (var i = 0; i < meterNum; i++) {
271 | var value = array[i * step] * cheight / 256;
272 | if (capYPositionArray.length < Math.round(meterNum)) {
273 | capYPositionArray.push(value);
274 | };
275 | ctx.fillStyle = capStyle;
276 | //绘制能量帽
277 | if (value < capYPositionArray[i]) {
278 | ctx.fillRect(i * (meterWidth + gap), cheight - (--capYPositionArray[i]), meterWidth, capHeight);
279 | } else {
280 | ctx.fillRect(i * (meterWidth + gap), cheight - value, meterWidth, capHeight);
281 | capYPositionArray[i] = value;
282 | };
283 | //使用渐变填充得到更好的效果
284 | ctx.fillStyle = gradient;
285 | ctx.fillRect(i * (meterWidth + gap), cheight - value + capHeight, meterWidth, cheight); //the meter
286 | }
287 | //这个与后面一句区别在this和that。严格模式下播放时this为undefined,一般模式下this指向window。
288 | //_visualize,_audioEnd这些为MV的方法,所以函数内部this指向MV,但其内部嵌套的函数并非MV的函数,其原型为window!
289 | //所以在进入嵌套函数内部时,this已经被改变了,需要用一个that来保存this的指向对象MV
290 | that.animationId = requestAnimationFrame(drawMeter);
291 | };
292 | this.animationId = requestAnimationFrame(drawMeter);
293 | },
294 |
295 | _visualize_flow: function(analyser) {
296 | var that = this,
297 | cwidth = this.canvas.width,
298 | cheight = this.canvas.height,
299 | num = cwidth > 500 ? 50 : 30, //能量球的数量
300 | ctx = this.canvas.getContext('2d');
301 |
302 | var random = function (m, n) {
303 | return Math.round(Math.random()*(n - m) + m);
304 | };
305 | for (var i = 0; i < num; i++) {
306 | var x = random(0, cwidth),
307 | y = random(0, cheight),
308 | color= 'hsla(' + random(0, 360) + ',' + '100%,' + random(50, 60) + '%,1)';
309 | that.visualizer.push({
310 | x: x,
311 | y: y,
312 | dy: Math.random() + 0.1, //返回大于0.1的数据,防止静止
313 | color: color,
314 | radius: 30
315 | });
316 | }
317 |
318 | var drawMeter = function() {
319 | //创建8位整数数组保存频谱数据
320 | var array = new Uint8Array(analyser.frequencyBinCount);
321 | analyser.getByteFrequencyData(array);
322 | if (that.status === 0) {
323 | //能量球归零
324 | for (var i = array.length - 1; i >= 0; i--) {
325 | array[i] = 0;
326 | };
327 | var allBallstoZero = true;
328 | for (var i = that.visualizer.length - 1; i >= 0; i--) {
329 | allBallstoZero = allBallstoZero && ( that.visualizer[i].radius < 1);
330 | };
331 | if (allBallstoZero) {
332 | //!!!音频播完动画结束了!必须手动停止动画以防内存泄露!非常重要!!!
333 | cancelAnimationFrame(that.animationId);
334 | return;
335 | };
336 | };
337 | var cwidth = that.canvas.width,
338 | cheight = that.canvas.height, //画布宽高的重复声明是为了保证动画的自适应屏幕尺寸
339 | num = cwidth > 500 ? 50 : 30, //能量球的数量也一样自适应
340 | step = Math.round(array.length / (num + 10)); //计算步长
341 | ctx.clearRect(0, 0, cwidth, cheight);
342 | for (var n = 0; n < num; n++) {
343 | var s = that.visualizer[n];
344 | //能量球半径,与画布大小关联起来
345 | s.radius = Math.round(array[n * step] / 256 * (cwidth > cheight ? cwidth / 22 : cheight / 16));
346 | //加了一点模糊发光的效果
347 | var gradient = ctx.createRadialGradient(s.x, s.y, 0, s.x, s.y, s.radius);
348 | gradient.addColorStop(0, 'hsla(0,0%,100%,0.8)');
349 | gradient.addColorStop(0.6, s.color);
350 | gradient.addColorStop(1, 'hsla(0,0%,100%,0)');
351 | // gradient.addColorStop(0.6, 'hsla(0,0%,100%,1)');
352 | // gradient.addColorStop(1, s.color);
353 | ctx.fillStyle = gradient;
354 | ctx.beginPath();
355 | ctx.arc(s.x, s.y, s.radius, 0, Math.PI*2, true);
356 | ctx.fill();
357 | s.y = s.y - 2 * s.dy; //上飘效果
358 | //到顶部后返回底部,随机化
359 | if ((s.y <= 0)&&(that.status != 0)) {
360 | s.y = cheight;
361 | s.x = random(0, cwidth);
362 | s.color = 'hsla(' + random(0, 360) + ',' + '100%,' + random(50, 60) + '%,1)';
363 | }
364 | }
365 | //严格模式下播放时this为undefined,一般模式下this指向window。
366 | //_visualize,_audioEnd这些为MV的方法,所以函数内部this指向MV,但其内部嵌套的函数并非MV的函数,其原型为window!
367 | //所以在进入嵌套函数内部时,this已经被改变了,需要用一个that来保存this的指向对象MV
368 | that.animationId = requestAnimationFrame(drawMeter);
369 | };
370 | this.animationId = requestAnimationFrame(drawMeter); //启动动画
371 | },
372 | //音频播放结束,绑定了onended事件
373 | _audioEnd: function(instance) {
374 | if (this.forceStop) {
375 | this.forceStop = false;
376 | this.status = 1;
377 | return;
378 | };
379 | this.status = 0;
380 | var text = 'HTML5 Audio visualizer';
381 | document.getElementById('info').innerHTML = text;
382 | document.getElementById('uploader').value = '';
383 | },
384 | //信息输出
385 | _updateInfo: function(text, processing) {
386 | var infoBar = document.getElementById('info'),
387 | dots = '...',
388 | i = 0,
389 | that = this;
390 | infoBar.innerHTML = text + dots.substring(0, i++);
391 | if (this.infoUpdateId !== null) {
392 | clearTimeout(this.infoUpdateId);
393 | };
394 | if (processing) {
395 | //末尾3个点号的小动画
396 | var animateDot = function() {
397 | if (i > 3) {
398 | i = 0
399 | };
400 | infoBar.innerHTML = text + dots.substring(0, i++);
401 | that.infoUpdateId = setTimeout(animateDot, 250);
402 | }
403 | this.infoUpdateId = setTimeout(animateDot, 250);
404 | };
405 | }
406 | }
407 |
408 |
--------------------------------------------------------------------------------
/resource/Screenshot.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qieguo2016/AudioVisualizer/14b78c991358531dd6ae1816ec7ae71da2ccb925/resource/Screenshot.gif
--------------------------------------------------------------------------------
/resource/cd_blue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qieguo2016/AudioVisualizer/14b78c991358531dd6ae1816ec7ae71da2ccb925/resource/cd_blue.png
--------------------------------------------------------------------------------