Tone.js v15.0.4播放MIDI时 tempo调整异常问题求助
问题
使用Vue v3.5.12、Tone.js v15.0.4及@tonejs/midi库实现MIDI文件播放控制,已实现播放、暂停、继续、停止、音量控制,但调整tempo时出现两个问题:
- 调整tempo后停止再重启,要么无法正常启动,要么延迟很久才启动且回到原tempo;
- 调整tempo后暂停再继续,会回到错误位置,但后续暂停/继续操作正常且tempo正确。
相关代码
const onSelectedMidiFileChange = async () => { console.log('Selected example:', selectedMidiFile.value); try { if (isPlayingMidi.value) { togglePlayMidi() } currentMidiFile = await Midi.fromUrl(selectedMidiFile.value[1]) console.log(currentMidiFile) selectedMidiFileJSON.value = JSON.stringify(currentMidiFile, null, 2) } catch(error) { return `Invalid JSON ${error}` } } // volume control const gain = new Tone.Gain(1).toDestination(); // Initialize with volume at 0 dB const setGlobalVolume = (volumeLevel: number) => { const volumeDb = volumeLevelToDb(volumeLevel); gain.gain.setValueAtTime(Tone.dbToGain(volumeDb), Tone.now()); } const volumeLevelToDb = (percentage: number): number => { return percentage === 0 ? -Infinity : (percentage - 100) * 0.25; } // tempo control const setTempo = (newTempo: number) => { Tone.getTransport().bpm.value = newTempo; console.log(`Transport tempo set to ${round(Tone.getTransport().bpm.value, 0)}`) }; const synths: Tone.PolySynth<Tone.Synth<Tone.SynthOptions>>[] = [] const togglePlayMidi = () => { isPlayingMidi.value = !isPlayingMidi.value if (isPlayingMidi.value) { if (Tone.getTransport().state !== 'started') { Tone.getTransport().start(0) console.log(`Transport started at ${round(Tone.getTransport().seconds)} seconds, tempo: ${Tone.getTransport().bpm.value}bpm`) } const now = Tone.now() + 0.5 if (currentMidiFile) { currentMidiFile.tracks.forEach((track, trk) => { const synth = new Tone.PolySynth(Tone.Synth, { envelope: { attack: 0.02, decay: 0.1, sustain: 0.3, release: 1 } }).sync().connect(gain) synths.push(synth) track.notes.forEach((note, idx) => { synth.triggerAttackRelease(note.name, note.duration, note.time + now, note.velocity) }) }) } } else { if (isPausedMidi.value) { pauseContinueMidi() } console.log(`Transport stopped at ${round(Tone.getTransport().seconds)} seconds`); Tone.getTransport().stop(0) Tone.getTransport().cancel(); while (synths.length) { const synth = synths.shift() synth?.unsync() synth?.dispose() } } } let pauseTime = 0; const pauseContinueMidi = () => { const transport = Tone.getTransport(); if (!isPausedMidi.value) { pauseTime = transport.seconds; transport.pause(); console.log("Transport paused at:", round(pauseTime), `, state: ${transport.state}`); } else { transport.seconds = pauseTime transport.start(); console.log("Transport re-started at:", round(transport.seconds), `, state: ${transport.state}`); } isPausedMidi.value = !isPausedMidi.value } const round = (n: number, decimals: number = 2) => Math.round(n * Math.pow(10, decimals)) / Math.pow(10, decimals)
关键操作日志
启动:Transport于23.51秒启动,tempo:120bpm
停止:Transport于30.86秒停止
启动:Transport于36.13秒启动,tempo:120bpm
暂停:Transport于42.12秒暂停,状态:paused
继续:Transport于42.12秒重启,状态:started
停止:Transport于45.9秒停止
启动:Transport于51.48秒启动,tempo:120bpm // 至此一切正常
调整tempo:Transport tempo设置为159 // 此处调整tempo
暂停:Transport于62.13秒暂停,状态:paused
继续:Transport于62.13秒重启,状态:started // 从错误位置继续
暂停:Transport于67.85秒暂停,状态:paused
继续:Transport于67.85秒重启,状态:started // 从暂停位置继续
停止:Transport于71.94秒停止
启动:Transport于85.32秒启动,tempo:159bpm // 延迟近15秒才启动,且未使用调整后的tempo
停止:Transport于100.21秒停止
解决方案
问题根源
- 重启延迟/原tempo问题:停止播放时仅停止transport并取消事件,但未重置transport内部时间,导致下次启动从之前的时间点继续,结合tempo变化造成时间映射错误;同时启动时用绝对时间
Tone.now()调度事件,未与transport时间同步,tempo变化后事件时间未重新计算。 - 暂停继续位置错误:暂停时记录的
transport.seconds是基于原tempo的时间,调整tempo后该时间对应的实际播放位置已变化,直接赋值会导致位置偏移。
修复代码及说明
1. 修正tempo设置函数(确保bpm立即生效)
const setTempo = (newTempo: number) => { // 取消之前的bpm自动化,立即设置新tempo Tone.getTransport().bpm.cancel(); Tone.getTransport().bpm.value = newTempo; console.log(`Transport tempo set to ${round(Tone.getTransport().bpm.value, 0)}`) };
2. 修正播放切换函数(停止时重置状态,启动时同步transport时间)
const togglePlayMidi = () => { isPlayingMidi.value = !isPlayingMidi.value if (isPlayingMidi.value) { isPausedMidi.value = false; if (Tone.getTransport().state !== 'started') { // 重置transport时间到0,确保从开头启动 Tone.getTransport().seconds = 0; Tone.getTransport().start(0); console.log(`Transport started at ${round(Tone.getTransport().seconds)} seconds, tempo: ${Tone.getTransport().bpm.value}bpm`) } // 基于transport当前时间调度事件,而非绝对时间 const transportTime = Tone.getTransport().seconds; if (currentMidiFile) { currentMidiFile.tracks.forEach((track, trk) => { const synth = new Tone.PolySynth(Tone.Synth, { envelope: { attack: 0.02, decay: 0.1, sustain: 0.3, release: 1 } }).sync().connect(gain); synths.push(synth); // 只调度当前时间之后的事件,避免重复播放 track.notes.forEach((note, idx) => { if (note.time >= transportTime) { synth.triggerAttackRelease(note.name, note.duration, note.time, note.velocity); } }); }); } } else { if (isPausedMidi.value) { pauseContinueMidi(); } console.log(`Transport stopped at ${round(Tone.getTransport().seconds)} seconds`); // 停止并完全重置transport状态 Tone.getTransport().stop(0); Tone.getTransport().seconds = 0; Tone.getTransport().cancel(); while (synths.length) { const synth = synths.shift(); synth?.unsync(); synth?.dispose(); } } }
3. 修正暂停/继续函数(用节拍位置记录暂停点)
let pausePosition = "0:0:0"; // 基于节拍的位置,不受tempo变化影响 const pauseContinueMidi = () => { const transport = Tone.getTransport(); if (!isPausedMidi.value) { // 记录节拍位置而非秒数 pausePosition = transport.position; transport.pause(); console.log("Transport paused at:", pausePosition, `, state: ${transport.state}`); } else { // 恢复到记录的节拍位置 transport.position = pausePosition; transport.start(); console.log("Transport re-started at:", transport.position, `, state: ${transport.state}`); } isPausedMidi.value = !isPausedMidi.value }
核心要点
- 始终基于transport的内部时间或节拍位置调度事件,而非绝对时间,确保tempo变化时事件播放时间自动同步。
- 停止播放后必须重置transport时间到0,避免下次启动从历史时间点继续。
- 用
position记录暂停点,它是基于MIDI节拍的位置,不受tempo变化影响,比秒数更准确。
内容的提问来源于stack exchange,提问作者apps2go
相关产品推荐
相关产品推荐

