MusicVisualizer

Passionate Music 音乐可视化的实现


今天我们来实现一个node+canvas搭建的音乐可视化的小demo,话不多说上图

选择了当时风靡校园的三首金曲 勿喷 orz.

node环境搭建

安装express生成器,搭建express脚手架 :$ npm install express-generator -g
然后进入目录 $ express myapp && cd myapp && npm install,再运行npm start跑起来

代表编写

路由

请求页面时,向node端拦截请求,返回音频数据源

1
2
3
4
5
6
7
8
9
10
11
12
var media=path.join(__dirname,'../public/media');
router.get('/',function (req, res) {
var fs=require('fs')
fs.readdir(media,function (err, names) {
if(err){
console.log(err)
}else{
res.render('index',{title:'Passionate Music',music:names})
}
})
})

index页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<html>
<head>
<meta name="renderer" content="webkit">
<meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no">
<title><%= title %></title>
<link rel='stylesheet' href='/stylesheets/index.css' />
</head>
<body>
<header>
<h1><%= title %></h1>
<ul class="type" id="type">
<li data-type="dot">Dot</li>
<li data-type="column" class="selected">Column</li>
</ul>
<p>
Volume <input type="range" id="volume" oninput="change(this)" min="0" max="100" value="60">
</p>
</header>
<div class="left">
<ul id="list">
<% music.forEach(function(name){ %>
<li title="<%= name%>"><%= name %></li>
<% }) %>
</ul>
</div>
<div class="right" id="box"></div>
<script src="/javascripts/index.js"></script>
</body>
</html>

这里用ejs当做模板,渲染后台的数据源,格式请参考官网.
Volume 为音量调节 dot 和 column是两种模式 music.forEach为循环出来的点歌列表

index.js

习惯性封装选择器

1
2
3
function $(node){
return document.querySelectorAll(node);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
let xhr=new XMLHttpRequest();
let ac = new(window.AudioContext || window.webkitAudiocontext)();
// 创建音频上下文,并做兼容处理
let gainNode = ac[ac.createGain ? 'createGain' : 'createGainNode']();
gainNode.connect(ac.destination);
// 创建音量控制节点,兼容,并且连接到音频节点
let anylyser = ac.createAnalySer();
let size = 128 // 自定义柱状/散点数
anylyser.fftSize = size*2;
//frequencyBinCount 的值固定为 AnalyserNode 接口中fftSize值的一半. 该属性通常用于可视化的数据值的数量.
anylyser.connect(gainNode);
//链接
let source = null // 声明source变量
let firstPlay = true // 是否为第一次播放
let loading = false // 是否加载中
let type = 'column' // 默认为柱状条模式
// 为模式,歌单设置点击事件
window.onload = function(){
// 设置模式切换
let types=$('#type li');
types.map((type, index) => {
type.onclick = () => {
types.map((item,index) => {
item.className = ''
})
this.className = 'selected';
type = this.getAttribute('data-type');
resize()
}
})
let lists = $('#list li');
lists.map((list, index) => {
lsit.onclick = () => {
lists.map((item,index) => {
item.className = ''
})
this.className = 'selected';
load('/media'+this.title);
}
})
}
// 音量改变
function change(obj){
let percent = obj.value/obj.max; // 通过range获取百分比
gainNode.gain.value= percent*percent;
}
// 以下为加载数据,并处理
function load(url){
if(loading){return;} //loading状态直接return 防止快速重复点击
loading = true;
source&&source[source.stop?'stop':'noteOff'](); 如果在播放资源的话就暂停
xhr.abort(); // 终止正在进行中的ajax请求
xhr.open('GET',url);
xhr.responseType = 'arraybuffer';
xhr.onload = function(){
ac.decodeAudioData(xhr.response,function(buffer){
// 音频数据buffer解析回调完成回调
let bufferSource = ac.createBufferSource();
bufferSource.buffer = buffer;
bufferSource.connect(analyser); 链接数据分析源
bufferSource[bufferSource.start?'start':'noteOn'](0) //从0开始播放
source = bufferSource;
if(firstPlay&&source){ visualizer(); // 初始化可视窗口,只用一次 }
loading = false;
firstPlay = false;
}, function(err){console.log(err);})
}
xhr.send();
}
// 可视化
function visualizer(){
let arr = new Uint8Array(analyser.frenquencyBinCount); //为fftSize的一半
// 创建一个空buffer数组 ,方便getByteFrequencyData的实时数据写入其中
requestAnimationFrame = window.requestAnimationFrame||
window.webkitRequestAnimationFrame||
window.mozRequestAnimationFrame;
function v(){
analyser.getByteFrequencyData(arr);
draw(arr);
requestAnimationFrame (v);
}
}
let box=$('#box')[0];
let height,width;
let canvas=document.createElement('canvas');
box.appendChild(canvas)
let ctx=canvas.getContext('2d')
let Dots=[];
// 随机辅助函数
function random(){
return Math.round(Math.random()*(n-m)+m);
}
// 生成点
funtion getDots(){
Dots = [] // 每次初始化
for(let i = 0 ; i < size ; i ++){
let x = random(0,width);
let y = random(0,height);
let color = "rgb("+random(0,255)+","+random(0,255)+","+random(0,255)+")";
Dots.push({
x:x,
y:y,
color:color
})
}
}
// 分辨率改变响应函数
function resize(){
height = box.clientHeight;
width = box.clientWidth;
canvas.width = width;
canvas.height = height;
// 设置条状图画笔状态
let line = ctx.createLinearGradient(0,0,0,height);
line.addColorStop(0,'red');
line.addColorStop(0.5,'yellow');
line.addColorStop(1,'green');
ctx.fillStyle = line;
getDots();
}
resize()
window.onresize = resize;
// 最终BOSS,画!!!!!
function draw(arr){
// arr为解析过后的数组 纯数字
ctx.clearReact(0,0,width,height);
let w = width/size; //每一条宽度
for(let i = 0 ;i <size ; i++){
if(type === 'colunm'){
let h = arr[i]/256*height; //根据音频大小来判断h
ctx.fillRect(w*i,height-h,w*0.7,h)
}else if(type === 'dot'){
ctx.beginPath();
let dot = Dots[i];
let r = arr[i]/256*50; //根据音频大小来判断r
ctx.arc(dot.x,dot.y,r,0,Math.PI*2);
let g =ctx.createRadialGradient(dot.x,dot.y,0,dot.x,dot.y,r);
//线性背景的起始坐标x,y和半径 线性背景的结束坐标x,y和半径
g.addColorStop(0,'#fff');
g.addColorStop(1,dot.color)
ctx.fillStyle=g;
ctx.fill()
}
}
}

不容易啊 终于搞好了,有兴趣的同学以后可以自己写个高端大气上档次的音乐播放器,想听什么任均君选,这里奉上代码地址,欢迎戳戳戳