C++从零创建MIDI文件异常:音符错位、BPM不符求助
问题:MIDI文件生成异常——BPM不生效、音符位置错位
我编写的C++代码预期生成一个开头为C5四分音符(时长96 ticks)的.mid文件,但出现两个异常:
- 设置的170 BPM在FL Studio 20中显示为140,OpenMPT中显示默认的120
- C5音符从第56小节中间开始,位置完全错位
生成文件的十六进制数据:
0x4D, 0x54, 0x68, 0x64, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x01, 0x00, 0x60, 0x4D, 0x54, 0x72, 0x6B, 0x00, 0x00, 0x00, 0x0E, 0xFF, 0x51, 0x03, 0x05, 0x62, 0xAD, 0x00, 0x90, 0x3C, 0x7F, 0x60, 0x80, 0x3C, 0x00, 0x00, 0xFF, 0x2F, 0x00
原代码如下:
#include <iostream> #include <fstream> #include <windows.h> #include <cmath> namespace Global { int m_BPM = 0; } class Midi { public: static void WriteVarLength(std::ofstream& file, int value) { int buffer[4]; int i = 0; do { buffer[i++] = value & 0x7F; value >>= 7; } while (value > 0); for (int j = i - 1; j >= 0; j--) { if (j > 0) { buffer[j] |= 0x80; } file.put(static_cast<char>(buffer[j])); } } static void WriteHChunk(std::ofstream& file, int format, int nTracks, int division) { file.write("MThd",4); file.put(0x00); file.put(0x00); file.put(0x00); file.put(0x06); if (format == 0 || format == 1 || format == 2) { file.put(static_cast<char>(format >> 8)); file.put(static_cast<char>(format)); } else { MessageBox( nullptr, (LPCSTR)L"Invalid format number.", (LPCSTR)L"MidiSaver", MB_ICONERROR | MB_OK ); exit(1); } file.put(static_cast<char>(nTracks >> 8)); file.put(static_cast<char>(nTracks)); file.put(static_cast<char>(division >> 8)); file.put(static_cast<char>(division)); } static std::streampos WriteTrackChunk(std::ofstream& file) { file.write("MTrk", 4); std::streampos pos = file.tellp(); std::cout << "TrackLengthpos: " << pos << std::endl; file.put(0x00); file.put(0x00); file.put(0x00); file.put(0x00); return pos; } static void MetaEventBPM(std::ofstream& file, int m_BPM) { int tempo = 60000000 / m_BPM; file.put(static_cast<char>(0xFF)); file.put(static_cast<char>(0x51)); file.put(static_cast<char>(0x03)); file.put(static_cast<char>(tempo >> 16)); file.put(static_cast<char>(tempo >> 8)); file.put(static_cast<char>(tempo)); } static void MetaEndTrack(std::ofstream& file) { file.put(0x00); file.put(static_cast<char>(0xFF)); file.put(0x2F); file.put(0x00); } static void DeltaEvent(std::ofstream& file, int delta) { int size = 1; for (int d = delta; d >= 128; d >>= 7) size++; for (int i = size - 1; i >= 0; i--) { int shift = i * 7; int mask = (i == size - 1) ? 0x7F : 0xFF; file.put(static_cast<char>(((delta >> shift) & mask) | (i > 0 ? 0x80 : 0x00))); } } static void CalTHeaderLength(std::ofstream& file, std::streampos trackLengthPos) { std::streampos currentPos = file.tellp(); file.seekp(0, std::ios::end); uint32_t trackLength = file.tellp() - trackLengthPos - 8; file.seekp(trackLengthPos); file.put(static_cast<char>(trackLength >> 24)); file.put(static_cast<char>(trackLength >> 16)); file.put(static_cast<char>(trackLength >> 8)); file.put(static_cast<char>(trackLength)); file.seekp(currentPos); } static void EventNoteOn(std::ofstream& file, int delta_time, int note, int velocity, int channel) { DeltaEvent(file, delta_time); int statusByte = 0x90 | (channel & 0x0F); file.put(static_cast<char>(statusByte)); file.put(static_cast<char>(note)); file.put(static_cast<char>(velocity)); } static void EventNoteOff(std::ofstream& file, int delta_time, int note, int channel) { DeltaEvent(file, delta_time); int statusByte = 0x80 | (channel & 0x0F); file.put(static_cast<char>(statusByte)); file.put(static_cast<char>(note)); file.put(0x00); } }; int main() { Global::m_BPM = 170; std::string filename = "test.mid"; std::ofstream file(filename, std::ios::binary); int format = 0; int nTracks = 1; int division = 96; Midi::WriteHChunk(file, format, nTracks, division); std::streampos trackLengthPos = Midi::WriteTrackChunk(file); std::cout << "trackLengthPos value: " << trackLengthPos << std::endl; Midi::MetaEventBPM(file, Global::m_BPM); Midi::EventNoteOn(file, 0, 60, 127, 0); Midi::EventNoteOff(file, 96, 60, 0); Midi::MetaEndTrack(file); Midi::CalTHeaderLength(file, trackLengthPos); file.close(); }
问题排查与修复
1. 核心问题:元事件缺少Delta时间前缀
MIDI文件中所有事件(包括元事件)必须以Delta时间开头,哪怕Delta值为0。你的MetaEventBPM函数直接写入了元事件数据,没有前置Delta=0,导致播放器错误地将轨道长度字段后的字节当作Delta时间解析,生成了超大的时间偏移,直接导致音符位置错位,同时BPM事件被错误解析,无法生效。
2. DeltaEvent函数实现错误
MIDI的可变长度量编码规则是:
- 每次取数值的低7位作为一个字节
- 除了最后一个字节,其余字节的最高位设为1
- 字节顺序是从低位到高位依次写入
你的DeltaEvent函数逻辑完全搞反,从高位到低位处理,导致Delta值被错误编码,进一步加剧时间轴错位。
3. 轨道长度计算错误
CalTHeaderLength中的轨道长度计算公式错误,正确的轨道长度是从轨道长度字段结束位置到当前文件指针位置的字节数,即currentPos - trackLengthPos - 4(trackLengthPos是长度字段的起始位置,长度字段本身占4字节)。原公式file.tellp() - trackLengthPos -8会导致长度计算偏小,播放器无法正确识别轨道内容。
修复后的代码
#include <iostream> #include <fstream> #include <windows.h> #include <cstdint> namespace Global { int m_BPM = 0; } class Midi { public: static void WriteVarLength(std::ofstream& file, int value) { uint8_t buffer[4]; int i = 0; do { buffer[i] = value & 0x7F; value >>= 7; if (value > 0) { buffer[i] |= 0x80; } i++; } while (value > 0); for (int j = 0; j < i; j++) { file.put(static_cast<char>(buffer[j])); } } static void WriteHChunk(std::ofstream& file, int format, int nTracks, int division) { file.write("MThd",4); file.put(0x00); file.put(0x00); file.put(0x00); file.put(0x06); if (format == 0 || format == 1 || format == 2) { file.put(static_cast<char>(format >> 8)); file.put(static_cast<char>(format)); } else { MessageBox( nullptr, (LPCSTR)L"Invalid format number.", (LPCSTR)L"MidiSaver", MB_ICONERROR | MB_OK ); exit(1); } file.put(static_cast<char>(nTracks >> 8)); file.put(static_cast<char>(nTracks)); file.put(static_cast<char>(division >> 8)); file.put(static_cast<char>(division)); } static std::streampos WriteTrackChunk(std::ofstream& file) { file.write("MTrk", 4); std::streampos pos = file.tellp(); std::cout << "TrackLengthpos: " << pos << std::endl; file.put(0x00); file.put(0x00); file.put(0x00); file.put(0x00); return pos; } static void MetaEventBPM(std::ofstream& file, int m_BPM) { WriteVarLength(file, 0); // 添加Delta=0前缀 int tempo = 60000000 / m_BPM; file.put(static_cast<char>(0xFF)); file.put(static_cast<char>(0x51)); file.put(static_cast<char>(0x03)); file.put(static_cast<char>(tempo >> 16)); file.put(static_cast<char>(tempo >> 8)); file.put(static_cast<char>(tempo)); } static void MetaEndTrack(std::ofstream& file) { WriteVarLength(file, 0); file.put(static_cast<char>(0xFF)); file.put(0x2F); file.put(0x00); } // 直接复用WriteVarLength即可,无需单独的DeltaEvent static void EventNoteOn(std::ofstream& file, int delta_time, int note, int velocity, int channel) { WriteVarLength(file, delta_time); int statusByte = 0x90 | (channel & 0x0F); file.put(static_cast<char>(statusByte)); file.put(static_cast<char>(note)); file.put(static_cast<char>(velocity)); } static void EventNoteOff(std::ofstream& file, int delta_time, int note, int channel) { WriteVarLength(file, delta_time); int statusByte = 0x80 | (channel & 0x0F); file.put(static_cast<char>(statusByte)); file.put(static_cast<char>(note)); file.put(0x00); } static void CalTHeaderLength(std::ofstream& file, std::streampos trackLengthPos) { std::streampos currentPos = file.tellp(); uint32_t trackLength = static_cast<uint32_t>(currentPos - trackLengthPos - 4); file.seekp(trackLengthPos); file.put(static_cast<char>(trackLength >> 24)); file.put(static_cast<char>(trackLength >> 16)); file.put(static_cast<char>(trackLength >> 8)); file.put(static_cast<char>(trackLength)); file.seekp(currentPos); } }; int main() { Global::m_BPM = 170; std::string filename = "test.mid"; std::ofstream file(filename, std::ios::binary); int format = 0; int nTracks = 1; int division = 96; Midi::WriteHChunk(file, format, nTracks, division); std::streampos trackLengthPos = Midi::WriteTrackChunk(file); std::cout << "trackLengthPos value: " << trackLengthPos << std::endl; Midi::MetaEventBPM(file, Global::m_BPM); Midi::EventNoteOn(file, 0, 60, 127, 0); Midi::EventNoteOff(file, 96, 60, 0); Midi::MetaEndTrack(file); Midi::CalTHeaderLength(file, trackLengthPos); file.close(); }
修复说明
- 给
MetaEventBPM和MetaEndTrack添加了Delta=0的前缀,确保播放器正确识别事件起始位置 - 修正了
WriteVarLength的编码逻辑(原逻辑错误,现在符合MIDI可变长度量规则),并删除了冗余的DeltaEvent函数,直接复用WriteVarLength - 修正了轨道长度计算的公式,确保播放器能正确读取轨道所有内容
修复后生成的MIDI文件会正确显示170 BPM,C5音符会从开头第一小节正常播放。
内容的提问来源于stack exchange,提问作者ShatterS
相关产品推荐
相关产品推荐

