You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 10:47:05