纯前端实现音频波形预览:AudioContext、降采样与 Canvas 绘制
给音频文件生成波形预览,最直接的想法是把每个采样点都画到 Canvas 上。但一分钟 48 kHz 立体声音频就接近六百万个采样点,远超画布宽度,既慢又没有更多视觉信息。更合理的方式是按屏幕像素分桶,为每个桶计算代表值。
一、先解码成可计算的采样
浏览器可以用 AudioContext.decodeAudioData 读取 ArrayBuffer,得到声道、采样率和时长。解码前先检查文件大小,避免超大文件造成明显内存峰值;解码期间也要允许用户取消或重新选择文件,防止旧任务覆盖新结果。
const context = new AudioContext();
const buffer = await context.decodeAudioData(arrayBuffer.slice(0));
const channel = buffer.getChannelData(0);
示例只取第一声道做概览,正式产品可以把左右声道合成后再计算。解码和绘制结束后调用 context.close,释放音频处理资源。
二、按画布像素分桶
画布有多少个横向像素,就大致需要多少个波形桶。先根据容器宽度和设备像素比算出目标宽度,再把采样数组均匀划分成 bucket 数量:
const bucketSize = Math.max(1, Math.ceil(samples.length / width));
const peaks = [];
for (let start = 0; start < samples.length; start += bucketSize) {
let max = 0;
const end = Math.min(start + bucketSize, samples.length);
for (let i = start; i < end; i++) {
max = Math.max(max, Math.abs(samples[i]));
}
peaks.push(max);
}
每桶取绝对值最大值能保留瞬态冲击,适合快速预览。若目标是更接近听感的响度展示,可以同时计算均方根值,再用最小值限制弱信号,避免整条波形看起来过平。
三、从声音数据到坐标
柱形波形以画布中线为零点,把振幅映射成上下对称的高度。绘制前统一缩放到 CSS 像素,避免在每个坐标里重复乘 devicePixelRatio:
const dpr = window.devicePixelRatio || 1;
canvas.width = Math.round(cssWidth * dpr);
canvas.height = Math.round(cssHeight * dpr);
ctx.scale(dpr, dpr);
for (let x = 0; x < width; x++) {
const amplitude = peaks[x] * (cssHeight / 2 - 4);
ctx.fillRect(x, cssHeight / 2 - amplitude, 1, amplitude * 2 || 1);
}
用 fillRect 一列一列画通常已经足够快。先把样式设置好,再进入循环,不要在循环里反复创建渐变、修改字体或查询布局。
四、处理高分屏与重绘
容器尺寸变化后要重新计算画布像素尺寸和分桶结果,并对重绘做 requestAnimationFrame 合并,避免拖拽窗口时连续触发大量计算。缓存原始采样或峰值数组,宽度小幅变化时可以复用数据,大幅变化时再重新分桶。
五、播放进度和交互
播放进度最好作为独立图层绘制,拖动时只重画画布,不重新解码。点击波形定位播放位置时,把横坐标换算成时长,并限制在零到总时长之间。进度和悬停时间应同时提供可读文本,不能只依赖颜色或线条。
六、可访问性与降级
Canvas 对辅助技术并不天然友好。外层容器应提供文件名、时长、播放状态和当前时间的文本信息,按钮要有明确名称。解码失败时显示错误原因和重新选择文件的入口;浏览器不支持 Web Audio 时,可以退化为音频控件,而不是只剩一张空画布。
七、小结
波形预览的关键不是把全部采样画完,而是解码后按像素分桶,计算每桶的峰值或均方根,再用统一的 DPR 坐标绘制。配合取消旧任务、合并重绘、释放音频上下文和文本化信息,预览才能既流畅又可用。
- 点赞
- 收藏
- 关注作者
评论(0)