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左右。
问题分析与修复方案
问题根源
- 跨轨道时间累计错误:代码中
totalDeltaTime是全局累加所有轨道的deltaTime,但MIDI的多个轨道是并行同步的,每个轨道的时间是独立累计的,全局累加会导致时间基准混乱,拿到错误的tempo值。 - 换行符事件的错误处理:对纯换行符事件强制计算时间,这类事件通常是歌词排版用的辅助事件,不对应实际播放时间,强行累加会打乱时序。
- tempo查询基准不匹配:
detectTempoMap中记录的tempo位置是基于单个轨道的累计时间,而readMidiFileContent中用全局累计时间查询tempo,基准不一致导致拿到错误的tempo值。
修复措施
- 轨道独立维护时间:将
totalDeltaTime替换为每个轨道的trackPosition,每个轨道单独累计自己的deltaTime,确保时间基准和tempo事件的基准一致。 - 过滤无效文本事件:只处理非空的歌词文本事件,忽略纯换行符的时间计算,避免干扰正常时序。
- 统一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
相关产品推荐
相关产品推荐

