如果你曾经想过像MilkDrop这样的音乐可视化工具是怎么做的,那么这篇文章就是为你准备的。我们将从使用Canvas API来做简单的可视化入手,然后慢慢转移到用WebGL着色器来做更复杂的可视化。
使用Canvas API的波形图可视化
做一个音频可视化工具所需的第一件东西就是一些音频。现在我们有两个选项:一个是从A3到A6的扫描和我做的一首歌(由Pye Corner Audio重建轨道名字叫“Zero Center”)。
Saw Sweep Play Song(译者注:原文这里是两个按钮可以听这两个音频的效果,下同)
所有的音频可视化工具都需要的第二件事是获取音频数据的方式。Web Audio API为此提供了 AnalyserNode
这个接口。除了提供了原始的波形(也叫做时间域)数据,它还提供了访问音频频谱(也叫频域)数据的方法。使用 AnalyserNode
这个接口很简单:创建一个 AnalyserNode.frequencyBinCount
长度的类型化数组,然后调用 AnalyserNode.getFloatTimeDomainData
这个方法用当前的波形数据来填充这个数组。
const analyser = audioContext.createAnalyser()
masterGain.connect(analyser)
const waveform = new Float32Array(analyser.frequencyBinCount)
analyser.getFloatTimeDomainData(waveform)
此时, waveform
数组将包含与通过 masterGain
节点播放的音频波形相对应的从-1到1的值。 这只是目前正在播放的一个片刻。为了使之有用,我们需要周期性的更新这个数组。在 requestAnimationFrame
的回调函数里更新这个数组是一个好主意。
;(function updateWaveform() {
requestAnimationFrame(updateWaveform)
analyser.getFloatTimeDomainData(waveform)
})()
现在将会每秒更新这个 waveform
数组60次,这样,我们最后一个需要的东西:一些绘图代码。在这个例子中,我们只需简单地像示波器在y轴上绘制波形。
const scopeCanvas = document.getElementById('oscilloscope')
scopeCanvas.width = waveform.length
scopeCanvas.height = 200
const scopeContext = scopeCanvas.getContext('2d')
;(function drawOscilloscope() {
requestAnimationFrame(drawOscilloscope)
scopeContext.clearRect(0, 0, scopeCanvas.width, scopeCanvas.height)
scopeContext.beginPath()
for (let i = 0; i < waveform.length; i ) {
const x = i
const y = (0.5 waveform[i] / 2) * scopeCanvas.height;
if (i == 0) {
scopeContext.moveTo(x, y)
} else {
scopeContext.lineTo(x, y)
}
}
scopeContext.stroke()
})()
Saw Sweep Play Song
尝试多次点击“锯切扫描”按钮,看看波形如何响应
使用Canvas API进行频谱可视化。
AnalyserNode接口还提供有关音频中当前存在的频率的数据。它对波形数据运行FFT(傅立叶变换),并将这些值暴露为一个数组。在这种情况下,我们将要求数据为 Uint8Array
,因为0-255范围内的值正是执行Canvas像素操作时所需要的值的范围。
const spectrum = new Uint8Array(analyser.frequencyBinCount)
;(function updateSpectrum() {
requestAnimationFrame(updateSpectrum)
analyser.getByteFrequencyData(spectrum)
})()
与 waveform
数组类似, spectrum
数组现在将使用当前的音频频谱每秒更新60次。这些值对应于频谱的给定片段的音量,从低频到高频排列。让我们看看如何使用这些数据来创建一个被称为声谱图的可视化。
const spectroCanvas = document.getElementById('spectrogram')
spectroCanvas.width = spectrum.length
spectroCanvas.height = 200
const spectroContext = spectroCanvas.getContext('2d')
let spectroOffset = 0
;(function drawSpectrogram() {
requestAnimationFrame(drawSpectrogram)
const slice = spectroContext.getImageData(0, spectroOffset, spectroCanvas.width, 1)
for (let i = 0; i < spectrum.length; i ) {
slice.data[4 * i 0] = spectrum[i] // R
slice.data[4 * i 1] = spectrum[i] // G
slice.data[4 * i 2] = spectrum[i] // B
slice.data[4 * i 3] = 255 // A
}
spectroContext.putImageData(slice, 0, spectroOffset)
spectroOffset = 1
spectroOffset %= spectroCanvas.height
})()
Saw Sweep Play Song
我发现谱图是分析音频的最有用的工具之一,例如找出正在播放的和弦或调试不正确的合成器。光谱图也适用于寻找复活节彩蛋!
可视化与WebGL着色器
我最喜欢的电脑图形技术是使用WebGL的全屏像素着色器。通常,几个像素着色器与3D几何结合使用来呈现场景,但是今天我们将使用单个像素着色器(也称为片段着色器)来跳过几何图形并渲染整个场景。与Canvas API相比,它需要引用更多的文件,但最终的结果是非常值得的。
首先,我们需要绘制一个覆盖整个屏幕的矩形(也称为四边形)。片段着色器将被绘制的在这上面。
代码语言:javascript复制function initQuad(gl) {
const vbo = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, vbo)
const vertices = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1])
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW)
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0)
}
function renderQuad(gl) {
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)
}
现在我们有全屏四边形(技术上它是两个半屏三角形),我们需要一个着色器程序。 这是一个使用顶点着色器和片段着色器的函数,并返回一个已经编译好的着色器程序。
代码语言:javascript复制function createShader(gl, vertexShaderSrc, fragmentShaderSrc) {
const vertexShader = gl.createShader(gl.VERTEX_SHADER)
gl.shaderSource(vertexShader, vertexShaderSrc)
gl.compileShader(vertexShader)
if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
throw new Error(gl.getShaderInfoLog(vertexShader))
}
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER)
gl.shaderSource(fragmentShader, fragmentShaderSrc)
gl.compileShader(fragmentShader)
if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
throw new Error(gl.getShaderInfoLog(fragmentShader))
}
const shader = gl.createProgram()
gl.attachShader(shader, vertexShader)
gl.attachShader(shader, fragmentShader)
gl.linkProgram(shader)
gl.useProgram(shader)
return shader
}
这个可视化的顶点着色器非常简单,它只是穿过顶点位置而不会修改它。
代码语言:javascript复制attribute vec2 position;
void main(void) {
gl_Position = vec4(position, 0, 1);
}
这个片段着色器就比较有趣了,我们将从由Danguafer提供的这个着色器开始,并做出一些战略性的修改,以便对音频进行响应。
代码语言:javascript复制precision mediump float;
uniform float time;
uniform vec2 resolution;
uniform sampler2D spectrum;
void main(void) {
vec3 c;
float z = 0.1 * time;
vec2 uv = gl_FragCoord.xy / resolution;
vec2 p = uv - 0.5;
p.x *= resolution.x / resolution.y;
float l = 0.2 * length(p);
for (int i = 0; i < 3; i ) {
z = 0.07;
uv = p / l * (sin(z) 1.0) * abs(sin(l * 9.0 - z * 2.0));
c[i] = 0.01 / length(abs(mod(uv, 1.0) - 0.5));
}
float intensity = texture2D(spectrum, vec2(l, 0.5)).x;
gl_FragColor = vec4(c / l * intensity, time);
}
这个关键是将输出颜色与声谱强度相乘。 另一个区别是我们将“l”缩放为0.2,因为大部分音频都在频谱纹理里的前20%。
到底什么是频谱纹理?它是从之前的声谱数组,复制到1024x1的图像。 以下讲的是如何实现(波形数据可以使用相同的技术):
代码语言:javascript复制function createTexture(gl) {
const texture = gl.createTexture()
gl.bindTexture(gl.TEXTURE_2D, texture)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
return texture
}
function copyAudioDataToTexture(gl, audioData, textureArray) {
for (let i = 0; i < audioData.length; i ) {
textureArray[4 * i 0] = audioData[i] // R
textureArray[4 * i 1] = audioData[i] // G
textureArray[4 * i 2] = audioData[i] // B
textureArray[4 * i 3] = 255 // A
}
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, audioData.length, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, textureArray)
}
随着所有这一切准备完毕,我们终于准备绘制可视化了。 首先,我们初始化画布并编译着色器。
代码语言:javascript复制const fragCanvas = document.getElementById('fragment')
fragCanvas.width = fragCanvas.parentNode.offsetWidth
fragCanvas.height = fragCanvas.width * 0.75
const gl = fragCanvas.getContext('webgl') || fragCanvas.getContext('experimental-webgl')
const vertexShaderSrc = document.getElementById('vertex-shader').textContent
const fragmentShaderSrc = document.getElementById('fragment-shader').textContent
const fragShader = createShader(gl, vertexShaderSrc, fragmentShaderSrc)
接下来,我们初始化这个着色器变量: position
, time
, resolution
,还有我们最感兴趣的一个变量 spectrum
。
const fragPosition = gl.getAttribLocation(fragShader, 'position')
gl.enableVertexAttribArray(fragPosition)
const fragTime = gl.getUniformLocation(fragShader, 'time')
gl.uniform1f(fragTime, audioContext.currentTime)
const fragResolution = gl.getUniformLocation(fragShader, 'resolution')
gl.uniform2f(fragResolution, fragCanvas.width, fragCanvas.height)
const fragSpectrumArray = new Uint8Array(4 * spectrum.length)
const fragSpectrum = createTexture(gl)
现在设置了这些变量,我们初始化全屏四边形并启动渲染循环。 在每个框架上,我们更新 time
变量和 spectrum
纹理,并渲染这个四边形。
initQuad(gl)
;(function renderFragment() {
requestAnimationFrame(renderFragment)
gl.uniform1f(fragTime, audioContext.currentTime)
copyAudioDataToTexture(gl, spectrum, fragSpectrumArray)
renderQuad(gl)
})()
Saw Sweep Play Song(两个按钮,可以演示效果)
如您所见,全屏片段着色器相当强大。 如果有更多的想法,可以花一些时间探索Shadertoy和The Book of Shaders。 使着色器对音频作出反应是吸引更多生命力的好方法,正如我们所看到的,Web Audio API使其易于操作。 如果您最终制作出酷炫的音乐可视化,请在评论中分享!
往期精选文章 |
---|
使用虚拟dom和JavaScript构建完全响应式的UI框架 |
扩展 Vue 组件 |
使用Three.js制作酷炫无比的无穷隧道特效 |
一个治愈JavaScript疲劳的学习计划 |
全栈工程师技能大全 |
WEB前端性能优化常见方法 |
一小时内搭建一个全栈Web应用框架 |
干货:CSS 专业技巧 |
四步实现React页面过渡动画效果 |
让你分分钟理解 JavaScript 闭包 |
小手一抖,资料全有。长按二维码关注京程一灯,阅读更多技术文章和业界动态。