乐趣区

利用AudioContext来实现网易云音乐的鲸鱼音效

一直觉得网易云音乐的用户体验是很不错的,很早就注意到了里面的鲸鱼音效,如下图,就是一个环形的跟着音乐节拍跳动的特效。

gif 动图可能效果不太理想,可以直接在手机上体验
身为前端凭着本能的好奇心和探索心当然会研究一番,如何在页面上实现该效果?
1.AudioContext
其实这类动效原理并不复杂,你需要一堆数据来表述每一块的高度,然后通过某种方式,让前台渲染可见即可。
如何获取音乐实时的节拍数据呢,这里用到了 AudioContext

AudioContext 接口表示由音频模块连接而成的音频处理图,每个模块对应一个 AudioNode。AudioContext 可以控制它所包含的节点的创建,以及音频处理、解码操作的执行。做任何事情之前都要先创建 AudioContext 对象,因为一切都发生在这个环境之中。
这一段是从 https://developer.mozilla.org/zh-CN/docs/Web/API/AudioContext 摘录下来的,里面有很多方法,详细可以看具体文档,这里只介绍我们下面用到的其中几个
1.1 AudioContext.createAnalyser()
AudioContext 的 createAnalyser() 方法能创建一个 AnalyserNode,可以用来获取音频时间和频率数据,以及实现数据可视化。
var audioCtx = new AudioContext();
var analyser = audioCtx.createAnalyser();
这里返回的是一个 AnalyserNode 对象。
AnalyserNode 赋予了节点可以提供实时频率及时间域分析的信息。它使一个 AudioNode 通过音频流不做修改的从输入到输出, 但允许你获取生成的数据, 处理它并创建音频可视化。

AnalyserNode 还有很多属性
AnalyserNode.fftSize
AnalyserNode 接口的 fftSize 属性的值是一个无符号长整型的值, 表示(信号)样本的窗口大小。当执行快速傅里叶变换(Fast Fourier Transfor (FFT))时,这些(信号)样本被用来获取频域数据。
fftSize 属性的值必须是从 32 到 32768 范围内的 2 的非零幂; 其默认值为 2048。
AnalyserNode.frequencyBinCount 只读

frequencyBinCount 的值固定为 AnalyserNode 接口中 fftSize 值的一半. 该属性通常用于可视化的数据值的数量.
1.2 AudioContext.createMediaElementSource()
AudioContext 的 createMediaElementSource() 方法用于创建一个新的 MediaElementAudioSourceNode 对象,输入某个存在的 HTML <audio> or <video> 元素, 对应的音频即可被播放或者修改。
var audioCtx = new AudioContext();
var source = audioCtx.createMediaStreamSource(stream);
2. 实现
上面很多 api 可能刚开始看的时候会犯晕,不过没事,下面一步一步写成一个例子就明白了。
这里我们采用 canvas 来绘制频谱图,下面简单写一个布局
<canvas id=’canvas’ width=”600″ height=”600″></canvas>
<audio id=”audio” controls autoplay loop></audio>
加点样式
body{
background: black;
}
canvas,audio{
display: block;
margin: 0 auto;
}
下面来通过音频来获取频谱数据
var audio = document.getElementById(‘audio’);
audio.crossOrigin = ‘anonymous’;
audio.src=’./406238.mp3′;
var ctx = new AudioContext();
var analyser = ctx.createAnalyser();
var audioSrc = ctx.createMediaElementSource(audio);

audioSrc.connect(analyser);
analyser.connect(ctx.destination);

analyser.fftSize = 512;

var array = new Uint8Array(analyser.frequencyBinCount);
console.log(array)
打印一下这个 array,是一个长度为 256 的数组

这就是音频的频谱数据,这个长度跟上面设置的 analyser.fftSize 有关,是他的一半,也就是说,设置的越大,得到的数据越多,分析的也越准确。这里只是绘制一些条形图,并不需要默认的 2048 那么大,所以这里设置了 512。
普通的频谱图
在此之前,我们先来实现一下常见的垂直频谱图,只需要用到 ctx.fillRect 来绘制一个个的方块就行了
var canvas = document.getElementById(‘canvas’);
var ctx = canvas.getContext(‘2d’);
var cwidth = canvas.width;
var cheight = canvas.height – 2;
var meterWidth = 5; // 方块的宽度
var gap = 2; // 方块的间距
var minHeight = 2;
var meterNum = cwidth / (meterWidth + gap);// 根据宽度和间距计算出可以放多少个方块

ctx.fillStyle = ‘rgba(255,255,255,.5)’;// 填充

function render() {
var array = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(array);
var step = Math.round(array.length / meterNum);// 从频谱数据中每隔 step 均匀取出 meterNum 个数据
ctx.clearRect(0, 0, cwidth, cheight);
for (var i = 0; i < meterNum; i++) {
var value = array[i * step];
ctx.fillRect(i * (meterWidth+gap) , cheight – value + capHeight, meterWidth, cheight||minHeight); // 绘制
}
requestAnimationFrame(render);
}
render();
如果需要渐变色的话,可以
var gradient = ctx.createLinearGradient(0, 0, 0, 300);
gradient.addColorStop(1, ‘#0f00f0’);
gradient.addColorStop(0.5, ‘#ff0ff0’);
gradient.addColorStop(0, ‘#f00f00’);
ctx.fillStyle = gradient ;// 填充

完整代码可以查看 demo
环形的频谱图
如果上面的频谱图很清楚了的话,下面的环形也轻而易举了,主要用到了坐标的旋转
这里注意的是在进行 translate 和 rotate 操作时需要进行 ctx.save() 和 ctx.restore(),因为操作的是坐标系,而不是元素本身,可以多尝试一下
var PI = Math.PI;
var canvas = document.getElementById(‘canvas’);
var ctx = canvas.getContext(‘2d’);
var cwidth = canvas.width;
var cheight = canvas.height;
var cr = 230;// 环形半径
var minHeight = 2;
var meterWidth = 5;
var meterNum = 180;// 设置方块的数量,考虑到闭环的关系
var gradient = ctx.createLinearGradient(0, -cr, 0, -cwidth/2);
gradient.addColorStop(0, ‘#0f0’);
gradient.addColorStop(0.5, ‘#ff0’);
gradient.addColorStop(1, ‘#f00’);
ctx.fillStyle = gradient;

function render() {
var array = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(array);
var step = Math.round(array.length / meterNum);
ctx.clearRect(0, 0, cwidth, cheight);
ctx.save();
ctx.translate(cwidth/2,cheight/2);
for (var i = 0; i < meterNum; i++) {
//ctx.save();
var value = array[i * step];
var meterHeight = value*(cheight/2 – cr)/256||minHeight;
ctx.rotate(2*PI/meterNum);
ctx.fillRect(-meterWidth/2 , -cr- meterHeight , meterWidth, meterHeight);
//ctx.restore();
}
ctx.restore();
requestAnimationFrame(render);
}
render();
小 tip
在进行旋转操作时,如果你每次旋转以后,都把坐标系还原,那么在循环的时候需要旋转 30,60,90… 这样
ctx.save();
ctx.rotate(2*PI/meterNum*i);
ctx.restore();
如果你在每次旋转以后,不还原坐标系,那么每次就是在上一次的基础上继续旋转
//ctx.save();
ctx.rotate(2*PI/meterNum*i);
//ctx.restore();
很显然,下面的方式更精简

完整代码可以查看 demo
小节
以上就实现了环形的频谱图,是不是越来越靠近网易云音乐的鲸鱼音效了呢,中间加一个自动旋转的专辑封面就可以了~
之前写过几篇都是关于 css 的文章,有人可能觉得是不是不会 js 啊,天天捣鼓 css,其实并不是这样的,各自有各自的职责范围,像界面 UI 之类的,本来就是样式上的事情,很多人一看看上去觉得 css 实现不了,马上就搬出 js,效果是出来了,但体验差了一大截。

如果喜欢的文章的话,可以点赞并收藏,多多关注我的博客

退出移动版