Tone.js多音符播放与Loop节拍器不同步问题求助
问题描述
我尝试将乐谱导入Tone.js,但同时启用节拍器时,tempo听起来不准确。我发现这与乐谱的音符数量有关:音符数量少时正常;实际场景中我使用了4n、8n.、t16等复杂节拍细分。为简化问题,我创建了仅使用8n的测试脚本仍能复现该问题,说明并非节拍细分计算错误。
测试代码基于官方示例修改:
节拍器函数
function metronome() { // 创建两个单音合成器 const synthA = new Tone.Synth().toDestination(); const synthB = new Tone.Synth().toDestination(); // 每小节播放一个音符 const loopA = new Tone.Loop((time) => { synthA.triggerAttackRelease("C5b", "8n", time); }, "1m").start(0); // 每四分音符播放另一个音符,从0时刻开始 const loopB = new Tone.Loop((time) => { synthB.triggerAttackRelease("C4b", "8n", time); }, "4n").start(0); // 启动Transport时所有循环开始播放 Tone.getTransport().start(); }
乐谱播放函数
function play() { const synth = new Tone.Synth().toDestination(); const now = Tone.now(); Array(4) .fill(['C3', 'D3', 'E3', 'F3', 'G3', 'A3', 'B3', 'C4']) .flat() .forEach((note, i) => { const subdivision = '8n' const duration = Tone.Time(subdivision).toSeconds() * i + now synth.triggerAttackRelease(note, "8n", duration); }) }
触发逻辑
document.querySelector('#play').addEventListener('click', () => { play() metronome() })
当Array(4)生成32个音符时,所有声音与tempo匹配正常,但改为Array(40)生成320个音符时,tempo就不再匹配。请问我在使用Tone.js时哪里出现了错误?
问题原因与解决方案
核心问题
你手动计算音符时间的方式和Tone.js的Transport时序系统不兼容。当大量调用triggerAttackRelease并传入手动计算的时间时,会导致音频调度队列过载,打乱Transport的时序精度。
Tone.now()返回的是音频上下文的当前时间,而Transport的时序是基于自身维护的节拍、速度机制,手动累加时间的方式没有和Transport时钟同步,音符数量越多,调度延迟的累积效应越明显,最终导致节拍器和乐谱播放不同步。
正确实现方式
统一使用Tone.js的Transport系统调度所有音符,用Tone.Part批量管理乐谱事件,让它和节拍器共享同一个时钟:
修改play函数如下:
function play() { const synth = new Tone.Synth().toDestination(); // 创建Part管理所有音符事件 const notesPart = new Tone.Part((time, note) => { synth.triggerAttackRelease(note, "8n", time); }, []); // 生成音符序列 const noteSequence = Array(40) .fill(['C3', 'D3', 'E3', 'F3', 'G3', 'A3', 'B3', 'C4']) .flat(); // 基于Transport节拍位置定义每个音符的触发时间 noteSequence.forEach((note, i) => { notesPart.at(`${i * 0.5}n`, note); }); // 让Part和Transport同步启动 notesPart.start(0); }
额外优化点
- 复用合成器实例:避免每次调用函数都创建新合成器,减少资源浪费:
- 统一启动时机:先初始化所有调度,再启动Transport,确保所有事件同步触发:
优化后的完整代码:
// 全局复用合成器 const metronomeSynthA = new Tone.Synth().toDestination(); const metronomeSynthB = new Tone.Synth().toDestination(); const playSynth = new Tone.Synth().toDestination(); let loopA, loopB, notesPart; function initMetronome() { loopA = new Tone.Loop((time) => { metronomeSynthA.triggerAttackRelease("C5b", "8n", time); }, "1m").start(0); loopB = new Tone.Loop((time) => { metronomeSynthB.triggerAttackRelease("C4b", "8n", time); }, "4n").start(0); } function initPlay() { notesPart = new Tone.Part((time, note) => { playSynth.triggerAttackRelease(note, "8n", time); }, []); const noteSequence = Array(40) .fill(['C3', 'D3', 'E3', 'F3', 'G3', 'A3', 'B3', 'C4']) .flat(); noteSequence.forEach((note, i) => { notesPart.at(`${i * 0.5}n`, note); }); notesPart.start(0); } document.querySelector('#play').addEventListener('click', () => { initMetronome(); initPlay(); // 统一启动Transport Tone.getTransport().start(); });
这样所有音频事件都由Transport统一调度,无论音符数量多少,时序都会完全同步,不会出现tempo不匹配的问题。
内容的提问来源于stack exchange,提问作者Qimat Luo
相关产品推荐
相关产品推荐

