音频可视化基础:用AnalyserNode看声音的样子
说实话,我第一次接触WebRTC的时候,最兴奋的不是视频通话,而是音频可视化。你想啊,能把看不见的声音变成跳动的柱状图,这事儿本身就挺酷的。今天我们就来聊聊怎么用AnalyserNode把声音“画”出来。
AnalyserNode是什么?
AnalyserNode是Web Audio API里的一个节点。它不改变音频数据,只是“偷看”一下。说白了,它就像个监听器,实时告诉你当前音频的频率和波形信息。
我在项目中遇到过一个问题:用户说录音有杂音,但我听不出来。后来用AnalyserNode一看频谱图,立马发现某个频段异常偏高。嗯,这就是可视化的价值。
核心概念:AnalyserNode不会修改音频流,它只做分析。你可以把它插在音频链路的任何位置。
创建AnalyserNode
先来个最简单的例子。假设你已经有了音频上下文和麦克风流:
const audioContext = new AudioContext();
const analyser = audioContext.createAnalyser();
// 连接到麦克风
navigator.mediaDevices.getUserMedia({ audio: true })
.then(stream => {
const source = audioContext.createMediaStreamSource(stream);
source.connect(analyser);
// analyser可以连接到destination,也可以不连
// analyser.connect(audioContext.destination);
});
这里要注意:analyser不连到destination也没关系。它只是“旁路”监听,不影响主音频流。我曾经犯过这个错——把analyser连到destination后,声音变了两遍,听起来像回声。
理解FFT size
AnalyserNode有个关键参数:fftSize。它决定了频率分析的精度。
| fftSize | 频率分辨率 | 频域数据长度 | 适用场景 |
|---|---|---|---|
| 256 | 低 | 128 | 快速预览 |
| 512 | 中 | 256 | 一般波形 |
| 1024 | 高 | 512 | 精细频谱 |
| 2048 | 极高 | 1024 | 专业分析 |
为什么是2的幂?因为FFT算法要求这样。我个人习惯用1024,精度够用,性能也不差。
小技巧:fftSize越大,数据越精细,但计算量也越大。移动端建议用512或256。
获取频域数据:频谱图
频谱图展示的是各个频率的能量分布。代码很简单:
function drawSpectrum() {
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
analyser.getByteFrequencyData(dataArray);
// 现在dataArray里就是0-255的频域数据
// 你可以用canvas画出来
const canvas = document.getElementById('spectrum');
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
const barWidth = canvas.width / bufferLength;
for (let i = 0; i < bufferLength; i++) {
const barHeight = dataArray[i] / 255 * canvas.height;
ctx.fillStyle = `hsl(${i * 360 / bufferLength}, 100%, 50%)`;
ctx.fillRect(i * barWidth, canvas.height - barHeight, barWidth - 1, barHeight);
}
requestAnimationFrame(drawSpectrum);
}
这里有个坑:getByteFrequencyData返回的是0-255的整数。如果你需要更精细的数据,可以用getFloatFrequencyData,返回的是分贝值。
获取时域数据:波形图
波形图展示的是声音的振幅随时间的变化。和频谱图类似,只是换了个API:
function drawWaveform() {
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
analyser.getByteTimeDomainData(dataArray);
const canvas = document.getElementById('waveform');
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.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) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
x += sliceWidth;
}
ctx.stroke();
requestAnimationFrame(drawWaveform);
}
波形图的中线是128。为什么?因为getByteTimeDomainData返回的是0-255,128代表振幅为0。低于128是负半周,高于128是正半周。
注意:不要同时用getByteFrequencyData和getByteTimeDomainData在同一个requestAnimationFrame里。它们会互相干扰。我建议隔帧交替读取,或者用两个独立的analyser节点。
性能优化:别让浏览器卡死
音频可视化如果写得不好,CPU占用率能飙到80%以上。我踩过这个坑,后来总结了几条经验:
- 降低采样率:不需要每帧都画。用
requestAnimationFrame默认是60fps,但音频可视化30fps就够用了。可以加个计数器跳帧。 - 减少canvas操作:不要每帧都
clearRect整个画布。如果背景不变,可以只清除变化区域。 - 使用离屏canvas:把静态部分画到离屏canvas上,主canvas只画动态部分。
- 选择合适的fftSize:前面说了,移动端用512或256。
- 避免频繁创建对象:
dataArray数组可以复用,不要每帧都new。
// 优化示例:复用数组 + 跳帧
const dataArray = new Uint8Array(analyser.frequencyBinCount);
let frameCount = 0;
function optimizedDraw() {
frameCount++;
if (frameCount % 2 !== 0) { // 每两帧画一次
requestAnimationFrame(optimizedDraw);
return;
}
analyser.getByteFrequencyData(dataArray);
// ... 绘图逻辑
requestAnimationFrame(optimizedDraw);
}
知识体系总览
下面这张图帮你理清本章的核心逻辑:
实战小贴士
最后分享几个我在项目中积累的经验:
- 颜色映射:频谱图用HSL颜色渐变效果最好。低频用暖色(红/橙),高频用冷色(蓝/紫)。人眼对暖色更敏感,低频能量通常也更大。
- 平滑处理:直接画出来的频谱图会跳得很厉害。可以加个平滑系数:
smoothedValue = current * 0.3 + previous * 0.7。这样看起来更舒服。 - 移动端适配:canvas的像素比要设置
devicePixelRatio,否则在Retina屏上会模糊。 - 调试技巧:如果发现频谱图全是0,先检查麦克风权限。如果全是255,说明音频输入过载了,需要加增益控制。
我的习惯:在开发阶段,我会同时显示频谱图和波形图。频谱图帮我定位频率问题,波形图帮我检查音量是否正常。两个图一对照,问题基本就清楚了。
好了,音频可视化的基础就这些。说白了就是三步:创建AnalyserNode、获取数据、画到canvas上。但细节决定成败,fftSize的选择、性能优化、颜色映射,这些才是真正拉开差距的地方。
下一章我们会聊更高级的话题——但那是下一章的事了。先把今天的内容消化掉,动手写个demo试试。相信我,当你看到声音在屏幕上跳动的那一刻,会觉得一切都值得。
公众号:蓝海资料掘金营,微信deep3321