使用 Web 音频 API 进行可视化

Web 音频 API 最有趣的功能之一是能够从音频源中提取频率、波形和其他数据,然后使用这些数据创建可视化效果。本文解释了如何实现,并提供了一些基本用例。

注意:您可以在我们的 Voice-change-O-matic 演示中找到所有代码片段的工作示例。

基本概念

要从音频源中提取数据,您需要一个 AnalyserNode,该节点使用 BaseAudioContext.createAnalyser 方法创建,例如

js
const audioCtx = new AudioContext();
const analyser = audioCtx.createAnalyser();

然后将此节点连接到音频源,该源位于源节点和目标节点之间的某个位置,例如

js
const source = audioCtx.createMediaStreamSource(stream);
source.connect(analyser);
analyser.connect(distortion);
distortion.connect(audioCtx.destination);

注意:只要输入连接到源节点(直接或通过其他节点),您就不需要将分析器的输出连接到其他节点。

分析器节点将使用快速傅立叶变换 (fft) 在特定频域中捕获音频数据,具体取决于您指定为 AnalyserNode.fftSize 属性值的频率(如果未指定,则默认值为 2048。)

注意:您还可以使用 AnalyserNode.minDecibelsAnalyserNode.maxDecibels 指定 fft 数据缩放范围的最小值和最大值,以及使用 AnalyserNode.smoothingTimeConstant 指定不同的数据平均常数。阅读这些页面以获取有关如何使用它们的更多信息。

要捕获数据,您需要使用 AnalyserNode.getFloatFrequencyData()AnalyserNode.getByteFrequencyData() 方法捕获频率数据,以及 AnalyserNode.getByteTimeDomainData()AnalyserNode.getFloatTimeDomainData() 方法捕获波形数据。

这些方法将数据复制到指定的数组中,因此您需要在调用其中一个方法之前创建一个新数组来接收数据。第一个方法生成 32 位浮点数,第二个和第三个方法生成 8 位无符号整数,因此标准 JavaScript 数组无法满足要求,您需要使用 Float32ArrayUint8Array 数组,具体取决于您要处理的数据类型。

例如,假设我们要处理一个 fft 大小为 2048 的数据。我们返回 AnalyserNode.frequencyBinCount 值,该值是 fft 的一半,然后用 frequencyBinCount 作为其长度参数调用 Uint8Array() - 这就是我们要收集的点数,针对该 fft 大小。

js
analyser.fftSize = 2048;
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);

要实际检索数据并将数据复制到我们的数组中,我们然后调用我们想要的数据收集方法,并将数组作为参数传递给该方法。例如

js
analyser.getByteTimeDomainData(dataArray);

现在我们拥有了该时刻的音频数据,并将其捕获到我们的数组中,并且可以随意可视化它,例如将其绘制到 HTML <canvas> 上。

让我们继续查看一些具体的示例。

创建波形/示波器

要创建示波器可视化效果(向 Soledad Penadés 致敬,感谢她在 Voice-change-O-matic 中提供的原始代码),我们首先按照上一节中描述的标准模式设置缓冲区

js
analyser.fftSize = 2048;
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);

接下来,我们将画布上的先前绘制内容清除,以便为新的可视化显示做好准备

js
canvasCtx.clearRect(0, 0, WIDTH, HEIGHT);

现在我们定义 draw() 函数

js
function draw() {

在此,我们使用 requestAnimationFrame() 在启动绘制函数后保持循环绘制。

js
const drawVisual = requestAnimationFrame(draw);

接下来,我们获取时域数据并将其复制到我们的数组中

js
analyser.getByteTimeDomainData(dataArray);

接下来,用纯色填充画布,以开始绘制。

js
canvasCtx.fillStyle = "rgb(200 200 200)";
canvasCtx.fillRect(0, 0, WIDTH, HEIGHT);

为我们将要绘制的波形设置线宽和描边颜色,然后开始绘制路径。

js
canvasCtx.lineWidth = 2;
canvasCtx.strokeStyle = "rgb(0 0 0)";
canvasCtx.beginPath();

通过将画布宽度除以数组长度(等于 FrequencyBinCount,如之前定义),确定要绘制的每段线的宽度,然后定义一个 x 变量来定义要移动到的位置以绘制每段线。

js
const sliceWidth = WIDTH / bufferLength;
let x = 0;

现在我们运行一个循环,根据来自数组的数据点值,定义波形每一点的小段位置,并在特定高度进行绘制,然后将线条横向移动到应绘制下一个波形段的位置。

js
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;
}

最后,我们在画布右侧中间结束线条,然后绘制我们定义的描边。

js
  canvasCtx.lineTo(WIDTH, HEIGHT / 2);
  canvasCtx.stroke();
}

在本部分代码的末尾,我们调用 draw() 函数以启动整个过程。

js
draw();

这将提供一个不错的波形显示,每秒更新几次。

a black oscilloscope line, showing the waveform of an audio signal

创建频率条形图

另一个不错的音频可视化效果是类似 Winamp 风格的频率条形图。我们在 Voice-change-O-matic 中有一个可视化效果;让我们看看它是如何实现的。

首先,我们再次设置分析器和数据数组,然后使用 clearRect() 清除当前画布显示。与之前唯一的区别是我们将 fft 大小设置为更小;这样,图表中的每个条形都足够大,看起来像条形,而不是细线。

js
analyser.fftSize = 256;
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);

canvasCtx.clearRect(0, 0, WIDTH, HEIGHT);

接下来,我们启动 draw() 函数,同样使用 requestAnimationFrame() 设置循环,以便显示的数据不断更新,并在每个动画帧中清除显示。

js
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 变量来记录在屏幕上绘制当前条形的距离。

js
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,因此每个条形都将从画布下方绘制,直到底部。

js
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() 函数以启动整个过程。

js
draw();

这段代码将产生如下结果

a series of red bars in a bar graph, showing intensity of different frequencies in an audio signal

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