音频可视化基础:用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是正半周。

注意:不要同时用getByteFrequencyDatagetByteTimeDomainData在同一个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);
}

知识体系总览

下面这张图帮你理清本章的核心逻辑:

音频可视化核心流程 音频源 麦克风/文件/网络流 AnalyserNode fftSize: 1024 getByteFrequencyData 频域数据 → 频谱图 getByteTimeDomainData 时域数据 → 波形图 性能优化策略 降低采样率 | 复用数组 | 离屏canvas | 选择合适的fftSize 关键点:AnalyserNode不修改音频流,只做分析 fftSize决定精度,1024是平衡点

实战小贴士

最后分享几个我在项目中积累的经验:

  • 颜色映射:频谱图用HSL颜色渐变效果最好。低频用暖色(红/橙),高频用冷色(蓝/紫)。人眼对暖色更敏感,低频能量通常也更大。
  • 平滑处理:直接画出来的频谱图会跳得很厉害。可以加个平滑系数:smoothedValue = current * 0.3 + previous * 0.7。这样看起来更舒服。
  • 移动端适配:canvas的像素比要设置devicePixelRatio,否则在Retina屏上会模糊。
  • 调试技巧:如果发现频谱图全是0,先检查麦克风权限。如果全是255,说明音频输入过载了,需要加增益控制。

我的习惯:在开发阶段,我会同时显示频谱图和波形图。频谱图帮我定位频率问题,波形图帮我检查音量是否正常。两个图一对照,问题基本就清楚了。

好了,音频可视化的基础就这些。说白了就是三步:创建AnalyserNode、获取数据、画到canvas上。但细节决定成败,fftSize的选择、性能优化、颜色映射,这些才是真正拉开差距的地方。

下一章我们会聊更高级的话题——但那是下一章的事了。先把今天的内容消化掉,动手写个demo试试。相信我,当你看到声音在屏幕上跳动的那一刻,会觉得一切都值得。


公众号:蓝海资料掘金营,微信deep3321