在浏览器中展示音乐频谱

你指间跃动的电光

Posted by tsengkasing on 2017-04-15

对于这学期的专业方向综合项目,我们沿用了上学期JavaEE的项目来做,但是对于金爷爷刁钻的看法,直接导致我们要更改需求,添加一些新的功能。

项目之前的听歌放图太单一,于是我们添加了弹幕功能和频谱展示功能。

本章简单介绍一下如何使用浏览器的AudioContext接口画出音乐的频谱。

AudioContext

AudioContext是HTML5新出的特性,然而其兼容性目前不是太好(这里针对IE

Feature Chrome Firefox(Gecko) Internet Explorer Opera Safari(WebKit)
Basic support 10.0 25.0 未实现 15.0 6.0

这里引用一下Mozilla的文档

AudioContext接口表示由音频模块连接而成的音频处理图,每个模块对应一个AudioNode。AudioContext可以控制它所包含的节点的创建,以及音频处理、解码操作的执行。做任何事情之前都要先创建AudioContext对象,因为一切都发生在这个环境之中。

创建AudioContext对象

如上面所说,在使用之前要先创建AudioContext对象,同时为了考虑兼容性,使用以下的代码:

(requestAnimationFrame 和 cancelAnimationFrame 是为了绘制频谱动画用的)

1
2
3
4
5
6
7
8
9
10
11
12
window.AudioContext = window.AudioContext || window.webkitAudioContext || window.mozAudioContext || window.msAudioContext;

window.requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame;

window.cancelAnimationFrame = window.cancelAnimationFrame || window.webkitCancelAnimationFrame || window.mozCancelAnimationFrame || window.msCancelAnimationFrame;

try {
this.state.audioContext = new AudioContext();
} catch (e) {
console.log(e);
console.log('贵浏览器不支持AudioContext');
}

连接音乐

为了解析音乐生成频谱,按以下步骤

  1. 要获取到音乐的源(source)
  2. 连接到解析器(Analyser)
  3. 连接到音频渲染设备(destination) (可以理解为扬声器)

AudioContext有多种方法获取音乐的源,网上很多示例代码都是本地上传文件,所以使用的是AudioContext.createBufferSource()来将上传的文件读取成buffer解析。而我的需求是一遍播放一遍展示频谱,所以这里使用了<Audio>标签来播放音乐,使用AudioContext.createMediaElementSource()来获取标签正在播放的音乐。

1
let mediaElementAudioSourceNode = window.AudioContext.createMediaElementSource($audio标签);

解析器

createMediaElementSource()返回的是一个mediaElementAudioSourceNode,然后我们再将sourceNode连接到我们的analyser.

1
2
let analyser = window.AudioContext.createAnalyser();
mediaElementAudioSourceNode.connect(analyser);

音频渲染设备

1
analyser.connect(this.state.audioContext.destination);

到这里,所有前期准备就结束了。
下面开始画频谱。

获取频域数据

想在视图中画出高度不一的频谱,要获取每一个柱子的高度,我们可以在之前创建的Analyser获取。
具体方法是先创建一个无符号字节数组,长度为AnalyserNode.frequencyBinCount的值。

1
2
let array = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(array);

展示

本文绘制频谱的方式是使用<canvas>标签,然后使用window.requestAnimationFrame()

window.requestAnimationFrame()函数接收一个参数,参数为一个函数,然后大概每秒调用60次这个函数。

首先在页面中创建一个<canvas>标签,获取到这个标签后往里面填充rect,高度设为上面获取到的频率数组array。

在这里我是参考了网上的代码,稍作改动然后使用了。
Demo

绘图代码:

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
drawSpectrum = (analyser) => {
let canvas = $Canvas标签对象,
cwidth = canvas.width,
cheight = canvas.height - 2,
meterWidth = 10, //width of the meters in the spectrum
capHeight = 2,
capStyle = '#fff',
meterNum = 800 / (2), //count of the meters
capYPositionArray = []; ////store the vertical position of hte caps for the preivous frame
const ctx = canvas.getContext('2d'),
gradient = ctx.createLinearGradient(0, 0, 0, 300);
gradient.addColorStop(1, '#0f0');
gradient.addColorStop(0.5, '#ff0');
gradient.addColorStop(0, '#f00');
const drawMeter = () => {
let array = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(array);
if (this.state.status === 0) {
//fix when some sounds end the value still not back to zero
for (let i = array.length - 1; i >= 0; i--) {
array[i] = 0;
}
let allCapsReachBottom = true;
for (let i = capYPositionArray.length - 1; i >= 0; i--) {
allCapsReachBottom = allCapsReachBottom && (capYPositionArray[i] === 0);
}
if (allCapsReachBottom) {
window.cancelAnimationFrame(this.state.animationId); //since the sound is stoped and animation finished, stop the requestAnimation to prevent potential memory leak,THIS IS VERY IMPORTANT!
return;
}
}
let step = Math.round(array.length / meterNum); //sample limited data from the total array
ctx.clearRect(0, 0, cwidth, cheight);
for (let i = 0; i < meterNum; i++) {
let value = array[i * step];
// console.log(JSON.stringify(array.slice(0, 50)));
if (capYPositionArray.length < Math.round(meterNum)) {
capYPositionArray.push(value);
}
ctx.fillStyle = capStyle;
//draw the cap, with transition effect
if (value < capYPositionArray[i]) {
ctx.fillRect(i * 12, cheight - (--capYPositionArray[i]), meterWidth, capHeight);
} else {
ctx.fillRect(i * 12, cheight - value, meterWidth, capHeight);
capYPositionArray[i] = value;
}
ctx.fillStyle = gradient; //set the filllStyle to gradient for a better look
ctx.fillRect(i * 12 /*meterWidth+gap*/ , cheight - value + capHeight, meterWidth, cheight); //the meter
}
this.state.animationId = window.requestAnimationFrame(drawMeter);
};
this.state.animationId = window.requestAnimationFrame(drawMeter);
};

在这里放上AudioVisualer的js文件
AudioVisualer
使用的方式如下:

1
2
3
import AudioVisualizer from './AudioVisualizer.js';

new AudioVisualizer($audio标签的id, $canvas标签的id);