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

MIDI文件歌词时序异常问题排查求助

MIDI歌词时序检测程序的换行符相关时序错误问题

我用Qt C++开发了一款基础的MIDI歌词时序检测程序,多数MIDI文件上运行正常(已通过第三方软件验证),但部分文件出现时序计算错误。起初怀疑文件损坏,但第三方软件能正常解析这些文件的歌词时序。后来发现异常文件都用\n或\r作为换行符,正常文件用/,想了解这类MIDI文件特性来定位问题。

核心代码

#include <QFile>
#include <QDataStream>
#include <QDebug>
struct TempoChange {
    quint32 position;
    quint64 tempo;
};

QList<TempoChange> tempoMap;

void detectTempoMap(const QString &fileName)
{
    QFile file(fileName);
    if (!file.open(QIODevice::ReadOnly)) {
        qWarning("Could not open file");
        return;
    }

    QDataStream in(&file);
    in.setByteOrder(QDataStream::BigEndian);  // MIDI files are big-endian

    quint32 headerId;
    quint32 headerSize;
    quint16 formatType;
    quint16 trackCount;
    quint16 division;

    // Read header chunk
    in >> headerId >> headerSize >> formatType >> trackCount >> division;

    // Check that the header ID is 'MThd'
    if (headerId != 0x4D546864) {
        qWarning("Not a valid MIDI file");
        return;
    }

    // Process each track chunk
    for (int i = 0; i < trackCount; ++i) {
        quint32 trackId;
        quint32 trackSize;

        // Read track header
        in >> trackId >> trackSize;

        // Check that the track ID is 'MTrk'
        if (trackId != 0x4D54726B) {
            qWarning("Not a valid track chunk");
            return;
        }

        // Read track data
        QByteArray trackData = file.read(trackSize);

        // Process track data
        QDataStream trackStream(trackData);
        trackStream.setByteOrder(QDataStream::BigEndian);

        quint32 position = 0;
        quint32 deltaTime = 0;
        quint8 statusByte;
        quint8 lastStatusByte = 0; // Added for running status

        while (!trackStream.atEnd()) {
            // Read delta time
            quint8 byte;
            trackStream >> byte;
            deltaTime = (deltaTime << 7) | (byte & 0x7F);
            while (byte & 0x80) {
                trackStream >> byte;
                deltaTime = (deltaTime << 7) | (byte & 0x7F);
            }
            position += deltaTime;
            // Peek at the next byte without advancing the position in the stream
            QByteArray peeked = trackStream.device()->peek(1);
            if (!peeked.isEmpty()) {
                byte = peeked[0];
            }
            if (byte >= 0x80) {
                // This is a status byte, so read it
                trackStream >> statusByte;
                lastStatusByte = statusByte; // Remember this status byte for running status
            } else {
                // This is not a status byte, so use the last status byte
                statusByte = lastStatusByte;
            }

            // Check if this is a meta event
            if (statusByte == 0xFF) {
                quint8 metaType;
                quint8 length;
                // Read meta event type and length
                trackStream >> metaType >> length;
                // Process meta event based on type
                switch (metaType) {
                    case 0x51: {  // Set Tempo event
                        quint8 byte1, byte2, byte3;
                        trackStream >> byte1 >> byte2 >> byte3;
                        quint64 tempo = (byte1 << 16) | (byte2 << 8) | byte3;
                        tempoMap.append({position, tempo});
                        break;
                    }
                    default: {
                        trackStream.skipRawData(length);
                        break;
                    }
                }
            }
            deltaTime = 0;
        }
    }
}

quint64 getActualTempo(quint64 position)
{
    for(int i=(tempoMap.length()-1);i>=0;i--) {
        if(position >= tempoMap.at(i).position) {
            return tempoMap.at(i).tempo;
        }
    }
    return tempoMap.at(0).tempo;
}

quint64 readMidiFileContent(const QString &fileName)
{
    QFile file(fileName);
    if (!file.open(QIODevice::ReadOnly)) {
        return true;
    }

    QDataStream in(&file);
    in.setByteOrder(QDataStream::BigEndian);  // MIDI files are big-endian

    quint32 headerId;
    quint32 headerSize;
    quint16 formatType;
    quint16 trackCount;
    quint16 division;

    // Read header chunk
    in >> headerId >> headerSize >> formatType >> trackCount >> division;
    // Check that the header ID is 'MThd'
    if (headerId != 0x4D546864) {
        file.close();
        return true;
    }
    quint64 tempo = 500000; // default tempo 120 bpm
    quint64 generalTiming = 0;
    quint8 lastStatusByte = 0; // Added for running status

    for (int i = 0; i < trackCount; ++i) {
        quint32 trackId;
        quint32 trackSize;

        // Read track header
        in >> trackId >> trackSize;

        // Check that the track ID is 'MTrk'
        if (trackId != 0x4D54726B) {
            file.close();
            return true;
        }

        // Read track data
        QByteArray trackData = file.read(trackSize);

        // Process track data
        QDataStream trackStream(trackData);
        trackStream.setByteOrder(QDataStream::BigEndian);

        quint64 deltaTime = 0;
        quint64 totalDeltaTime = 0;
        quint8 statusByte;

        while (!trackStream.atEnd()) {
            // Read delta time
            quint8 byte;
            trackStream >> byte;
            deltaTime = (deltaTime << 7) | (byte & 0x7F);
            while (byte & 0x80) {
                trackStream >> byte;
                deltaTime = (deltaTime << 7) | (byte & 0x7F);
            }
            totalDeltaTime += deltaTime;

            // Peek at the next byte without advancing the position in the stream
            QByteArray peeked = trackStream.device()->peek(1);
            if (!peeked.isEmpty()) {
                byte = peeked[0];
            }
            if (byte >= 0x80) {
                // This is a status byte, so read it
                trackStream >> statusByte;
                lastStatusByte = statusByte; // Remember this status byte for running status
            } else {
                // This is not a status byte, so use the last status byte
                statusByte = lastStatusByte;
            }

            // Check if this is a meta event
            if (statusByte == 0xFF) {
                quint8 metaType;
                quint8 length;

                // Read meta event type and length
                trackStream >> metaType >> length;
                switch (metaType) {
                    case 0x05:
                    case 0x01: {
                        QByteArray textData = trackData.mid(trackStream.device()->pos(), length);
                        QString text = QString::fromLatin1(textData);
                        if(deltaTime > 0 || (text == "\n" || text == "\r" || text == "\n\r" || text == "\r\n")) {
                            tempo = getActualTempo(totalDeltaTime);
                            // Convert delta time to milliseconds

                            quint64 time = (1.0*deltaTime/division)*(tempo/1000.0);
                            generalTiming += time;
                            qDebug()<<text<<"-"<<time;
                        }
                        trackStream.skipRawData(length);
                        break;
                    }
                    default: {
                        trackStream.skipRawData(length);
                        break;
                    }
                }
            }
            deltaTime = 0;
        }
    }
    file.close();
    return generalTiming;
}

bool readMidiFile(const QString &fileName)
{
    detectTempoMap(fileName);
    if(tempoMap.length() == 0) {
        //put default tempo 120 bpm
        tempoMap.append({0, 500000});
    }

    for (const TempoChange &tempoChange : tempoMap) {
            qDebug() << "Position:" << tempoChange.position << "Tempo:" << tempoChange.tempo;
        }

    quint64 generalTiming = readMidiFileContent(fileName);
    return true;
}



int main() {
    QString path = "here path to file";
    readMidiFile(path);
    return 0;
}

输出示例

正常文件输出

Position: 0 Tempo: 465116
"Some" - 33720
"many" - 236
"mi-" - 228
"nd/" - 232
...

异常文件输出

Position: 0 Tempo: 810811
Position: 39332 Tempo: 769231
"\r" - 0
"\n" - 128
"A " - 397
...

注:其中"A "计算显示为开始后525ms出现,实际应在20s左右。


问题分析与修复方案

问题根源

  1. 跨轨道时间累计错误:代码中totalDeltaTime是全局累加所有轨道的deltaTime,但MIDI的多个轨道是并行同步的,每个轨道的时间是独立累计的,全局累加会导致时间基准混乱,拿到错误的tempo值。
  2. 换行符事件的错误处理:对纯换行符事件强制计算时间,这类事件通常是歌词排版用的辅助事件,不对应实际播放时间,强行累加会打乱时序。
  3. tempo查询基准不匹配:detectTempoMap中记录的tempo位置是基于单个轨道的累计时间,而readMidiFileContent中用全局累计时间查询tempo,基准不一致导致拿到错误的tempo值。

修复措施

  1. 轨道独立维护时间:将totalDeltaTime替换为每个轨道的trackPosition,每个轨道单独累计自己的deltaTime,确保时间基准和tempo事件的基准一致。
  2. 过滤无效文本事件:只处理非空的歌词文本事件,忽略纯换行符的时间计算,避免干扰正常时序。
  3. 统一tempo查询逻辑:使用当前轨道的累计位置查询tempo,确保和detectTempoMap中记录的位置基准一致。

关键修改代码片段

// 在readMidiFileContent的轨道循环内,替换totalDeltaTime为trackPosition
quint64 trackPosition = 0; // 每个轨道单独维护累计时间
while (!trackStream.atEnd()) {
    // 读取deltaTime的逻辑不变
    trackPosition += deltaTime; // 替换原totalDeltaTime += deltaTime;

    // ... 其他逻辑不变 ...

    case 0x05:
    case 0x01: {
        QByteArray textData = trackData.mid(trackStream.device()->pos(), length);
        QString text = QString::fromLatin1(textData);
        // 只处理非空的有效歌词事件
        if(deltaTime > 0 && !text.trimmed().isEmpty()) {
            tempo = getActualTempo(trackPosition); // 用轨道累计位置查询tempo
            quint64 time = (1.0 * deltaTime / division) * (tempo / 1000.0);
            generalTiming += time;
            qDebug() << text << "-" << time;
        }
        trackStream.skipRawData(length);
        break;
    }
}

内容的提问来源于stack exchange,提问作者Andrey

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 13:07:33