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);
注:
只要输入直接或通过其他节点连接到源,就可使其工作,不需要将分析器的输出连接到另一个节点。
然后,分析器节点将根据你为 AnalyserNode.fftSize
属性指定的值(如果未指定,则默认值为 2048
),使用快速傅里叶变换(Fast Fourier Transform,简称 fft
)在特定频域内捕获音频数据。
注:
你还可以使用AnalyserNode.minDecibels
(最小分贝值)和AnalyserNode.maxDecibels
(最大分贝值)为快速傅里叶变换(fft
)数据的缩放范围指定最小和最大指数值,也可以使用AnalyserNode.smoothingTimeConstant
(平滑时间常数)来指定不同的数据平均常数。
要捕获数据,你需要使用以下方法:使用 AnalyserNode.getFloatFrequencyData()
和AnalyserNode.getByteFrequencyData()
方法来捕获频率数据,使用AnalyserNode.getByteTimeDomainData()
和AnalyserNode.getFloatTimeDomainData()
方法来捕获波形数据。
这些方法会将数据复制到一个指定的数组中,所以在调用某个方法之前,你需要创建一个新数组来接收数据。第一个方法生成的是 32
位浮点数,而第二和第三个方法生成的是 8
位无符号整数,因此,标准的 JavaScript
数组是不行的 —— 你需要根据正在处理的数据类型,使用 Float32Array
(浮点型 32
位数组)或者 Uint8Array
(无符号 8
位整数数组)类型的数组。
例如,假设我们处理的快速傅里叶变换(fft
)大小为 2048
。我们返回 AnalyserNode.frequencyBinCount
(频率段数量)的值,它是快速傅里叶变换大小的一半,然后使用这个 frequencyBinCount
值作为长度参数来调用 Uint8Array()
—— 对于该快速傅里叶变换大小而言,这就是我们将要收集的数据点数量。
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 完全相同,只不过它使用的是浮点数据,而非无符号字节数据。详情请查看 源代码的相关部分。
评论0
暂时没有评论