rtmidi不能读取MIDI文件,仅支持实时I/O;应使用libsmf等专用解析库,它支持SMF所有格式,需手动编译并定义SMF_USE_STD_STRING宏,注意文件头校验、delta-time与tick区别及tempo单位转换。

用 rtmidi 读取 MIDI 文件?不行,它只处理实时 I/O
很多人搜“C++ MIDI 读取”第一反应是 rtmidi,但它根本不能解析 .mid 文件——它只负责收发实时 MIDI 消息(比如从键盘或发送到声卡)。想读文件内容,必须用专门的 MIDI 文件解析库。
真正能读取标准 MIDI 文件(SMF)结构的库有:mingus(Python)、music21(Python),但 C++ 生态里最轻量、无依赖、可嵌入的只有 cpp-midi 和 libsmf。其中 libsmf 更成熟,支持所有 SMF 格式(0/1/2),且 API 直接暴露 track、event、delta-time 等原始字段。
实操建议:
- 用
git clone https://github.com/craigsapp/libsmf.git获取源码,它只有一个Smf.h+Smf.cpp,直接加进你的项目即可编译 - 不要用包管理器装(如 vcpkg 的
libsmf版本陈旧,不支持 UTF-8 track 名、缺失 tempo event 解析) - 确保编译时定义
SMF_USE_STD_STRING宏,否则 track 名等字符串会用自定义 string 类,增加转换成本
Smf::read() 成功但 getTrackCount() 返回 0?检查文件头和格式
常见错误现象:调用 smf.read("song.mid") 返回 true,但 smf.getTrackCount() 是 0,或 getEventCount(0) 崩溃。
立即学习“C++免费学习笔记(深入)”;
原因通常是文件不是标准 SMF 格式:
组合式C++代码评审方案,融合静态分析、AI推理、多轮迭代评审和C++专项检查,适用于PR审查、增量代码审查、全项目评审和代码质量评分,触发词包括review cpp、cpp代码评审、C++review、代码审查。
- DAW 导出时选了 “compressed MIDI” 或 “.midi” 扩展名但实际是 Apple MIDI(MIDF chunk)——
libsmf不支持 - 文件开头被截断(少前 14 字节 header),或末尾多写了垃圾字节(某些导出器 bug)
- 格式为 type 2(异步多序列)——
libsmf能读,但多数工具不生成,你手头文件大概率不是
快速验证方法:用 xxd -l 20 song.mid 查看前 20 字节,合法 SMF 必须以 4D 54 68 64("MThd" ASCII)开头,紧接着是 4 字节 length(通常为 0x00000006),再是 format(2 字节)、track count(2 字节)、division(2 字节)。
遍历每个 MidiEvent 时,getTick() 和 getDeltaTime() 到底哪个该用?
SMF 中时间用的是 tick(非秒),而每个 event 存储的是相对于上一个 event 的 delta time(不是绝对 tick)。所以:
-
event->getDeltaTime()是原始 delta 值(即 MIDI 文件里存的那个整数),用于做差分编码还原 -
event->getTick()是该 event 在整个 track 中的**累计绝对 tick 位置**(libsmf内部已帮你累加好),绝大多数场景该用这个 - 如果你要重写 MIDI 文件或做精确时序对齐(比如同步视频帧),才需要手动处理
getDeltaTime()并维护自己的 tick counter
示例:打印第一个 track 所有 note on/off
for (int i = 0; i < smf.getTrackCount(); ++i) {
for (int j = 0; j < smf.getEventCount(i); ++j) {
MidiEvent* e = smf.getEvent(i, j);
if (e->isNoteOn() || e->isNoteOff()) {
printf("track %d, tick %d: %s %d vel %d\n",
i, e->getTick(),
e->isNoteOn() ? "on" : "off",
e->getP1(), e->getP2());
}
}
}
如何获取 tempo、time signature、track name?它们不在 MidiEvent 里
这些是 Meta Event,类型为 0xFF,但 libsmf 把它们做了语义封装,不混在普通 event 流中:
- tempo(BPM):用
smf.getTempoMap().getTempoAtTick(tick)查询任意 tick 对应的 tempo;或遍历smf.getTempoMap().getTempoCount()拿所有 tempo change 点 - time signature:同理,用
smf.getTimeSignatureMap(),返回每段 signature 及其起始 tick - track name:调用
smf.getTrackName(i),返回std::string(前提是编译时定义了SMF_USE_STD_STRING)
注意:getTempoMap() 和 getTimeSignatureMap() 是 lazy-init 的,首次调用才解析 meta events;如果没调用过就直接访问,可能返回空结果——这不是 bug,是设计如此。
容易被忽略的一点:SMF 中 tempo 单位是 microseconds per quarter note(不是 BPM),转成 BPM 要算 60000000.0 / tempo_us。很多初学者直接打印 getTempoAtTick() 得到 500000 就以为是 500 BPM,其实那是 120 BPM(60000000 / 500000 = 120)。

















