算法与数据结构 / 动画引擎原理 · 从插值到播放控制 / 声音驱动:FFT 频谱与节拍 待审核 19 / 47
Web Audio · FFT · audio-reactive

声音驱动:FFT 频谱与节拍

前面每一页的「值」都来自时间(补间)或物理(弹簧)。但驱动动画的信号也可以来自外部——比如声音。Web Audio 的 AnalyserNode 对正在播放的音频做快速傅里叶变换,每一帧给出一条频谱,把它喂给视觉即得随声而动。

本页在页内合成一段循环节拍,既不上传音频也不申请麦克风权限:kick 是一条从 150 Hz 滑到 45 Hz 的正弦,hat 是白噪声过高通滤波,bass 是锯齿波过低通。

1 · 频谱驱动的可视化

图 1-1 · 页内合成节拍与它的实时频谱。浏览器要求音频由用户手势启动,故须先点播放;可调 BPM 与灵敏度。

时域里声音是一条随时间起伏的振幅曲线;FFT 把一小段波形变换到频域,得到「每个频率上有多少能量」。fftSize = 256 给出 128 个频段,getByteFrequencyData 每帧把它们填进一个 Uint8Array。视觉只需选几个频段映射到属性:低频段均值驱动脉冲缩放,各频段驱动频谱条高度。

2 · 两种时钟

这一页有两个互不相干的时钟,分工很清楚。

音符的排布走音频时钟setInterval 每 25 ms 醒一次,把 currentTime + 120 ms 之内的音符按精确时间戳全部预约进去。节奏因此由音频硬件的采样时钟决定,主线程卡顿也不会让鼓点跑偏——这是 Web Audio 排布的标准做法,直接在 setInterval 回调里发声则会随主线程一起抖。

视觉的更新走 requestAnimationFrame,与显示刷新对齐。两个时钟不需要同步:视觉每帧只是去读一次分析器的当前快照。

注 · smoothingTimeConstant = 0.82 是让画面不抖的关键。它在分析器内部对相邻两帧的频谱做指数平滑,取值 0 时频谱条会剧烈跳动——因为 FFT 的每个频段本就随窗口位置有较大方差。代价是响应变钝:0.82 意味着一个突变要约 1/(10.82)5.61/(1-0.82) \approx 5.6 帧才收敛到位,鼓点的脉冲因此比听到的略晚一点。

const analyser = ctx.createAnalyser();
analyser.fftSize = 256;                 // → 128 个频段
analyser.smoothingTimeConstant = 0.82;  // 帧间平滑
source.connect(analyser);
const freq = new Uint8Array(analyser.frequencyBinCount);

function frame() {
  analyser.getByteFrequencyData(freq);  // 当前频谱 (0–255)
  const low = avg(freq, 0, 6);          // 低频段 = 鼓点
  pulse.style.transform = `scale(${1 + low / 255})`;
  for (let i = 0; i < NB; i++)          // 频谱条
    bars[i].style.height = `${freq[i * k] / 255 * 120}px`;
  requestAnimationFrame(frame);
}

声音是「外部信号驱动」的一种,噪声是「内部程序化信号」的一种——两者都不走时间补间,却同样能源源不断地驱动动画。映射曲线仍可借缓动整形,例如对能量值再过一条 easeOut 让脉冲回落更柔和。把频谱画到 canvas 上则见渲染目标可换