使用 Web 音频 API 进行可视化
Web 音频 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()
函数
function draw() {
在此,我们使用 requestAnimationFrame()
在启动绘制函数后保持循环绘制。
const drawVisual = requestAnimationFrame(draw);
接下来,我们获取时域数据并将其复制到我们的数组中
analyser.getByteTimeDomainData(dataArray);
接下来,用纯色填充画布,以开始绘制。
canvasCtx.fillStyle = "rgb(200 200 200)";
canvasCtx.fillRect(0, 0, WIDTH, HEIGHT);
为我们将要绘制的波形设置线宽和描边颜色,然后开始绘制路径。
canvasCtx.lineWidth = 2;
canvasCtx.strokeStyle = "rgb(0 0 0)";
canvasCtx.beginPath();
通过将画布宽度除以数组长度(等于 FrequencyBinCount,如之前定义),确定要绘制的每段线的宽度,然后定义一个 x 变量来定义要移动到的位置以绘制每段线。
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;
}
最后,我们在画布右侧中间结束线条,然后绘制我们定义的描边。
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);
接下来,我们启动 draw()
函数,同样使用 requestAnimationFrame()
设置循环,以便显示的数据不断更新,并在每个动画帧中清除显示。
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
变量来记录在屏幕上绘制当前条形的距离。
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
,因此每个条形都将从画布下方绘制,直到底部。
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 完全相同,只是它使用的是浮点数据,而不是无符号字节数据。有关详细信息,请参阅源代码中的 此部分