使用 Web Audio API 进行可视化
Web Audio API 最有趣的功能之一是能够从音频源中提取频率、波形和其他数据,然后可以使用这些数据创建可视化。本文将解释如何做到这一点,并提供几个基本用例。
注意:您可以在我们的 Voice-change-O-matic 演示中找到所有代码片段的工作示例。
基本概念
要从音频源中提取数据,您需要一个 AnalyserNode,它是使用 BaseAudioContext.createAnalyser 方法创建的,例如:
const audioCtx = new AudioContext();
const analyser = audioCtx.createAnalyser();
然后,该节点会连接到您的音频源,位于您的源和目标之间的某个点,例如:
const source = audioCtx.createMediaStreamSource(stream);
source.connect(analyser);
analyser.connect(distortion);
distortion.connect(audioCtx.destination);
注意:只要输入连接到源(直接或通过另一个节点),您就不需要将分析器的输出连接到另一个节点即可使其工作。
然后,分析器节点将使用快速傅里叶变换 (fft) 在特定频率域中捕获音频数据,具体取决于您为 AnalyserNode.fftSize 属性指定的值(如果未指定值,则默认为 2048)。
注意:您还可以使用 AnalyserNode.minDecibels 和 AnalyserNode.maxDecibels 为 fft 数据缩放范围指定最小和最大功率值,并使用 AnalyserNode.smoothingTimeConstant 指定不同的数据平均常量。请阅读这些页面以获取有关如何使用它们的更多信息。
要捕获数据,您需要使用 AnalyserNode.getFloatFrequencyData() 和 AnalyserNode.getByteFrequencyData() 方法来捕获频率数据,并使用 AnalyserNode.getByteTimeDomainData() 和 AnalyserNode.getFloatTimeDomainData() 来捕获波形数据。
这些方法会将数据复制到指定的数组中,因此在调用其中一个方法之前,您需要创建一个新数组来接收数据。第一个方法生成 32 位浮点数,第二个和第三个方法生成 8 位无符号整数,因此标准的 JavaScript 数组不行——您需要根据您处理的数据使用 Float32Array 或 Uint8Array 数组。
因此,例如,假设我们处理的 fft 大小为 2048。我们返回 AnalyserNode.frequencyBinCount 值,该值是 fft 的一半,然后使用 frequencyBinCount 作为其长度参数调用 Uint8Array()——这就是对于该 fft 大小,我们将收集多少数据点。
analyser.fftSize = 2048;
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
要实际检索数据并将其复制到我们的数组中,我们然后调用我们想要的数据收集方法,并将数组作为其参数。例如:
analyser.getByteTimeDomainData(dataArray);
此时,我们已经捕获了该时刻的音频数据并将其存储在我们的数组中,然后可以根据需要对其进行可视化,例如将其绘制到 HTML <canvas> 上。
让我们继续看一些具体的例子。
创建波形/示波器
为了创建示波器可视化(感谢 Soledad Penadés 在 Voice-change-O-matic 中提供的原始代码),我们首先按照上一节中描述的标准模式设置缓冲区:
analyser.fftSize = 2048;
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
接下来,我们清除之前在画布上绘制的内容,为新的可视化显示做好准备。
canvasCtx.clearRect(0, 0, WIDTH, HEIGHT);
现在我们定义 draw() 函数。在这里,我们执行以下操作:
- 使用
requestAnimationFrame()在绘图函数启动后持续循环。 - 获取时域数据并将其复制到我们的数组中。
- 用纯色填充画布以开始。
- 设置要绘制的波形的线宽和描边颜色,然后开始绘制路径。
- 通过将画布宽度除以数组长度(等于前面定义的 FrequencyBinCount)来确定要绘制的线的每个段的宽度,然后定义一个 x 变量来定义绘制每段线的移动位置。
- 现在我们通过一个循环,为缓冲区中的每个点定义波形小段的位置,其高度基于数组中的数据点值,然后将线条移动到下一个波形段应绘制的位置。
- 最后,我们在画布右侧中间完成线条,然后绘制我们定义的描边。
function draw() {
const drawVisual = requestAnimationFrame(draw);
analyser.getByteTimeDomainData(dataArray);
// Fill solid color
canvasCtx.fillStyle = "rgb(200 200 200)";
canvasCtx.fillRect(0, 0, WIDTH, HEIGHT);
// Begin the path
canvasCtx.lineWidth = 2;
canvasCtx.strokeStyle = "rgb(0 0 0)";
canvasCtx.beginPath();
// Draw each point in the waveform
const sliceWidth = WIDTH / bufferLength;
let x = 0;
for (let i = 0; i < bufferLength; i++) {
const v = dataArray[i] / 128.0;
const y = v * (HEIGHT / 2);
if (i === 0) {
canvasCtx.moveTo(x, y);
} else {
canvasCtx.lineTo(x, y);
}
x += sliceWidth;
}
// Finish the line
canvasCtx.lineTo(WIDTH, HEIGHT / 2);
canvasCtx.stroke();
}
在此代码段的末尾,我们调用 draw() 函数来启动整个过程。
draw();
这为我们提供了每秒更新几次的良好波形显示。

创建频率条形图
另一个很棒的小声音可视化是创建那些类似 Winamp 的频率条形图。我们在 Voice-change-O-matic 中有一个可用的,让我们看看它是如何实现的。
首先,我们再次设置分析器和数据数组,然后使用 clearRect() 清除当前画布显示。与之前唯一的区别是我们将 fft 大小设置得更小;这是为了让图中的每个条形足够大,看起来像一个条形而不是一条细线。
analyser.fftSize = 256;
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
canvasCtx.clearRect(0, 0, WIDTH, HEIGHT);
接下来,我们再次使用 requestAnimationFrame() 启动我们的 draw() 函数,设置一个循环,以便显示的数据持续更新,并在每个动画帧中清除显示。
function draw() {
drawVisual = requestAnimationFrame(draw);
analyser.getByteFrequencyData(dataArray);
canvasCtx.fillStyle = "rgb(0 0 0)";
canvasCtx.fillRect(0, 0, WIDTH, HEIGHT);
// ...
}
现在我们将 barWidth 设置为画布宽度除以条形数量(缓冲区长度)。但是,我们也将其宽度乘以 2.5,因为大多数频率将返回为没有音频,因为我们每天听到的大多数声音都处于某个较低的频率范围内。我们不想显示大量空条形,因此我们将显示规律的条形移开,使其具有明显的高度,以便它们填充画布显示。
我们还设置了一个 barHeight 变量和一个 x 变量来记录要在画布上绘制当前条形的距离。
function draw() {
// ...
const barWidth = (WIDTH / bufferLength) * 2.5;
let barHeight;
let x = 0;
// ...
}
和以前一样,我们现在开始一个 for 循环,遍历 dataArray 中的每个值。对于每个值,我们将 barHeight 设置为等于数组值,根据 barHeight 设置填充颜色(较高的条形更亮),并在距画布 x 像素处绘制一个条形,该条形宽 barWidth,高 barHeight / 2(我们最终决定将每个条形分成两半,以便它们都能更好地适应画布)。
需要解释的一个值是绘制每个条形的垂直偏移位置:HEIGHT - barHeight / 2。我这样做是因为我希望每个条形从画布底部向上延伸,而不是从顶部向下延伸,就像如果我们设置垂直位置为 0 那样。因此,我们每次都改为将垂直位置设置为画布高度减去 barHeight / 2,这样每个条形都会从画布中间向下绘制到底部。
function draw() {
// ...
for (let i = 0; i < bufferLength; i++) {
barHeight = dataArray[i] / 2;
canvasCtx.fillStyle = `rgb(${barHeight + 100} 50 50)`;
canvasCtx.fillRect(x, HEIGHT - barHeight / 2, barWidth, barHeight);
x += barWidth + 1;
}
// ...
}
同样,在代码末尾,我们调用 draw() 函数来启动整个过程。
draw();
这段代码的结果如下:

注意:本文中的示例展示了 AnalyserNode.getByteFrequencyData() 和 AnalyserNode.getByteTimeDomainData() 的用法。有关展示 AnalyserNode.getFloatFrequencyData() 和 AnalyserNode.getFloatTimeDomainData() 的工作示例,请参考我们的 Voice-change-O-matic-float-data 演示——这与原始的 Voice-change-O-matic 完全相同,只是它使用了 Float 数据,而不是无符号字节数据。有关详细信息,请参阅源代码的 此部分。