TonyChyi

我在那一角落患过抽风

填掉了 LMMS 一个长达两年的大坑
2017年01月20日
 

 

LMMS 的代码里其实已经有了一个 MIDI 文件的导出功能。但是代码里的注释告诉我,那个是坏的,而且主界面的代码里把 MIDI 导出的菜单项给注释掉了。

既然 Midi 导入的功能已经改成了 Drumstick,那就更干脆点,直接把导出功能也解决了吧。

原代码功能

原来的 Midi 导出代码是将项目的内容转成 Qt 的 DOM 视图,然后从视图中读取信息,另外再实现一个自己的 Midi 写入的类。

但是这个代码似乎只能导出音符信息,控制器信息却不能导出,这就意味着,像 Pitch bend/Volume/Panning 这些信息要丢掉,也没法导入到其他的软件中。

原代码可以参考这里

新的需求

我的目标是,做一个 GM 兼容的 MIDI 导出功能,不仅导出音符,还要能让导出的 MIDI 文件直接可以拿来听!

接下来就是试验一下 Drumstick 如何写入一个 MIDI 文件。

我们需要这样做1

  1. 首先调用 setFileFormat() 设定文件版本,这里就强制设 1 吧2
  2. 然后调用 setDivision() 设定一个四分音符的 Tick 数,同样强制 960 吧。
  3. 这样,一个 Header 就完成了,接下来写每个轨道的信息。
  4. 扫描整个项目,将使用 Sf2 Player 的轨道记下来,统计数量,调用 setTracks() 设为轨道数 +1s
  5. 同样也把 AutomationTrack 也记下来。
  6. Drumstick 按轨写入的方法是,发送一个 signalSMFWriteTrack(int track),然后设置一个 Slot 去接收它。
  7. 分配一个 Channel。
  8. track 为 0 时,写入全局事件3,这也是为什么要把轨道数加 1。
  9. InstrumentTrackTrackContentObject 中获取音符信息,写入一个事件列表4
  10. Program Change/Control Change/Pitch Bend 事件从 AutomationTrack 取出,写入事件列表,并指定好 Channel。
  11. 对事件按时间排序,并计算出事件之间的时间差5
  12. 写入事件到文件。
  13. 最后写入一个 EndOfTrack 的事件。

这里分配 Channel 的机制我要单独拿出来说一下。

LMMS 并不像 FL Studio 那样有一个“准备 MIDI 输出”的功能,所以对于 16 个 Channel,我的思路是这样的:

  1. 准备一个 16 个元素的列表,所有的值设为 -1。准备一个变量 currentChannel,记录当前 Channel
  2. currentChannel 作为索引遍历列表。
  3. 第一步:如果当前 Channel 所在的元素值为 -1,且不是打击乐通道6,就使用此 Channel,并将该元素的值设为当前轨道所用的乐器,并结束分配过程
  4. 第二步:如果 16 个通道除打击乐通道全部被分配,那么找乐器相同的 Channel7,结束分配
  5. 最后一步:如果还找不到,那就 XJBL 吧。

测试

我有一个《我在那一角落患过伤风摇滚版》的 MIDI 文件,测试的方法是,把这个文件导入,然后再导出,听起来应该和原文件一样。

经过测试,效果非常好。

代码已提交Github


  1. http://drumstick.sourceforge.net/docs/classdrumstick_1_1QSmf.html [return]
  2. SMF格式1支持多轨(Track) [return]
  3. 比如速度变化,几几拍这样的信息,还有SysEx和一些文本。 [return]
  4. QMultiMap实现 [return]
  5. Drumstick在写入事件时,是通过两个事件的时间差来决定一个事件的位置的。 [return]
  6. 通道10是打击乐轨。 [return]
  7. 这种情况在工程文件中乐器轨超过16个时出现。 [return]
Tags: #LMMS · #MIDI · #C++ · #Qt

 

TonyChyi © 2018 GPLv2