本文有两个关键词:音频可视化
和Web Audio
。前者是实践,后者是其背后的技术支持。 Web Audio 是很大的知识点,本文会将重点放在如何获取音频数据这块,对于其 API 的更多内容,能够查看 MDN。javascript
另外,要将音频数据转换成可视化图形,除了了解 Web Audio 以外,还须要对 Canvas (特指2D,下同),甚至 WebGL (可选)有必定了解。若是读者对它们没有任何学习基础,能够先从如下资源入手:前端
经过获取频率、波形和其余来自声源的数据,将其转换成图形或图像在屏幕上显示出来,再进行交互处理。java
云音乐有很多跟音频动效相关的案例,但其中有些过于复杂,又或者太偏业务。所以这里就现找了两个相对简单,但有表明性的例子。c++
第一个是用 Canvas 实现的音频柱形图。git
↑点击播放↑github
第二个是用 WebGL 实现的粒子效果。web
↑点击播放↑算法
在具体实践中,除了这些基本图形(矩形、圆形等)的变换,还能够把音频和天然运动、3D 图形结合到一块儿。数组
Web Audio 是 Web 端处理和分析音频的一套 API 。它能够设置不一样的音频来源(包括
<audio>
节点、 ArrayBuffer 、用户设备等),对音频添加音效,生成可视化图形等。
接下来重点介绍 Web Audio 在可视化中扮演的角色,见下图。
简单来讲,就是取数据 + 映射数据两个过程。咱们先把“取数据”这个问题解决,能够按如下5步操做。
在音频的任何操做以前,都必须先建立 AudioContext 。它的做用是关联音频输入,对音频进行解码、控制音频的播放暂停等基础操做。
建立方式以下:
const AudioContext = window.AudioContext || window.webkitAudioContext;
const ctx = new AudioContext();
复制代码
AnalyserNode 用于获取音频的频率数据( FrequencyData )和时域数据( TimeDomainData )。从而实现音频的可视化。
它只会对音频进行读取,而不会对音频进行任何改变。
const analyser = ctx.createAnalyser();
analyser.fftSize = 512;
复制代码
关于 fftSize ,在 MDN 上的介绍可能很难理解,说是快速傅里叶变换的一个参数。
能够从如下角度理解:
1. 它的取值是什么?
fftSize 的要求是 2 的幂次方,好比 256 、 512 等。数字越大,获得的结果越精细。
对于移动端网页来讲,自己音频的比特率大可能是 128Kbps ,没有必要用太大的频率数组去存储自己就不够精细的源数据。另外,手机屏幕的尺寸比桌面端小,所以最终展现图形也不须要每一个频率都采到。只须要体现节奏便可,所以 512 是较为合理的值。
2. 它的做用是什么?
fftSize 决定了 frequencyData 的长度,具体为 fftSize 的一半。
至于为何是 1 / 2,感兴趣的能够看下这篇文章:Why is the FFT “mirrored”?
如今,咱们须要将音频节点,关联到 AudioContext 上,做为整个音频分析过程的输入。
在 Web Audio 中,有三种类型的音频源:
<audio>
节点直接做为输入,可作到流式播放。navigator.getUserMedia
获取用户的音频或视频流后,生成音频源。这 3 种音频源中,除了 MediaStreamAudioSourceNode 有它不可替代的使用场景(好比语音或视频直播)以外。 MediaElementAudioSourceNode 和 AudioBufferSourceNode 相对更容易混用,所以这里着重介绍一下。
MediaElementAudioSourceNode 将<audio>
标签做为音频源。它的 API 调用很是简单。
// 获取<audio>节点
const audio = document.getElementById('audio');
// 经过<audio>节点建立音频源
const source = ctx.createMediaElementSource(audio);
// 将音频源关联到分析器
source.connect(analyser);
// 将分析器关联到输出设备(耳机、扬声器)
analyser.connect(ctx.destination);
复制代码
有一种状况是,在安卓端,测试了在Chrome/69
(不含)如下的版本,用 MediaElementAudioSourceNode 时,获取到的 frequencyData 是全为 0 的数组。
所以,想要兼容这类机器,就须要换一种预加载的方式,即便用 AudioBufferSourceNode ,加载方式以下:
// 建立一个xhr
var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/audio.mp3', true);
// 设置响应类型为 arraybuffer
xhr.responseType = 'arraybuffer';
xhr.onload = function() {
var source = ctx.createBufferSource();
// 对响应内容进行解码
ctx.decodeAudioData(xhr.response, function(buffer) {
// 将解码后获得的值赋给buffer
source.buffer = buffer;
// 完成。将source绑定到ctx。也能够链接AnalyserNode
source.connect(ctx.destination);
});
};
xhr.send();
复制代码
若是将 AnalyserNode 类比中间件,会不会好理解一些?
能够对比一下常规的<audio>
播放,和 Web Audio 中的播放流程:
对于<audio>
节点,即便用 MediaElementAudioSourceNode 的话,播放相对比较熟悉:
audio.play();
复制代码
但若是是 AudioBufferSourceNode ,它不存在 play 方法,而是:
// 建立AudioBufferSourceNode
const source = ctx.createBufferSource();
// buffer是经过xhr获取的音频文件
source.buffer = buffer;
// 调用start方法进行播放
source.start(0);
复制代码
到此,咱们已经将音频输入关联到一个 AnalyserNode ,而且开始播放音频。对于 Web Audio 这部分来讲,它只剩最后一个任务:获取频率数据。
关于频率, Web Audio 提供了两个相关的 API,分别是:
analyser.getByteFrequencyData
analyser.getFloatFrequencyData
二者都是返回 TypedArray ,惟一的区别是精度不一样。
getByteFrequencyData 返回的是 0 - 255 的 Uint8Array 。而 getFloatFrequencyData 返回的是 0 - 22050 的 Float32Array 。
相比较而言,若是项目中对性能的要求高于精度,那建议使用 getByteFrequencyData 。下图展现了一个具体例子:
关于数组的长度( 256 ),在上文已经解释过,它是 fftSize 的一半。
如今,咱们来看下如何获取频率数组:
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
analyser.getByteFrequencyData(dataArray);
复制代码
须要注意的是, getByteFrequencyData 是对已有的数组元素进行赋值,而不是建立后返回新的数组。
它的好处是,在代码中只会有一个 dataArray 的引用,不用经过函数调用和参数传递的方式来从新取值。
在了解 Web Audio 以后,已经能用 getByteFrequencyData 取到一个 Uint8Array 的数组,暂时命名为 dataArray 。
从原理上讲,可视化所依赖的数据能够是音频,也能够是温度变化,甚至能够是随机数。因此,接下来的内容,咱们只须要关心如何将 dataArray 映射为图形数据,不用再考虑 Web Audio 的操做。
(为了简化 Canvas 和 WebGL 的描述,下文提到 Canvas 特指 Canvas 2D
。)
Canvas 自己是一个序列帧的播放。它在每一帧中,都要先清空 Canvas ,再从新绘制。
如下是从示例代码中摘取的一段:
function renderFrame() {
requestAnimationFrame(renderFrame);
// 更新频率数据
analyser.getByteFrequencyData(dataArray);
// bufferLength表示柱形图中矩形的个数
for (var i = 0, x = 0; i < bufferLength; i++) {
// 根据频率映射一个矩形高度
barHeight = dataArray[i];
// 根据每一个矩形高度映射一个背景色
var r = barHeight + 25 * (i / bufferLength);
var g = 250 * (i / bufferLength);
var b = 50;
// 绘制一个矩形,并填充背景色
ctx.fillStyle = "rgb(" + r + "," + g + "," + b + ")";
ctx.fillRect(x, HEIGHT - barHeight, barWidth, barHeight);
x += barWidth + 1;
}
}
renderFrame();
复制代码
对于可视化来讲,核心逻辑在于:如何把频率数据映射成图形参数。在上例中,只是简单地改变了柱形图中每个矩形的高度和颜色。
Canvas 提供了丰富的绘制API,仅从 2D 的角度考虑,它也能实现不少酷炫的效果。类比 DOM 来讲,若是只是<div>
的组合就能作出丰富多彩的页面,那么 Canvas 同样能够。
Canvas 是 CPU 计算,对于 for 循环计算 10000 次,并且每一帧都要重复计算, CPU 是负载不了的。因此咱们不多看到用 Canvas 2D 去实现粒子效果。取而代之的,是使用 WebGL ,借助 GPU 的计算能力。
在 WebGL 中,有一个概念相对比较陌生——着色器。它是运行在 GPU 中负责渲染算法的一类总称。它使用 GLSL( OpenGL Shading Language )编写,简单来讲是一种类 C 风格的语言。如下是简单的示例:
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
复制代码
关于着色器更详细的介绍,能够查看这篇文章。
WebGL 的原生 API 是很是复杂的,所以咱们使用Three.js
做为基础库,它会让业务逻辑的编写变得简单。
先来看下整个开发流程中作的事情,以下图:
在这个过程当中, uniforms 的类型是简单 Object ,咱们会将音频数组做为 uniforms 的一个属性,传到着色器中。至于着色器作的事情,能够简单理解为,它将 uniforms 中定义的一系列属性,映射为屏幕上的顶点和颜色。
顶点着色器和片元着色器的编写每每不须要前端开发参与,对于学过 Unity3D 等技术的游戏同窗可能会熟悉一些。读者能够到 ShaderToy 上寻找现成的着色器。
而后介绍如下3个 Three.js 中的类:
1. THREE.Geometry
能够理解为形状。也就是说,最后展现的物体是球体、仍是长方体、仍是其余不规则的形状,是由这个类决定的。
所以,你须要给它传入一些顶点的坐标。好比三角形,有3个顶点,则传入3个顶点坐标。
固然, Three.js 内置了不少经常使用的形状,好比 BoxGeometry 、 CircleGeometry 等。
2. THREE.ShaderMaterial
能够理解为颜色。仍是以三角形为例,一个三角形能够是黑色、白色、渐变色等,这些颜色是由 ShaderMaterial 决定的。
ShaderMaterial 是 Material 的一种,它由顶点着色器和片元着色器进行定义。
3. THREE.Mesh
定义好物体的形状和颜色后,须要把它们组合在一块儿,称做 Mesh (网格)。有了 Mesh 以后,即可以将它添加到画布中。而后就是常规的 requestAnimationFrame 的流程。
一样的,咱们摘取了示例中比较关键的代码,并作了标注。
i. 建立 Geometry (这是从 THREE.BufferGeometry 继承的类):
var geometry = ParticleBufferGeometry({
// TODO 一些参数
});
复制代码
ii. 定义 uniforms :
var uniforms = {
dataArray: {
value: null,
type: 't' // 对应THREE.DataTexture
},
// TODO 其余属性
};
复制代码
iii. 建立 ShaderMaterial :
var material = new THREE.ShaderMaterial({
uniforms: uniforms,
vertexShader: '', // TODO 传入顶点着色器
fragmentShader: '', // TODO 传入片元着色器
// TODO 其余参数
});
复制代码
iv. 建立 Mesh :
var mesh = new THREE.Mesh(geometry, material);
复制代码
v. 建立 Three.js 中一些必须的渲染对象,包括场景和摄像头:
var scene, camera, renderer;
renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true
});
camera = new THREE.PerspectiveCamera(45, 1, .1, 1e3);
scene = new THREE.Scene();
复制代码
vi. 常规的渲染逻辑:
function animate() {
requestAnimationFrame(animate);
// TODO 此处能够触发事件,用于更新频率数据
renderer.render(scene, camera);
}
复制代码
本文首先介绍了如何经过 Web Audio 的相关 API 获取音频的频率数据。
而后介绍了 Canvas 和 WebGL 两种可视化方案,将频率数据映射为图形数据的一些经常使用方式。
另外,云音乐客户端上线鲸云动效已经有一段时间,看过本文以后,有没有同窗想尝试实现一个本身的音频动效呢?
最后附上文中提到的两段 codepen 示例:
本文发布自 网易云音乐前端团队,欢迎自由转载,转载请保留出处。咱们一直在招人,若是你刚好准备换工做,又刚好喜欢云音乐,那就 加入咱们!