用libmidifile读取MIDI文件得不到音符时间戳,是因为MIDI只存tick而非秒,必须调用doTimeAnalysis()解析tempo map和PPQ才能换算;漏掉setTempo事件会导致全部时间偏移错误。

用 libmidifile 读取 MIDI 文件时,为什么得不到音符时间戳?
因为 MIDI 文件本身不直接存储“秒”或“频率”,它存的是事件(NoteOn、NoteOff)、通道、音高(pitch)和 tick 时间。必须先解析 tempo map 和 ticks per quarter note 才能换算成真实时间。漏掉 setTempo 事件会导致所有时间偏移全错。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 用
MidiFile::read()加载后,立即调用doTimeAnalysis()—— 它会自动扫描SetTempo和TimeSignature事件,构建内部 tempo map - 遍历每个 track 的每个 event,只保留
isNoteOn()和isNoteOff();注意:有些库把velocity == 0的NoteOn当作NoteOff,需统一处理 - 用
tick2second()把 event 的tick转为秒级时间,再按时间排序(MIDI events 在 track 内按 tick 排序,但多 track 需合并)
pitch 到频率的转换公式为什么在 C++ 里容易出错?
标准公式是 f = 440.0 * pow(2.0, (pitch - 69) / 12.0),但常见错误是把 pitch 当成 MIDI 文件里的原始字节值直接用——它其实是 0–127 的整数,对应 A4=440Hz 的 pitch=69。若没减去 69 或除以 12 写成整数除法(如 /12),结果会完全失真。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 确认拿到的是
event.getData()[1](对NoteOnevent,第二个 data 字节是 pitch),不是event.getData()[0](那是 channel + status) - 强制用浮点运算:
(pitch - 69.0) / 12.0,避免整数截断 - 加个边界检查:
if (pitch 127) continue;,防止非法值传入pow
如何生成“频率序列”而不是简单打印?
所谓“序列”,通常指按播放时间顺序排列的(时间点,频率)对,用于后续做频谱分析、音频合成或机器学习输入。关键不是逐个输出,而是构造可索引、可插值的数据结构。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 用
std::vector<:pair double>></:pair>存储{time_in_seconds, frequency_in_hz},插入前按time_in_seconds排序 - 对同一时刻多个音符(和弦),是否合并?取决于用途:做钢琴卷帘图就保留全部;做单音旋律提取就得选 velocity 最大或持续时间最长的那个
- 如果要等间隔采样(比如每 10ms 一个频率值),别用线性插值——MIDI 是离散事件,应做“保持上一值直到下一 NoteOn”(zero-order hold),否则会伪造出不存在的滑音
Windows 下编译 libmidifile 链接失败怎么办?
常见报错是 LNK2019: unresolved external symbol _main 或 cannot open file 'libmidifile.lib',本质是没正确生成静态库或没设对运行时库(MT/MD 混用)。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 用 CMake 构建:
cmake -G "Visual Studio 17 2022" -A x64 -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF ..,确保生成.lib而非.dll - 项目属性 → 配置属性 → C/C++ → 代码生成 → 运行时库,必须和
libmidifile编译时一致(例如都选/MT) - 包含头文件路径加
midifile/include,库目录加midifile/lib,附加依赖项填midifile.lib
实际最难的部分不是公式或读取,而是处理 MIDI 的“非精确性”:同一音符可能被多个 track 发送、NoteOff 缺失导致音符悬停、tempo 变化频繁时 tick-to-second 映射误差累积。这些不会报错,但会让频率序列在长曲目中逐渐漂移。调试时务必拿一个已知时长的单音 MIDI 文件(比如纯 C4 长音 2 秒)手动验算前 3 个 event 的秒数和频率。


















