PixiJS节奏游戏中MIDI文件与画面音符同步问题求助
MIDI转节奏游戏音符同步问题解决思路
我用PixiJS+TypeScript开发课程作业的节奏游戏,玩法是当对应按键落入屏幕指定区域时按下。游戏支持用户自选.mid文件(生成屏幕音符)和.ogg文件(背景音乐)。已将.mid解析为JSON并提取数据生成音符,但无论调整公式或解析逻辑,屏幕音符始终与音乐不同步。
当前解析代码
function processMidiJson(json: any) { console.log('Processing MIDI JSON:', json); const jsonArray = convertArraysToObjectArrays(Object.values(json)); let notesArray: [string, number][] = []; const tempo = json.tracks[0][1].setTempo.microsecondsPerQuarter; // Tempo in microseconds per quarter note const bpm = 60000 / (tempo / 1000); const division = json.division; // Division value from MIDI file const msPerTick = 60000 / (bpm * division) console.log(msPerTick); for (let i = 0; i < jsonArray[2][1].length; i++) { const currentObject = jsonArray[2][1][i]; if (currentObject.noteOn) { if (jsonArray[2][1][i - 1].noteOn) { // If the previous note is a noteOn, then it is a chord and only the delta of one note is necessary. notesArray.push([String(currentObject.noteOn.noteNumber - 96), ((currentObject.delta) * msPerTick)]); } else { notesArray.push([String(currentObject.noteOn.noteNumber - 96), ((currentObject.delta + jsonArray[2][1][i + 1].delta) * msPerTick)]); } } } console.log(notesArray); return notesArray; }
测试情况
测试曲为120BPM,解析出的间隔数值看似合理(如八分音符250ms、附点四分音符750ms),背景音乐是MIDI直接转换的1:1版本。尝试过currentObject.delta + jsonArray[2][1][i + 1].delta和currentObject.delta + jsonArray[2][1][i - 1].delta两种组合,使用当前音符noteOff的delta效果稍好,但仍会随游戏进程逐渐不同步。
核心问题排查与解决建议
1. 改用绝对时间戳替代相对间隔
当前用相对间隔累加的方式,会因为JS定时器精度、游戏帧率波动产生累积误差。正确做法是计算每个noteOn事件的绝对毫秒时间戳(从MIDI文件开头算起):
- 遍历事件时累加delta得到绝对tick数
- 将绝对tick转成绝对毫秒,游戏中直接用音频播放的当前时间和这个绝对时间对比,而非靠间隔累加
2. 修正MIDI事件处理逻辑
- 区分有效noteOn事件:部分MIDI用
noteOn+velocity=0替代noteOff,需要跳过这类事件,避免误判为新音符 - 和弦处理:和弦的多个noteOn是同时触发的,它们的绝对时间应该一致,靠相邻事件判断的逻辑容易出错,统一用绝对时间处理即可
- 全轨道遍历:当前只处理了单个轨道,要遍历所有轨道的音符事件,避免遗漏
- 动态tempo处理:如果MIDI中途有变速(setTempo事件),固定的msPerTick会导致错误,需要实时更新msPerTick
3. 以音频播放时间为同步基准
- 不要用
setTimeout/setInterval控制音符生成,改用音频的timeupdate事件或Pixi的ticker回调,以audio.currentTime为基准计算当前播放进度 - 记录音频开始播放的时间偏移:比如
const audioStart = Date.now() - audio.currentTime * 1000,后续用Date.now() - audioStart得到当前播放的绝对毫秒数,和音符的绝对时间对比
修正后的解析代码示例
function processMidiJson(json: any) { const tracks = json.tracks; const division = json.division; // 默认120BPM(500000微秒/四分音符) let currentTempo = json.tracks[0].find(evt => evt.setTempo)?.setTempo.microsecondsPerQuarter || 500000; // 微秒每tick转毫秒每tick,计算更直接 let msPerTick = (currentTempo / 1000) / division; let absoluteTick = 0; const notes: { note: string, absoluteMs: number }[] = []; // 遍历所有轨道的事件 for (const track of tracks) { for (const evt of track) { // 累加delta得到从文件开头的绝对tick数 absoluteTick += evt.delta || 0; if (evt.noteOn) { const noteOn = evt.noteOn; // 跳过velocity=0的noteOn(等效于noteOff) if (noteOn.velocity === 0) continue; // 计算绝对毫秒时间戳 const absoluteMs = absoluteTick * msPerTick; notes.push({ note: String(noteOn.noteNumber - 96), absoluteMs }); } // 处理中途变速事件 if (evt.setTempo) { currentTempo = evt.setTempo.microsecondsPerQuarter; msPerTick = (currentTempo / 1000) / division; } } } // 按绝对时间排序,确保音符顺序正确 notes.sort((a, b) => a.absoluteMs - b.absoluteMs); // 如果需要相对间隔,可在此计算,游戏中建议直接用绝对时间 const notesArray = notes.map((note, idx) => { const prevMs = idx > 0 ? notes[idx-1].absoluteMs : 0; return [note.note, note.absoluteMs - prevMs] as [string, number]; }); console.log(notesArray); return notesArray; }
游戏端同步逻辑示例
// 初始化音频后记录起始时间 const audio = new Audio('your-track.ogg'); let audioStart: number; audio.addEventListener('play', () => { // 计算音频实际播放的起始时间偏移 audioStart = Date.now() - audio.currentTime * 1000; }); // 在Pixi的ticker回调中检查音符是否该生成 app.ticker.add(() => { if (!audioStart) return; // 当前音频播放的绝对毫秒数 const currentAudioMs = Date.now() - audioStart; // 遍历音符数组,判断是否到达生成时间(提前1000ms生成,根据下落速度调整) while (notesQueue.length > 0 && notesQueue[0].absoluteMs <= currentAudioMs + 1000) { const note = notesQueue.shift(); // 生成Pixi音符元素并添加到舞台 spawnNote(note.note, note.absoluteMs); } });
内容的提问来源于stack exchange,提问作者Juani
相关产品推荐
相关产品推荐

