Tone.js 播放超过10条音轨时出现爆音,离线导出正常,如何排查?
结论:优先排查实时处理负载,但不能仅凭离线导出正常确定原因
这组现象可能与实时音频处理来不及完成有关,尤其是在移动设备上。目前还不能直接认定是 CPU 过载或 audio buffer underrun。调度不及时、音符或参数突变,以及实时与离线处理路径的差异,也可能产生类似的爆音。
Web Audio 没有统一的“最多 10 条音轨”限制。实际承载能力取决于同时发声的 voice 数量、合成器复杂度、效果器、浏览器和设备。更合适的做法是管理实时处理预算,并为复杂音轨提供冻结和降级机制。
标准实时上下文的名称是 AudioContext;通常所说的“实时音频上下文”并不是名为 RealTimeAudioContext 的标准接口。
为什么 Tone.Offline() 正常,实时播放却会爆音?
实时播放必须在音频设备需要下一段数据之前完成计算。如果处理没有赶上期限,声音就可能中断或出现不连续。
Tone.Offline() 使用离线渲染,不需要按扬声器播放的速度交付音频。因此,同一段编曲能够正确渲染,不代表设备能够实时完成这些计算。
离线结果干净时,可以优先检查实时性能和调度,但仍不能完全排除信号问题。对比时,还要确认两条路径的音源、效果器、增益、自动化和采样率等条件是否一致。
先用对照测试缩小范围
针对同一段容易出问题的编曲逐项测试,每次只改变一个条件:
- 保留音轨数量,减少同时发声的音符。如果单音播放正常、密集和弦容易爆音,重点检查 voice 数量及音符重叠。
- 逐个旁路各轨效果器和主输出链中的效果器,观察哪一步会显著改变故障表现。
- 逐步复制同一条简化音轨。与比较不同音色的音轨相比,这样更容易判断负载增长与爆音的关系。
- 暂时关闭频谱、波形动画及高频 UI 更新。如果问题改善,继续检查主线程任务是否影响了事件调度。
- 观察爆音出现的时间。咔嗒声如果只出现在音符起止、循环接缝或参数变化处,优先检查信号是否不连续;如果杂音随整体负载上升而频繁出现,则更值得检查实时处理能力。
这些测试只能帮助缩小排查范围。主线程性能面板也不能完整代表音频渲染线程的负载,因此不能仅凭页面 CPU 看起来不高就排除实时处理问题。
lookAhead、compressor 和 limiter 为什么没有解决问题?
lookAhead 让调度器有机会提前安排音频事件,可以缓解 JavaScript 回调延迟的影响,但不会降低合成器和效果器每秒需要完成的计算量。
如果故障来自音频处理本身超时,继续增加 lookAhead 通常帮助有限,还可能让交互响应变慢。latencyHint: “playback” 也是一种延迟偏好,实际采用的延迟由浏览器和设备决定。
Compressor 和 limiter 可以控制动态或峰值,但无法补回没有及时生成的音频数据,而且它们也需要计算资源。降低音量后如果问题仍然存在,仍有必要检查性能,也不能彻底排除处理链中的失真。
复音限制要覆盖整首编曲
每个 synth 的 maxPolyphony 设置为 4 到 6,并不等于整个应用只有 4 到 6 个 voice。多条音轨同时发声时,这些数量会累加;具体哪些音符仍占用 voice,还取决于音源实现及包络状态。
可以按以下顺序减少负载:
- 缩短不必要的 release,减少前后和弦的尾音重叠。
- 检查重复触发、重复注册的调度事件,以及没有正确释放的音符。
- 为各音轨分配复音预算,并关注全局同时发声量。
- 对低优先级声部制定 voice stealing 或省略音符的规则,避免高负载时任意丢音。
- 在暂停、删除音轨和切换工程时,清理相应事件与音频资源,防止旧对象继续参与处理。
voice stealing 也可能产生咔嗒声。如果需要自行实现,应考虑短暂淡出,避免直接截断仍在发声的波形。
调度时使用回调提供的音频时间
使用 Tone.js 的时间线调度功能时,应把回调提供的 time 传给音源,避免在回调执行时直接按“现在”触发。
下面的示例假设已有可用的 Tone.PolySynth 实例 synth,且应用已完成音频启动和 Transport 的播放控制:
const events = [
{
time: "0:0:0",
notes: ["C4", "E4", "G4"],
duration: "8n",
velocity: 0.5
}
];
const part = new Tone.Part((time, event) => {
synth.triggerAttackRelease(
event.notes,
event.duration,
time,
event.velocity
);
}, events);
part.start(0);
音源、采样数据和效果链应尽量提前准备。避免在调度回调里解码音频、创建大量节点或执行耗时的数据处理。UI 可以跟随播放位置更新,但不要让动画帧或普通定时器承担精确的音符触发。
比固定限制音轨数更合适的架构:冻结复杂音轨
对于编曲已确定、暂时不需要编辑音色的音轨,可以采用 track freezing:
- 使用离线渲染生成该音轨的音频,保留需要的 release 和效果器尾音。
- 实时播放时,用缓冲音频替代原来的合成器及已经渲染进去的效果处理。
- 停止或断开被替代的实时处理链,避免它继续消耗资源。
- 用户修改音符、音色或相关自动化后,让缓存失效并重新渲染。
冻结音轨通常能减少实时合成和效果处理的开销,但缓冲播放仍有混音成本,也会占用内存。移动设备上应控制缓存时长和数量,及时释放不再使用的缓存。
还要明确冻结的处理边界:如果已将某个效果器渲染进音频,实时链中再次经过同一个效果器就会改变声音;依赖其他音轨的 sidechain 或总线处理,也不能简单地按单轨独立冻结。
移动端如何设置限制?
建议根据目标设备的测试结果设置默认预算,再逐步降级:优先减少非必要复音、缩短尾音、简化效果器,并冻结较复杂的音轨。需要即时演奏的轨道保留实时处理,稳定的伴奏轨道优先使用缓冲音频。
音轨数量可以作为产品层的保护上限,但不能准确代表负载。一条复杂合成器轨道可能比多条简单采样轨道更重。没有目标设备、音色配置和故障复现数据时,无法给出可靠的统一音轨数或 maxPolyphony 数值。
备注:内容仅供参考。
