HTML原生标签无法混音,必须用Web Audio API;核心是将多轨解码为AudioBuffer,在AudioContext中按采样点相加,并手动防削波。

直接说结论:HTML 原生 <audio></audio> 标签做不到混音,必须用 Web Audio API;所谓“多音轨混音”,本质是多个 AudioBuffer 或实时流在 AudioContext 中按采样点相加,且必须手动防削波(clipping)。
为什么 <audio></audio> 不能混音
浏览器对每个 <audio></audio> 元素独立管理解码、缓冲和播放时序,它们之间没有共享时间轴或信号通路。调用多个 audio.play() 只是并发触发,实际播放起始时刻误差常达 ±100ms,且无法控制各轨增益、相位或延迟对齐。更关键的是:你拿不到原始 PCM 数据,也就无从做加法运算。
常见错误现象包括:
- 两段音频看似“同时播放”,但几秒后明显漂移(缺乏统一时钟)
- 音量忽大忽小,甚至爆音(未归一化,叠加后超出 [-1, 1] 范围)
- 切换音轨时卡顿或中断(
load()触发重新解码,阻塞主线程)
用 AudioContext 实现多轨混音的最小可行路径
核心不是“播放多个文件”,而是把所有音轨预先解码为 AudioBuffer,再用节点连接模型合成一路输出。重点在于复用上下文、避免重复解码、显式控制起始时间。
立即学习“前端免费学习笔记(深入)”;
- 创建上下文必须在用户交互后(如
click事件中),否则 Chrome/Firefox 会静音:const context = new (window.AudioContext || window.webkitAudioContext)(); - 所有音频用
fetch()+arrayBuffer预加载,然后统一调用context.decodeAudioData()—— 这步 CPU 开销大,只做一次 - 缓存解码后的
AudioBuffer,后续播放直接复用:const source = context.createBufferSource(); source.buffer = cachedBuffer; - 每轨必须用
source.start(context.currentTime)启动,传入精确时间戳,而非source.start()—— 否则各轨起始时间由 JS 执行顺序决定,不可控
ChannelMergerNode 和直接 .connect() 到同一目标的区别
混音有两种主流链路写法,适用场景不同:
- 多路
connect()到同一GainNode:适合简单叠加,比如背景乐 + 人声 + 音效。代码简洁:sourceA.connect(mixGain); sourceB.connect(mixGain); mixGain.connect(context.destination);。但注意:Web Audio 默认对多路输入做**无缩放相加**,两路满幅信号直接相加必 clipping,所以mixGain.gain.setValueAtTime(0.5)是安全起点 -
ChannelMergerNode:仅当需要保持多声道布局时才用,比如双轨立体声输入 → 四声道输出(L/R/L/R)。它不自动混音,只是把通道“拼起来”。若目标仍是双声道扬声器,最终还得接ChannelSplitterNode+GainNode再合并,徒增复杂度 - 真正需要分离/合并声道(如只取左声道、左右平均成单声道),必须用
buffer.getChannelData(0)手动读写Float32Array,再填入新AudioBuffer
防削波(clipping)和响度管理的实际操作
混音后爆音不是 bug,是物理事实:两个 0dBFS 信号相加就是 +3dBFS,超出 DAC 能力范围。浏览器不会自动压缩,必须人工干预。
- 最简方案:每轨播放前设固定衰减,例如
source.playbackRate = 1; gainNode.gain.value = 0.7;(三轨就用 0.7,四轨用 0.6) - 动态方案:用
ScriptProcessorNode已废弃,改用AudioWorklet实现峰值检测+限幅,但开发成本高,一般项目没必要 - 离线预处理更可靠:混音前对每轨
AudioBuffer做 RMS 归一化,再按权重相加。计算公式:output[i] = (left[i] * 0.5 + right[i] * 0.5)(平均法),或output[i] = Math.min(1, Math.max(-1, left[i] + right[i]))(硬限幅,不推荐) - 验证是否 clipping:监听
context.onstatechange或用AnalyzerNode抽样检查输出幅度,但实时性差,适合调试
最容易被忽略的一点:移动端 Safari 对 AudioContext 的资源释放极其敏感,长时间运行多轨混音必须主动调用 source.stop() 清理已结束节点,否则内存泄漏导致卡顿甚至崩溃。



















