12、音频可视化:AnalyserNode节点、获取频域与时域数据、绘制音频波形图
音频可视化,说白了就是把「听不见的声音」变成「看得见的图形」。我在做第一个WebRTC项目时,客户非要看实时音频波形,我当时心想:声音还能画出来?后来一查,原来浏览器早就给我们准备好了——AnalyserNode。
这一节,我们就来聊聊怎么用这个节点,把麦克风采集到的音频数据,变成漂亮的波形图和频谱图。
12.1 AnalyserNode 是什么?
AnalyserNode 是 Web Audio API 里的一个分析节点。它不改变音频流,只是「偷看」一下数据。你可以把它想象成一个示波器——接在电路里,但不影响电路本身。
我习惯把它放在音频处理链的末端,专门用来做可视化。比如这样:
麦克风 → MediaStreamSource → AnalyserNode → 画布绘制
它不会改变音量,不会加特效,纯粹就是给你数据用的。
核心能力:
- 获取时域数据(波形)—— 声音的振幅随时间变化
- 获取频域数据(频谱)—— 声音在不同频率上的能量分布
12.2 创建 AnalyserNode
创建它很简单。先拿到音频上下文,然后调用 createAnalyser() 方法。
const audioContext = new AudioContext();
const analyser = audioContext.createAnalyser();
这里有个参数我要提一下——fftSize。它决定了分析的精度。默认是 2048,数值越大,频率分辨率越高,但响应速度会变慢。
我在项目中一般这样设置:
analyser.fftSize = 2048;
const bufferLength = analyser.frequencyBinCount;
// bufferLength = fftSize / 2 = 1024
frequencyBinCount 是只读属性,等于 fftSize / 2。你想想看,FFT 之后,有效数据只有一半,另一半是对称的,没必要重复。
我的习惯:如果做实时波形,fftSize 用 1024 就够了,响应更快。如果做精细频谱分析,可以用 4096。
12.3 获取时域数据(波形)
时域数据,就是声音的原始波形。用 getByteTimeDomainData() 方法获取。
const dataArray = new Uint8Array(bufferLength);
analyser.getByteTimeDomainData(dataArray);
返回的数组里,每个值范围是 0~255。128 代表静音(中心线),大于 128 是正半周,小于 128 是负半周。
嗯,这里要注意:如果你想要浮点数精度,可以用 getFloatTimeDomainData(),范围是 -1 到 1。
12.4 获取频域数据(频谱)
频域数据,就是声音的频率分布。用 getByteFrequencyData() 方法。
const frequencyArray = new Uint8Array(bufferLength);
analyser.getByteFrequencyData(frequencyArray);
每个值范围也是 0~255。0 表示该频率没有能量,255 表示能量最大。
我曾经踩过一个坑:忘记在每次绘制前清空数组。结果波形图拖尾严重,看起来像鬼影。后来我养成习惯,每次 requestAnimationFrame 里都重新获取一次数据。
避坑指南:AnalyserNode 的数据是「一次性读取」的。如果你在同一个帧里多次调用 getByteFrequencyData,只有第一次有效。所以,请确保每次绘制循环只读取一次。
12.5 绘制音频波形图
波形图,说白了就是把时域数据画在 canvas 上。横轴是时间,纵轴是振幅。
我一般用 Canvas 2D 来画,配合 requestAnimationFrame 做动画循环。
function drawWaveform() {
requestAnimationFrame(drawWaveform);
analyser.getByteTimeDomainData(dataArray);
canvasCtx.fillStyle = 'rgb(200, 200, 200)';
canvasCtx.fillRect(0, 0, canvas.width, canvas.height);
canvasCtx.lineWidth = 2;
canvasCtx.strokeStyle = 'rgb(0, 0, 0)';
canvasCtx.beginPath();
const sliceWidth = canvas.width / bufferLength;
let x = 0;
for (let i = 0; i < bufferLength; i++) {
const v = dataArray[i] / 128.0;
const y = v * canvas.height / 2;
if (i === 0) {
canvasCtx.moveTo(x, y);
} else {
canvasCtx.lineTo(x, y);
}
x += sliceWidth;
}
canvasCtx.lineTo(canvas.width, canvas.height / 2);
canvasCtx.stroke();
}
这段代码的核心逻辑:
- 每次动画帧,从 AnalyserNode 读取最新的时域数据
- 把 0~255 映射到画布高度(除以 128,得到 0~2 的范围)
- 逐点连线,形成波形
你想想看,如果数据是 128(静音),画出来的就是一条直线在中间。如果有人在说话,波形就会上下跳动。
12.6 绘制频谱图
频谱图稍微复杂一点。它展示的是不同频率的能量分布。横轴是频率(低频在左,高频在右),纵轴是能量。
function drawSpectrum() {
requestAnimationFrame(drawSpectrum);
analyser.getByteFrequencyData(frequencyArray);
canvasCtx.fillStyle = 'rgb(0, 0, 0)';
canvasCtx.fillRect(0, 0, canvas.width, canvas.height);
const barWidth = (canvas.width / bufferLength) * 2.5;
let x = 0;
for (let i = 0; i < bufferLength; i++) {
const barHeight = frequencyArray[i] / 255 * canvas.height;
canvasCtx.fillStyle = `rgb(${barHeight + 100}, 50, 50)`;
canvasCtx.fillRect(x, canvas.height - barHeight, barWidth, barHeight);
x += barWidth + 1;
}
}
这里我用了渐变色——能量越高,颜色越红。你也可以用 HSL 或者渐变对象,效果会更炫酷。
我的经验:频谱图的柱子不要画太宽,留一点间隙看起来更清晰。另外,低频部分(左边)通常能量更大,所以柱子会更高。这是正常的,不是 bug。
12.7 完整示例:波形 + 频谱双视图
在实际项目中,我经常把波形图和频谱图放在一起。上面是波形,下面是频谱,一目了然。
// 初始化
const audioContext = new AudioContext();
const analyser = audioContext.createAnalyser();
analyser.fftSize = 2048;
const bufferLength = analyser.frequencyBinCount;
const timeData = new Uint8Array(bufferLength);
const freqData = new Uint8Array(bufferLength);
// 连接麦克风
navigator.mediaDevices.getUserMedia({ audio: true })
.then(stream => {
const source = audioContext.createMediaStreamSource(stream);
source.connect(analyser);
draw();
});
// 绘制循环
function draw() {
requestAnimationFrame(draw);
analyser.getByteTimeDomainData(timeData);
analyser.getByteFrequencyData(freqData);
// 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 上半部分:波形
drawWaveform(ctx, timeData, 0, canvas.height / 2);
// 下半部分:频谱
drawSpectrum(ctx, freqData, 0, canvas.height / 2, canvas.height / 2);
}
你看,代码其实不多。核心就是三步:创建 AnalyserNode → 连接音频源 → 循环读取并绘制。
12.8 知识体系图
下面这张 SVG 图,帮你理清整个音频可视化的流程:
12.9 性能优化小贴士
做可视化时,性能是个大问题。我遇到过 canvas 绘制卡顿的情况,后来做了几件事:
- 降低 fftSize:不需要太高精度时,用 1024 就够了
- 减少绘制频率:不一定每帧都画,可以用计时器控制,比如每秒 30 帧
- 使用离屏 canvas:如果图形复杂,先画到离屏 canvas,再一次性复制到主 canvas
- 避免频繁创建对象:Uint8Array 和 Canvas 路径对象,尽量复用
注意:不要在音频处理线程里做 UI 操作。AnalyserNode 的数据读取和 canvas 绘制,都应该在主线程(requestAnimationFrame)里完成。
12.10 总结
音频可视化,说白了就是三步走:接数据、读数据、画数据。AnalyserNode 是浏览器给我们的礼物,用好了,你的 WebRTC 应用就能「看得见声音」。
我个人觉得,波形图适合展示实时语音的节奏感,频谱图适合展示音乐或环境音的频率分布。两者结合,效果最好。
嗯,这一节的内容就到这里。代码不多,但都是实战经验。你拿去跑一跑,对着麦克风说几句话,看看波形怎么跳,很有意思的。
公众号:蓝海资料掘金营,微信deep3321